diff --git a/.gitignore b/.gitignore index 85bd82b7ff..d8cacc6b13 100644 --- a/.gitignore +++ b/.gitignore @@ -322,3 +322,6 @@ Kopie von* src/SmartStoreNET.Packager.sln Log.txt src/Presentation/SmartStore.Web/Themes/FlexMuseo/ + +# Used by SSL certificates management tools (e.g. Certify The Web). +/src/Presentation/SmartStore.Web/.well-known diff --git a/CREDITS.txt b/CREDITS.txt index 5ea4c4a6fb..ff0d418111 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -135,11 +135,11 @@ Copyright: Copyright (c) 2007 James Newton-King License: MIT -GeoLite Free by MaxMind +DB-IP Country Lite by Eris Networks S.A.S --------------------------------------------- -Website: http://www.maxmind.com -Copyright: Copyright (c) 2014 Maxmind -License: Creative Commons Attribution-ShareAlike 3.0 Unported License +https://db-ip.com/ +Copyright: Copyright (c) 2020 db-ip.com +License: Creative Commons Attribution 4.0 International License Linearicons by Perxis diff --git a/README.md b/README.md index f88a9608ed..51e24bdf95 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,25 @@ -

- - Smartstore.NET - - - -
-
- -

- Smartstore.NET -

-

Ready. Sell. Grow.

- -

- GitHub release - GitHub All Releases - GitHub repo size - GitHub closed issues - GitHub commit activity - GitHub -

- -

- Try Online ∙ - Docs ∙ - Forum ∙ - Marketplace ∙ - Translations -

-
- -Smartstore.NET is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack. - -**Smartstore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more. +# Deprecation warning :warning: -A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes Smartstore.NET a powerful all-in-one solution fulfilling all your needs. +This repository is outdated and no longer being actively maintained by the Smartstore devs. +Users & developers are encouraged to check out the successor built on **ASP.NET Core**: +**Smartstore 5**. -
-

- Smartstore.NET Demoshop +

+ + Announcing Smartstore 5 +

-**Smartstore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device. - -The state-of-the-art architecture of Smartstore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-) - -* **Website:** [http://www.smartstore.com/en/net](http://www.smartstore.com/en/net) -* **Forum:** [http://community.smartstore.com](http://community.smartstore.com) -* **Marketplace:** [http://community.smartstore.com/marketplace](http://community.smartstore.com/marketplace) -* **Translations:** [http://translate.smartstore.com/](http://translate.smartstore.com/) -* **Documentation:** [Smartstore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) - -

 

- -## Highlights - -### Technology & Design - -* State of the art architecture thanks to `ASP.NET 4.5`, `ASP.NET MVC 5`, `Entity Framework 6` and Domain Driven Design -* Easy to extend and extremely flexible thanks to modular design -* Highly scalable thanks to full page caching and web farm support -* A powerful theming engine lets you create themes & skins with minimum effort thanks to theme inheritance -* Point&Click Theme configuration -* Highly professional search framework based on Lucene.NET, delivering ultra fast faceted search results -* Consistent and sophisticated use of modern components such as `jQuery`, `Bootstrap 4`, `Sass` & more in the front and back end. -* Easy shop management thanks to modern and clean UI - -### Features - -* NEW: CMS Page Builder -* NEW: CMS Menu Builder -* Multi-Store support -* Unlimited number of products and categories -* Product Bundles -* RESTful WebApi -* 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. -* Trusted Shops precertification -* 100% compliant with German jurisdiction -* Sales-, Customer- & Inventory-management -* Comprehensive CRM features -* Powerful Discount System -* Powerful layered navigation in the shop -* Numerous Payment and Shipping Providers and options -* Sophisticated Marketing & Promotion capabilities (Gift cards, Reward Points, discounts of any type and more) -* Reviews & Ratings -* CMS (Blog, Forum, custom pages & HTML content etc.) -* and many more... - - - -## Project Status - -Smartstore.NET V3.2.0 has been released on 10 May, 2019. The highlights are: - -* (NEW) **Page Builder**: Create fascinating content that will boost your sales. No coding is required thanks to a powerful WYSIWYG editor which utilizes the revolutionary *CSS Grid* system (commercial plugin) -* (NEW) **Menu Builder**: Visual manager for all sorts of menus. Change existing menus or create your own and place them anywhere you want. -* (Perf) **Faster MegaSearch** thanks to Lucene.NET 4.8 -* (Perf) **Huge performance increase** in discount resolution and calculation. - - - -## Try it online - -We have set up a live online demo for you so you are able to test Smartstore.NET without local installation. Get a first impression and test all available features in the front- and in the backend. Please keep in mind that the backend demo is shared and other testers can modify data at the same time. - -* [**Frontend**](http://frontend.smartstore.net/en) (User: demo, PWD: 1234) -* [**Backend**](http://backend.smartstore.net/en/login) (User: demo, PWD: 1234) - - - -## How to install - -* Download the latest stable release from the download tab and unzip it to your web folder -* Setup a website in IIS and point the file directory to your unzipped folder -* Fire up your browser and follow the installation instructions -* Enjoy ;-) - -NOTE: Smartstore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you. - ### System requirements * IIS 7+ * ASP.NET 4.5+ * MS SQL Server 2008 Express (or higher) OR MS SQL Server Compact 4 -* Visual C++ Redistributable für Visual Studio 2015 ([Download](https://www.microsoft.com/en-US/download/details.aspx?id=52685)) +* Visual C++ Redistributable für Visual Studio 2015-2019 ([Download](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads)) * Full Trust ## License -Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt). +Smartstore 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 080c78d497..f67aba4987 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -39,7 +39,7 @@ x86 $(BUILD_NUMBER) - 3.2.2 + 4.2.0 $(StageFolder) .$(Version) @@ -97,13 +97,15 @@ - - - - + + + + + + - + diff --git a/assets/ready.set.go.jpg b/assets/ready.set.go.jpg new file mode 100644 index 0000000000..e69e37758c Binary files /dev/null and b/assets/ready.set.go.jpg differ diff --git a/assets/smartstore-text.png b/assets/smartstore-text.png index 25bf9558fb..0d8cfe6bb8 100644 Binary files a/assets/smartstore-text.png and b/assets/smartstore-text.png differ diff --git a/changelog.md b/changelog.md index 9e52619298..8ba63a78f8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,27 +1,241 @@ # Release Notes -## SmartStore.NET 4.0.0 +## Smartstore 4.2.0 + +### New Features +**Dimension Pricing** (commercial plugin) + +* Freely configurable dimension templates +* Price calculation based on user input for predefined product dimensions +* Shipping rates based on dimension and weight calculation +* Freely configurable shipping formulas for irregular shapes + +**File Manager** (commercial plugin) + +* Freely configurable tabs for product detail pages +* Unlimited upload of documents for each product tab +* CSV Import + +**Digital Product Advisor** (commercial plugin) + +* Product presentation based on configurable questions and predefined answers +* Product filter based on rule builder rules + +### Improvements +* Web API: added RewardPointsHistory entity +* Added canonical HTML link for blog, forum and news page +* Better semaphore handling +* Several OpenTrans improvements +* BMEcat: use stream writer to write large xml files & fixed catalog structure + +### Bugfixes + +* #379 & #264 Login failed if login type is user and email and if both username and email are an email address and are not identical. +* #2197 Web API error "The specified type member 'MergedDataValues' is not supported in LINQ to Entities..." +* Web API: fixes OData Connected Service error when accessing MediaFiles and MediaFolders lists +* PayPal Express: fixed order may have been placed even if DoExpressCheckoutPayment failed +* Roxy Filemanager: icons were not displayed anymore +* Azure CloudBlockBlob stream throws on Length access. Fetch Position instead. +* Fixed the uploaded file of a product attribute is lost as soon as another attribute is selected +* Fixes display problem with uploaded media files which contain a + character + +## Smartstore 4.1.1 +### New Features +* **Page Builder** + * Added newsletter block to subscribe/unsubscribe to the newsletter. +* Rule Builder: added CartItemQuantity rule to specify conditions for the quantity per cart product. +* Added an option to display blog posts and news only for a certain language. +* #2171 Created SEO setting for ExtraAllows. + +### Improvements +* WebDAVModule is removed in Web.config because it disables PUT and DELETE requests (required by Web API). +* Newsletter subscriptions: added filter for customer roles in export profile and backend list. +* #2167 Added an activity log for order editing. +* Inherit ACL and store mappings into all sub-categories, not only into direct sub-categories. +* #2186 MegaMenu: link for background image had no effect. +* Added honeypot protection to blog post comments. + +### Bugfixes +* #2162 A plugin could not be uninstalled if it's the only one installed. +* Sometimes no picture was displayed when navigating in the product details' image gallery. +* Deactivated slugs were falsely always redirected without SSL. +* Selected products could not be deleted in the backend product list. +* Web-API: fixed "failed to serialize the response body" when uploading product images and missing product media file mapping if an equal image was found in catalog folder. +* Categories: fixed missing cache invalidation when inheriting ACL or stores into child categories and products. +* Link Builder: the selected media file was not applied. +* Selecting an attribute of type date caused an error on the product detail page. +* AmazonPay: fixed rare "client ID is a required parameter and is not set" when using the login function. +* Various fixes for anti forgery request handling. +* Used mediasettings value for picture rendering in MegaMenu (pictures weren't displayed in MegaMenu dropdowns). +* Mediamanager uploads were marked as being transient. + +## Smartstore 4.1.0 ### Highlights -* (NEW) **Granular permissions**: Extensive permission management based on customer groups via tree view. Supports inheritance to allow or deny entire permission ranges. -* (NEW) **Granular permissions**: All permissions of a customer directly visible on the customer edit page. +* **Web API:** now supports the widely extended version 4 of OData. New endpoints for uploading and managing media files have been added to the API. +* (NEW) **Delivery dates:** product info now shows estimated delivery date (exact date, range, *Not before*, *Not later than*). Merchant can enter optional Min/Max delivery days and also specify business hours. +* (DEV) [VS Extension](https://marketplace.visualstudio.com/items?itemName=SmartStoreAG.Smartstore) to create plugins. Helps developing plugins for Smartstore by creating the basic structure of a Smartstore plugin with all the required files. +* **OCI** provides connection to e-procurement systems via industry standard OCI (commercial plugin) +* **cXmlPunchout** provides connection to e-procurement systems via industry standard cXmlPunchout (commercial plugin) +* **Video** support in product media gallery. +* **Media Gallery** displays a multitude of media files in various styles as Page Builder block. +* (PERF) **~10% faster page rendering** speed thanks to better eager loading & caching strategies ### Breaking changes -* *Install\UninstallPermissions* has been removed from *IPermissionService*. No longer needs to be called by plugins. Permissions are now automatically installed\uninstalled. -* *IDiscountRequirementRule* and *IShippingMethodFilter* has been removed. Discounts and shipping methods can now be filtered by rule sets. -* Customer navigation property *CustomerRoles* has been replaced by *CustomerRoleMappings*, a new entity for role mappings. +* The **Glimpse** plugin is obsolete and has been removed! Does not work with .NET 4.7+ and hasn't been maintained for 7 years now. +* (DEV) **Web API**: porting to OData v4 has minor incompatibilities with v3. See this [blog post](https://smartstore.com/en/web-api-supports-odata-v4-as-from-smartstore-4-1) for details. +* **Blogs\News**: the direct language association has been removed. Localized content is now entered using the localization editor. +Merging the contents of different languages must be done manually (migration is not possible). +* **PostFinance**: plugin has been extended and renamed. Users of this payment method must activate PostFinance e-Commerce in backend and enter the API access data again. + +### New Features +* Theming: responsive font-sizing and spacing +* **Page Builder** + * #2042 Added gallery block to display a multitude of media files in various styles. + * #2036 Added block to display content slider. + * #2035 Added block to display news. + * #1729 Added option for title and intro inline background color. +* **PostFinance**: added new payment methods "Checkout All-in-one" and "Checkout Flex". +* #1644 Graying out unavailable attribute combinations. +* #1744 Added 'Orders' tab to the product editor to display all orders in which the product has been purchased. +* #1735 Added a setting to open a product directly if the search term matches a SKU, MPN or GTIN. +* #1239 Added configurability for password strength. +* #1678 Determine initial working currency based on IP/country. +* #1697 Added picture and color options to checkout attribute values. +* #1788 Added setting to restrict order amount in general and for customer roles in specific. +* #1978 Added minimum and maximum days to delivery times to show a specific delivery date. +* #1715 Added product attributes selected by customer to RFQ and ask question message. +* (DEV) Full localization support for properties of setting classes. +* #2012 Added paging and sorting for bestsellers report. +* #2014 Added paging and sorting for top customers report. +* #2092 Added meta properties for social media sharing to category, manufacturer, news and blog pages. +* Added option for SameSite mode property of cookies +* #1307 Add 'apple-touch-icon' meta with all common sizes +* #213 Allow special title and meta tags for home + +### Improvements +* (PERF) 10% faster page rendering speed thanks to better eager loading & caching strategies +* #1981 Added grouping for cart rules. +* #1407 Added menu to set order and payment status for orders checked in backend grid. +* #1778 Back button closes offcanvas menu/cart. +* #2069 MegaSearch: product order is applied to all categories in which a product appears. +* #2052 When editing a product, the stock quantity is only updated if it has not changed since the page was loaded. +* #213 Added options for special title and meta tags on home page. +* #1977 CMS Menu Builder: added new builder types for blogs and news. +* Search Log: added option to hide top search terms in instant search. +* CookieManager: Admin area for user defined cookie information +* CookieManager: More compact frontend layout for mobile devices +* Seo optimization for News, Blog & Boards +* (Admin) More filter options for news & blog items +* #1901 News: Made CreatedOn configurable +* #2078 Hidden news items should be rendered opaque if current user is in admin role. +* #1902 Form registration clears values when switching between site languages +* #1984 Render breadcrumb for pages that are part of the main menu. +* #2121 Media Manager: implement direct file "replace" support. +* #1338 Display 'PanGV' in OffCanvas cart +* #1988 Preconnect all external resources and load them async +* #1968 Dropzone: upload the folder structure when a folder is dropped. +* #1996 Page Builder: Added 'Loading behaviour' option to picture block +* #2100 Improved schema for news, blogs and products + +### Bugfixes +* **Media Manager** + * The file date should be displayed in local time and not in UTC. +* **Page Builder** + * Reveal effect on story block conflicted with nested block reveal effects. + * #2068 Fixed RTL display issues with dropdown menus and slider. +* **Export** + * An empty export file was created at the end of an export when using the batch-size option. + * The offset option skipped one record too much. + * The XML export of CountryOfOrigin caused an error. +* Message template attachments were not attached to the email. +* Shipment templates showed all products of the order instead of only those of the shipment. +* EasyCredit: fixed "Could not convert setting 'EasyCreditSettings.LastPurchaseLimitUpdatedOn' to type 'DateTime'". +* #2076 Attributes were not pre-selected for all associated products of a grouped product. +* Customer tab on customer role edit page showed deleted customers. +* #2041 Product with call for price did show as free in product list and grid view. +* #2079 Order item editing: order total was not updated when both unit and item price were changed. +* RSS feeds: relative URLs of objects embedded in HTML are replaced by absolute URLs. +* Rules: automatic category assignments of products were not taken into account in a search index updates. +* Campaign preview did not work if there are no newsletter subscribers. +* Many alt & title attribute fixes for pictures +* #2000 Escape toxic chars in meta attributes +* Google Analytics: Fixed script error in conjunction with CookieManager +* #2085 RTL: searchbar button wasn't aligned correctly +* #2068 RTL: Fixed Page Builder slider positioning, dropdown and other alignments +* #2019 OffCancas compare: hide horizontal scrollbar +* #2146 LimitedToStores property was not set in a rare case. + +## Smartstore 4.0.1 +### New Features +* Check on app startup whether any plugin has changed and refresh all plugin locale resources. + +### Improvements +* Theming: minimal but effective tweaks (rounding, shadows, typo) +* (SEO) (PERF) Preload logo to prevent flashing and web fonts to avoid FOUT on slow internet connection +* **Rule Builder**: + * #1969 The preview grid is now displayed in a modal dialog. + * Added more cart rules: weekday, total quantity of products in cart. + * Added list with example values for browser, operating system and device family rule. +* #1995 The localized ALT text of media files is now respected for all frontend images. +* SEO: most images in frontend now carry `loading="lazy"` attribute which loads images only when they enter visible viewport. +* **Media Manager**: + * Improved image lazy loading behavior. + * The reference table of a file now remains open if its data has been loaded. + * Migration: detect duplicate file names. +* Improved handling of customer roles, e.g. paging for customer role grid. +* (PERF) Use direct SQL to delete guest customers. +* Blog & News items: Unpublished items are visible for shop admins now + + +### Bugfixes +* Order placement validation now also checks whether the selected payment method has been filtered out. +* #1976 The button to cancel recurring payments in my-account was out of function. +* (pb) Flat Box did not invert text color if concrete colors were specified for text parts +* (mm) Store logo is handled by "content" album, not "file" +* (mm) Tracker tracks only the last media file property +* (mm) Migration: `Download` entity was teared down too early in the migration process. +* (perf) Plugin hash calculation was broken in a way that every plugin was redeployed on each app startup +* A cart rule was not applied if more than one cart rule was assigned to an object. +* MegaSearch: fixed the incorrect featured sorting. +* Added missing paging for shipment list. +* #2009 "Show all" link in top brands list is always rendered in new line but should be last item in last row. + + +## Smartstore 4.0.0 +### Highlights +* (NEW) **Media Manager**: Powerful and lightning-fast explorer/manager for media files (commercial plugin exclusively bundled with Pro Edition) +* (NEW) **Rule Builder**: Powerful rule system for visual business rule creation + * Dozens of predefined rules out-of-the-box + * Supports logical operators (AND/OR) and unlimited grouping/nesting + * Cart rules: can be applied to discounts, shipping and payment methods + * Customer rules: used to auto-assign customers to customer groups for 1-to-1 content targeting + * Product rules: used to auto-assign products to categories based on filter definitions (e.g. price, brand, color, stock etc.) +* (NEW) **New permission (ACL) system**: Granular tree-based permission management based on customer groups. Supports inheritance to allow or deny entire permission ranges +* (NEW) **Personalized product recommendations**: Replaces homepage products dynamically with personalized product recommendations based on customer interests. (commercial plugin) +* (NEW) **Geo Blocker**: Restricts shop access based on visitor country or IP address ranges (commercial plugin) +* (NEW) **Dashboard with charts**: New dashboard widgets display key data like orders, customers, bestsellers etc. in nice graphical charts. + +### Breaking changes +* (Dev) *Install\UninstallPermissions* has been removed from *IPermissionService*. No longer needs to be called by plugins. Permissions are now automatically installed\uninstalled. +* (Dev) *IDiscountRequirementRule* and *IShippingMethodFilter* has been removed. Discounts and shipping methods can now be filtered by rule sets. +* (Dev) Customer navigation property *CustomerRoles* has been replaced by *CustomerRoleMappings*, a new entity for role mappings. * The search index must be rebuilt due to various enhancements. -* The wrong name of API endpoint *LocalizedPropertys* has been corrected and renamed to *LocalizedProperties*. +* (Dev) The wrong name of API endpoint *LocalizedPropertys* has been corrected and renamed to *LocalizedProperties*. ### New Features +* **Granular permissions**: All permissions of a customer directly visible on the customer edit page. +* DEV: Added Visual Studio extension to create Smartstore Plugins +* GDPR conform Cookie Consent Manager +* SEO: XML Sitemap now includes blog, news and forum +* SEO: do 301 redirect to URL without trailing slash +* SVG support for image uploads. +* Schedule tasks: new property "Priority" runs tasks with higher priority first when multiple tasks are pending. +* #1805 Added XML Sitemap settings to backend UI. * #1598 Add published property to ProductTag entity. * #1669 Apply percentage discounts also on tier prices. * #1618 Implement ACL and multistore capability on menu item level. * #1683 Menu Builder items: implement support for icon (brand) color. * #1584 Show bundle item images in order details like in shopping cart details. -* SEO: XML Sitemap now includes blog, news and forum -* SEO: do 301 redirect to URL without trailing slash -* SVG support for image uploads. -* Schedule tasks: new property "Priority" runs tasks with higher priority first when multiple tasks are pending. * **MegaSearch**: * Added more text analysis options (e.g. Lucene.Net classic analyzer). * Added tool to display internal information about a search, such as Lucene.Net terms. @@ -30,8 +244,8 @@ * Perf. Added option to ignore attribute filtering property on product level. * **Web-API**: * #1809 Added a parameter to start an import after uploading import files. - * #1801 Added endpoints for ProductPictures, ProductCategories and ProductManufacturers to allow to update DisplayOrder. - * #1858 Added endpoints for NewsLetterSubscription. + * #1801 Added endpoints for *ProductPictures*, *ProductCategories* and *ProductManufacturers* to allow updating *DisplayOrder*. + * #1858 Added endpoints for *NewsLetterSubscription*. * #1714 Direct links to variations on a product. * #1733 Add ability to hide products from catalog, but not search. * #1754 Implement hidden boolean setting that controls which catalog search engine implementation should be used in backend. @@ -40,9 +254,10 @@ * #1116 Add new field for product condition. * #1790 ACL support for manufacturers. * #1616 Add new field for tracking URL to shipment entity to better track shipments. +* (Page Builder) Added a Page Builder block to display blog posts. ### Improvements -* #1663 Make MeasureDimension and MeasureWeight localizable. +* #1663 Make *MeasureDimension* and *MeasureWeight* localizable. * #1600 Show an example currency value for custom formatting value changes. * Card desk instead of a grid for customer addresses on customer edit page. * Display "price from" in product lists if any attribute combination price exists. @@ -58,6 +273,13 @@ * #1848 Send the billing address when redirecting to PayPal. * Hide cart payment button for payment methods without match of applied rule sets. * #1839 MegaSearch: Support exact value match for numeric range filters. +* #1920 Hide option prizes if "Call for price" is enabled. +* Debitoor: added an option to force a price type on invoices. +* BeezUp: export product costs. +* #1915 RTL: fixed alignment of product art badge. +* #1738 Sending of mails to customers uses generic message template now. +* #1805 UI: Added XmlSitemap settings to backend +* And many more other minor improvements ### Bugfixes * Export: Fixed KeyNotFoundException when batch size was 1. @@ -72,20 +294,25 @@ * **Page Builder**: * #1689 Block with z-index < 0 can not be selected in story view - Block tools can reduce z-index below 0. * #1691 Manually editing/removing block cols/rows does not refresh grid state correctly. + * #1684 Edit Mode rendering bug in Mac Safari + * #1836 Added "Order" property to page Builder block to control rendering order of blocks in HTML output * **PayPal**: * Fixed rare exception "Unsupported Media Type" (415) in PayPal PLUS. * Only process a partially refund IPN when the order refunded amount is zero. Otherwise the order refunded amount will be wrong. * **Customer import**: * VatNumber and other fields were ignored. * Customer roles sometimes inserted several times. - * Changed billing\shipping address was added instead of updating the existing one. + * Changed billing/shipping address was added instead of updating the existing one. * Region assigned to an address was not updated. * **Product tags**: * #1730 Product tag count should filter also based on Visibility. * Product tag count sometimes not up-to-date due to missing cache clearance. +* **Debitoor**: + * Avoid errors due to invalid quantity unit Id of 0. + * #1952 rework quantity units. + * Sometimes the SKU was missing on invoices. * Multistore mapping was ignored for manufacturers in sitemap. * Categories limited to stores were not displayed in tree view. -* Debitoor: avoid errors due to invalid product unitId of 0. * Fixed InvalidOperationException in CreatePdfInvoiceAttachment when an order is placed by a guest. * The ShowDescriptionInSubPages setting should also be applied to the bottom category description. * #1774 Recaptcha: doesn't work for product reviews, blog and news comments if hidden captcha is activated. @@ -104,6 +331,14 @@ * #1835 Removing gift card issue. * #1875 A delivery time cannot be deleted if it is assigned to a variant combination of a deleted product. * BeezUp: fixes ArgumentNullException, parameter name "source". +* #1743 Avoid redirecting to the account activation page when trying to log in for the first time. +* #1895 The checkout button disappears when moving a product from the shopping cart to the wishlist. +* Menu builder: in case of system menus the template can changes accidentally when saving. +* Structured data: Replace length with depth property. +* #1701 'View All' button from the offcanvas manufacturer menu causes 404 on mobile. +* Added missing sitemap task. +* #1696 ESD: browser freezes when editing file changelog +* And many more other minor fixes... ## SmartStore.NET 3.2.2 diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 17bf4e2ad9..61354aae6a 100644 Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ diff --git a/src/AssemblySharedInfo.cs b/src/AssemblySharedInfo.cs index b58492422f..92179ee59a 100644 --- a/src/AssemblySharedInfo.cs +++ b/src/AssemblySharedInfo.cs @@ -7,6 +7,6 @@ [assembly: AssemblyDescription("SmartStore.NET")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("SmartStore AG")] -[assembly: AssemblyCopyright("Copyright © SmartStore AG 2019")] +[assembly: AssemblyCopyright("Copyright © SmartStore AG 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index 719cd1d6ff..9ca54df467 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.2.2.0")] +[assembly: AssemblyVersion("4.2.0.0")] -[assembly: AssemblyFileVersion("3.2.2.0")] -[assembly: AssemblyInformationalVersion("3.2.2.0")] +[assembly: AssemblyFileVersion("4.2.0.0")] +[assembly: AssemblyInformationalVersion("4.2.0.0")] diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs index 8327c7b40d..91ffb0db95 100644 --- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs +++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Web.Hosting; @@ -9,423 +8,417 @@ namespace SmartStore.Core.Async { - using EventQueue = ConcurrentQueue>; - - public class AsyncRunner : IDisposable - { - #region Instance members - - private ExclusiveSynchronizationContext CurrentContext; - private SynchronizationContext OldContext; - private int TaskCount; - - private AsyncRunner() - { - OldContext = SynchronizationContext.Current; - CurrentContext = new ExclusiveSynchronizationContext(OldContext); - SynchronizationContext.SetSynchronizationContext(CurrentContext); - } - - public void Dispose() - { - SynchronizationContext.SetSynchronizationContext(OldContext); - } - - private void Increment() - { - Interlocked.Increment(ref TaskCount); - } - - private void Decrement() - { - Interlocked.Decrement(ref TaskCount); - if (TaskCount == 0) - { - CurrentContext.EndMessageLoop(); - } - } - - /// - /// Executes an async Task method which has a void return value synchronously - /// - /// Task execute - public void Run(Task task, Action continuation = null) - { - CurrentContext.Post(async _ => - { - try - { - Increment(); - await task; - - continuation?.Invoke(task); - } - catch (Exception e) - { - CurrentContext.InnerException = e; - } - finally - { - Decrement(); - } - }, null); + using EventQueue = ConcurrentQueue>; + + public class AsyncRunner : IDisposable + { + #region Instance members + + private readonly ExclusiveSynchronizationContext CurrentContext; + private readonly SynchronizationContext OldContext; + private int TaskCount; + + private AsyncRunner() + { + OldContext = SynchronizationContext.Current; + CurrentContext = new ExclusiveSynchronizationContext(OldContext); + SynchronizationContext.SetSynchronizationContext(CurrentContext); + } + + public void Dispose() + { + SynchronizationContext.SetSynchronizationContext(OldContext); + } + + private void Increment() + { + Interlocked.Increment(ref TaskCount); + } + + private void Decrement() + { + Interlocked.Decrement(ref TaskCount); + if (TaskCount == 0) + { + CurrentContext.EndMessageLoop(); + } + } + + /// + /// Executes an async Task method which has a void return value synchronously + /// + /// Task execute + public void Run(Task task, Action continuation = null) + { + CurrentContext.Post(async _ => + { + try + { + Increment(); + await task; + + continuation?.Invoke(task); + } + catch (Exception e) + { + CurrentContext.InnerException = e; + } + finally + { + Decrement(); + } + }, null); CurrentContext.BeginMessageLoop(); } - /// - /// Executes an async Task method which has a TResult return type synchronously - /// - /// Return Type - /// Task to execute - public TResult Run(Task task, Action> continuation = null) - { - var result = default(TResult); + /// + /// Executes an async Task method which has a TResult return type synchronously + /// + /// Return Type + /// Task to execute + public TResult Run(Task task, Action> continuation = null) + { + var result = default(TResult); var curContext = CurrentContext; curContext.Post(async _ => - { - try - { - Increment(); - result = await task; - - continuation?.Invoke(task); - } - catch (Exception e) - { - CurrentContext.InnerException = e; - } - finally - { - Decrement(); - } - }, null); + { + try + { + Increment(); + result = await task; + + continuation?.Invoke(task); + } + catch (Exception e) + { + CurrentContext.InnerException = e; + } + finally + { + Decrement(); + } + }, null); curContext.BeginMessageLoop(); return result; - } - - #endregion - - private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); - - /// - /// Gets the global cancellation token which signals the application shutdown - /// - public static CancellationToken AppShutdownCancellationToken - { - get { return _host.ShutdownCancellationTokenSource.Token; } - } - - public static AsyncRunner Create() - { - return new AsyncRunner(); - } - - /// - /// Executes an async Task method which has a void return value synchronously - /// - /// Task method to execute - public static void RunSync(Func func, Action continuation = null) - { - using (var runner = new AsyncRunner()) - { - runner.Run(func(), continuation); - } - } - - /// - /// Executes an async Task method which has a TResult return type synchronously - /// - /// Return Type - /// Task method to execute - /// - public static TResult RunSync(Func> func, Action> continuation = null) - { - var result = default(TResult); - - using (var runner = new AsyncRunner()) - { - result = runner.Run(func(), continuation); - } - - return result; - } - - public static Task Run( - Action action, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, - TaskScheduler scheduler = null) - { - Guard.NotNull(action, nameof(action)); - - var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - - var t = Task.Factory.StartNew(() => { - var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (accessor.BeginContextAwareScope()) - { - action(accessor.GetLifetimeScope(null), ct); - } - }, ct, options, scheduler ?? TaskScheduler.Default); - - _host.Register(t, ct); - - return t; - } - - public static Task Run( - Action action, - object state, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, - TaskScheduler scheduler = null) - { - Guard.NotNull(state, nameof(state)); - Guard.NotNull(action, nameof(action)); - - var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - - var t = Task.Factory.StartNew((o) => - { - var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (accessor.BeginContextAwareScope()) - { - action(accessor.GetLifetimeScope(null), ct, o); - } - }, state, ct, options, scheduler ?? TaskScheduler.Default); - - _host.Register(t, ct); - - return t; - } - - public static Task Run( - Func function, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, - TaskScheduler scheduler = null) - { - Guard.NotNull(function, nameof(function)); - - var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - - var t = Task.Factory.StartNew(() => - { - var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (accessor.BeginContextAwareScope()) - { - return function(accessor.GetLifetimeScope(null), ct); - } - }, ct, options, scheduler ?? TaskScheduler.Default); - - _host.Register(t, ct); - - return t; - } - - public static Task Run( - Func function, - object state, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, - TaskScheduler scheduler = null) - { - Guard.NotNull(state, nameof(state)); - Guard.NotNull(function, nameof(function)); - - var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - - var t = Task.Factory.StartNew((o) => - { - var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (accessor.BeginContextAwareScope()) - { - return function(accessor.GetLifetimeScope(null), ct, o); - } - }, state, ct, options, scheduler ?? TaskScheduler.Default); - - _host.Register(t, ct); - - return t; - } - - public static Task Run( - Func function, - object state, - CancellationToken cancellationToken = default(CancellationToken)) - { - Guard.NotNull(state, nameof(state)); - Guard.NotNull(function, nameof(function)); - - var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - - var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - var scope = accessor.BeginLifetimeScope(null); - - Task task = null; - - try - { - task = function(scope, ct).ContinueWith(x => - { - scope.Dispose(); - }); - _host.Register(task, ct); - } - catch - { - scope.Dispose(); - } - - return task; - } - - private class ExclusiveSynchronizationContext : SynchronizationContext - { - private bool _done; - private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); - private readonly EventQueue _items; - - public ExclusiveSynchronizationContext(SynchronizationContext old) - { - if (old is ExclusiveSynchronizationContext oldEx) - { - this._items = oldEx._items; - } - else - { - this._items = new EventQueue(); - } - } - - public Exception InnerException { get; set; } - - public override void Send(SendOrPostCallback d, object state) - { - throw new NotSupportedException("We cannot send to the same thread"); - } - - public override void Post(SendOrPostCallback d, object state) - { - _items.Enqueue(Tuple.Create(d, state)); - _workItemsWaiting.Set(); - } - - public void EndMessageLoop() - { - Post(_ => _done = true, null); - } - - public void BeginMessageLoop() - { - while (!_done) - { - if (!_items.TryDequeue(out var task)) - { - _workItemsWaiting.WaitOne(); - } - else - { - task.Item1(task.Item2); - - if (InnerException != null) // method threw an exeption - { - throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException); - } - } - } - } - - public override SynchronizationContext CreateCopy() - { - return this; - } - } - - } - - internal class BackgroundWorkHost : IRegisteredObject - { - private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); - private int _numRunningWorkItems; - - public BackgroundWorkHost() - { - HostingEnvironment.RegisterObject(this); - } - - public CancellationTokenSource ShutdownCancellationTokenSource - { - get { return _shutdownCancellationTokenSource; } - } - - public void Stop(bool immediate) - { - int num; - lock (this) - { - _shutdownCancellationTokenSource.Cancel(); - num = _numRunningWorkItems; - } - if (num == 0) - { - FinalShutdown(); - } - } - - public CancellationTokenSource CreateCompositeCancellationTokenSource(CancellationToken userCancellationToken) - { - if (userCancellationToken == CancellationToken.None) - { - return _shutdownCancellationTokenSource; - } - return CancellationTokenSource.CreateLinkedTokenSource(_shutdownCancellationTokenSource.Token, userCancellationToken); - } - - public void Register(Task work, CancellationToken cancellationToken) - { - if (!cancellationToken.IsCancellationRequested) - { - lock (this) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - _numRunningWorkItems++; - } - - work.ContinueWith( - WorkItemComplete, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - } - - private void WorkItemComplete(Task work) - { - int num; - bool isCancellationRequested; - lock (this) - { - num = --_numRunningWorkItems; - isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested; - } - if (num == 0 && isCancellationRequested) - { - FinalShutdown(); - } - } - - private void FinalShutdown() - { - HostingEnvironment.UnregisterObject(this); - } - - } -} + } + + #endregion + + private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); + + /// + /// Gets the global cancellation token which signals the application shutdown + /// + public static CancellationToken AppShutdownCancellationToken => _host.ShutdownCancellationTokenSource.Token; + + public static AsyncRunner Create() + { + return new AsyncRunner(); + } + + /// + /// Executes an async Task method which has a void return value synchronously + /// + /// Task method to execute + public static void RunSync(Func func, Action continuation = null) + { + using (var runner = new AsyncRunner()) + { + runner.Run(func(), continuation); + } + } + + /// + /// Executes an async Task method which has a TResult return type synchronously + /// + /// Return Type + /// Task method to execute + /// + public static TResult RunSync(Func> func, Action> continuation = null) + { + var result = default(TResult); + + using (var runner = new AsyncRunner()) + { + result = runner.Run(func(), continuation); + } + + return result; + } + + public static Task Run( + Action action, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, + TaskScheduler scheduler = null) + { + Guard.NotNull(action, nameof(action)); + + var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; + + var t = Task.Factory.StartNew(() => + { + var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; + using (accessor.BeginContextAwareScope()) + { + action(accessor.GetLifetimeScope(null), ct); + } + }, ct, options, scheduler ?? TaskScheduler.Default); + + _host.Register(t, ct); + + return t; + } + + public static Task Run( + Action action, + object state, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, + TaskScheduler scheduler = null) + { + Guard.NotNull(state, nameof(state)); + Guard.NotNull(action, nameof(action)); + + var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; + + var t = Task.Factory.StartNew((o) => + { + var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; + using (accessor.BeginContextAwareScope()) + { + action(accessor.GetLifetimeScope(null), ct, o); + } + }, state, ct, options, scheduler ?? TaskScheduler.Default); + + _host.Register(t, ct); + + return t; + } + + public static Task Run( + Func function, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, + TaskScheduler scheduler = null) + { + Guard.NotNull(function, nameof(function)); + + var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; + + var t = Task.Factory.StartNew(() => + { + var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; + using (accessor.BeginContextAwareScope()) + { + return function(accessor.GetLifetimeScope(null), ct); + } + }, ct, options, scheduler ?? TaskScheduler.Default); + + _host.Register(t, ct); + + return t; + } + + public static Task Run( + Func function, + object state, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, + TaskScheduler scheduler = null) + { + Guard.NotNull(state, nameof(state)); + Guard.NotNull(function, nameof(function)); + + var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; + + var t = Task.Factory.StartNew((o) => + { + var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; + using (accessor.BeginContextAwareScope()) + { + return function(accessor.GetLifetimeScope(null), ct, o); + } + }, state, ct, options, scheduler ?? TaskScheduler.Default); + + _host.Register(t, ct); + + return t; + } + + public static Task Run( + Func function, + object state, + CancellationToken cancellationToken = default(CancellationToken)) + { + Guard.NotNull(state, nameof(state)); + Guard.NotNull(function, nameof(function)); + + var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; + + var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; + var scope = accessor.BeginLifetimeScope(null); + + Task task = null; + + try + { + task = function(scope, ct).ContinueWith(x => + { + scope.Dispose(); + }); + _host.Register(task, ct); + } + catch + { + scope.Dispose(); + } + + return task; + } + + private class ExclusiveSynchronizationContext : SynchronizationContext + { + private bool _done; + private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); + private readonly EventQueue _items; + + public ExclusiveSynchronizationContext(SynchronizationContext old) + { + if (old is ExclusiveSynchronizationContext oldEx) + { + this._items = oldEx._items; + } + else + { + this._items = new EventQueue(); + } + } + + public Exception InnerException { get; set; } + + public override void Send(SendOrPostCallback d, object state) + { + throw new NotSupportedException("We cannot send to the same thread"); + } + + public override void Post(SendOrPostCallback d, object state) + { + _items.Enqueue(Tuple.Create(d, state)); + _workItemsWaiting.Set(); + } + + public void EndMessageLoop() + { + Post(_ => _done = true, null); + } + + public void BeginMessageLoop() + { + while (!_done) + { + if (!_items.TryDequeue(out var task)) + { + _workItemsWaiting.WaitOne(); + } + else + { + task.Item1(task.Item2); + + if (InnerException != null) // method threw an exeption + { + throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException); + } + } + } + } + + public override SynchronizationContext CreateCopy() + { + return this; + } + } + } + + internal class BackgroundWorkHost : IRegisteredObject + { + private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); + private int _numRunningWorkItems; + + public BackgroundWorkHost() + { + HostingEnvironment.RegisterObject(this); + } + + public CancellationTokenSource ShutdownCancellationTokenSource => _shutdownCancellationTokenSource; + + public void Stop(bool immediate) + { + int num; + lock (this) + { + _shutdownCancellationTokenSource.Cancel(); + num = _numRunningWorkItems; + } + if (num == 0) + { + FinalShutdown(); + } + } + + public CancellationTokenSource CreateCompositeCancellationTokenSource(CancellationToken userCancellationToken) + { + if (userCancellationToken == CancellationToken.None) + { + return _shutdownCancellationTokenSource; + } + return CancellationTokenSource.CreateLinkedTokenSource(_shutdownCancellationTokenSource.Token, userCancellationToken); + } + + public void Register(Task work, CancellationToken cancellationToken) + { + if (!cancellationToken.IsCancellationRequested) + { + lock (this) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + _numRunningWorkItems++; + } + + work.ContinueWith( + WorkItemComplete, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + private void WorkItemComplete(Task work) + { + int num; + bool isCancellationRequested; + lock (this) + { + num = --_numRunningWorkItems; + isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested; + } + if (num == 0 && isCancellationRequested) + { + FinalShutdown(); + } + } + + private void FinalShutdown() + { + HostingEnvironment.UnregisterObject(this); + } + + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs b/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs index 8bc3898736..d4f9493575 100644 --- a/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs +++ b/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs @@ -2,34 +2,34 @@ namespace SmartStore.Core.Async { - [Serializable] - public class AsyncStateInfo - { - // used for serialization compatibility - public static readonly string Version = "1"; + [Serializable] + public class AsyncStateInfo + { + // used for serialization compatibility + public static readonly string Version = "1"; - public object Progress - { - get; - set; - } + public object Progress + { + get; + set; + } - public DateTime CreatedOnUtc - { - get; - set; - } + public DateTime CreatedOnUtc + { + get; + set; + } - public DateTime LastAccessUtc - { - get; - set; - } + public DateTime LastAccessUtc + { + get; + set; + } - public TimeSpan Duration - { - get; - set; - } - } + public TimeSpan Duration + { + get; + set; + } + } } diff --git a/src/Libraries/SmartStore.Core/Async/IAsyncState.cs b/src/Libraries/SmartStore.Core/Async/IAsyncState.cs index 3c4033d951..232ef299d3 100644 --- a/src/Libraries/SmartStore.Core/Async/IAsyncState.cs +++ b/src/Libraries/SmartStore.Core/Async/IAsyncState.cs @@ -4,12 +4,12 @@ namespace SmartStore.Core.Async { - /// + /// /// Stores status information about long-running processes. /// public interface IAsyncState - { - /// + { + /// /// Checks whether a status object exists. /// /// The type of status to check for. @@ -73,5 +73,5 @@ public interface IAsyncState bool Cancel(string name = null); - } + } } diff --git a/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs b/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs index e34800f3d2..ba66c15bf0 100644 --- a/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs +++ b/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs @@ -2,98 +2,96 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; -using System.Text; using System.Threading; -using System.Threading.Tasks; namespace SmartStore.Core.Async { - public partial class LocalAsyncState : IAsyncState - { - private readonly MemoryCache _states = new MemoryCache("SmartStore.AsyncState.Progress"); - private readonly MemoryCache _cancelTokens = new MemoryCache("SmartStore.AsyncState.CancelTokenSources"); - - public virtual bool Exists(string name = null) - { - var value = GetStateInfo(name); - return value != null && !object.Equals(value.Progress, default(T)); - - } - - public virtual T Get(string name = null) - { - var value = GetStateInfo(name); - - if (value != null) - { - return (T)value.Progress; - } - - return default(T); - } - - public virtual IEnumerable GetAll() - { - var keyPrefix = BuildKey(null); - return _states - .Where(x => x.Key.StartsWith(keyPrefix)) - .Select(x => x.Value) - .OfType() - .Select(x => x.Progress) - .OfType(); - } - - - public virtual void Set(T state, string name = null, bool neverExpires = false) - { - Guard.NotNull(state, nameof(state)); - - var value = GetStateInfo(name); - - if (value != null) - { - // exists already, so update - if (state != null) - { - value.Progress = state; - } - } - else - { - // add new entry - var duration = neverExpires ? TimeSpan.Zero : TimeSpan.FromMinutes(15); - var policy = new CacheItemPolicy - { + public partial class LocalAsyncState : IAsyncState + { + private readonly MemoryCache _states = new MemoryCache("SmartStore.AsyncState.Progress"); + private readonly MemoryCache _cancelTokens = new MemoryCache("SmartStore.AsyncState.CancelTokenSources"); + + public virtual bool Exists(string name = null) + { + var value = GetStateInfo(name); + return value != null && !object.Equals(value.Progress, default(T)); + + } + + public virtual T Get(string name = null) + { + var value = GetStateInfo(name); + + if (value != null) + { + return (T)value.Progress; + } + + return default(T); + } + + public virtual IEnumerable GetAll() + { + var keyPrefix = BuildKey(null); + return _states + .Where(x => x.Key.StartsWith(keyPrefix)) + .Select(x => x.Value) + .OfType() + .Select(x => x.Progress) + .OfType(); + } + + + public virtual void Set(T state, string name = null, bool neverExpires = false) + { + Guard.NotNull(state, nameof(state)); + + var value = GetStateInfo(name); + + if (value != null) + { + // exists already, so update + if (state != null) + { + value.Progress = state; + } + } + else + { + // add new entry + var duration = neverExpires ? TimeSpan.Zero : TimeSpan.FromMinutes(15); + var policy = new CacheItemPolicy + { SlidingExpiration = duration, Priority = CacheItemPriority.NotRemovable }; - var key = BuildKey(name); + var key = BuildKey(name); // On expiration or removal: remove corresponding cancel token also. policy.RemovedCallback = (x) => OnRemoveCancelTokenSource(key); _states.Set(key, new AsyncStateInfo { Progress = state, Duration = duration }, policy); - } - } - - public virtual void Update(Action update, string name = null) - { - Guard.NotNull(update, nameof(update)); - - var value = GetStateInfo(name); - - if (value != null) - { - var state = (T)value.Progress; - if (state != null) - { - update(state); - } - } - } - - public virtual bool Remove(string name = null) - { + } + } + + public virtual void Update(Action update, string name = null) + { + Guard.NotNull(update, nameof(update)); + + var value = GetStateInfo(name); + + if (value != null) + { + var state = (T)value.Progress; + if (state != null) + { + update(state); + } + } + } + + public virtual bool Remove(string name = null) + { var key = BuildKey(name); if (!OnRemoveStateInfo(key)) @@ -104,14 +102,14 @@ public virtual bool Remove(string name = null) OnRemoveCancelTokenSource(key); return false; } - + return true; - } + } - protected virtual bool OnRemoveStateInfo(string key) - { - return _states.Remove(key) != null; - } + protected virtual bool OnRemoveStateInfo(string key) + { + return _states.Remove(key) != null; + } public virtual bool RemoveCancelTokenSource(string name = null) { @@ -119,83 +117,83 @@ public virtual bool RemoveCancelTokenSource(string name = null) } protected virtual bool OnRemoveCancelTokenSource(string key, bool successive = false) - { - Guard.NotEmpty(key, nameof(key)); + { + Guard.NotEmpty(key, nameof(key)); - var token = _cancelTokens.Remove(key) as CancellationTokenSource; + var token = _cancelTokens.Remove(key) as CancellationTokenSource; - if (token != null) - { - token.Dispose(); - return true; - } + if (token != null) + { + token.Dispose(); + return true; + } - return false; - } + return false; + } - public CancellationTokenSource GetCancelTokenSource(string name = null) - { - return OnGetCancelTokenSource(BuildKey(name)); - } + public CancellationTokenSource GetCancelTokenSource(string name = null) + { + return OnGetCancelTokenSource(BuildKey(name)); + } - protected virtual CancellationTokenSource OnGetCancelTokenSource(string key, bool successive = false) - { - Guard.NotEmpty(key, nameof(key)); + protected virtual CancellationTokenSource OnGetCancelTokenSource(string key, bool successive = false) + { + Guard.NotEmpty(key, nameof(key)); - return _cancelTokens.Get(key) as CancellationTokenSource; - } + return _cancelTokens.Get(key) as CancellationTokenSource; + } - public virtual void SetCancelTokenSource(CancellationTokenSource cancelTokenSource, string name = null) - { - Guard.NotNull(cancelTokenSource, nameof(cancelTokenSource)); + public virtual void SetCancelTokenSource(CancellationTokenSource cancelTokenSource, string name = null) + { + Guard.NotNull(cancelTokenSource, nameof(cancelTokenSource)); - var key = BuildKey(name); + var key = BuildKey(name); - if (Exists(name)) - { - OnRemoveCancelTokenSource(key); - } + if (Exists(name)) + { + OnRemoveCancelTokenSource(key); + } - var policy = new CacheItemPolicy { Priority = CacheItemPriority.NotRemovable }; + var policy = new CacheItemPolicy { Priority = CacheItemPriority.NotRemovable }; - _cancelTokens.Set(key, cancelTokenSource, policy); - } + _cancelTokens.Set(key, cancelTokenSource, policy); + } - public bool Cancel(string name = null) - { - return OnCancel(BuildKey(name)); - } + public bool Cancel(string name = null) + { + return OnCancel(BuildKey(name)); + } - protected virtual bool OnCancel(string key, bool successive = false) - { - Guard.NotEmpty(key, nameof(key)); + protected virtual bool OnCancel(string key, bool successive = false) + { + Guard.NotEmpty(key, nameof(key)); - var cts = OnGetCancelTokenSource(key); + var cts = OnGetCancelTokenSource(key); - if (cts != null) - { - cts.Cancel(); - return true; - } + if (cts != null) + { + cts.Cancel(); + return true; + } - return false; - } + return false; + } - protected virtual AsyncStateInfo GetStateInfo(string name = null) - { - return _states.Get(BuildKey(name)) as AsyncStateInfo; - } + protected virtual AsyncStateInfo GetStateInfo(string name = null) + { + return _states.Get(BuildKey(name)) as AsyncStateInfo; + } - protected string BuildKey(string name) - { - return BuildKey(typeof(T), name); - } + protected string BuildKey(string name) + { + return BuildKey(typeof(T), name); + } - protected virtual string BuildKey(Type type, string name) - { - return "{0}{1}".FormatInvariant(type.FullName, name.HasValue() ? ":" + name : ""); - } - } + protected virtual string BuildKey(Type type, string name) + { + return "{0}{1}".FormatInvariant(type.FullName, name.HasValue() ? ":" + name : ""); + } + } } diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs index deed799cc7..e2882f9851 100644 --- a/src/Libraries/SmartStore.Core/BaseEntity.cs +++ b/src/Libraries/SmartStore.Core/BaseEntity.cs @@ -1,106 +1,104 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.Core.Objects; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.Serialization; namespace SmartStore.Core -{ +{ /// /// Base class for entities /// [DataContract] public abstract partial class BaseEntity : IEquatable - { - /// + { + /// /// Gets or sets the entity identifier /// - [DataMember] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [DataMember] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual string GetEntityName() - { - return GetUnproxiedType().Name; - } + { + return GetUnproxiedType().Name; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Type GetUnproxiedType() { - #region Old - //var t = GetType(); - //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity.")) - //{ - // // it's a proxied type - // t = t.BaseType; - //} - - //return t; - #endregion - - return ObjectContext.GetObjectType(GetType()); - } - - /// - /// Transient objects are not associated with an item already in storage. For instance, - /// a Product entity is transient if its Id is 0. - /// - public virtual bool IsTransientRecord() - { - return Id == 0; - } - - public override bool Equals(object obj) - { - return this.Equals(obj as BaseEntity); - } - - bool IEquatable.Equals(BaseEntity other) - { - return this.Equals(other); - } - - protected virtual bool Equals(BaseEntity other) - { - if (other == null) - return false; - - if (ReferenceEquals(this, other)) - return true; - - if (HasSameNonDefaultIds(other)) - { - var otherType = other.GetUnproxiedType(); - var thisType = GetUnproxiedType(); - return thisType.Equals(otherType); - } - - return false; - } - - [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] - public override int GetHashCode() + #region Old + //var t = GetType(); + //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity.")) + //{ + // // it's a proxied type + // t = t.BaseType; + //} + + //return t; + #endregion + + return ObjectContext.GetObjectType(GetType()); + } + + /// + /// Transient objects are not associated with an item already in storage. For instance, + /// a Product entity is transient if its Id is 0. + /// + public virtual bool IsTransientRecord() + { + return Id == 0; + } + + public override bool Equals(object obj) + { + return this.Equals(obj as BaseEntity); + } + + bool IEquatable.Equals(BaseEntity other) + { + return this.Equals(other); + } + + protected virtual bool Equals(BaseEntity other) { - if (IsTransientRecord()) - { - return base.GetHashCode(); - } - else - { - unchecked - { - // It's possible for two objects to return the same hash code based on - // identically valued properties, even if they're of two different types, - // so we include the object's type in the hash calculation - var hashCode = GetUnproxiedType().GetHashCode(); - return (hashCode * 31) ^ Id.GetHashCode(); - } - } + if (other == null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (HasSameNonDefaultIds(other)) + { + var otherType = other.GetUnproxiedType(); + var thisType = GetUnproxiedType(); + return thisType.Equals(otherType); + } + + return false; + } + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + if (IsTransientRecord()) + { + return base.GetHashCode(); + } + else + { + unchecked + { + // It's possible for two objects to return the same hash code based on + // identically valued properties, even if they're of two different types, + // so we include the object's type in the hash calculation + var hashCode = GetUnproxiedType().GetHashCode(); + return (hashCode * 31) ^ Id.GetHashCode(); + } + } } public static bool operator ==(BaseEntity x, BaseEntity y) @@ -113,9 +111,9 @@ public override int GetHashCode() return !(x == y); } - private bool HasSameNonDefaultIds(BaseEntity other) - { - return !this.IsTransientRecord() && !other.IsTransientRecord() && this.Id == other.Id; - } - } + private bool HasSameNonDefaultIds(BaseEntity other) + { + return !this.IsTransientRecord() && !other.IsTransientRecord() && this.Id == other.Id; + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs index b705ab10b6..b983a2a6cf 100644 --- a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs +++ b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs @@ -1,41 +1,34 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace SmartStore.Core.Caching { - public class CacheScope - { - private HashSet _dependencies; + public class CacheScope + { + private HashSet _dependencies; - public CacheScope(string key) - { - Key = key; - } + public CacheScope(string key) + { + Key = key; + } - public string Key { get; private set; } + public string Key { get; private set; } - public void AddDependency(string key) - { - if (key == null) - { - return; - } + public void AddDependency(string key) + { + if (key == null) + { + return; + } - if (_dependencies == null) - { - _dependencies = new HashSet(); - } + if (_dependencies == null) + { + _dependencies = new HashSet(); + } - _dependencies.Add(key); - } + _dependencies.Add(key); + } - public IEnumerable Dependencies - { - get - { - return _dependencies ?? Enumerable.Empty(); - } - } - } + public IEnumerable Dependencies => _dependencies ?? Enumerable.Empty(); + } } diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs index b04e0a7d45..b8e23759a9 100644 --- a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs @@ -5,58 +5,58 @@ namespace SmartStore.Core.Caching { - public class CacheScopeAccessor : ICacheScopeAccessor - { - private readonly Stack _stack = new Stack(); - - public CacheScopeAccessor() - { - } - - public CacheScope Current - { - get - { - _stack.TryPeek(out var result); - return result; - } - } - - public bool HasScope(string key) - { - return _stack.Any(x => x.Key.IsCaseInsensitiveEqual(key)); - } - - public void PropagateKey(string key) - { - var current = Current; - if (current != null) - { - current.AddDependency(key); - } - } - - public IDisposable BeginScope(string key, bool independent = false) - { - if (Current?.Key == key) - { - // Void if parent scope has the same key. - return ActionDisposable.Empty; - } - - _stack.Push(new CacheScope(key)); - - var action = new ActionDisposable(() => - { - _stack.TryPop(out _); - - if (!independent) - { - PropagateKey(key); - } - }); - - return action; - } - } + public class CacheScopeAccessor : ICacheScopeAccessor + { + private readonly Stack _stack = new Stack(); + + public CacheScopeAccessor() + { + } + + public CacheScope Current + { + get + { + _stack.TryPeek(out var result); + return result; + } + } + + public bool HasScope(string key) + { + return _stack.Any(x => x.Key.IsCaseInsensitiveEqual(key)); + } + + public void PropagateKey(string key) + { + var current = Current; + if (current != null) + { + current.AddDependency(key); + } + } + + public IDisposable BeginScope(string key, bool independent = false) + { + if (Current?.Key == key) + { + // Void if parent scope has the same key. + return ActionDisposable.Empty; + } + + _stack.Push(new CacheScope(key)); + + var action = new ActionDisposable(() => + { + _stack.TryPop(out _); + + if (!independent) + { + PropagateKey(key); + } + }); + + return action; + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs index 3491021f32..43931b9edd 100644 --- a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs @@ -9,60 +9,60 @@ namespace SmartStore.Core.Caching /// public interface ICacheManager { - /// - /// Gets a cache item associated with the specified key - /// - /// The type of the item to get - /// The cache item key - /// When true, no attemp will be made to invalidate depending/parent cache entries. - /// Cached item value or null if item with specified key does not exist in the cache - T Get(string key, bool independent = false); + /// + /// Gets a cache item associated with the specified key + /// + /// The type of the item to get + /// The cache item key + /// When true, no attemp will be made to invalidate depending/parent cache entries. + /// Cached item value or null if item with specified key does not exist in the cache + T Get(string key, bool independent = false); - /// - /// Gets a cache item associated with the specified key or adds the item - /// if it doesn't exist in the cache. - /// - /// The type of the item to get or add - /// The cache item key - /// Func which returns value to be added to the cache - /// Absolute expiration time - /// When true, no attemp will be made to invalidate depending/parent cache entries. - /// When false, an exception will be thrown when the acquirer tries to acces the same cache item. - /// Cached item value - T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false); + /// + /// Gets a cache item associated with the specified key or adds the item + /// if it doesn't exist in the cache. + /// + /// The type of the item to get or add + /// The cache item key + /// Func which returns value to be added to the cache + /// Absolute expiration time + /// When true, no attemp will be made to invalidate depending/parent cache entries. + /// When false, an exception will be thrown when the acquirer tries to acces the same cache item. + /// Cached item value + T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false); - /// - /// Gets a cache item associated with the specified key or - if it doesn't exist in the cache - - /// adds the item obtained from the asynchronous acquirer . - /// - /// The type of the item to get or add - /// The cache item key - /// Func which returns value to be added to the cache - /// Absolute expiration time - /// When true, no attemp will be made to invalidate depending/parent cache entries. - /// When false, an exception will be thrown when the acquirer tries to acces the same cache item. - /// Cached item value - Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false); + /// + /// Gets a cache item associated with the specified key or - if it doesn't exist in the cache - + /// adds the item obtained from the asynchronous acquirer . + /// + /// The type of the item to get or add + /// The cache item key + /// Func which returns value to be added to the cache + /// Absolute expiration time + /// When true, no attemp will be made to invalidate depending/parent cache entries. + /// When false, an exception will be thrown when the acquirer tries to acces the same cache item. + /// Cached item value + Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false); - /// - /// Gets a hashset associated with the specified key or. If key does not exist, - /// a new set is created and put to cache automatically - /// - /// The set cache item key - /// Optional acquirer callback that is invoked when requested set does not exist yet. - /// Cached item value - ISet GetHashSet(string key, Func> acquirer = null); + /// + /// Gets a hashset associated with the specified key or. If key does not exist, + /// a new set is created and put to cache automatically + /// + /// The set cache item key + /// Optional acquirer callback that is invoked when requested set does not exist yet. + /// Cached item value + ISet GetHashSet(string key, Func> acquirer = null); - /// - /// Adds a cache item with the specified key - /// - /// Key - /// Value - /// Absolute expiration time - /// - /// A list of keys for cache entries that should be removed from the cache whenever this entry changes. - /// - void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null); + /// + /// Adds a cache item with the specified key + /// + /// Key + /// Value + /// Absolute expiration time + /// + /// A list of keys for cache entries that should be removed from the cache whenever this entry changes. + /// + void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null); /// /// Gets a value indicating whether the value associated with the specified key is cached @@ -77,44 +77,44 @@ public interface ICacheManager /// /key void Remove(string key); - /// - /// Scans for all keys matching the input pattern - /// - /// A key pattern. Can be null. - /// An array of matching key names - /// - /// Supported glob-style patterns: - /// - h?llo matches hello, hallo and hxllo - /// - h*llo matches hllo and heeeello - /// - h[ae]llo matches hello and hallo, but not hillo - /// - h[^e]llo matches hallo, hbllo, ... but not hello - /// - h[a-b]llo matches hallo and hbllo - /// - IEnumerable Keys(string pattern); + /// + /// Scans for all keys matching the input pattern + /// + /// A key pattern. Can be null. + /// An array of matching key names + /// + /// Supported glob-style patterns: + /// - h?llo matches hello, hallo and hxllo + /// - h*llo matches hllo and heeeello + /// - h[ae]llo matches hello and hallo, but not hillo + /// - h[^e]llo matches hallo, hbllo, ... but not hello + /// - h[a-b]llo matches hallo and hbllo + /// + IEnumerable Keys(string pattern); - /// - /// Removes all entries with keys matching the input pattern - /// - /// Glob pattern - /// Count of removed cache items - /// - /// Supported glob-style patterns: - /// - h?llo matches hello, hallo and hxllo - /// - h*llo matches hllo and heeeello - /// - h[ae]llo matches hello and hallo, but not hillo - /// - h[^e]llo matches hallo, hbllo, ... but not hello - /// - h[a-b]llo matches hallo and hbllo - /// - int RemoveByPattern(string pattern); + /// + /// Removes all entries with keys matching the input pattern + /// + /// Glob pattern + /// Count of removed cache items + /// + /// Supported glob-style patterns: + /// - h?llo matches hello, hallo and hxllo + /// - h*llo matches hllo and heeeello + /// - h[ae]llo matches hello and hallo, but not hillo + /// - h[^e]llo matches hallo, hbllo, ... but not hello + /// - h[a-b]llo matches hallo and hbllo + /// + int RemoveByPattern(string pattern); /// /// Clear all cache data /// void Clear(); - /// - /// Gets a value indicating whether the cache is distributed (e.g. Redis) - /// - bool IsDistributedCache { get; } + /// + /// Gets a value indicating whether the cache is distributed (e.g. Redis) + /// + bool IsDistributedCache { get; } } } diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs index a640e6b200..20d219e631 100644 --- a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs @@ -2,33 +2,33 @@ namespace SmartStore.Core.Caching { - public interface ICacheScopeAccessor - { - /// - /// Returns the current cache acquire scope (for current thread) - /// - CacheScope Current { get; } + public interface ICacheScopeAccessor + { + /// + /// Returns the current cache acquire scope (for current thread) + /// + CacheScope Current { get; } - /// - /// Whether any scope with the passed exists already. Used to prevent lock recursions. - /// - /// Key - bool HasScope(string key); + /// + /// Whether any scope with the passed exists already. Used to prevent lock recursions. + /// + /// Key + bool HasScope(string key); - /// - /// Notifies the current scope about a cache dependency. - /// - /// - void PropagateKey(string key); + /// + /// Notifies the current scope about a cache dependency. + /// + /// + void PropagateKey(string key); - /// - /// Begins a new cache acquire scope. - /// - /// Key - /// - /// true means that no key propagation will be performed, making the current cache entry independent from its parent. - /// Meaning: parent cache entries will not be auto-invalidated when this entry changes. - /// - IDisposable BeginScope(string key, bool independent = false); - } + /// + /// Begins a new cache acquire scope. + /// + /// Key + /// + /// true means that no key propagation will be performed, making the current cache entry independent from its parent. + /// Meaning: parent cache entries will not be auto-invalidated when this entry changes. + /// + IDisposable BeginScope(string key, bool independent = false); + } } diff --git a/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs b/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs index 2ecd14f0b8..e37a607bff 100644 --- a/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace SmartStore.Core.Caching { @@ -7,31 +6,31 @@ namespace SmartStore.Core.Caching /// Request cache interface /// public interface IRequestCache - { - /// - /// Gets a cache item associated with the specified key - /// - /// The type of the item to get - /// The cache item key - /// Cached item value or null if item with specified key does not exist in the cache - T Get(string key); - - /// - /// Gets a cache item associated with the specified key or adds the item - /// if it doesn't exist in the cache. - /// - /// The type of the item to get or add - /// The cache item key - /// Func which returns value to be added to the cache - /// Cached item value - T Get(string key, Func acquirer); - - /// - /// Adds a cache item with the specified key - /// - /// Key - /// Value - void Put(string key, object value); + { + /// + /// Gets a cache item associated with the specified key + /// + /// The type of the item to get + /// The cache item key + /// Cached item value or null if item with specified key does not exist in the cache + T Get(string key); + + /// + /// Gets a cache item associated with the specified key or adds the item + /// if it doesn't exist in the cache. + /// + /// The type of the item to get or add + /// The cache item key + /// Func which returns value to be added to the cache + /// Cached item value + T Get(string key, Func acquirer); + + /// + /// Adds a cache item with the specified key + /// + /// Key + /// Value + void Put(string key, object value); /// /// Gets a value indicating whether the value associated with the specified key is cached @@ -46,63 +45,60 @@ public interface IRequestCache /// /key void Remove(string key); - /// - /// Removes items by pattern - /// - /// pattern - void RemoveByPattern(string pattern); - - /// - /// Clear all cache data - /// - void Clear(); - } - - /// - /// For testing purposes - /// - public class NullRequestCache : IRequestCache - { - private static readonly IRequestCache s_instance = new NullRequestCache(); - - public static IRequestCache Instance - { - get { return s_instance; } - } - - public void Clear() - { - } - - public bool Contains(string key) - { - return false; - } - - public T Get(string key) - { - return default(T); - } - - public T Get(string key, Func acquirer) - { - if (acquirer == null) - { - return default(T); - } - return acquirer(); - } - - public void Remove(string key) - { - } - - public void RemoveByPattern(string pattern) - { - } - - public void Put(string key, object value) - { - } - } + /// + /// Removes items by pattern + /// + /// pattern + void RemoveByPattern(string pattern); + + /// + /// Clear all cache data + /// + void Clear(); + } + + /// + /// For testing purposes + /// + public class NullRequestCache : IRequestCache + { + private static readonly IRequestCache s_instance = new NullRequestCache(); + + public static IRequestCache Instance => s_instance; + + public void Clear() + { + } + + public bool Contains(string key) + { + return false; + } + + public T Get(string key) + { + return default(T); + } + + public T Get(string key, Func acquirer) + { + if (acquirer == null) + { + return default(T); + } + return acquirer(); + } + + public void Remove(string key) + { + } + + public void RemoveByPattern(string pattern) + { + } + + public void Put(string key, object value) + { + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/ISet.cs b/src/Libraries/SmartStore.Core/Caching/ISet.cs index d0bff72bd7..3c2c7ff2b1 100644 --- a/src/Libraries/SmartStore.Core/Caching/ISet.cs +++ b/src/Libraries/SmartStore.Core/Caching/ISet.cs @@ -1,21 +1,20 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Core.Caching { - public interface ISet : IEnumerable - { - bool Add(string item); - void AddRange(IEnumerable items); - void Clear(); - bool Contains(string item); - bool Remove(string item); - bool Move(string destinationKey, string item); + public interface ISet : IEnumerable + { + bool Add(string item); + void AddRange(IEnumerable items); + void Clear(); + bool Contains(string item); + bool Remove(string item); + bool Move(string destinationKey, string item); - long UnionWith(params string[] keys); - long IntersectWith(params string[] keys); - long ExceptWith(params string[] keys); + long UnionWith(params string[] keys); + long IntersectWith(params string[] keys); + long ExceptWith(params string[] keys); - int Count { get; } - } + int Count { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs index 78087b3f38..332b199c61 100644 --- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs @@ -1,259 +1,254 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; -using System.Threading.Tasks; -using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using System.Threading; -using System.Collections.Generic; +using System.Threading.Tasks; +using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.Utilities; -using System.Text.RegularExpressions; -using System.Diagnostics; using SmartStore.Utilities.Threading; -using SmartStore.Core.Infrastructure.DependencyManagement; -using System.Runtime.CompilerServices; namespace SmartStore.Core.Caching { public partial class MemoryCacheManager : DisposableObject, ICacheManager { - const string LockRecursionExceptionMessage = "Acquiring identical cache items recursively is not supported. Key: {0}"; - - // Wwe put a special string into cache if value is null, - // otherwise our 'Contains()' would always return false, - // which is bad if we intentionally wanted to save NULL values. - public const string FakeNull = "__[NULL]__"; - - private readonly Work _scopeAccessor; - private MemoryCache _cache; - private readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary(); - - public MemoryCacheManager(Work scopeAccessor) - { - _scopeAccessor = scopeAccessor; - _cache = CreateCache(); - } - - private MemoryCache CreateCache() - { - return new MemoryCache("SmartStore"); - } - - public bool IsDistributedCache - { - get { return false; } - } - - private bool TryGet(string key, bool independent, out T value) - { - value = default(T); - - object obj = _cache.Get(key); - - if (obj != null) - { - // Make the parent scope's entry depend on this - if (!independent) - { - _scopeAccessor.Value.PropagateKey(key); - } - - if (obj.Equals(FakeNull)) - { - return true; - } - - value = (T)obj; - return true; - } - - return false; - } + const string LockRecursionExceptionMessage = "Acquiring identical cache items recursively is not supported. Key: {0}"; + + // We put a special string into cache if value is null, + // otherwise our 'Contains()' would always return false, + // which is bad if we intentionally wanted to save NULL values. + public const string FakeNull = "__[NULL]__"; + + private readonly Work _scopeAccessor; + private MemoryCache _cache; + + public MemoryCacheManager(Work scopeAccessor) + { + _scopeAccessor = scopeAccessor; + _cache = CreateCache(); + } + + private MemoryCache CreateCache() + { + return new MemoryCache("SmartStore"); + } + + public bool IsDistributedCache => false; + + private bool TryGet(string key, bool independent, out T value) + { + value = default(T); + + object obj = _cache.Get(key); + + if (obj != null) + { + // Make the parent scope's entry depend on this + if (!independent) + { + _scopeAccessor.Value.PropagateKey(key); + } + + if (obj.Equals(FakeNull)) + { + return true; + } + + value = (T)obj; + return true; + } + + return false; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Get(string key, bool independent = false) - { - TryGet(key, independent, out T value); - return value; - } + { + TryGet(key, independent, out T value); + return value; + } public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) { - if (TryGet(key, independent, out T value)) - { - return value; - } - - if (!allowRecursion && _scopeAccessor.Value.HasScope(key)) - { - throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key)); - } - - // Get the (semaphore) locker specific to this key - using (KeyedLock.Lock("cache:" + key, TimeSpan.FromSeconds(5))) - { - // Atomic operation must be outer locked - if (!TryGet(key, independent, out value)) - { - var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty; - using (scope) - { - value = acquirer(); - var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null; - Put(key, value, duration, dependencies); - return value; - } - } - } - - return value; - } - - public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) - { - if (TryGet(key, independent, out T value)) - { - return value; - } - - if (!allowRecursion && _scopeAccessor.Value.HasScope(key)) - { - throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key)); - } - - // Get the async (semaphore) locker specific to this key - using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1))) - { - if (!TryGet(key, independent, out value)) - { - var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty; - using (scope) - { - value = await acquirer(); - var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null; - Put(key, value, duration, dependencies); - return value; - } - } - } - - return value; - } - - public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null) - { - _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies)); - } + if (TryGet(key, independent, out T value)) + { + return value; + } + + if (!allowRecursion && _scopeAccessor.Value.HasScope(key)) + { + throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key)); + } + + // Get the (semaphore) locker specific to this key + using (KeyedLock.Lock("cache:" + key, TimeSpan.FromSeconds(5))) + { + // Atomic operation must be outer locked + if (!TryGet(key, independent, out value)) + { + var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty; + using (scope) + { + value = acquirer(); + var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null; + Put(key, value, duration, dependencies); + return value; + } + } + } + + return value; + } + + public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) + { + if (TryGet(key, independent, out T value)) + { + return value; + } + + if (!allowRecursion && _scopeAccessor.Value.HasScope(key)) + { + throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key)); + } + + // Get the async (semaphore) locker specific to this key + using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1))) + { + if (!TryGet(key, independent, out value)) + { + var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty; + using (scope) + { + value = await acquirer(); + var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null; + Put(key, value, duration, dependencies); + return value; + } + } + } + + return value; + } + + public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null) + { + _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies)); + } public bool Contains(string key) { - return _cache.Contains(key); + return _cache.Contains(key); } public void Remove(string key) { - _cache.Remove(key); + _cache.Remove(key); } - public IEnumerable Keys(string pattern) - { - Guard.NotEmpty(pattern, nameof(pattern)); + public IEnumerable Keys(string pattern) + { + Guard.NotEmpty(pattern, nameof(pattern)); - var keys = _cache.AsParallel().Select(x => x.Key); + var keys = _cache.AsParallel().Select(x => x.Key); - if (pattern.IsEmpty() || pattern == "*") - { - return keys.ToArray(); - } + if (pattern.IsEmpty() || pattern == "*") + { + return keys.ToArray(); + } - var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); - return keys.Where(x => wildcard.IsMatch(x)).ToArray(); - } + var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); + return keys.Where(x => wildcard.IsMatch(x)).ToArray(); + } - public int RemoveByPattern(string pattern) + public int RemoveByPattern(string pattern) { - lock (_cache) - { - var keysToRemove = Keys(pattern); - int count = 0; - - // lock atomic operation - foreach (string key in keysToRemove) - { - _cache.Remove(key); - count++; - } - - return count; - } - } + lock (_cache) + { + var keysToRemove = Keys(pattern); + int count = 0; + + // lock atomic operation + foreach (string key in keysToRemove) + { + _cache.Remove(key); + count++; + } + + return count; + } + } public void Clear() { - // Faster way of clearing cache: https://stackoverflow.com/questions/8043381/how-do-i-clear-a-system-runtime-caching-memorycache - var oldCache = Interlocked.Exchange(ref _cache, CreateCache()); - oldCache.Dispose(); - GC.Collect(); - } - - public virtual ISet GetHashSet(string key, Func> acquirer = null) - { - var result = Get(key, () => - { - var set = new MemorySet(this); - var items = acquirer?.Invoke(); - if (items != null) - { - set.AddRange(items); - } - - return set; - }); - - return result; - } - - private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration, IEnumerable dependencies) - { - var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration; - - if (duration.HasValue) - { - absoluteExpiration = DateTime.UtcNow + duration.Value; - } - - var cacheItemPolicy = new CacheItemPolicy - { - AbsoluteExpiration = absoluteExpiration, - SlidingExpiration = ObjectCache.NoSlidingExpiration - }; - - if (dependencies != null && dependencies.Any()) - { - // INFO: we can only depend on existing items, otherwise this entry will be removed immediately. - dependencies = dependencies.Where(x => x != null && _cache.Contains(x)); - if (dependencies.Any()) - { - cacheItemPolicy.ChangeMonitors.Add(_cache.CreateCacheEntryChangeMonitor(dependencies)); - } - } - - //cacheItemPolicy.RemovedCallback = OnRemoveEntry; - - return cacheItemPolicy; - } - - //private void OnRemoveEntry(CacheEntryRemovedArguments args) - //{ - // if (args.RemovedReason == CacheEntryRemovedReason.ChangeMonitorChanged) - // { - // Debug.WriteLine("MEMCACHE: remove depending entry '{0}'.".FormatInvariant(args.CacheItem.Key)); - // } - //} - - protected override void OnDispose(bool disposing) - { - if (disposing) - _cache.Dispose(); - } - } + // Faster way of clearing cache: https://stackoverflow.com/questions/8043381/how-do-i-clear-a-system-runtime-caching-memorycache + var oldCache = Interlocked.Exchange(ref _cache, CreateCache()); + oldCache.Dispose(); + GC.Collect(); + } + + public virtual ISet GetHashSet(string key, Func> acquirer = null) + { + var result = Get(key, () => + { + var set = new MemorySet(this); + var items = acquirer?.Invoke(); + if (items != null) + { + set.AddRange(items); + } + + return set; + }); + + return result; + } + + private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration, IEnumerable dependencies) + { + var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration; + + if (duration.HasValue) + { + absoluteExpiration = DateTime.UtcNow + duration.Value; + } + + var cacheItemPolicy = new CacheItemPolicy + { + AbsoluteExpiration = absoluteExpiration, + SlidingExpiration = ObjectCache.NoSlidingExpiration + }; + + if (dependencies != null && dependencies.Any()) + { + // INFO: we can only depend on existing items, otherwise this entry will be removed immediately. + dependencies = dependencies.Where(x => x != null && _cache.Contains(x)); + if (dependencies.Any()) + { + cacheItemPolicy.ChangeMonitors.Add(_cache.CreateCacheEntryChangeMonitor(dependencies)); + } + } + + //cacheItemPolicy.RemovedCallback = OnRemoveEntry; + + return cacheItemPolicy; + } + + //private void OnRemoveEntry(CacheEntryRemovedArguments args) + //{ + // if (args.RemovedReason == CacheEntryRemovedReason.ChangeMonitorChanged) + // { + // Debug.WriteLine("MEMCACHE: remove depending entry '{0}'.".FormatInvariant(args.CacheItem.Key)); + // } + //} + + protected override void OnDispose(bool disposing) + { + if (disposing) + _cache.Dispose(); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs index d8aa392712..8c1e13245b 100644 --- a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs +++ b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs @@ -1,65 +1,63 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Core.Caching { - [Serializable] - internal class MemorySet : HashSet, ISet - { - private readonly ICacheManager _cache; + [Serializable] + internal class MemorySet : HashSet, ISet + { + private readonly ICacheManager _cache; - public MemorySet(ICacheManager cache) + public MemorySet(ICacheManager cache) : base(StringComparer.OrdinalIgnoreCase) - { - _cache = cache; - } - - public void AddRange(IEnumerable items) - { - base.UnionWith(items); - } - - public bool Move(string destinationKey, string item) - { - if (this.Contains(item)) - { - var target = _cache.GetHashSet(destinationKey); - return target.Add(item); - } - - return false; - } - - public long ExceptWith(params string[] keys) - { - return Combine(x => this.Except(x), keys); - } - - public long IntersectWith(params string[] keys) - { - return Combine(x => this.Intersect(x), keys); - } - - public long UnionWith(params string[] keys) - { - return Combine(x => this.Union(x), keys); - } - - private long Combine(Func, IEnumerable> func, params string[] keys) - { - if (keys.Length == 0) - return 0; - - var other = keys.SelectMany(x => _cache.GetHashSet(x)).Distinct(); - var result = func(other); - - this.Clear(); - this.AddRange(other); - - return this.Count; - } - } + { + _cache = cache; + } + + public void AddRange(IEnumerable items) + { + base.UnionWith(items); + } + + public bool Move(string destinationKey, string item) + { + if (this.Contains(item)) + { + var target = _cache.GetHashSet(destinationKey); + return target.Add(item); + } + + return false; + } + + public long ExceptWith(params string[] keys) + { + return Combine(x => this.Except(x), keys); + } + + public long IntersectWith(params string[] keys) + { + return Combine(x => this.Intersect(x), keys); + } + + public long UnionWith(params string[] keys) + { + return Combine(x => this.Union(x), keys); + } + + private long Combine(Func, IEnumerable> func, params string[] keys) + { + if (keys.Length == 0) + return 0; + + var other = keys.SelectMany(x => _cache.GetHashSet(x)).Distinct(); + var result = func(other); + + this.Clear(); + this.AddRange(other); + + return this.Count; + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs index 548bdfa18c..8910d0b004 100644 --- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs @@ -1,60 +1,49 @@ using System; -using SmartStore.Utilities; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace SmartStore.Core.Caching { - /// - /// Represents a null cache - /// public partial class NullCache : ICacheManager { - private static readonly ICacheManager s_instance = new NullCache(); + private static readonly ICacheManager s_instance = new NullCache(); - public static ICacheManager Instance - { - get { return s_instance; } - } + public static ICacheManager Instance => s_instance; - public bool IsDistributedCache - { - get { return false; } - } + public bool IsDistributedCache => false; - public T Get(string key, bool independent = false) - { - return default(T); - } + public T Get(string key, bool independent = false) + { + return default(T); + } - public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) - { - if (acquirer == null) - { - return default(T); - } - return acquirer(); - } + public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) + { + if (acquirer == null) + { + return default(T); + } + return acquirer(); + } - public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) - { - if (acquirer == null) - { - return Task.FromResult(default(T)); - } - return acquirer(); - } + public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false) + { + if (acquirer == null) + { + return Task.FromResult(default(T)); + } + return acquirer(); + } - public ISet GetHashSet(string key, Func> acquirer = null) - { - return new MemorySet(this); - } + public ISet GetHashSet(string key, Func> acquirer = null) + { + return new MemorySet(this); + } - public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null) - { - } + public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null) + { + } public bool Contains(string key) { @@ -65,18 +54,18 @@ public void Remove(string key) { } - public IEnumerable Keys(string pattern) - { - return new string[0]; - } + public IEnumerable Keys(string pattern) + { + return new string[0]; + } - public int RemoveByPattern(string pattern) + public int RemoveByPattern(string pattern) { - return 0; + return 0; } public void Clear() { } - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs index 28a05372d5..f43b485741 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs @@ -8,244 +8,249 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; 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 - { - private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary - { - [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(ProductMediaFile)] = (x, c) => new[] { "p" + ((ProductMediaFile)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 IComponentContext _componentContext; - - private bool _isIdle; - private bool? _isUncacheableRequest; - - 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) - { - 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; - return new ActionDisposable(() => _isIdle = false); - } - - public virtual void Announce(BaseEntity entity) - { - if (!_isIdle && entity != null) - { - _entities.Add(entity); - } - } - - public bool IsDisplayed(BaseEntity entity) - { - if (entity == null) - return false; - - return _entities.Contains(entity); - } - - public void MarkRequestAsUncacheable() - { - // First wins: subsequent calls should not be able to cancel this - if (!_isIdle) - _isUncacheableRequest = true; - } - - public bool IsUncacheableRequest - { - get - { - return _isUncacheableRequest.GetValueOrDefault() == true; - } - } - - public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity) - { - var empty = Enumerable.Empty(); - - if (entity == null || entity.IsTransientRecord()) - { - return empty; - } - - var type = entity.GetUnproxiedType(); - - if (!_handlers.TryGetValue(type, out var handler)) - { - return empty; - } - - return handler.Invoke(entity, _componentContext); - } - - public IEnumerable GetAllCacheControlTags() - { - var tags = _entities - .Where(x => x.Id > 0) - .SelectMany(x => GetCacheControlTagsFor(x)) - .Where(x => x != null) - .Distinct() - .ToArray(); - - return tags; - } - } + public delegate IEnumerable DisplayControlHandler(BaseEntity entity, IComponentContext ctx); + + public partial class DisplayControl : IDisplayControl + { + private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary + { + [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(ProductMediaFile)] = (x, c) => new[] { "p" + ((ProductMediaFile)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(MediaFile)] = (x, c) => new[] { "mf" + 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 IComponentContext _componentContext; + + private bool _isIdle; + private bool? _isUncacheableRequest; + + 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) + { + 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(BlogPost): + prefix = "b"; + break; + case nameof(Product): + prefix = "p"; + break; + case nameof(Category): + prefix = "c"; + break; + case nameof(Manufacturer): + prefix = "m"; + break; + case nameof(NewsItem): + prefix = "n"; + break; + case nameof(Topic): + prefix = "t"; + break; + case nameof(MediaFile): + prefix = "mf"; + 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; + return new ActionDisposable(() => _isIdle = false); + } + + public virtual void Announce(BaseEntity entity) + { + if (!_isIdle && entity != null) + { + _entities.Add(entity); + } + } + + public bool IsDisplayed(BaseEntity entity) + { + if (entity == null) + return false; + + return _entities.Contains(entity); + } + + public void MarkRequestAsUncacheable() + { + // First wins: subsequent calls should not be able to cancel this + if (!_isIdle) + _isUncacheableRequest = true; + } + + public bool IsUncacheableRequest => _isUncacheableRequest.GetValueOrDefault() == true; + + public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity) + { + var empty = Enumerable.Empty(); + + if (entity == null || entity.IsTransientRecord()) + { + return empty; + } + + var type = entity.GetUnproxiedType(); + + if (!_handlers.TryGetValue(type, out var handler)) + { + return empty; + } + + return handler.Invoke(entity, _componentContext); + } + + public IEnumerable GetAllCacheControlTags() + { + var tags = _entities + .Where(x => x.Id > 0) + .SelectMany(x => GetCacheControlTagsFor(x)) + .Where(x => x != null) + .Distinct() + .ToArray(); + + return tags; + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs index b30feeb1e4..4898095adb 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs @@ -1,21 +1,19 @@ -using System; - -namespace SmartStore.Core.Caching +namespace SmartStore.Core.Caching { - public interface ICacheableRouteRegistrar - { - void RegisterCacheableRoute(params string[] routes); - void RemoveCacheableRoute(params string[] routes); - } + public interface ICacheableRouteRegistrar + { + void RegisterCacheableRoute(params string[] routes); + void RemoveCacheableRoute(params string[] routes); + } - public class NullCacheableRouteRegistrar : ICacheableRouteRegistrar - { - public void RegisterCacheableRoute(params string[] routes) - { - } + public class NullCacheableRouteRegistrar : ICacheableRouteRegistrar + { + public void RegisterCacheableRoute(params string[] routes) + { + } - public void RemoveCacheableRoute(params string[] routes) - { - } - } + public void RemoveCacheableRoute(params string[] routes) + { + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs index 9646cf649e..f9549bc92c 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs @@ -1,37 +1,34 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Core.Caching { - public interface IDisplayControl - { - void Announce(BaseEntity entity); + public interface IDisplayControl + { + void Announce(BaseEntity entity); - bool IsDisplayed(BaseEntity entity); + bool IsDisplayed(BaseEntity entity); - void MarkRequestAsUncacheable(); + void MarkRequestAsUncacheable(); - bool IsUncacheableRequest { get; } + bool IsUncacheableRequest { get; } - IEnumerable GetCacheControlTagsFor(BaseEntity entity); + IEnumerable GetCacheControlTagsFor(BaseEntity entity); - IEnumerable GetAllCacheControlTags(); + IEnumerable GetAllCacheControlTags(); - IDisposable BeginIdleScope(); - } + IDisposable BeginIdleScope(); + } - public static class IDisplayControlExtensions - { - public static void AnnounceRange(this IDisplayControl displayedEntities, IEnumerable entities) - { - if (entities != null) - { - entities.Each(x => displayedEntities.Announce(x)); - } - } - } + public static class IDisplayControlExtensions + { + public static void AnnounceRange(this IDisplayControl displayedEntities, IEnumerable entities) + { + if (entities != null) + { + entities.Each(x => displayedEntities.Announce(x)); + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs index 234b67ced2..069a0bd62c 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs @@ -9,154 +9,151 @@ namespace SmartStore.Core.Caching { - public class ObserveEntityContext - { - public IOutputCacheProvider OutputCacheProvider { get; set; } - public IDisplayControl DisplayControl { get; set; } - public BaseEntity Entity { get; set; } - public IHookedEntity EntityEntry { get; set; } - public bool Handled { get; set; } - public ContainerManager ServiceContainer { get; set; } - } - - /// - /// Allows registration of output cache invalidation handlers - /// - public interface IOutputCacheInvalidationObserver - { - /// - /// Registers an entity observer. The passed observer is responsible for invalidating the output cache - /// by calling one of the invalidation methods in the instance. - /// The observer must then set the property to true - /// to signal the framework that it should skip executing subsequent observers. - /// - /// The observer action - /// - /// The implementation of this interface is singleton scoped. - /// Don't use objects with shorter lifetime in your handler as this will lead to memory leaks. - /// If your handler needs to call service methods, resolve required services - /// with . - /// - void ObserveEntity(Action observer); - - IEnumerable> GetEntityObservers(); - - /// - /// Registers a setting key to be observed by the framework. If the value for the passed - /// setting key changes, the framework calls the handler. - /// The key can either be fully qualified - e.g. "CatalogSettings.ShowProductSku" -, - /// or prefixed - e.g. "CatalogSettings.*". The latter calls the invalidator when ANY CatalogSetting changes. - /// - /// - /// The invalidation action handler. If null is passed, the framework - /// uses the default invalidator, which is . - /// - void ObserveSetting(string settingKey, Action invalidationAction); - - Action GetInvalidationActionForSetting(string settingKey); - } - - public class NullOutputCacheInvalidationObserver : IOutputCacheInvalidationObserver - { - private static readonly IOutputCacheInvalidationObserver _instance = new NullOutputCacheInvalidationObserver(); - - public static IOutputCacheInvalidationObserver Instance - { - get { return _instance; } - } - - public void ObserveEntity(Action observer) - { - } - - public IEnumerable> GetEntityObservers() - { - return Enumerable.Empty>(); - } - - public void ObserveSetting(string settingKey, Action invalidationAction) - { - } - - public Action GetInvalidationActionForSetting(string settingKey) - { - return null; - } - } - - public static class IOutputCacheInvalidationObserverExtensions - { - public static void ObserveSetting(this IOutputCacheInvalidationObserver observer, string settingKey) - { - observer.ObserveSetting(settingKey, null); - } - - /// - /// Registers a concrete setting class to be observed by the framework. If any setting property - /// of changes, the framework will purge the cache. - /// - /// The type of the concrete setting class to observe - /// - /// A property observer precedes a class observer. - /// - public static void ObserveSettings(this IOutputCacheInvalidationObserver observer) - where TSetting : ISettings - { - ObserveSettings(observer, null); - } - - /// - /// Registers a concrete setting class to be observed by the framework. If any setting property - /// of changes, the framework will call the handler. - /// - /// The type of the concrete setting class to observe - /// - /// The invalidation action handler. If null is passed, the framework - /// uses the default invalidator, which is . - /// - /// - /// A property observer precedes a class observer. - /// - public static void ObserveSettings( - this IOutputCacheInvalidationObserver observer, - Action invalidationAction) where TSetting : ISettings - { - var key = typeof(TSetting).Name + ".*"; - observer.ObserveSetting(key, invalidationAction); - } - - /// - /// Registers a setting property to be observed by the framework. If the value for the passed - /// property changes, the framework will purge the cache. - /// - /// The type of the concrete setting class which contains the property - /// The property lambda - public static void ObserveSettingProperty( - this IOutputCacheInvalidationObserver observer, - Expression> propertyAccessor) where TSetting : ISettings - { - ObserveSettingProperty(observer, propertyAccessor, null); - } - - /// - /// Registers a setting property to be observed by the framework. If the value for the passed - /// property changes, the framework will call the handler. - /// - /// The type of the concrete setting class which contains the property - /// The property lambda - /// - /// The invalidation action handler. If null is passed, the framework - /// uses the default invalidator, which is . - /// - public static void ObserveSettingProperty( - this IOutputCacheInvalidationObserver observer, - Expression> propertyAccessor, - Action invalidationAction) where TSetting : ISettings - { - Guard.NotNull(propertyAccessor, nameof(propertyAccessor)); - - var key = TypeHelper.NameOf(propertyAccessor, true); - observer.ObserveSetting(key, invalidationAction); - } - } + public class ObserveEntityContext + { + public IOutputCacheProvider OutputCacheProvider { get; set; } + public IDisplayControl DisplayControl { get; set; } + public BaseEntity Entity { get; set; } + public IHookedEntity EntityEntry { get; set; } + public bool Handled { get; set; } + public ContainerManager ServiceContainer { get; set; } + } + + /// + /// Allows registration of output cache invalidation handlers + /// + public interface IOutputCacheInvalidationObserver + { + /// + /// Registers an entity observer. The passed observer is responsible for invalidating the output cache + /// by calling one of the invalidation methods in the instance. + /// The observer must then set the property to true + /// to signal the framework that it should skip executing subsequent observers. + /// + /// The observer action + /// + /// The implementation of this interface is singleton scoped. + /// Don't use objects with shorter lifetime in your handler as this will lead to memory leaks. + /// If your handler needs to call service methods, resolve required services + /// with . + /// + void ObserveEntity(Action observer); + + IEnumerable> GetEntityObservers(); + + /// + /// Registers a setting key to be observed by the framework. If the value for the passed + /// setting key changes, the framework calls the handler. + /// The key can either be fully qualified - e.g. "CatalogSettings.ShowProductSku" -, + /// or prefixed - e.g. "CatalogSettings.*". The latter calls the invalidator when ANY CatalogSetting changes. + /// + /// + /// The invalidation action handler. If null is passed, the framework + /// uses the default invalidator, which is . + /// + void ObserveSetting(string settingKey, Action invalidationAction); + + Action GetInvalidationActionForSetting(string settingKey); + } + + public class NullOutputCacheInvalidationObserver : IOutputCacheInvalidationObserver + { + private static readonly IOutputCacheInvalidationObserver _instance = new NullOutputCacheInvalidationObserver(); + + public static IOutputCacheInvalidationObserver Instance => _instance; + + public void ObserveEntity(Action observer) + { + } + + public IEnumerable> GetEntityObservers() + { + return Enumerable.Empty>(); + } + + public void ObserveSetting(string settingKey, Action invalidationAction) + { + } + + public Action GetInvalidationActionForSetting(string settingKey) + { + return null; + } + } + + public static class IOutputCacheInvalidationObserverExtensions + { + public static void ObserveSetting(this IOutputCacheInvalidationObserver observer, string settingKey) + { + observer.ObserveSetting(settingKey, null); + } + + /// + /// Registers a concrete setting class to be observed by the framework. If any setting property + /// of changes, the framework will purge the cache. + /// + /// The type of the concrete setting class to observe + /// + /// A property observer precedes a class observer. + /// + public static void ObserveSettings(this IOutputCacheInvalidationObserver observer) + where TSetting : ISettings + { + ObserveSettings(observer, null); + } + + /// + /// Registers a concrete setting class to be observed by the framework. If any setting property + /// of changes, the framework will call the handler. + /// + /// The type of the concrete setting class to observe + /// + /// The invalidation action handler. If null is passed, the framework + /// uses the default invalidator, which is . + /// + /// + /// A property observer precedes a class observer. + /// + public static void ObserveSettings( + this IOutputCacheInvalidationObserver observer, + Action invalidationAction) where TSetting : ISettings + { + var key = typeof(TSetting).Name + ".*"; + observer.ObserveSetting(key, invalidationAction); + } + + /// + /// Registers a setting property to be observed by the framework. If the value for the passed + /// property changes, the framework will purge the cache. + /// + /// The type of the concrete setting class which contains the property + /// The property lambda + public static void ObserveSettingProperty( + this IOutputCacheInvalidationObserver observer, + Expression> propertyAccessor) where TSetting : ISettings + { + ObserveSettingProperty(observer, propertyAccessor, null); + } + + /// + /// Registers a setting property to be observed by the framework. If the value for the passed + /// property changes, the framework will call the handler. + /// + /// The type of the concrete setting class which contains the property + /// The property lambda + /// + /// The invalidation action handler. If null is passed, the framework + /// uses the default invalidator, which is . + /// + public static void ObserveSettingProperty( + this IOutputCacheInvalidationObserver observer, + Expression> propertyAccessor, + Action invalidationAction) where TSetting : ISettings + { + Guard.NotNull(propertyAccessor, nameof(propertyAccessor)); + + var key = TypeHelper.NameOf(propertyAccessor, true); + observer.ObserveSetting(key, invalidationAction); + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheProvider.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheProvider.cs index 3e2aead128..da9c985476 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheProvider.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheProvider.cs @@ -1,23 +1,21 @@ -using System; -using System.Collections.Generic; -using SmartStore.Core.Plugins; +using SmartStore.Core.Plugins; namespace SmartStore.Core.Caching { - public interface IOutputCacheProvider : IProvider - { - OutputCacheItem Get(string key); - void Set(string key, OutputCacheItem item); - bool Exists(string key); + public interface IOutputCacheProvider : IProvider + { + OutputCacheItem Get(string key); + void Set(string key, OutputCacheItem item); + bool Exists(string key); - void Remove(params string[] keys); - void RemoveAll(); + void Remove(params string[] keys); + void RemoveAll(); - IPagedList All(int pageIndex, int pageSize, bool withContent = false); - int Count(); + IPagedList All(int pageIndex, int pageSize, bool withContent = false); + int Count(); - int InvalidateByRoute(params string[] routes); - int InvalidateByPrefix(string keyPrefix); - int InvalidateByTag(params string[] tags); - } + int InvalidateByRoute(params string[] routes); + int InvalidateByPrefix(string keyPrefix); + int InvalidateByTag(params string[] tags); + } } diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/NullOutputCacheProvider.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/NullOutputCacheProvider.cs index f9d2ba828a..0e3d290ce7 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/NullOutputCacheProvider.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/NullOutputCacheProvider.cs @@ -1,65 +1,61 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Plugins; namespace SmartStore.Core.Caching { - [SystemName("OutputCacheProvider.Idle")] - [FriendlyName("Idle")] - public class NullOutputCacheProvider : IOutputCacheProvider - { - private static readonly IOutputCacheProvider s_instance = new NullOutputCacheProvider(); - - public static IOutputCacheProvider Instance - { - get { return s_instance; } - } - - public IPagedList All(int pageIndex, int pageSize, bool withContent = false) - { - return new PagedList(new List(), pageIndex, pageSize); - } - - public int Count() - { - return 0; - } - - public bool Exists(string key) - { - return false; - } - - public OutputCacheItem Get(string key) - { - return null; - } - - public int InvalidateByRoute(params string[] routes) - { - return 0; - } - - public int InvalidateByPrefix(string keyPrefix) - { - return 0; - } - - public int InvalidateByTag(params string[] tags) - { - return 0; - } - - public void Remove(params string[] keys) - { - } - - public void RemoveAll() - { - } - - public void Set(string key, OutputCacheItem item) - { - } - } + [SystemName("OutputCacheProvider.Idle")] + [FriendlyName("Idle")] + public class NullOutputCacheProvider : IOutputCacheProvider + { + private static readonly IOutputCacheProvider s_instance = new NullOutputCacheProvider(); + + public static IOutputCacheProvider Instance => s_instance; + + public IPagedList All(int pageIndex, int pageSize, bool withContent = false) + { + return new PagedList(new List(), pageIndex, pageSize); + } + + public int Count() + { + return 0; + } + + public bool Exists(string key) + { + return false; + } + + public OutputCacheItem Get(string key) + { + return null; + } + + public int InvalidateByRoute(params string[] routes) + { + return 0; + } + + public int InvalidateByPrefix(string keyPrefix) + { + return 0; + } + + public int InvalidateByTag(params string[] tags) + { + return 0; + } + + public void Remove(params string[] keys) + { + } + + public void RemoveAll() + { + } + + public void Set(string key, OutputCacheItem item) + { + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/OutputCacheItem.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/OutputCacheItem.cs index 6ad1ec3c65..ebb024c737 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/OutputCacheItem.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/OutputCacheItem.cs @@ -3,56 +3,53 @@ namespace SmartStore.Core.Caching { - [Serializable] - [DebuggerDisplay("{CacheKey}, Url: {Url}, Query: {QueryString}, Duration: {Duration}, Tags: {Tags}")] - public class OutputCacheItem : ICloneable - { - // used for serialization compatibility - public static readonly string Version = "1"; - - public string CacheKey { get; set; } - public string RouteKey { get; set; } - public DateTime CachedOnUtc { get; set; } - public string Url { get; set; } - public string QueryString { get; set; } - public int Duration { get; set; } - public string[] Tags { get; set; } - - public string Theme { get; set; } - public int StoreId { get; set; } - public int LanguageId { get; set; } - public int CurrencyId { get; set; } - public string CustomerRoles { get; set; } - - public string ContentType { get; set; } - public string Content { get; set; } - - public DateTime ExpiresOnUtc - { - get { return CachedOnUtc.AddSeconds(Duration); } - } - - public bool IsValid(DateTime utcNow) - { - return utcNow < ExpiresOnUtc; - } - - public string JoinTags() - { - if (Tags == null || Tags.Length == 0) - return ""; - - return String.Join(";", Tags); - } - - public OutputCacheItem Clone() - { - return (OutputCacheItem)this.MemberwiseClone(); - } - - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } - } + [Serializable] + [DebuggerDisplay("{CacheKey}, Url: {Url}, Query: {QueryString}, Duration: {Duration}, Tags: {Tags}")] + public class OutputCacheItem : ICloneable + { + // used for serialization compatibility + public static readonly string Version = "1"; + + public string CacheKey { get; set; } + public string RouteKey { get; set; } + public DateTime CachedOnUtc { get; set; } + public string Url { get; set; } + public string QueryString { get; set; } + public int Duration { get; set; } + public string[] Tags { get; set; } + + public string Theme { get; set; } + public int StoreId { get; set; } + public int LanguageId { get; set; } + public int CurrencyId { get; set; } + public string CustomerRoles { get; set; } + + public string ContentType { get; set; } + public string Content { get; set; } + + public DateTime ExpiresOnUtc => CachedOnUtc.AddSeconds(Duration); + + public bool IsValid(DateTime utcNow) + { + return utcNow < ExpiresOnUtc; + } + + public string JoinTags() + { + if (Tags == null || Tags.Length == 0) + return ""; + + return String.Join(";", Tags); + } + + public OutputCacheItem Clone() + { + return (OutputCacheItem)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs index 5fb74d2400..601afc2fef 100644 --- a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs @@ -8,127 +8,127 @@ namespace SmartStore.Core.Caching { - public class RequestCache : DisposableObject, IRequestCache - { - const string RegionName = "SmartStoreNET:"; - - private IDictionary _emptyDictionary = new Dictionary(); - - private readonly HttpContextBase _context; - - public RequestCache(HttpContextBase context) - { - _context = context; - } - - public T Get(string key) - { - return Get(key, null); - } - - public T Get(string key, Func acquirer) - { - var items = GetItems(); - - key = BuildKey(key); - - if (items.Contains(key)) - { - return (T)items[key]; - } - - if (acquirer != null) - { - var value = acquirer(); - items.Add(key, value); - return value; - } - - return default(T); - } - - public void Put(string key, object value) - { - var items = GetItems(); - - key = BuildKey(key); - - if (items.Contains(key)) - items[key] = value; - else - items.Add(key, value); - } - - public void Clear() - { - RemoveByPattern("*"); - } - - public bool Contains(string key) - { - return GetItems().Contains(BuildKey(key)); - } - - public void Remove(string key) - { - GetItems().Remove(BuildKey(key)); - } - - public void RemoveByPattern(string pattern) - { - var items = GetItems(); - - var keysToRemove = Keys(pattern).ToArray(); - - foreach (string key in keysToRemove) - { - items.Remove(BuildKey(key)); - } - } - - protected IDictionary GetItems() - { - return _context.Items ?? _emptyDictionary; - } - - public IEnumerable Keys(string pattern) - { - var items = GetItems(); - - if (items.Count == 0) - yield break; - - var prefixLen = RegionName.Length; - - pattern = pattern.NullEmpty() ?? "*"; - var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); - - var enumerator = items.GetEnumerator(); - while (enumerator.MoveNext()) - { - if (enumerator.Key is string key) - { - if (key.StartsWith(RegionName)) - { - key = key.Substring(prefixLen); - if (pattern == "*" || wildcard.IsMatch(key)) - { - yield return key; - } - } - } - } - } - - private string BuildKey(string key) - { - return RegionName + key.EmptyNull(); - } - - protected override void OnDispose(bool disposing) - { - if (disposing) - Clear(); - } - } + public class RequestCache : DisposableObject, IRequestCache + { + const string RegionName = "SmartStoreNET:"; + + private readonly IDictionary _emptyDictionary = new Dictionary(); + + private readonly HttpContextBase _context; + + public RequestCache(HttpContextBase context) + { + _context = context; + } + + public T Get(string key) + { + return Get(key, null); + } + + public T Get(string key, Func acquirer) + { + var items = GetItems(); + + key = BuildKey(key); + + if (items.Contains(key)) + { + return (T)items[key]; + } + + if (acquirer != null) + { + var value = acquirer(); + items.Add(key, value); + return value; + } + + return default(T); + } + + public void Put(string key, object value) + { + var items = GetItems(); + + key = BuildKey(key); + + if (items.Contains(key)) + items[key] = value; + else + items.Add(key, value); + } + + public void Clear() + { + RemoveByPattern("*"); + } + + public bool Contains(string key) + { + return GetItems().Contains(BuildKey(key)); + } + + public void Remove(string key) + { + GetItems().Remove(BuildKey(key)); + } + + public void RemoveByPattern(string pattern) + { + var items = GetItems(); + + var keysToRemove = Keys(pattern).ToArray(); + + foreach (string key in keysToRemove) + { + items.Remove(BuildKey(key)); + } + } + + protected IDictionary GetItems() + { + return _context.Items ?? _emptyDictionary; + } + + public IEnumerable Keys(string pattern) + { + var items = GetItems(); + + if (items.Count == 0) + yield break; + + var prefixLen = RegionName.Length; + + pattern = pattern.NullEmpty() ?? "*"; + var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); + + var enumerator = items.GetEnumerator(); + while (enumerator.MoveNext()) + { + if (enumerator.Key is string key) + { + if (key.StartsWith(RegionName)) + { + key = key.Substring(prefixLen); + if (pattern == "*" || wildcard.IsMatch(key)) + { + yield return key; + } + } + } + } + } + + private string BuildKey(string key) + { + return RegionName + key.EmptyNull(); + } + + protected override void OnDispose(bool disposing) + { + if (disposing) + Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs index 86d5b00249..ad8945ae12 100644 --- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs @@ -1,312 +1,292 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using SmartStore.Utilities; -using System.Threading; namespace SmartStore.Collections { - /// - /// A thread-safe data structure that contains multiple values for each key. - /// - /// The type of key. - /// The type of value. - [JsonConverter(typeof(ConcurrentMultiMapConverter))] - [Serializable] - public class ConcurrentMultimap : IEnumerable>> - { - private readonly ConcurrentDictionary> _dict; - private readonly Func, ICollection> _collectionCreator; - - public ConcurrentMultimap() - : this(null, null, null) - { - } - - public ConcurrentMultimap(IEqualityComparer comparer) - : this(null, comparer, null) - { - } - - public ConcurrentMultimap(IEnumerable>> items) - : this(items, null, null) - { - // for serialization - } - - public ConcurrentMultimap( - IEnumerable>> items, - IEqualityComparer comparer) - : this(items, comparer, null) - { - // for serialization - } - - public ConcurrentMultimap( - IEnumerable>> items, - IEqualityComparer comparer, - Func, ICollection> collectionCreator) - { - _collectionCreator = collectionCreator; - _dict = new ConcurrentDictionary>( - ConvertItems(items), - comparer ?? EqualityComparer.Default); - } - - private IEnumerable>> ConvertItems(IEnumerable>> items) - { - if (items == null) - yield break; - - foreach (var item in items) - { - yield return new KeyValuePair>(item.Key, CreateCollection(item.Value)); - } - } - - protected virtual SyncedCollection CreateCollection(IEnumerable values) - { - var creator = _collectionCreator ?? Multimap.DefaultCollectionCreator; - var col = creator(values ?? Enumerable.Empty()); - - return col.AsSynchronized(); - } - - /// - /// Gets the count of groups/keys. - /// - public int Count - { - get - { - return _dict.Keys.Count; - } - } - - /// - /// Gets the total count of items in all groups. - /// - public int TotalValueCount - { - get - { - return this._dict.Values.Sum(x => x.Count); - } - } - - /// - /// Gets the count of items in the requested group. - /// - public int ValueCount(TKey key) - { - if (_dict.TryGetValue(key, out var col)) - { - return col.Count; - } - - return 0; - } - - /// - /// Gets the collection of values stored under the specified key. - /// - /// The key. - public virtual SyncedCollection this[TKey key] - { - get - { - GetOrCreateValues(key, null, out var col); - return col; - } - } - - /// - /// Gets the collection of keys. - /// - public virtual ICollection Keys - { - get { return _dict.Keys; } - } - - /// - /// Gets all value collections. - /// - public virtual ICollection> Values - { - get { return _dict.Values; } - } - - /// - /// Attempts to add the specified value for the specified key. - /// - /// The key. - /// The value. - public virtual void TryAdd(TKey key, TValue value) - { - if (!GetOrCreateValues(key, new[] { value }, out var col)) - { - col.Add(value); - } - } - - /// - /// Attempts to add the specified values to the specified key. - /// - /// The key. - /// The values. - public virtual void TryAddRange(TKey key, IEnumerable values) - { - Guard.NotNull(values, nameof(values)); - - if (!GetOrCreateValues(key, values, out var col)) - { - col.AddRange(values); - } - } - - /// - /// Attempts to remove the specified value for the specified key. - /// - /// The key. - /// The value. - /// True if such a value existed and was removed; otherwise false. - public virtual bool TryRemove(TKey key, TValue value) - { - if (!_dict.ContainsKey(key)) - return false; - - var col = _dict[key]; - var removed = false; - - removed = col.Remove(value); - if (col.Count == 0) - { - _dict.TryRemove(key, out _); - } - - return removed; - } - - /// - /// Attempts to remove a range of values for the specified key. - /// - /// The key. - /// The values to remove from the group. - /// True if at least one item in group has been removed; otherwise false. - public virtual bool TryRemoveRange(TKey key, IEnumerable values) - { - Guard.NotNull(values, nameof(values)); - - int numRemoved = 0; - - if (_dict.TryGetValue(key, out var col)) - { - numRemoved = col.RemoveRange(values); - } - - return numRemoved > 0; - } - - /// - /// Attempts to remove and return all values for the specified key. - /// - /// The key. - /// True if any such values existed; otherwise false. - public virtual bool TryRemoveAll(TKey key, out SyncedCollection collection) - { - return _dict.TryRemove(key, out collection); - } - - /// - /// Removes all values. - /// - public virtual void Clear() - { - _dict.Clear(); - } - - /// - /// Determines whether the multimap contains any values for the specified key. - /// - /// The key. - /// True if the multimap has one or more values for the specified key, otherwise false. - public virtual bool ContainsKey(TKey key) - { - return _dict.ContainsKey(key); - } - - /// - /// Determines whether the multimap contains the specified value for the specified key. - /// - /// The key. - /// The value. - /// True if the multimap contains such a value; otherwise, false. - public virtual bool ContainsValue(TKey key, TValue value) - { - return _dict.ContainsKey(key) && _dict[key].Contains(value); - } - - /// - /// Returns an enumerator that iterates through the multimap. - /// - /// An object that can be used to iterate through the multimap. - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through the multimap. - /// - /// An object that can be used to iterate through the multimap. - public virtual IEnumerator>> GetEnumerator() - { - foreach (var pair in _dict) - yield return pair; - } - - private bool GetOrCreateValues(TKey key, IEnumerable initial, out SyncedCollection col) - { - // Return true when created - var created = false; - - col = _dict.GetOrAdd(key, k => - { - created = true; - return CreateCollection(initial); - }); - - return created; - } - - #region Static members - - public static ConcurrentMultimap CreateFromLookup(ILookup source) - { - Guard.NotNull(source, nameof(source)); - - var map = new ConcurrentMultimap(); - - foreach (IGrouping group in source) - { - map.TryAddRange(group.Key, group); - } - - return map; - } - - #endregion - } - - public class ConcurrentMultiMapConverter : MultiMapConverter - { - public override bool CanConvert(Type objectType) - { - var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ConcurrentMultimap<,>); - return canConvert; - } - } + /// + /// A thread-safe data structure that contains multiple values for each key. + /// + /// The type of key. + /// The type of value. + [JsonConverter(typeof(ConcurrentMultiMapConverter))] + [Serializable] + public class ConcurrentMultimap : IEnumerable>> + { + private readonly ConcurrentDictionary> _dict; + private readonly Func, ICollection> _collectionCreator; + + public ConcurrentMultimap() + : this(null, null, null) + { + } + + public ConcurrentMultimap(IEqualityComparer comparer) + : this(null, comparer, null) + { + } + + public ConcurrentMultimap(IEnumerable>> items) + : this(items, null, null) + { + // for serialization + } + + public ConcurrentMultimap( + IEnumerable>> items, + IEqualityComparer comparer) + : this(items, comparer, null) + { + // for serialization + } + + public ConcurrentMultimap( + IEnumerable>> items, + IEqualityComparer comparer, + Func, ICollection> collectionCreator) + { + _collectionCreator = collectionCreator; + _dict = new ConcurrentDictionary>( + ConvertItems(items), + comparer ?? EqualityComparer.Default); + } + + private IEnumerable>> ConvertItems(IEnumerable>> items) + { + if (items == null) + yield break; + + foreach (var item in items) + { + yield return new KeyValuePair>(item.Key, CreateCollection(item.Value)); + } + } + + protected virtual SyncedCollection CreateCollection(IEnumerable values) + { + var creator = _collectionCreator ?? Multimap.DefaultCollectionCreator; + var col = creator(values ?? Enumerable.Empty()); + + return col.AsSynchronized(); + } + + /// + /// Gets the count of groups/keys. + /// + public int Count => _dict.Keys.Count; + + /// + /// Gets the total count of items in all groups. + /// + public int TotalValueCount => this._dict.Values.Sum(x => x.Count); + + /// + /// Gets the count of items in the requested group. + /// + public int ValueCount(TKey key) + { + if (_dict.TryGetValue(key, out var col)) + { + return col.Count; + } + + return 0; + } + + /// + /// Gets the collection of values stored under the specified key. + /// + /// The key. + public virtual SyncedCollection this[TKey key] + { + get + { + GetOrCreateValues(key, null, out var col); + return col; + } + } + + /// + /// Gets the collection of keys. + /// + public virtual ICollection Keys => _dict.Keys; + + /// + /// Gets all value collections. + /// + public virtual ICollection> Values => _dict.Values; + + /// + /// Attempts to add the specified value for the specified key. + /// + /// The key. + /// The value. + public virtual void TryAdd(TKey key, TValue value) + { + if (!GetOrCreateValues(key, new[] { value }, out var col)) + { + col.Add(value); + } + } + + /// + /// Attempts to add the specified values to the specified key. + /// + /// The key. + /// The values. + public virtual void TryAddRange(TKey key, IEnumerable values) + { + Guard.NotNull(values, nameof(values)); + + if (!GetOrCreateValues(key, values, out var col)) + { + col.AddRange(values); + } + } + + /// + /// Attempts to remove the specified value for the specified key. + /// + /// The key. + /// The value. + /// True if such a value existed and was removed; otherwise false. + public virtual bool TryRemove(TKey key, TValue value) + { + if (!_dict.ContainsKey(key)) + return false; + + var col = _dict[key]; + var removed = false; + + removed = col.Remove(value); + if (col.Count == 0) + { + _dict.TryRemove(key, out _); + } + + return removed; + } + + /// + /// Attempts to remove a range of values for the specified key. + /// + /// The key. + /// The values to remove from the group. + /// True if at least one item in group has been removed; otherwise false. + public virtual bool TryRemoveRange(TKey key, IEnumerable values) + { + Guard.NotNull(values, nameof(values)); + + int numRemoved = 0; + + if (_dict.TryGetValue(key, out var col)) + { + numRemoved = col.RemoveRange(values); + } + + return numRemoved > 0; + } + + /// + /// Attempts to remove and return all values for the specified key. + /// + /// The key. + /// True if any such values existed; otherwise false. + public virtual bool TryRemoveAll(TKey key, out SyncedCollection collection) + { + return _dict.TryRemove(key, out collection); + } + + /// + /// Removes all values. + /// + public virtual void Clear() + { + _dict.Clear(); + } + + /// + /// Determines whether the multimap contains any values for the specified key. + /// + /// The key. + /// True if the multimap has one or more values for the specified key, otherwise false. + public virtual bool ContainsKey(TKey key) + { + return _dict.ContainsKey(key); + } + + /// + /// Determines whether the multimap contains the specified value for the specified key. + /// + /// The key. + /// The value. + /// True if the multimap contains such a value; otherwise, false. + public virtual bool ContainsValue(TKey key, TValue value) + { + return _dict.ContainsKey(key) && _dict[key].Contains(value); + } + + /// + /// Returns an enumerator that iterates through the multimap. + /// + /// An object that can be used to iterate through the multimap. + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the multimap. + /// + /// An object that can be used to iterate through the multimap. + public virtual IEnumerator>> GetEnumerator() + { + foreach (var pair in _dict) + yield return pair; + } + + private bool GetOrCreateValues(TKey key, IEnumerable initial, out SyncedCollection col) + { + // Return true when created + var created = false; + + col = _dict.GetOrAdd(key, k => + { + created = true; + return CreateCollection(initial); + }); + + return created; + } + + #region Static members + + public static ConcurrentMultimap CreateFromLookup(ILookup source) + { + Guard.NotNull(source, nameof(source)); + + var map = new ConcurrentMultimap(); + + foreach (IGrouping group in source) + { + map.TryAddRange(group.Key, group); + } + + return map; + } + + #endregion + } + + public class ConcurrentMultiMapConverter : MultiMapConverter + { + public override bool CanConvert(Type objectType) + { + var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ConcurrentMultimap<,>); + return canConvert; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs index a9014b0c80..fd11e92b90 100644 --- a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs @@ -6,131 +6,128 @@ namespace SmartStore.Collections { - /// - /// Manages data keys and offers a combination of eager and lazy data loading - /// - public class LazyMultimap : Multimap - { - private readonly Func> _load; - private readonly HashSet _loaded; // to avoid database round trips with empty results - private HashSet _collect; - private bool _fullyLoaded; - //private int _roundTripCount; - - /// - /// Constructor - /// - /// int[] keys like Entity.Id, Multimap{int, T}> delegate to load data - /// Keys of eager loaded data - public LazyMultimap(Func> load, IEnumerable collect = null) - { - _load = load; - _loaded = new HashSet(); - - _collect = collect == null ? new HashSet() : new HashSet(collect); - } - - public bool FullyLoaded - { - get { return _fullyLoaded; } - } - - /// - /// Get data. Load it if not already loaded yet. - /// - /// Data key - /// Collection of data - public virtual ICollection GetOrLoad(int key) - { - if (key == 0) - { - return new List(); - } - - if (!_loaded.Contains(key)) - { - Load(new int[] { key }); - } - - // better not override indexer cause of stack overflow risk - var result = base[key]; - - Debug.Assert(_loaded.Contains(key), "Possible missing multimap result for key {0} and type {1}.".FormatInvariant(key, typeof(T).Name), ""); - - return result; - } - - public void LoadAll() - { - Load(_collect); - _fullyLoaded = true; - } - - protected virtual void Load(IEnumerable keys) - { - if (keys == null) - return; - - var loadKeys = (_collect.Count == 0 ? keys : _collect.Concat(keys)) - .Distinct() - .Except(_loaded) - .ToArray(); - - _collect.Clear(); // invalidate, do not load again - - if (loadKeys.Any()) - { - //++_roundTripCount; - //Debug.WriteLine("Round trip {0} of {1}: {2}", _roundTripCount, typeof(T).Name, string.Join(",", loadKeys.OrderBy(x => x))); - - var items = _load(loadKeys); - - _loaded.AddRange(loadKeys); - - if (items != null) - { - foreach (var range in items) - { - base.AddRange(range.Key, range.Value); - } - } - } - } - - /// - /// Collect keys for combined loading - /// - /// Data keys - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - public virtual void Collect(IEnumerable keys) - { - if (keys != null && keys.Any()) - { - //_collect = _collect.Union(keys).ToList(); - _collect.UnionWith(keys); - } - } - - /// - /// Collect single key for combined loading - /// - /// Data key - public virtual void Collect(int key) - { - if (key != 0 && !_collect.Contains(key)) - { - _collect.Add(key); - } - } - - public override void Clear() - { - _loaded.Clear(); - _collect.Clear(); - _fullyLoaded = false; - //_roundTripCount = 0; - - base.Clear(); - } - } + /// + /// Manages data keys and offers a combination of eager and lazy data loading + /// + public class LazyMultimap : Multimap + { + private readonly Func> _load; + private readonly HashSet _loaded; // to avoid database round trips with empty results + private readonly HashSet _collect; + private bool _fullyLoaded; + //private int _roundTripCount; + + /// + /// Constructor + /// + /// int[] keys like Entity.Id, Multimap{int, T}> delegate to load data + /// Keys of eager loaded data + public LazyMultimap(Func> load, IEnumerable collect = null) + { + _load = load; + _loaded = new HashSet(); + + _collect = collect == null ? new HashSet() : new HashSet(collect); + } + + public bool FullyLoaded => _fullyLoaded; + + /// + /// Get data. Load it if not already loaded yet. + /// + /// Data key + /// Collection of data + public virtual ICollection GetOrLoad(int key) + { + if (key == 0) + { + return new List(); + } + + if (!_loaded.Contains(key)) + { + Load(new int[] { key }); + } + + // better not override indexer cause of stack overflow risk + var result = base[key]; + + Debug.Assert(_loaded.Contains(key), "Possible missing multimap result for key {0} and type {1}.".FormatInvariant(key, typeof(T).Name), ""); + + return result; + } + + public void LoadAll() + { + Load(_collect); + _fullyLoaded = true; + } + + protected virtual void Load(IEnumerable keys) + { + if (keys == null) + return; + + var loadKeys = (_collect.Count == 0 ? keys : _collect.Concat(keys)) + .Distinct() + .Except(_loaded) + .ToArray(); + + _collect.Clear(); // invalidate, do not load again + + if (loadKeys.Any()) + { + //++_roundTripCount; + //Debug.WriteLine("Round trip {0} of {1}: {2}", _roundTripCount, typeof(T).Name, string.Join(",", loadKeys.OrderBy(x => x))); + + var items = _load(loadKeys); + + _loaded.AddRange(loadKeys); + + if (items != null) + { + foreach (var range in items) + { + base.AddRange(range.Key, range.Value); + } + } + } + } + + /// + /// Collect keys for combined loading + /// + /// Data keys + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + public virtual void Collect(IEnumerable keys) + { + if (keys != null && keys.Any()) + { + //_collect = _collect.Union(keys).ToList(); + _collect.UnionWith(keys); + } + } + + /// + /// Collect single key for combined loading + /// + /// Data key + public virtual void Collect(int key) + { + if (key != 0 && !_collect.Contains(key)) + { + _collect.Add(key); + } + } + + public override void Clear() + { + _loaded.Clear(); + _collect.Clear(); + _fullyLoaded = false; + //_roundTripCount = 0; + + base.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs index 2f2aa798d3..91c2957375 100644 --- a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs +++ b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs @@ -2,62 +2,61 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Newtonsoft.Json; namespace SmartStore.Collections { - /// - /// A data structure that contains multiple values for each key. - /// - /// The type of key. - /// The type of value. - [JsonConverter(typeof(MultiMapConverter))] - [Serializable] - public class Multimap : IEnumerable>> + /// + /// A data structure that contains multiple values for each key. + /// + /// The type of key. + /// The type of value. + [JsonConverter(typeof(MultiMapConverter))] + [Serializable] + public class Multimap : IEnumerable>> { - private readonly IDictionary> _dict; - private readonly Func, ICollection> _collectionCreator; + private readonly IDictionary> _dict; + private readonly Func, ICollection> _collectionCreator; private readonly bool _isReadonly = false; - internal readonly static Func, ICollection> DefaultCollectionCreator = - x => new List(x ?? Enumerable.Empty()); + internal readonly static Func, ICollection> DefaultCollectionCreator = + x => new List(x ?? Enumerable.Empty()); - public Multimap() + public Multimap() : this(EqualityComparer.Default) { } - public Multimap(IEqualityComparer comparer) + public Multimap(IEqualityComparer comparer) { - _dict = new Dictionary>(comparer ?? EqualityComparer.Default); + _dict = new Dictionary>(comparer ?? EqualityComparer.Default); _collectionCreator = DefaultCollectionCreator; } - public Multimap(Func, ICollection> collectionCreator) - : this(new Dictionary>(), collectionCreator) + public Multimap(Func, ICollection> collectionCreator) + : this(new Dictionary>(), collectionCreator) { } - public Multimap(IEqualityComparer comparer, Func, ICollection> collectionCreator) - : this(new Dictionary>(comparer ?? EqualityComparer.Default), collectionCreator) - { - } + public Multimap(IEqualityComparer comparer, Func, ICollection> collectionCreator) + : this(new Dictionary>(comparer ?? EqualityComparer.Default), collectionCreator) + { + } - internal Multimap(IDictionary> dictionary, Func, ICollection> collectionCreator) + internal Multimap(IDictionary> dictionary, Func, ICollection> collectionCreator) { - Guard.NotNull(dictionary, nameof(dictionary)); - Guard.NotNull(collectionCreator, nameof(collectionCreator)); + Guard.NotNull(dictionary, nameof(dictionary)); + Guard.NotNull(collectionCreator, nameof(collectionCreator)); - _dict = dictionary; + _dict = dictionary; _collectionCreator = collectionCreator; } - protected Multimap(IDictionary> dictionary, bool isReadonly) + protected Multimap(IDictionary> dictionary, bool isReadonly) { - Guard.NotNull(dictionary, nameof(dictionary)); + Guard.NotNull(dictionary, nameof(dictionary)); - _dict = dictionary; + _dict = dictionary; if (isReadonly && dictionary != null) { @@ -70,54 +69,42 @@ protected Multimap(IDictionary> dictionary, bool isRea _isReadonly = isReadonly; } - public Multimap(IEnumerable>> items) - : this(items, null) - { - // for serialization - } - - public Multimap(IEnumerable>> items, IEqualityComparer comparer) - { - // for serialization - Guard.NotNull(items, nameof(items)); - - _dict = new Dictionary>(comparer ?? EqualityComparer.Default); - - if (items != null) - { - foreach (var kvp in items) - { - _dict[kvp.Key] = CreateCollection(kvp.Value); - } - } - } - - protected virtual ICollection CreateCollection(IEnumerable values) - { - return (_collectionCreator ?? DefaultCollectionCreator)(values ?? Enumerable.Empty()); - } - - /// - /// Gets the count of groups/keys. - /// - public int Count + public Multimap(IEnumerable>> items) + : this(items, null) { - get + // for serialization + } + + public Multimap(IEnumerable>> items, IEqualityComparer comparer) + { + // for serialization + Guard.NotNull(items, nameof(items)); + + _dict = new Dictionary>(comparer ?? EqualityComparer.Default); + + if (items != null) { - return this._dict.Keys.Count; + foreach (var kvp in items) + { + _dict[kvp.Key] = CreateCollection(kvp.Value); + } } } + protected virtual ICollection CreateCollection(IEnumerable values) + { + return (_collectionCreator ?? DefaultCollectionCreator)(values ?? Enumerable.Empty()); + } + + /// + /// Gets the count of groups/keys. + /// + public int Count => this._dict.Keys.Count; + /// /// Gets the total count of items in all groups. /// - public int TotalValueCount - { - get - { - return this._dict.Values.Sum(x => x.Count); - } - } + public int TotalValueCount => this._dict.Values.Sum(x => x.Count); /// /// Gets the collection of values stored under the specified key. @@ -142,25 +129,19 @@ public virtual ICollection this[TKey key] /// /// Gets the collection of keys. /// - public virtual ICollection Keys - { - get { return _dict.Keys; } - } + public virtual ICollection Keys => _dict.Keys; - /// - /// Gets all value collections. - /// - public virtual ICollection> Values - { - get { return _dict.Values; } - } + /// + /// Gets all value collections. + /// + public virtual ICollection> Values => _dict.Values; public IEnumerable Find(TKey key, Func predicate) { - Guard.NotNull(key, nameof(key)); - Guard.NotNull(predicate, nameof(predicate)); + Guard.NotNull(key, nameof(key)); + Guard.NotNull(predicate, nameof(predicate)); - if (_dict.ContainsKey(key)) + if (_dict.ContainsKey(key)) { return _dict[key].Where(predicate); } @@ -187,12 +168,12 @@ public virtual void Add(TKey key, TValue value) /// The values. public virtual void AddRange(TKey key, IEnumerable values) { - if (values == null || !values.Any()) - return; + if (values == null || !values.Any()) + return; - CheckNotReadonly(); + CheckNotReadonly(); - this[key].AddRange(values); + this[key].AddRange(values); } /// @@ -271,7 +252,7 @@ IEnumerator IEnumerable.GetEnumerator() /// An object that can be used to iterate through the multimap. public virtual IEnumerator>> GetEnumerator() { - foreach (KeyValuePair> pair in _dict) + foreach (KeyValuePair> pair in _dict) yield return pair; } @@ -285,9 +266,9 @@ private void CheckNotReadonly() public static Multimap CreateFromLookup(ILookup source) { - Guard.NotNull(source, nameof(source)); + Guard.NotNull(source, nameof(source)); - var map = new Multimap(); + var map = new Multimap(); foreach (IGrouping group in source) { @@ -297,56 +278,56 @@ public static Multimap CreateFromLookup(ILookup sour return map; } - #endregion - } - - public class MultiMapConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Multimap<,>); - return canConvert; - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // typeof TKey - var keyType = objectType.GetGenericArguments()[0]; - - // typeof TValue - var valueType = objectType.GetGenericArguments()[1]; - - // typeof IEnumerable> - var sequenceType = typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<,>).MakeGenericType(keyType, typeof(IEnumerable<>).MakeGenericType(valueType))); - - // serialize JArray to sequenceType - var list = serializer.Deserialize(reader, sequenceType); - - if (keyType == typeof(string)) - { - // call constructor Multimap(IEnumerable>> items, IEqualityComparer comparer) - // TBD: we always assume string keys to be case insensitive. Serialize it somehow and fetch here! - return Activator.CreateInstance(objectType, new object[] { list, StringComparer.OrdinalIgnoreCase }); - } - else - { - // call constructor Multimap(IEnumerable>> items) - return Activator.CreateInstance(objectType, new object[] { list }); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteStartArray(); - { - var enumerable = value as IEnumerable; - foreach (var item in enumerable) - { - // Json.Net uses a converter for KeyValuePair here - serializer.Serialize(writer, item); - } - } - writer.WriteEndArray(); - } - } + #endregion + } + + public class MultiMapConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Multimap<,>); + return canConvert; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // typeof TKey + var keyType = objectType.GetGenericArguments()[0]; + + // typeof TValue + var valueType = objectType.GetGenericArguments()[1]; + + // typeof IEnumerable> + var sequenceType = typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<,>).MakeGenericType(keyType, typeof(IEnumerable<>).MakeGenericType(valueType))); + + // serialize JArray to sequenceType + var list = serializer.Deserialize(reader, sequenceType); + + if (keyType == typeof(string)) + { + // call constructor Multimap(IEnumerable>> items, IEqualityComparer comparer) + // TBD: we always assume string keys to be case insensitive. Serialize it somehow and fetch here! + return Activator.CreateInstance(objectType, new object[] { list, StringComparer.OrdinalIgnoreCase }); + } + else + { + // call constructor Multimap(IEnumerable>> items) + return Activator.CreateInstance(objectType, new object[] { list }); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteStartArray(); + { + var enumerable = value as IEnumerable; + foreach (var item in enumerable) + { + // Json.Net uses a converter for KeyValuePair here + serializer.Serialize(writer, item); + } + } + writer.WriteEndArray(); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Collections/Querystring.cs b/src/Libraries/SmartStore.Core/Collections/Querystring.cs index c8a3b550a7..84de3dd797 100644 --- a/src/Libraries/SmartStore.Core/Collections/Querystring.cs +++ b/src/Libraries/SmartStore.Core/Collections/Querystring.cs @@ -6,10 +6,10 @@ namespace SmartStore.Collections { - /// - /// http://weblogs.asp.net/bradvincent/archive/2008/10/27/helper-class-querystring-builder-chainable.aspx - /// - public class QueryString : NameValueCollection + /// + /// http://weblogs.asp.net/bradvincent/archive/2008/10/27/helper-class-querystring-builder-chainable.aspx + /// + public class QueryString : NameValueCollection { public QueryString() { @@ -20,27 +20,21 @@ public QueryString(string queryString) FillFromString(queryString); } - public QueryString(NameValueCollection queryString) - : base(queryString) - { - } - - public static QueryString Current + public QueryString(NameValueCollection queryString) + : base(queryString) { - get { return new QueryString().FromCurrent(); } } - public static QueryString CurrentUnvalidated - { - get { return new QueryString().FromCurrent(true); } - } - - /// - /// Extracts a querystring from a full URL - /// - /// the string to extract the querystring from - /// a string representing only the querystring - [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] + public static QueryString Current => new QueryString().FromCurrent(); + + public static QueryString CurrentUnvalidated => new QueryString().FromCurrent(true); + + /// + /// Extracts a querystring from a full URL + /// + /// the string to extract the querystring from + /// a string representing only the querystring + [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] public static string ExtractQuerystring(string s) { if (!string.IsNullOrEmpty(s)) @@ -90,21 +84,21 @@ public QueryString FromCurrent(bool unvalidated = false) { if (HttpContext.Current != null) { - if (unvalidated) - { - return FillFromString(HttpContext.Current.Request.Unvalidated.QueryString.ToString(), true); - } - else - { - return FillFromString(HttpContext.Current.Request.QueryString.ToString(), true); - } + if (unvalidated) + { + return FillFromString(HttpContext.Current.Request.Unvalidated.QueryString.ToString(), true); + } + else + { + return FillFromString(HttpContext.Current.Request.QueryString.ToString(), true); + } } base.Clear(); return this; } /// - /// add a name value pair to the collection + /// Add a name value pair to the collection /// /// the name /// the value associated to the name @@ -121,7 +115,7 @@ public QueryString FromCurrent(bool unvalidated = false) /// the value associated to the name /// true if the name is unique within the querystring. This allows us to override existing values /// the QueryString object - public QueryString Add(string name, string value, bool isUnique) + public virtual QueryString Add(string name, string value, bool isUnique) { string existingValue = base[name]; if (string.IsNullOrEmpty(existingValue)) @@ -140,12 +134,12 @@ public QueryString Add(string name, string value, bool isUnique) return this; } - /// - /// Removes a name value pair from the querystring collection - /// - /// name of the querystring value to remove - /// the QueryString object - public new QueryString Remove(string name) + /// + /// Removes a name value pair from the querystring collection + /// + /// name of the querystring value to remove + /// the QueryString object + public new QueryString Remove(string name) { string existingValue = base[name]; if (!string.IsNullOrEmpty(existingValue)) @@ -166,24 +160,18 @@ public QueryString Reset() } /// - /// overrides the default + /// Overrides the default indexer /// /// /// the associated decoded value for the specified name - public new string this[string name] - { - get { return HttpUtility.UrlDecode(base[name]); } - } + public new string this[string name] => HttpUtility.UrlDecode(base[name]); /// /// overrides the default indexer /// /// /// the associated decoded value for the specified index - public new string this[int index] - { - get { return HttpUtility.UrlDecode(base[index]); } - } + public new string this[int index] => HttpUtility.UrlDecode(base[index]); /// /// checks if a name already exists within the query string collection @@ -202,16 +190,16 @@ public bool Contains(string name) /// the encoded querystring as it would appear in a browser public override string ToString() { - return ToString(true); + return ToString(true); } - /// - /// Outputs the querystring object to a string - /// - /// Whether to create entries for each comma-separated value - /// the encoded querystring as it would appear in a browser - public string ToString(bool splitValues) - { + /// + /// Outputs the querystring object to a string + /// + /// Whether to create entries for each comma-separated value + /// the encoded querystring as it would appear in a browser + public string ToString(bool splitValues) + { var psb = PooledStringBuilder.Rent(); var builder = (StringBuilder)psb; @@ -239,6 +227,6 @@ public string ToString(bool splitValues) } return psb.ToStringAndReturn(); - } - } + } + } } diff --git a/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs index b70b4302d2..ca66ab7367 100644 --- a/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs +++ b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs @@ -1,15 +1,21 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace SmartStore.ComponentModel { - public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer - { - public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer(); + public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer + { + public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer(); - public new bool Equals(object x, object y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); - } + public new bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + + public int GetHashCode(object obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } } diff --git a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs index d8100effd8..2122f56ad5 100644 --- a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs +++ b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs @@ -5,218 +5,218 @@ namespace SmartStore.Collections { - public sealed class SyncedCollection : ICollection - { - // INFO: Don't call it SynchronizedCollection because of framework dupe. - private readonly ICollection _col; - - public SyncedCollection(ICollection wrappedCollection) - : this(wrappedCollection, new object()) - { - } - - public SyncedCollection(ICollection wrappedCollection, object syncRoot) - { - Guard.NotNull(wrappedCollection, nameof(wrappedCollection)); - Guard.NotNull(syncRoot, nameof(syncRoot)); - - _col = wrappedCollection; - SyncRoot = syncRoot; - } - - public object SyncRoot { get; } - - public bool ReadLockFree { get; set; } - - public void AddRange(IEnumerable collection) - { - lock (SyncRoot) - { - _col.AddRange(collection); - } - } - - public void Insert(int index, T item) - { - if (_col is List list) - { - lock (SyncRoot) - { - list.Insert(index, item); - } - } - - throw new NotSupportedException(); - } - - public void InsertRange(int index, IEnumerable values) - { - if (_col is List list) - { - lock (SyncRoot) - { - list.InsertRange(index, values); - } - } - - throw new NotSupportedException(); - } - - public int RemoveRange(IEnumerable values) - { - int numRemoved = 0; - - lock (SyncRoot) - { - foreach (var value in values) - { - if (_col.Remove(value)) - numRemoved++; - } - } - - return numRemoved; - } - - public void RemoveRange(int index, int count) - { - if (_col is List list) - { - lock (SyncRoot) - { - list.RemoveRange(index, count); - } - } - - throw new NotSupportedException(); - } - - public void RemoveAt(int index) - { - lock (SyncRoot) - { - if (_col is List list) - { - list.RemoveAt(index); - } - else - { - var item = _col.ElementAtOrDefault(index); - if (item != null) - { - _col.Remove(item); - } - } - } - } - - public T this[int index] - { - get - { - if (ReadLockFree) - { - return _col.ElementAt(index); - } - else - { - lock (SyncRoot) - { - return _col.ElementAt(index); - } - } - } - } - - #region ICollection - - public int Count - { - get - { - if (ReadLockFree) - { - return _col.Count(); - } - else - { - lock (SyncRoot) - { - return _col.Count(); - } - } - } - } - - public bool IsReadOnly => _col.IsReadOnly; - - public void Add(T item) - { - lock (SyncRoot) - { - _col.Add(item); - } - } - - public void Clear() - { - lock (SyncRoot) - { - _col.Clear(); - } - } - - public bool Contains(T item) - { - if (ReadLockFree) - { - return _col.Contains(item); - } - else - { - lock (SyncRoot) - { - return _col.Contains(item); - } - } - } - - public void CopyTo(T[] array, int arrayIndex) - { - lock (SyncRoot) - { - _col.CopyTo(array, arrayIndex); - } - } - - public bool Remove(T item) - { - lock (SyncRoot) - { - return _col.Remove(item); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - public IEnumerator GetEnumerator() - { - if (ReadLockFree) - { - return _col.GetEnumerator(); - } - else - { - lock (SyncRoot) - { - return _col.GetEnumerator(); - } - } - } - - #endregion - } + public sealed class SyncedCollection : ICollection + { + // INFO: Don't call it SynchronizedCollection because of framework dupe. + private readonly ICollection _col; + + public SyncedCollection(ICollection wrappedCollection) + : this(wrappedCollection, new object()) + { + } + + public SyncedCollection(ICollection wrappedCollection, object syncRoot) + { + Guard.NotNull(wrappedCollection, nameof(wrappedCollection)); + Guard.NotNull(syncRoot, nameof(syncRoot)); + + _col = wrappedCollection; + SyncRoot = syncRoot; + } + + public object SyncRoot { get; } + + public bool ReadLockFree { get; set; } + + public void AddRange(IEnumerable collection) + { + lock (SyncRoot) + { + _col.AddRange(collection); + } + } + + public void Insert(int index, T item) + { + if (_col is List list) + { + lock (SyncRoot) + { + list.Insert(index, item); + } + } + + throw new NotSupportedException(); + } + + public void InsertRange(int index, IEnumerable values) + { + if (_col is List list) + { + lock (SyncRoot) + { + list.InsertRange(index, values); + } + } + + throw new NotSupportedException(); + } + + public int RemoveRange(IEnumerable values) + { + int numRemoved = 0; + + lock (SyncRoot) + { + foreach (var value in values) + { + if (_col.Remove(value)) + numRemoved++; + } + } + + return numRemoved; + } + + public void RemoveRange(int index, int count) + { + if (_col is List list) + { + lock (SyncRoot) + { + list.RemoveRange(index, count); + } + } + + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + lock (SyncRoot) + { + if (_col is List list) + { + list.RemoveAt(index); + } + else + { + var item = _col.ElementAtOrDefault(index); + if (item != null) + { + _col.Remove(item); + } + } + } + } + + public T this[int index] + { + get + { + if (ReadLockFree) + { + return _col.ElementAt(index); + } + else + { + lock (SyncRoot) + { + return _col.ElementAt(index); + } + } + } + } + + #region ICollection + + public int Count + { + get + { + if (ReadLockFree) + { + return _col.Count(); + } + else + { + lock (SyncRoot) + { + return _col.Count(); + } + } + } + } + + public bool IsReadOnly => _col.IsReadOnly; + + public void Add(T item) + { + lock (SyncRoot) + { + _col.Add(item); + } + } + + public void Clear() + { + lock (SyncRoot) + { + _col.Clear(); + } + } + + public bool Contains(T item) + { + if (ReadLockFree) + { + return _col.Contains(item); + } + else + { + lock (SyncRoot) + { + return _col.Contains(item); + } + } + } + + public void CopyTo(T[] array, int arrayIndex) + { + lock (SyncRoot) + { + _col.CopyTo(array, arrayIndex); + } + } + + public bool Remove(T item) + { + lock (SyncRoot) + { + return _col.Remove(item); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + if (ReadLockFree) + { + return _col.GetEnumerator(); + } + else + { + lock (SyncRoot) + { + return _col.GetEnumerator(); + } + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Collections/TopologicalSorter.cs b/src/Libraries/SmartStore.Core/Collections/TopologicalSorter.cs index e8902302c1..1b291e8ac3 100644 --- a/src/Libraries/SmartStore.Core/Collections/TopologicalSorter.cs +++ b/src/Libraries/SmartStore.Core/Collections/TopologicalSorter.cs @@ -6,215 +6,215 @@ namespace SmartStore.Collections { - public interface ITopologicSortable - { - TKey Key { get; } - TKey[] DependsOn { get; } - } - - public static class TopologicalSortExtensions - { - - public static ITopologicSortable[] SortTopological(this ITopologicSortable[] items) - { - return SortTopological(items, null); - } - - public static ITopologicSortable[] SortTopological(this ITopologicSortable[] items, IEqualityComparer comparer) - { - Guard.NotNull(items, nameof(items)); - - var sortedIndexes = SortIndexesTopological(items, comparer); - var sortedList = new List>(sortedIndexes.Length); - - for (var i = 0; i < sortedIndexes.Length; i++) - { - //sortedList[i] = items[sortedIndexes[i]]; - sortedList.Add(items[sortedIndexes[i]]); - } - - return sortedList.ToArray(); - } - - public static int[] SortIndexesTopological(this ITopologicSortable[] items) - { - return SortIndexesTopological(items, null); - } - - public static int[] SortIndexesTopological(this ITopologicSortable[] items, IEqualityComparer comparer) - { - Guard.NotNull(items, nameof(items)); - - if (items.Length == 0) - { - return new int[] { }; - } - - if (comparer == null) - { - comparer = EqualityComparer.Default; - } - - var sorter = new TopologicalSorter(items.Length); - var indexes = new Dictionary(comparer); - - // add vertices - for (int i = 0; i < items.Length; i++) - { - indexes[items[i].Key] = sorter.AddVertex(i); - } - - // add edges - for (int i = 0; i < items.Length; i++) - { - if (items[i].DependsOn != null) - { - for (int j = 0; j < items[i].DependsOn.Length; j++) - { - if (indexes.ContainsKey(items[i].DependsOn[j])) - { - sorter.AddEdge(i, indexes[items[i].DependsOn[j]]); - } - } - } - } - - int[] result = sorter.Sort().Reverse().ToArray(); - return result; - } - } - - public class CyclicDependencyException : Exception - { - public CyclicDependencyException() - : base("Cyclic dependency detected") - { - } - - public CyclicDependencyException(string message) - : base(message) - { - } - } - - internal class TopologicalSorter - { - #region Private Members - - private readonly int[] _vertices; // list of vertices - private readonly int[,] _matrix; // adjacency matrix - private int _numVerts; // current number of vertices - private readonly int[] _sortedArray; - - #endregion - - #region Ctor - - public TopologicalSorter(int size) - { - _vertices = new int[size]; - _matrix = new int[size, size]; - _numVerts = 0; - for (int i = 0; i < size; i++) - { - for (int j = 0; j < size; j++) - { - _matrix[i, j] = 0; - } - } - _sortedArray = new int[size]; // sorted vert labels - } - - #endregion - - #region Public Methods - - public int AddVertex(int vertex) - { - _vertices[_numVerts++] = vertex; - return _numVerts - 1; - } - - public void AddEdge(int start, int end) - { - _matrix[start, end] = 1; - } - - public int[] Sort() // topological sort - { - while (_numVerts > 0) // while vertices remain, - { - // get a vertex with no successors, or -1 - int currentVertex = NoSuccessors(); - if (currentVertex == -1) - { - // must be a cycle - throw new CyclicDependencyException(); - } - - // insert vertex label in sorted array (start at end) - _sortedArray[_numVerts - 1] = _vertices[currentVertex]; - - DeleteVertex(currentVertex); // delete vertex - } - - // vertices all gone; return sortedArray - return _sortedArray; - } - - #endregion - - #region Private Helper Methods - - // returns vert with no successors (or -1 if no such verts) - private int NoSuccessors() - { - for (int row = 0; row < _numVerts; row++) - { - bool isEdge = false; // edge from row to column in adjMat - for (int col = 0; col < _numVerts; col++) - { - if (_matrix[row, col] > 0) // if edge to another, - { - isEdge = true; - break; // this vertex has a successor try another - } - } - if (!isEdge) // if no edges, has no successors - return row; - } - return -1; // no - } - - private void DeleteVertex(int delVert) - { - // if not last vertex, delete from vertexList - if (delVert != _numVerts - 1) - { - for (int j = delVert; j < _numVerts - 1; j++) - _vertices[j] = _vertices[j + 1]; - - for (int row = delVert; row < _numVerts - 1; row++) - MoveRowUp(row, _numVerts); - - for (int col = delVert; col < _numVerts - 1; col++) - MoveColLeft(col, _numVerts - 1); - } - _numVerts--; // one less vertex - } - - private void MoveRowUp(int row, int length) - { - for (int col = 0; col < length; col++) - _matrix[row, col] = _matrix[row + 1, col]; - } - - private void MoveColLeft(int col, int length) - { - for (int row = 0; row < length; row++) - _matrix[row, col] = _matrix[row, col + 1]; - } - - #endregion - } + public interface ITopologicSortable + { + TKey Key { get; } + TKey[] DependsOn { get; } + } + + public static class TopologicalSortExtensions + { + + public static ITopologicSortable[] SortTopological(this ITopologicSortable[] items) + { + return SortTopological(items, null); + } + + public static ITopologicSortable[] SortTopological(this ITopologicSortable[] items, IEqualityComparer comparer) + { + Guard.NotNull(items, nameof(items)); + + var sortedIndexes = SortIndexesTopological(items, comparer); + var sortedList = new List>(sortedIndexes.Length); + + for (var i = 0; i < sortedIndexes.Length; i++) + { + //sortedList[i] = items[sortedIndexes[i]]; + sortedList.Add(items[sortedIndexes[i]]); + } + + return sortedList.ToArray(); + } + + public static int[] SortIndexesTopological(this ITopologicSortable[] items) + { + return SortIndexesTopological(items, null); + } + + public static int[] SortIndexesTopological(this ITopologicSortable[] items, IEqualityComparer comparer) + { + Guard.NotNull(items, nameof(items)); + + if (items.Length == 0) + { + return new int[] { }; + } + + if (comparer == null) + { + comparer = EqualityComparer.Default; + } + + var sorter = new TopologicalSorter(items.Length); + var indexes = new Dictionary(comparer); + + // add vertices + for (int i = 0; i < items.Length; i++) + { + indexes[items[i].Key] = sorter.AddVertex(i); + } + + // add edges + for (int i = 0; i < items.Length; i++) + { + if (items[i].DependsOn != null) + { + for (int j = 0; j < items[i].DependsOn.Length; j++) + { + if (indexes.ContainsKey(items[i].DependsOn[j])) + { + sorter.AddEdge(i, indexes[items[i].DependsOn[j]]); + } + } + } + } + + int[] result = sorter.Sort().Reverse().ToArray(); + return result; + } + } + + public class CyclicDependencyException : Exception + { + public CyclicDependencyException() + : base("Cyclic dependency detected") + { + } + + public CyclicDependencyException(string message) + : base(message) + { + } + } + + internal class TopologicalSorter + { + #region Private Members + + private readonly int[] _vertices; // list of vertices + private readonly int[,] _matrix; // adjacency matrix + private int _numVerts; // current number of vertices + private readonly int[] _sortedArray; + + #endregion + + #region Ctor + + public TopologicalSorter(int size) + { + _vertices = new int[size]; + _matrix = new int[size, size]; + _numVerts = 0; + for (int i = 0; i < size; i++) + { + for (int j = 0; j < size; j++) + { + _matrix[i, j] = 0; + } + } + _sortedArray = new int[size]; // sorted vert labels + } + + #endregion + + #region Public Methods + + public int AddVertex(int vertex) + { + _vertices[_numVerts++] = vertex; + return _numVerts - 1; + } + + public void AddEdge(int start, int end) + { + _matrix[start, end] = 1; + } + + public int[] Sort() // topological sort + { + while (_numVerts > 0) // while vertices remain, + { + // get a vertex with no successors, or -1 + int currentVertex = NoSuccessors(); + if (currentVertex == -1) + { + // must be a cycle + throw new CyclicDependencyException(); + } + + // insert vertex label in sorted array (start at end) + _sortedArray[_numVerts - 1] = _vertices[currentVertex]; + + DeleteVertex(currentVertex); // delete vertex + } + + // vertices all gone; return sortedArray + return _sortedArray; + } + + #endregion + + #region Private Helper Methods + + // returns vert with no successors (or -1 if no such verts) + private int NoSuccessors() + { + for (int row = 0; row < _numVerts; row++) + { + bool isEdge = false; // edge from row to column in adjMat + for (int col = 0; col < _numVerts; col++) + { + if (_matrix[row, col] > 0) // if edge to another, + { + isEdge = true; + break; // this vertex has a successor try another + } + } + if (!isEdge) // if no edges, has no successors + return row; + } + return -1; // no + } + + private void DeleteVertex(int delVert) + { + // if not last vertex, delete from vertexList + if (delVert != _numVerts - 1) + { + for (int j = delVert; j < _numVerts - 1; j++) + _vertices[j] = _vertices[j + 1]; + + for (int row = delVert; row < _numVerts - 1; row++) + MoveRowUp(row, _numVerts); + + for (int col = delVert; col < _numVerts - 1; col++) + MoveColLeft(col, _numVerts - 1); + } + _numVerts--; // one less vertex + } + + private void MoveRowUp(int row, int length) + { + for (int col = 0; col < length; col++) + _matrix[row, col] = _matrix[row + 1, col]; + } + + private void MoveColLeft(int col, int length) + { + for (int row = 0; row < length; row++) + _matrix[row, col] = _matrix[row, col + 1]; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs index c18c657139..78418736fe 100644 --- a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs +++ b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs @@ -1,262 +1,257 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; using System.Linq; -using System.Linq.Expressions; using System.Runtime.CompilerServices; -using System.Text; using Newtonsoft.Json; using SmartStore.ComponentModel; namespace SmartStore.Collections { - [JsonConverter(typeof(TreeNodeConverter))] - public class TreeNode : TreeNodeBase> - { - public TreeNode(TValue value) - { - Guard.NotNull(value, nameof(value)); - - Value = value; - } - - public TreeNode(TValue value, IEnumerable children) - : this(value) - { - if (children != null && children.Any()) - { - AppendRange(children); - } - } - - public TreeNode(TValue value, IEnumerable> children) - : this(value) - { - // for serialization - if (children != null && children.Any()) - { - AppendRange(children); - } - } - - public TValue Value - { - get; - private set; - } - - protected override TreeNode CreateInstance() - { - TValue value = this.Value; - - if (value is ICloneable) - { - value = ((ICloneable)value).Clone(); - } - - var clonedNode = new TreeNode(value); - - // Assign or clone Metadata - if (_metadata != null && _metadata.Count > 0) - { - foreach (var kvp in _metadata) - { - var metadataValue = kvp.Value is ICloneable - ? ((ICloneable)kvp.Value).Clone() - : kvp.Value; - clonedNode.SetMetadata(kvp.Key, metadataValue); - } - } - - if (_id != null) - { - clonedNode._id = _id; - } - - return clonedNode; - } - - public TreeNode Append(TValue value, object id = null) - { - var node = new TreeNode(value); - node._id = id; - this.Append(node); - return node; - } + [JsonConverter(typeof(TreeNodeConverter))] + public class TreeNode : TreeNodeBase> + { + public TreeNode(TValue value) + { + Guard.NotNull(value, nameof(value)); + + Value = value; + } + + public TreeNode(TValue value, IEnumerable children) + : this(value) + { + if (children != null && children.Any()) + { + AppendRange(children); + } + } + + public TreeNode(TValue value, IEnumerable> children) + : this(value) + { + // for serialization + if (children != null && children.Any()) + { + AppendRange(children); + } + } + + public TValue Value + { + get; + private set; + } + + protected override TreeNode CreateInstance() + { + TValue value = this.Value; + + if (value is ICloneable) + { + value = ((ICloneable)value).Clone(); + } + + var clonedNode = new TreeNode(value); + + // Assign or clone Metadata + if (_metadata != null && _metadata.Count > 0) + { + foreach (var kvp in _metadata) + { + var metadataValue = kvp.Value is ICloneable + ? ((ICloneable)kvp.Value).Clone() + : kvp.Value; + clonedNode.SetMetadata(kvp.Key, metadataValue); + } + } + + if (_id != null) + { + clonedNode._id = _id; + } + + return clonedNode; + } + + public TreeNode Append(TValue value, object id = null) + { + var node = new TreeNode(value); + node._id = id; + this.Append(node); + return node; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendRange(IEnumerable values) - { - values.Each(x => Append(x)); - } + { + values.Each(x => Append(x)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendRange(IEnumerable values, Func idSelector) - { - Guard.NotNull(idSelector, nameof(idSelector)); + { + Guard.NotNull(idSelector, nameof(idSelector)); - values.Each(x => Append(x, idSelector(x))); - } + values.Each(x => Append(x, idSelector(x))); + } - public TreeNode Prepend(TValue value, object id = null) - { - var node = new TreeNode(value); - node._id = id; - this.Prepend(node); - return node; - } + public TreeNode Prepend(TValue value, object id = null) + { + var node = new TreeNode(value); + node._id = id; + this.Prepend(node); + return node; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public IEnumerable Flatten(bool includeSelf = true) - { - return this.Flatten(null, includeSelf); - } - - public IEnumerable Flatten(Func predicate, bool includeSelf = true) - { - IEnumerable list; - if (includeSelf) - { - list = new[] { this.Value }; - } - else - { - list = Enumerable.Empty(); - } - - if (!HasChildren) - return list; - - var result = list.Union(Children.SelectMany(x => x.Flatten())); - if (predicate != null) - { - result = result.Where(predicate); - } - - return result; - } - } - - public class TreeNodeConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(TreeNode<>); - return canConvert; - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var valueType = objectType.GetGenericArguments()[0]; - var sequenceType = typeof(List<>).MakeGenericType(objectType); - - object objValue = null; - object objChildren = null; - string id = null; - Dictionary metadata = null; - - reader.Read(); - while (reader.TokenType == JsonToken.PropertyName) - { - string a = reader.Value.ToString(); - if (string.Equals(a, "Value", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - objValue = serializer.Deserialize(reader, valueType); - } - else if (string.Equals(a, "Metadata", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - metadata = serializer.Deserialize>(reader); - } - else if (string.Equals(a, "Children", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - objChildren = serializer.Deserialize(reader, sequenceType); - } - else if (string.Equals(a, "Id", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - id = serializer.Deserialize(reader); - } - else - { - reader.Skip(); - } - - reader.Read(); - } - - var ctorParams = objChildren != null - ? new object[] { objValue, objChildren } - : new object[] { objValue }; - - var treeNode = Activator.CreateInstance(objectType, ctorParams); - - // Set Metadata - if (metadata != null && metadata.Count > 0) - { - var metadataProp = FastProperty.GetProperty(objectType, "Metadata", PropertyCachingStrategy.Cached); - metadataProp.SetValue(treeNode, metadata); - } - - // Set Id - if (id.HasValue()) - { - var idProp = FastProperty.GetProperty(objectType, "Id", PropertyCachingStrategy.Cached); - idProp.SetValue(treeNode, id); - } - - return treeNode; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteStartObject(); - { - // Id - if (GetPropValue("Id", value) is object o) - { - writer.WritePropertyName("Id"); - serializer.Serialize(writer, o); - } - - // Value - writer.WritePropertyName("Value"); - serializer.Serialize(writer, GetPropValue("Value", value)); - - // Metadata - if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0) - { - writer.WritePropertyName("Metadata"); - - var typeNameHandling = serializer.TypeNameHandling; - try - { - serializer.TypeNameHandling = TypeNameHandling.All; - serializer.Serialize(writer, dict); - } - finally - { - serializer.TypeNameHandling = typeNameHandling; - } - } - - // Children - if (GetPropValue("HasChildren", value) is bool b && b == true) - { - writer.WritePropertyName("Children"); - serializer.Serialize(writer, GetPropValue("Children", value)); - } - } - writer.WriteEndObject(); - } - - private object GetPropValue(string name, object instance) - { - return FastProperty.GetProperty(instance.GetType(), name, PropertyCachingStrategy.Cached).GetValue(instance); - } - } + { + return this.Flatten(null, includeSelf); + } + + public IEnumerable Flatten(Func predicate, bool includeSelf = true) + { + IEnumerable list; + if (includeSelf) + { + list = new[] { this.Value }; + } + else + { + list = Enumerable.Empty(); + } + + if (!HasChildren) + return list; + + var result = list.Union(Children.SelectMany(x => x.Flatten())); + if (predicate != null) + { + result = result.Where(predicate); + } + + return result; + } + } + + public class TreeNodeConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(TreeNode<>); + return canConvert; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var valueType = objectType.GetGenericArguments()[0]; + var sequenceType = typeof(List<>).MakeGenericType(objectType); + + object objValue = null; + object objChildren = null; + string id = null; + Dictionary metadata = null; + + reader.Read(); + while (reader.TokenType == JsonToken.PropertyName) + { + string a = reader.Value.ToString(); + if (string.Equals(a, "Value", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + objValue = serializer.Deserialize(reader, valueType); + } + else if (string.Equals(a, "Metadata", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + metadata = serializer.Deserialize>(reader); + } + else if (string.Equals(a, "Children", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + objChildren = serializer.Deserialize(reader, sequenceType); + } + else if (string.Equals(a, "Id", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + id = serializer.Deserialize(reader); + } + else + { + reader.Skip(); + } + + reader.Read(); + } + + var ctorParams = objChildren != null + ? new object[] { objValue, objChildren } + : new object[] { objValue }; + + var treeNode = Activator.CreateInstance(objectType, ctorParams); + + // Set Metadata + if (metadata != null && metadata.Count > 0) + { + var metadataProp = FastProperty.GetProperty(objectType, "Metadata", PropertyCachingStrategy.Cached); + metadataProp.SetValue(treeNode, metadata); + } + + // Set Id + if (id.HasValue()) + { + var idProp = FastProperty.GetProperty(objectType, "Id", PropertyCachingStrategy.Cached); + idProp.SetValue(treeNode, id); + } + + return treeNode; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteStartObject(); + { + // Id + if (GetPropValue("Id", value) is object o) + { + writer.WritePropertyName("Id"); + serializer.Serialize(writer, o); + } + + // Value + writer.WritePropertyName("Value"); + serializer.Serialize(writer, GetPropValue("Value", value)); + + // Metadata + if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0) + { + writer.WritePropertyName("Metadata"); + + var typeNameHandling = serializer.TypeNameHandling; + try + { + serializer.TypeNameHandling = TypeNameHandling.All; + serializer.Serialize(writer, dict); + } + finally + { + serializer.TypeNameHandling = typeNameHandling; + } + } + + // Children + if (GetPropValue("HasChildren", value) is bool b && b == true) + { + writer.WritePropertyName("Children"); + serializer.Serialize(writer, GetPropValue("Children", value)); + } + } + writer.WriteEndObject(); + } + + private object GetPropValue(string name, object instance) + { + return FastProperty.GetProperty(instance.GetType(), name, PropertyCachingStrategy.Cached).GetValue(instance); + } + } } diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs index dec3f85b27..64b8570a4e 100644 --- a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs +++ b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs @@ -1,233 +1,223 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Runtime.CompilerServices; using Newtonsoft.Json; using SmartStore.Core.Infrastructure; namespace SmartStore.Collections { - public abstract class TreeNodeBase where T : TreeNodeBase - { - private T _parent; - private List _children = new List(); - private int? _depth = null; - private int _index = -1; - - protected object _id; - private IDictionary> _idNodeMap; - - protected IDictionary _metadata; - private readonly static ContextState> _contextState = new ContextState>("TreeNodeBase.ThreadMetadata"); - - public TreeNodeBase() - { - } - - #region Id - - private void PropagateNodeId(object value /* Id */, IDictionary> idNodeMap) - { - if (value != null) - { - // We support multi-keys for single nodes - var ids = value as IEnumerable ?? new object[] { value }; - foreach (var id in ids) - { - idNodeMap[id] = this; - } - } - } - - private void RemoveNodeId(object value /* Id */, IDictionary> idNodeMap) - { - if (value != null) - { - // We support multi-keys for single nodes - var ids = value as IEnumerable ?? new object[] { value }; - foreach (var id in ids) - { - if (idNodeMap.ContainsKey(id)) - { - idNodeMap.Remove(id); - } - } - } - } - - /// - /// Responsible for propagating node ids when detaching/attaching nodes - /// - private void FixIdNodeMap(T prevParent, T newParent) - { - ICollection> keyedNodes = null; - - if (prevParent != null) - { - // A node is moved. We need to detach first. - keyedNodes = new List>(); - - // Detach ids from prev map - var prevMap = prevParent.GetIdNodeMap(); - - Traverse(x => - { - // Collect all child node's ids - if (x._id != null) - { - keyedNodes.Add(x); - // Remove from map - RemoveNodeId(x._id, prevMap); - } - }, true); - } - - if (keyedNodes == null && _idNodeMap != null) - { - // An orphan/root node is attached - keyedNodes = _idNodeMap.Values; - } - - if (newParent != null) - { - // Get new *root map - var map = newParent.GetIdNodeMap(); - - // Merge *this map with *root map - if (keyedNodes != null) - { - foreach (var node in keyedNodes) - { - node.PropagateNodeId(node._id, map); - } - - // Get rid of *this map after memorizing keyed nodes - if (_idNodeMap != null) - { - _idNodeMap.Clear(); - _idNodeMap = null; - } - } - - if (prevParent == null && _id != null) - { - // When *this was a root, but is keyed, then *this id - // was most likely missing in the prev id-node-map. - PropagateNodeId(_id, map); - } - } - } - - public object Id - { - get - { - return _id; - } - set - { - var prevId = _id; - _id = value; - - if (_parent != null) - { - var map = GetIdNodeMap(); - - // Remove old id(s) from map - RemoveNodeId(prevId, map); - - // Set id - PropagateNodeId(value, map); - } - } - } - - public T SelectNodeById(object id) - { - if (id == null || IsLeaf) - return null; - - var map = GetIdNodeMap(); - var node = (T)map?.Get(id); - - if (node != null && !this.IsAncestorOfOrSelf(node)) - { - // Found node is NOT a child of this node - return null; - } - - return node; - } - - private IDictionary> GetIdNodeMap() - { - var map = this.Root._idNodeMap; - if (map == null) - { - map = this.Root._idNodeMap = new Dictionary>(); - } - - return map; - } - - #endregion - - #region Metadata - - public IDictionary Metadata - { - get - { - return _metadata ?? (_metadata = new Dictionary()); - } - set - { - _metadata = value; - } - } + public abstract class TreeNodeBase where T : TreeNodeBase + { + private T _parent; + private List _children = new List(); + private int? _depth = null; + private int _index = -1; + + protected object _id; + private IDictionary> _idNodeMap; + + protected IDictionary _metadata; + private readonly static ContextState> _contextState = new ContextState>("TreeNodeBase.ThreadMetadata"); + + public TreeNodeBase() + { + } + + #region Id + + private void PropagateNodeId(object value /* Id */, IDictionary> idNodeMap) + { + if (value != null) + { + // We support multi-keys for single nodes + var ids = value as IEnumerable ?? new object[] { value }; + foreach (var id in ids) + { + idNodeMap[id] = this; + } + } + } + + private void RemoveNodeId(object value /* Id */, IDictionary> idNodeMap) + { + if (value != null) + { + // We support multi-keys for single nodes + var ids = value as IEnumerable ?? new object[] { value }; + foreach (var id in ids) + { + if (idNodeMap.ContainsKey(id)) + { + idNodeMap.Remove(id); + } + } + } + } + + /// + /// Responsible for propagating node ids when detaching/attaching nodes + /// + private void FixIdNodeMap(T prevParent, T newParent) + { + ICollection> keyedNodes = null; + + if (prevParent != null) + { + // A node is moved. We need to detach first. + keyedNodes = new List>(); + + // Detach ids from prev map + var prevMap = prevParent.GetIdNodeMap(); + + Traverse(x => + { + // Collect all child node's ids + if (x._id != null) + { + keyedNodes.Add(x); + // Remove from map + RemoveNodeId(x._id, prevMap); + } + }, true); + } + + if (keyedNodes == null && _idNodeMap != null) + { + // An orphan/root node is attached + keyedNodes = _idNodeMap.Values; + } + + if (newParent != null) + { + // Get new *root map + var map = newParent.GetIdNodeMap(); + + // Merge *this map with *root map + if (keyedNodes != null) + { + foreach (var node in keyedNodes) + { + node.PropagateNodeId(node._id, map); + } + + // Get rid of *this map after memorizing keyed nodes + if (_idNodeMap != null) + { + _idNodeMap.Clear(); + _idNodeMap = null; + } + } + + if (prevParent == null && _id != null) + { + // When *this was a root, but is keyed, then *this id + // was most likely missing in the prev id-node-map. + PropagateNodeId(_id, map); + } + } + } + + public object Id + { + get => _id; + set + { + var prevId = _id; + _id = value; + + if (_parent != null) + { + var map = GetIdNodeMap(); + + // Remove old id(s) from map + RemoveNodeId(prevId, map); + + // Set id + PropagateNodeId(value, map); + } + } + } + + public T SelectNodeById(object id) + { + if (id == null || IsLeaf) + return null; + + var map = GetIdNodeMap(); + var node = (T)map?.Get(id); + + if (node != null && !this.IsAncestorOfOrSelf(node)) + { + // Found node is NOT a child of this node + return null; + } + + return node; + } + + private IDictionary> GetIdNodeMap() + { + var map = this.Root._idNodeMap; + if (map == null) + { + map = this.Root._idNodeMap = new Dictionary>(); + } + + return map; + } + + #endregion + + #region Metadata + + public IDictionary Metadata + { + get => _metadata ?? (_metadata = new Dictionary()); + set => _metadata = value; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetMetadata(string key, object value) - { - Guard.NotEmpty(key, nameof(key)); - - Metadata[key] = value; - } - - public void SetThreadMetadata(string key, object value) - { - Guard.NotEmpty(key, nameof(key)); - - var state = _contextState.GetState(); - if (state == null) - { - state = new Dictionary(); - _contextState.SetState(state); - } - - state[GetContextKey(this, key)] = value; - } - - public TMetadata GetMetadata(string key, bool recursive = true) - { - Guard.NotEmpty(key, nameof(key)); - - object metadata; - - if (!recursive) - { - return TryGetMetadataForNode(this, key, out metadata) ? (TMetadata)metadata : default(TMetadata); - } - - // recursively search for the metadata value in current node and ancestors - var current = this; - while (!TryGetMetadataForNode(current, key, out metadata)) - { - current = current.Parent; - if (current == null) - break; - } + { + Guard.NotEmpty(key, nameof(key)); + + Metadata[key] = value; + } + + public void SetThreadMetadata(string key, object value) + { + Guard.NotEmpty(key, nameof(key)); + + var state = _contextState.GetState(); + if (state == null) + { + state = new Dictionary(); + _contextState.SetState(state); + } + + state[GetContextKey(this, key)] = value; + } + + public TMetadata GetMetadata(string key, bool recursive = true) + { + Guard.NotEmpty(key, nameof(key)); + + object metadata; + + if (!recursive) + { + return TryGetMetadataForNode(this, key, out metadata) ? (TMetadata)metadata : default(TMetadata); + } + + // recursively search for the metadata value in current node and ancestors + var current = this; + while (!TryGetMetadataForNode(current, key, out metadata)) + { + current = current.Parent; + if (current == null) + break; + } if (metadata != null) { @@ -235,434 +225,344 @@ public TMetadata GetMetadata(string key, bool recursive = true) } return default(TMetadata); - } - - private bool TryGetMetadataForNode(TreeNodeBase node, string key, out object metadata) - { - metadata = null; - - var state = _contextState.GetState(); - - if (state != null) - { - var contextKey = GetContextKey(node, key); - if (state.ContainsKey(contextKey)) - { - metadata = state[contextKey]; - return true; - } - } - - if (node._metadata != null && node._metadata.ContainsKey(key)) - { - metadata = node._metadata[key]; - return true; - } - - return false; - } - - private static string GetContextKey(TreeNodeBase node, string key) - { - return node.GetHashCode().ToString() + key; - } - - #endregion - - private List ChildrenInternal - { - get - { - if (_children == null) - { - _children = new List(); - } - return _children; - } - } - - private void AddChild(T node, bool clone, bool append = true) - { - var newNode = node; - if (clone) - { - newNode = node.Clone(true); - } - newNode.AttachTo((T)this, append ? (int?)null : 0); - } - - private void AttachTo(T newParent, int? index) - { - Guard.NotNull(newParent, nameof(newParent)); - - var prevParent = _parent; - - if (_parent != null) - { - // Detach from parent - _parent.Remove((T)this); - } - - if (index == null) - { - newParent.ChildrenInternal.Add((T)this); - _index = newParent.ChildrenInternal.Count - 1; - } - else - { - newParent.ChildrenInternal.Insert(index.Value, (T)this); - _index = index.Value; - FixIndexes(newParent._children, _index + 1, 1); - } - - _parent = newParent; - - FixIdNodeMap(prevParent, newParent); - } - - [JsonIgnore] - public T Parent - { - get - { - return _parent; - } - } - - public T this[int i] - { - get - { - return _children?[i]; - } - } - - public IReadOnlyList Children - { - get - { - return ChildrenInternal; - } - } - - [JsonIgnore] - public IEnumerable LeafNodes - { - get - { - return _children != null - ? _children.Where(x => x.IsLeaf) - : Enumerable.Empty(); - } - } - - [JsonIgnore] - public IEnumerable NonLeafNodes - { - get - { - return _children != null - ? _children.Where(x => !x.IsLeaf) - : Enumerable.Empty(); - } - } - - - [JsonIgnore] - public T FirstChild - { - get - { - return _children?.FirstOrDefault(); - } - } - - [JsonIgnore] - public T LastChild - { - get - { - return _children?.LastOrDefault(); - } - } - - [JsonIgnore] - public bool IsLeaf - { - get - { - return _children == null || _children.Count == 0; - } - } - - [JsonIgnore] - public bool HasChildren - { - get - { - return _children == null || _children.Count > 0; - } - } - - [JsonIgnore] - public bool IsRoot - { - get - { - return _parent == null; - } - } - - [JsonIgnore] - public int Index - { - get - { - return _index; - } - } - - /// - /// Root starts with 0 - /// - [JsonIgnore] - public int Depth - { - get - { - if (!_depth.HasValue) - { - var node = this; - int depth = 0; - while (node != null && !node.IsRoot) - { - depth++; - node = node.Parent; - } - _depth = depth; - } - - return _depth.Value; - } - } - - [JsonIgnore] - public T Root - { - get - { - var root = this; - while (root._parent != null) - { - root = root._parent; - } - return (T)root; - } - } - - [JsonIgnore] - public T First - { - get - { - return _parent?._children?.FirstOrDefault(); - } - } - - [JsonIgnore] - public T Last - { - get - { - return _parent?._children?.LastOrDefault(); - } - } - - [JsonIgnore] - public T Next - { - get - { - return _parent?._children?.ElementAtOrDefault(_index + 1); - } - } - - [JsonIgnore] - public T Previous - { - get - { - return _parent?._children?.ElementAtOrDefault(_index - 1); - } - } - - public bool IsDescendantOf(T node) - { - var parent = _parent; - while (parent != null) - { - if (parent == node) - { - return true; - } - parent = parent._parent; - } - - return false; - } - - public bool IsDescendantOfOrSelf(T node) - { - if (node == (T)this) - return true; - - return IsDescendantOf(node); - } - - public bool IsAncestorOf(T node) - { - return node.IsDescendantOf((T)this); - } - - public bool IsAncestorOfOrSelf(T node) - { - if (node == (T)this) - return true; - - return node.IsDescendantOf((T)this); - } - - [JsonIgnore] - public IEnumerable Trail - { - get - { - var trail = new List(); - - var node = (T)this; - do - { - trail.Insert(0, node); - node = node._parent; - } while (node != null); - - return trail; - } - } - - /// - /// Gets the first element that matches the predicate by testing the node itself - /// and traversing up through its ancestors in the tree. - /// - /// predicate - /// The closest node - public T Closest(Func predicate) - { - Guard.NotNull(predicate, nameof(predicate)); - - var test = predicate; - - if (test((T)this)) - { - return (T)this; - } - - var parent = _parent; - - while (parent != null) - { - if (test(parent)) - { - return parent; - } - parent = parent._parent; - } - - return null; - } + } + + private bool TryGetMetadataForNode(TreeNodeBase node, string key, out object metadata) + { + metadata = null; + + var state = _contextState.GetState(); + + if (state != null) + { + var contextKey = GetContextKey(node, key); + if (state.ContainsKey(contextKey)) + { + metadata = state[contextKey]; + return true; + } + } + + if (node._metadata != null && node._metadata.ContainsKey(key)) + { + metadata = node._metadata[key]; + return true; + } + + return false; + } + + private static string GetContextKey(TreeNodeBase node, string key) + { + return node.GetHashCode().ToString() + key; + } + + #endregion + + private List ChildrenInternal + { + get + { + if (_children == null) + { + _children = new List(); + } + return _children; + } + } + + private void AddChild(T node, bool clone, bool append = true) + { + var newNode = node; + if (clone) + { + newNode = node.Clone(true); + } + newNode.AttachTo((T)this, append ? (int?)null : 0); + } + + private void AttachTo(T newParent, int? index) + { + Guard.NotNull(newParent, nameof(newParent)); + + var prevParent = _parent; + + if (_parent != null) + { + // Detach from parent + _parent.Remove((T)this); + } + + if (index == null) + { + newParent.ChildrenInternal.Add((T)this); + _index = newParent.ChildrenInternal.Count - 1; + } + else + { + newParent.ChildrenInternal.Insert(index.Value, (T)this); + _index = index.Value; + FixIndexes(newParent._children, _index + 1, 1); + } + + _parent = newParent; + + FixIdNodeMap(prevParent, newParent); + } + + [JsonIgnore] + public T Parent => _parent; + + public T this[int i] => _children?[i]; + + public IReadOnlyList Children => ChildrenInternal; + + [JsonIgnore] + public IEnumerable LeafNodes => _children != null + ? _children.Where(x => x.IsLeaf) + : Enumerable.Empty(); + + [JsonIgnore] + public IEnumerable NonLeafNodes => _children != null + ? _children.Where(x => !x.IsLeaf) + : Enumerable.Empty(); + + + [JsonIgnore] + public T FirstChild => _children?.FirstOrDefault(); + + [JsonIgnore] + public T LastChild => _children?.LastOrDefault(); + + [JsonIgnore] + public bool IsLeaf => _children == null || _children.Count == 0; + + [JsonIgnore] + public bool HasChildren => _children == null || _children.Count > 0; + + [JsonIgnore] + public bool IsRoot => _parent == null; + + [JsonIgnore] + public int Index => _index; + + /// + /// Root starts with 0 + /// + [JsonIgnore] + public int Depth + { + get + { + if (!_depth.HasValue) + { + var node = this; + int depth = 0; + while (node != null && !node.IsRoot) + { + depth++; + node = node.Parent; + } + _depth = depth; + } + + return _depth.Value; + } + } + + [JsonIgnore] + public T Root + { + get + { + var root = this; + while (root._parent != null) + { + root = root._parent; + } + return (T)root; + } + } + + [JsonIgnore] + public T First => _parent?._children?.FirstOrDefault(); + + [JsonIgnore] + public T Last => _parent?._children?.LastOrDefault(); + + [JsonIgnore] + public T Next => _parent?._children?.ElementAtOrDefault(_index + 1); + + [JsonIgnore] + public T Previous => _parent?._children?.ElementAtOrDefault(_index - 1); + + public bool IsDescendantOf(T node) + { + var parent = _parent; + while (parent != null) + { + if (parent == node) + { + return true; + } + parent = parent._parent; + } + + return false; + } + + public bool IsDescendantOfOrSelf(T node) + { + if (node == (T)this) + return true; + + return IsDescendantOf(node); + } + + public bool IsAncestorOf(T node) + { + return node.IsDescendantOf((T)this); + } + + public bool IsAncestorOfOrSelf(T node) + { + if (node == (T)this) + return true; + + return node.IsDescendantOf((T)this); + } + + [JsonIgnore] + public IEnumerable Trail + { + get + { + var trail = new List(); + + var node = (T)this; + do + { + trail.Insert(0, node); + node = node._parent; + } while (node != null); + + return trail; + } + } + + /// + /// Gets the first element that matches the predicate by testing the node itself + /// and traversing up through its ancestors in the tree. + /// + /// predicate + /// The closest node + public T Closest(Func predicate) + { + Guard.NotNull(predicate, nameof(predicate)); + + var test = predicate; + + if (test((T)this)) + { + return (T)this; + } + + var parent = _parent; + + while (parent != null) + { + if (test(parent)) + { + return parent; + } + parent = parent._parent; + } + + return null; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Append(T value) - { - this.AddChild(value, false, true); - return value; - } + { + this.AddChild(value, false, true); + return value; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendRange(IEnumerable values) - { - values.Each(x => Append(x)); - } - - public void AppendChildrenOf(T node) - { - if (node?._children != null) - { - node._children.Each(x => this.AddChild(x, true, true)); - } - } + { + values.Each(x => Append(x)); + } + + public void AppendChildrenOf(T node) + { + if (node?._children != null) + { + node._children.Each(x => this.AddChild(x, true, true)); + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Prepend(T value) - { - this.AddChild(value, false, false); - return value; - } - - public void InsertAfter(int index) - { - var refNode = _children?.ElementAtOrDefault(index); - if (refNode != null) - { - InsertAfter(refNode); - } - - throw new ArgumentOutOfRangeException(nameof(index)); - } + { + this.AddChild(value, false, false); + return value; + } + + public void InsertAfter(int index) + { + var refNode = _children?.ElementAtOrDefault(index); + if (refNode != null) + { + InsertAfter(refNode); + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InsertAfter(T refNode) - { - this.Insert(refNode, true); - } - - public void InsertBefore(int index) - { - var refNode = _children?.ElementAtOrDefault(index); - if (refNode != null) - { - InsertBefore(refNode); - } + { + this.Insert(refNode, true); + } + + public void InsertBefore(int index) + { + var refNode = _children?.ElementAtOrDefault(index); + if (refNode != null) + { + InsertBefore(refNode); + } - throw new ArgumentOutOfRangeException(nameof(index)); - } + throw new ArgumentOutOfRangeException(nameof(index)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InsertBefore(T refNode) - { - this.Insert(refNode, false); - } + { + this.Insert(refNode, false); + } - private void Insert(T refNode, bool after) - { - Guard.NotNull(refNode, nameof(refNode)); + private void Insert(T refNode, bool after) + { + Guard.NotNull(refNode, nameof(refNode)); - var refParent = refNode._parent; - if (refParent == null) - { - throw Error.Argument("refNode", "The reference node cannot be a root node and must be attached to the tree."); - } + var refParent = refNode._parent; + if (refParent == null) + { + throw Error.Argument("refNode", "The reference node cannot be a root node and must be attached to the tree."); + } - AttachTo(refParent, refNode._index + (after ? 1 : 0)); - } + AttachTo(refParent, refNode._index + (after ? 1 : 0)); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T SelectNode(Func predicate, bool includeSelf = false) - { - Guard.NotNull(predicate, nameof(predicate)); + { + Guard.NotNull(predicate, nameof(predicate)); - return this.FlattenNodes(predicate, includeSelf).FirstOrDefault(); - } + return this.FlattenNodes(predicate, includeSelf).FirstOrDefault(); + } /// /// Selects all nodes (recursively) witch match the given predicate @@ -671,130 +571,130 @@ public T SelectNode(Func predicate, bool includeSelf = false) /// A readonly collection of node matches [MethodImpl(MethodImplOptions.AggressiveInlining)] public IEnumerable SelectNodes(Func predicate, bool includeSelf = false) - { - Guard.NotNull(predicate, nameof(predicate)); - - return this.FlattenNodes(predicate, includeSelf); - } - - private void FixIndexes(IList list, int startIndex, int summand = 1) - { - if (startIndex < 0 || startIndex >= list.Count) - return; - - for (var i = startIndex; i < list.Count; i++) - { - list[i]._index += summand; - } - } - - public void Remove(T node) - { - Guard.NotNull(node, nameof(node)); - - if (!node.IsRoot) - { - var list = node._parent?._children; - if (list.Remove(node)) - { - node.FixIdNodeMap(node._parent, null); - - FixIndexes(list, node._index, -1); - - node._index = -1; - node._parent = null; - node.Traverse(x => x._depth = null, true); - } - } - } - - public void Clear() - { - Traverse(x => x._depth = null, false); - if (_children != null) - { - _children.Clear(); - } - - FixIdNodeMap(_parent, null); - } - - public void Traverse(Action action, bool includeSelf = false) - { - Guard.NotNull(action, nameof(action)); - - if (includeSelf) - action((T)this); - - if (_children != null) - { - foreach (var child in _children) - child.Traverse(action, true); - } - } - - public void TraverseParents(Action action, bool includeSelf = false) - { - Guard.NotNull(action, nameof(action)); - - if (includeSelf) - action((T)this); - - var parent = _parent; - - while (parent != null) - { - action(parent); - parent = parent._parent; - } - } + { + Guard.NotNull(predicate, nameof(predicate)); + + return this.FlattenNodes(predicate, includeSelf); + } + + private void FixIndexes(IList list, int startIndex, int summand = 1) + { + if (startIndex < 0 || startIndex >= list.Count) + return; + + for (var i = startIndex; i < list.Count; i++) + { + list[i]._index += summand; + } + } + + public void Remove(T node) + { + Guard.NotNull(node, nameof(node)); + + if (!node.IsRoot) + { + var list = node._parent?._children; + if (list.Remove(node)) + { + node.FixIdNodeMap(node._parent, null); + + FixIndexes(list, node._index, -1); + + node._index = -1; + node._parent = null; + node.Traverse(x => x._depth = null, true); + } + } + } + + public void Clear() + { + Traverse(x => x._depth = null, false); + if (_children != null) + { + _children.Clear(); + } + + FixIdNodeMap(_parent, null); + } + + public void Traverse(Action action, bool includeSelf = false) + { + Guard.NotNull(action, nameof(action)); + + if (includeSelf) + action((T)this); + + if (_children != null) + { + foreach (var child in _children) + child.Traverse(action, true); + } + } + + public void TraverseParents(Action action, bool includeSelf = false) + { + Guard.NotNull(action, nameof(action)); + + if (includeSelf) + action((T)this); + + var parent = _parent; + + while (parent != null) + { + action(parent); + parent = parent._parent; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public IEnumerable FlattenNodes(bool includeSelf = true) - { - return this.FlattenNodes(null, includeSelf); - } - - protected IEnumerable FlattenNodes(Func predicate, bool includeSelf = true) - { - IEnumerable list; - if (includeSelf) - { - list = new[] { (T)this }; - } - else - { - list = Enumerable.Empty(); - } - - if (_children == null) - return list; - - var result = list.Union(_children.SelectMany(x => x.FlattenNodes())); - if (predicate != null) - { - result = result.Where(predicate); - } - - return result; - } + { + return this.FlattenNodes(null, includeSelf); + } + + protected IEnumerable FlattenNodes(Func predicate, bool includeSelf = true) + { + IEnumerable list; + if (includeSelf) + { + list = new[] { (T)this }; + } + else + { + list = Enumerable.Empty(); + } + + if (_children == null) + return list; + + var result = list.Union(_children.SelectMany(x => x.FlattenNodes())); + if (predicate != null) + { + result = result.Where(predicate); + } + + return result; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Clone() - { - return Clone(true); - } - - public virtual T Clone(bool deep) - { - var clone = CreateInstance(); - if (deep) - { - clone.AppendChildrenOf((T)this); - } - return clone; - } - - protected abstract T CreateInstance(); - } + { + return Clone(true); + } + + public virtual T Clone(bool deep) + { + var clone = CreateInstance(); + if (deep) + { + clone.AppendChildrenOf((T)this); + } + return clone; + } + + protected abstract T CreateInstance(); + } } diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs index db54bf8936..5a07192af5 100644 --- a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs +++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs @@ -4,103 +4,109 @@ namespace SmartStore.Collections { - public sealed class TrimmedBuffer : ICollection - { - private readonly int _maxSize; - private readonly List _list; - - public TrimmedBuffer(int maxSize) - : this((IEnumerable)null, default(T), maxSize) - { - } - - public TrimmedBuffer(IEnumerable collection, int maxSize) - : this(collection, default(T), maxSize) - { - } - - public TrimmedBuffer(string collection, int maxSize) - : this(collection.NullEmpty()?.Convert>()?.Distinct(), default(T), maxSize) - { - } - - public TrimmedBuffer(string collection, T newItem, int maxSize) - : this(collection.NullEmpty()?.Convert>()?.Distinct(), newItem, maxSize) - { - } - - public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize) - { + public sealed class TrimmedBuffer : ICollection + { + private readonly int _maxSize; + private readonly List _list; + + public TrimmedBuffer(int maxSize) + : this((IEnumerable)null, default(T), maxSize) + { + } + + public TrimmedBuffer(IEnumerable collection, int maxSize) + : this(collection, default(T), maxSize) + { + } + + public TrimmedBuffer(string collection, int maxSize) + : this(collection.NullEmpty()?.Convert>()?.Distinct(), default(T), maxSize) + { + } + + public TrimmedBuffer(string collection, T newItem, int maxSize) + : this(collection.NullEmpty()?.Convert>()?.Distinct(), newItem, maxSize) + { + } + + public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize) + { Guard.IsTrue(maxSize >= 0, nameof(maxSize)); - _maxSize = maxSize; - _list = new List(collection ?? Enumerable.Empty()); - - if (newItem != null) - { - Add(newItem); - } - - Trim(); - } - - private void Trim() - { - if (_maxSize >= 0) - { - while (_list.Count > _maxSize) - _list.RemoveAt(_list.Count - 1); - } - } - - public T this[int index] - { - get - { - return _list[index]; - } - set - { - _list[index] = value; - } - } - - public void Add(T item) - { - var i = _list.IndexOf(item); - if (i > -1) - _list.RemoveAt(i); - - _list.Insert(0, item); - - Trim(); - } - - public void Clear() => _list.Clear(); - - public bool Contains(T item) => _list.Contains(item); - - public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); - - public bool Remove(T item) => _list.Remove(item); - - public int Count => _list.Count; - - public bool IsReadOnly => false; - - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _list.GetEnumerator(); - } - - public override string ToString() - { - return string.Join(";", _list); - } - } + _maxSize = maxSize; + _list = new List(collection ?? Enumerable.Empty()); + + if (newItem != null) + { + Add(newItem); + } + + Trim(); + } + + private void Trim() + { + if (_maxSize >= 0) + { + while (_list.Count > _maxSize) + _list.RemoveAt(_list.Count - 1); + } + } + + public T this[int index] + { + get => _list[index]; + set => _list[index] = value; + } + + public void Add(T item) + { + var i = _list.IndexOf(item); + if (i > -1) + _list.RemoveAt(i); + + _list.Insert(0, item); + + Trim(); + } + + public void Clear() + { + _list.Clear(); + } + + public bool Contains(T item) + { + return _list.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + return _list.Remove(item); + } + + public int Count => _list.Count; + + public bool IsReadOnly => false; + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + public override string ToString() + { + return string.Join(";", _list); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs index 18a18937f2..e82db5c515 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs @@ -1,41 +1,41 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Linq.Expressions; -using System.Collections.Concurrent; +using System.Reflection; namespace SmartStore.ComponentModel { - public class FastActivator - { - private static readonly ConcurrentDictionary _activatorsCache = new ConcurrentDictionary(); + public class FastActivator + { + private static readonly ConcurrentDictionary _activatorsCache = new ConcurrentDictionary(); private Func _invoker; - public FastActivator(ConstructorInfo constructorInfo) - { - Guard.NotNull(constructorInfo, nameof(constructorInfo)); - - Constructor = constructorInfo; - ParameterTypes = constructorInfo.GetParameters().Select(p => p.ParameterType).ToArray(); - } - - /// - /// Gets the backing . - /// - public ConstructorInfo Constructor { get; private set; } - - /// - /// Gets the parameter types from the backing - /// - public Type[] ParameterTypes { get; private set; } - - /// - /// Gets the constructor invoker. - /// - public Func Invoker - { + public FastActivator(ConstructorInfo constructorInfo) + { + Guard.NotNull(constructorInfo, nameof(constructorInfo)); + + Constructor = constructorInfo; + ParameterTypes = constructorInfo.GetParameters().Select(p => p.ParameterType).ToArray(); + } + + /// + /// Gets the backing . + /// + public ConstructorInfo Constructor { get; private set; } + + /// + /// Gets the parameter types from the backing + /// + public Type[] ParameterTypes { get; private set; } + + /// + /// Gets the constructor invoker. + /// + public Func Invoker + { get { if (_invoker == null) @@ -44,174 +44,174 @@ public Func Invoker } return _invoker; - } + } + } + + /// + /// Creates an instance of the type using the specified parameters. + /// + /// A reference to the newly created object. + public object Activate(params object[] parameters) + { + return Invoker(parameters); + } + + #region Static + + /// + /// Creates a single fast constructor invoker. The result is not cached. + /// + /// constructorInfo to create invoker for. + /// a fast invoker. + public static Func MakeFastInvoker(ConstructorInfo constructorInfo) + { + var paramsInfo = constructorInfo.GetParameters(); + + var parametersExpression = Expression.Parameter(typeof(object[]), "args"); + var argumentsExpression = new Expression[paramsInfo.Length]; + + for (int paramIndex = 0; paramIndex < paramsInfo.Length; paramIndex++) + { + var indexExpression = Expression.Constant(paramIndex); + var parameterType = paramsInfo[paramIndex].ParameterType; + + var parameterIndexExpression = Expression.ArrayIndex(parametersExpression, indexExpression); + var convertExpression = Expression.Convert(parameterIndexExpression, parameterType); + argumentsExpression[paramIndex] = convertExpression; + + if (!parameterType.GetTypeInfo().IsValueType) + continue; + + var nullConditionExpression = Expression.Equal(parameterIndexExpression, Expression.Constant(null)); + argumentsExpression[paramIndex] = Expression.Condition(nullConditionExpression, Expression.Default(parameterType), convertExpression); + } + + var newExpression = Expression.New(constructorInfo, argumentsExpression); + var lambda = Expression.Lambda>(newExpression, parametersExpression); + + return lambda.Compile(); + } + + /// + /// Creates and caches fast constructor invokers + /// + /// The type to extract fast constructor invokers for + /// A cached array of all public instance constructors from the given type. + /// The parameterless default constructor is always excluded from the list of activators + public static FastActivator[] GetActivators(Type type) + { + return GetActivatorsCore(type); } - /// - /// Creates an instance of the type using the specified parameters. - /// - /// A reference to the newly created object. - public object Activate(params object[] parameters) - { - return Invoker(parameters); - } - - #region Static - - /// - /// Creates a single fast constructor invoker. The result is not cached. - /// - /// constructorInfo to create invoker for. - /// a fast invoker. - public static Func MakeFastInvoker(ConstructorInfo constructorInfo) - { - var paramsInfo = constructorInfo.GetParameters(); - - var parametersExpression = Expression.Parameter(typeof(object[]), "args"); - var argumentsExpression = new Expression[paramsInfo.Length]; - - for (int paramIndex = 0; paramIndex < paramsInfo.Length; paramIndex++) - { - var indexExpression = Expression.Constant(paramIndex); - var parameterType = paramsInfo[paramIndex].ParameterType; - - var parameterIndexExpression = Expression.ArrayIndex(parametersExpression, indexExpression); - var convertExpression = Expression.Convert(parameterIndexExpression, parameterType); - argumentsExpression[paramIndex] = convertExpression; - - if (!parameterType.GetTypeInfo().IsValueType) - continue; - - var nullConditionExpression = Expression.Equal(parameterIndexExpression, Expression.Constant(null)); - argumentsExpression[paramIndex] = Expression.Condition(nullConditionExpression, Expression.Default(parameterType), convertExpression); - } - - var newExpression = Expression.New(constructorInfo, argumentsExpression); - var lambda = Expression.Lambda>(newExpression, parametersExpression); - - return lambda.Compile(); - } - - /// - /// Creates and caches fast constructor invokers - /// - /// The type to extract fast constructor invokers for - /// A cached array of all public instance constructors from the given type. - /// The parameterless default constructor is always excluded from the list of activators - public static FastActivator[] GetActivators(Type type) - { - return GetActivatorsCore(type); - } - - private static FastActivator[] GetActivatorsCore(Type type) - { - if (!_activatorsCache.TryGetValue(type, out FastActivator[] activators)) - { - var candidates = GetCandidateConstructors(type); - activators = candidates.Select(c => new FastActivator(c)).ToArray(); - _activatorsCache.TryAdd(type, activators); - } - - return activators; - } - - /// - /// Creates an instance of the specified type using the constructor that best matches the specified parameters. - /// - /// The type of object to create. - /// - /// An array of arguments that match in number, order, and type the parameters of the constructor to invoke. - /// If args is an empty array or null, the constructor that takes no parameters (the default constructor) is invoked. - /// - /// A reference to the newly created object. - public static T CreateInstance(params object[] args) - { - return (T)CreateInstance(typeof(T), args); - } - - /// - /// Creates an instance of the specified type using the constructor that best matches the specified parameters. - /// - /// The type of object to create. - /// - /// An array of arguments that match in number, order, and type the parameters of the constructor to invoke. - /// If args is an empty array or null, the constructor that takes no parameters (the default constructor) is invoked. - /// - /// A reference to the newly created object. - public static object CreateInstance(Type type, params object[] args) - { - Guard.NotNull(type, nameof(type)); - - if (args == null || args.Length == 0) - { - // don't struggle with FastActivator: native reflection is really fast with default constructor! - return Activator.CreateInstance(type); - } - - var activators = GetActivatorsCore(type); - var matchingActivator = FindMatchingActivatorCore(activators, type, args); - - if (matchingActivator == null) - { - throw new ArgumentException("No matching contructor was found for the given arguments.", "args"); - } - - return matchingActivator.Activate(args); - } - - public static FastActivator FindMatchingActivator(Type type, params object[] args) - { - var activators = GetActivatorsCore(type); - var matchingActivator = FindMatchingActivatorCore(activators, type, args); - - return matchingActivator; - } - - private static FastActivator FindMatchingActivatorCore(FastActivator[] activators, Type type, object[] args) - { - if (activators.Length == 0) - { - return null; - } - + private static FastActivator[] GetActivatorsCore(Type type) + { + if (!_activatorsCache.TryGetValue(type, out FastActivator[] activators)) + { + var candidates = GetCandidateConstructors(type); + activators = candidates.Select(c => new FastActivator(c)).ToArray(); + _activatorsCache.TryAdd(type, activators); + } + + return activators; + } + + /// + /// Creates an instance of the specified type using the constructor that best matches the specified parameters. + /// + /// The type of object to create. + /// + /// An array of arguments that match in number, order, and type the parameters of the constructor to invoke. + /// If args is an empty array or null, the constructor that takes no parameters (the default constructor) is invoked. + /// + /// A reference to the newly created object. + public static T CreateInstance(params object[] args) + { + return (T)CreateInstance(typeof(T), args); + } + + /// + /// Creates an instance of the specified type using the constructor that best matches the specified parameters. + /// + /// The type of object to create. + /// + /// An array of arguments that match in number, order, and type the parameters of the constructor to invoke. + /// If args is an empty array or null, the constructor that takes no parameters (the default constructor) is invoked. + /// + /// A reference to the newly created object. + public static object CreateInstance(Type type, params object[] args) + { + Guard.NotNull(type, nameof(type)); + + if (args == null || args.Length == 0) + { + // don't struggle with FastActivator: native reflection is really fast with default constructor! + return Activator.CreateInstance(type); + } + + var activators = GetActivatorsCore(type); + var matchingActivator = FindMatchingActivatorCore(activators, type, args); + + if (matchingActivator == null) + { + throw new ArgumentException("No matching contructor was found for the given arguments.", "args"); + } + + return matchingActivator.Activate(args); + } + + public static FastActivator FindMatchingActivator(Type type, params object[] args) + { + var activators = GetActivatorsCore(type); + var matchingActivator = FindMatchingActivatorCore(activators, type, args); + + return matchingActivator; + } + + private static FastActivator FindMatchingActivatorCore(FastActivator[] activators, Type type, object[] args) + { + if (activators.Length == 0) + { + return null; + } + if (activators.Length == 1) - { - // this seems to be bad design, but it's on purpose for performance reasons. - // In nearly ALL cases there is only one constructor. - return activators[0]; - } - - var argTypes = args.Select(x => x.GetType()).ToArray(); - var constructor = type.GetConstructor( - BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - null, - argTypes, - null); - - if (constructor != null) - { - var matchingActivator = activators.FirstOrDefault(a => a.Constructor == constructor); - return matchingActivator; - } - - return null; - } - - private static IEnumerable GetCandidateConstructors(Type type) - { - var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - return constructors.Where(c => c.GetParameters().Length > 0); + { + // this seems to be bad design, but it's on purpose for performance reasons. + // In nearly ALL cases there is only one constructor. + return activators[0]; + } + + var argTypes = args.Select(x => x.GetType()).ToArray(); + var constructor = type.GetConstructor( + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + null, + argTypes, + null); + + if (constructor != null) + { + var matchingActivator = activators.FirstOrDefault(a => a.Constructor == constructor); + return matchingActivator; + } + return null; } - private static void CheckIsValidType(Type type) - { - if (type.IsAbstract || type.IsInterface) - { - throw new ArgumentException("The type to create activators for must be concrete. Type: {0}".FormatInvariant(type.ToString()), "type"); - } - } + private static IEnumerable GetCandidateConstructors(Type type) + { + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + return constructors.Where(c => c.GetParameters().Length > 0); + + } + + private static void CheckIsValidType(Type type) + { + if (type.IsAbstract || type.IsInterface) + { + throw new ArgumentException("The type to create activators for must be concrete. Type: {0}".FormatInvariant(type.ToString()), "type"); + } + } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs index ba43358e1a..961f5ca100 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs @@ -1,293 +1,305 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Linq.Expressions; -using System.Collections.Concurrent; +using System.Reflection; using SmartStore.Utilities; namespace SmartStore.ComponentModel { - public class FastInvoker - { - private static readonly ConcurrentDictionary _invokersCache = new ConcurrentDictionary(); - - public FastInvoker(MethodInfo methodInfo) - { - Guard.NotNull(methodInfo, nameof(methodInfo)); - - Method = methodInfo; - Invoker = MakeFastInvoker(methodInfo); - ParameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray(); - } - - /// - /// Gets the backing . - /// - public MethodInfo Method { get; private set; } - - /// - /// Gets the parameter types from the backing - /// - public Type[] ParameterTypes { get; private set; } - - /// - /// Gets the method invoker. - /// - public Func Invoker { get; private set; } - - /// - /// Invokes the method using the specified parameters. - /// - /// The method invocation result. - public object Invoke(object obj, params object[] parameters) - { - return Invoker(obj, parameters); - } - - #region Static - - /// - /// Creates a single fast method invoker. The result is not cached. - /// - /// Method to create invoker for. - /// The fast method invoker delegate. - public static Func MakeFastInvoker(MethodInfo method) - { - Guard.NotNull(method, nameof(method)); - - var instanceParameterExpression = Expression.Parameter(typeof(object), "instance"); - var argumentsParameterExpression = Expression.Parameter(typeof(object[]), "args"); - var index = 0; - - var argumentExtractionExpressions = - method - .GetParameters() - .Select(parameter => - Expression.Convert( - Expression.ArrayAccess( - argumentsParameterExpression, - Expression.Constant(index++) - ), - parameter.ParameterType - ) - ).ToList(); - - var callExpression = method.IsStatic - ? Expression.Call(method, argumentExtractionExpressions) - : Expression.Call( - Expression.Convert( - instanceParameterExpression, - method.DeclaringType - ), - method, - argumentExtractionExpressions - ); - - var endLabel = Expression.Label(typeof(object)); - - var finalExpression = method.ReturnType == typeof(void) - ? (Expression)Expression.Block( - callExpression, - Expression.Return(endLabel, Expression.Constant(null)), - Expression.Label(endLabel, Expression.Constant(null)) - ) - : Expression.Convert(callExpression, typeof(object)); - - var lambdaExpression = Expression.Lambda>( - finalExpression, - instanceParameterExpression, - argumentsParameterExpression - ); - - var lamdba = lambdaExpression.Compile(); - return lamdba; - } - - /// - /// Invokes a method using the specified object and parameter instances. - /// - /// The objectinstance - /// Method name - /// Argument types of the matching method overload (in exact order) - /// Parameter instances to pass to invocation - /// The method invocation result. - public static object Invoke(object obj, string methodName, Type[] parameterTypes, object[] parameters) - { - Guard.NotNull(obj, nameof(obj)); - - FastInvoker invoker; - - if (parameterTypes == null || parameterTypes.Length == 0) - { - invoker = GetInvoker(obj.GetType(), methodName); - } - else - { - invoker = GetInvoker(obj.GetType(), methodName, parameterTypes); - } - - //var hasAnyNullParam = parameters.Any(x => x == null); - //if (hasAnyNullParam) - //{ - // throw new ArgumentException("When invoking a method with parameter instances, no instance must be null.", nameof(parameters)); - //} - - return invoker.Invoke(obj, parameters ?? Array.Empty()); - } - - /// - /// Creates and caches a fast method invoker. - /// - /// Name of method to create an invoker for. - /// Argument types of method to create an invoker for. - /// The fast method invoker. - public static FastInvoker GetInvoker(string methodName, params Type[] argTypes) - { - return GetInvoker(typeof(T), methodName, argTypes); - } - - /// - /// Creates and caches a fast method invoker. - /// - /// The type to extract fast method invoker for. - /// Name of method to create an invoker for. - /// Argument types of method to create an invoker for. - /// The fast method invoker. - public static FastInvoker GetInvoker(Type type, string methodName, params Type[] argTypes) - { - Guard.NotNull(type, nameof(type)); - Guard.NotEmpty(methodName, nameof(methodName)); - - var cacheKey = MethodKey.Create(type, methodName, argTypes); - - if (!_invokersCache.TryGetValue(cacheKey, out var invoker)) - { - var method = FindMatchingMethod(type, methodName, argTypes); - - if (method == null) - { - throw new MethodAccessException("Could not find a matching method '{0}' in type {1}.".FormatInvariant(methodName, type)); - } - - invoker = new FastInvoker(method); - _invokersCache.TryAdd(cacheKey, invoker); - } - - return invoker; - } - - /// - /// Creates and caches a fast method invoker. - /// - /// Method info instance to create an invoker for. - /// The fast method invoker. - public static FastInvoker GetInvoker(MethodInfo method) - { - Guard.NotNull(method, nameof(method)); - - var cacheKey = MethodKey.Create(method); - - if (!_invokersCache.TryGetValue(cacheKey, out var invoker)) - { - invoker = new FastInvoker(method); - _invokersCache.TryAdd(cacheKey, invoker); - } - - return invoker; - } - - private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[] argTypes) - { - var method = argTypes == null || argTypes.Length == 0 - ? type.GetMethod(methodName) - : type.GetMethod( - methodName, - BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, - null, - argTypes ?? new Type[0], - null); - - return method; - } - - #endregion - - abstract class MethodKey - { - public override bool Equals(object obj) => - throw new NotImplementedException(); - - public override int GetHashCode() => - throw new NotImplementedException(); - - public static bool operator ==(MethodKey left, MethodKey right) => - object.Equals(left, right); - - public static bool operator !=(MethodKey left, MethodKey right) => - !(left == right); - - internal static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes) - { - return new HashMethodKey(type, methodName, parameterTypes); - } - - internal static MethodKey Create(MethodInfo method) - { - return new MethodInfoKey(method); - } - } - - class HashMethodKey : MethodKey, IEquatable - { - private readonly int _hash; - - public HashMethodKey(Type type, string methodName, IEnumerable parameterTypes) - { - _hash = HashCodeCombiner.Start().Add(type).Add(methodName).Add(parameterTypes).CombinedHash; - } - - public override bool Equals(object obj) => - this.Equals(obj as HashMethodKey); - - public bool Equals(HashMethodKey other) - { - if (other == null) - return false; - - return this._hash == other._hash; - } - - public override int GetHashCode() - { - return _hash; - } - } - - class MethodInfoKey : MethodKey, IEquatable - { - private readonly MethodInfo _method; - - public MethodInfoKey(MethodInfo method) - { - _method = method; - } - - public override bool Equals(object obj) => - this.Equals(obj as MethodInfoKey); - - public bool Equals(MethodInfoKey other) - { - if (other == null) - return false; - - return this._method == other._method; - } - - public override int GetHashCode() - { - return _method.GetHashCode(); - } - } - } + public class FastInvoker + { + private static readonly ConcurrentDictionary _invokersCache = new ConcurrentDictionary(); + + public FastInvoker(MethodInfo methodInfo) + { + Guard.NotNull(methodInfo, nameof(methodInfo)); + + Method = methodInfo; + Invoker = MakeFastInvoker(methodInfo); + ParameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray(); + } + + /// + /// Gets the backing . + /// + public MethodInfo Method { get; private set; } + + /// + /// Gets the parameter types from the backing + /// + public Type[] ParameterTypes { get; private set; } + + /// + /// Gets the method invoker. + /// + public Func Invoker { get; private set; } + + /// + /// Invokes the method using the specified parameters. + /// + /// The method invocation result. + public object Invoke(object obj, params object[] parameters) + { + return Invoker(obj, parameters); + } + + #region Static + + /// + /// Creates a single fast method invoker. The result is not cached. + /// + /// Method to create invoker for. + /// The fast method invoker delegate. + public static Func MakeFastInvoker(MethodInfo method) + { + Guard.NotNull(method, nameof(method)); + + var instanceParameterExpression = Expression.Parameter(typeof(object), "instance"); + var argumentsParameterExpression = Expression.Parameter(typeof(object[]), "args"); + var index = 0; + + var argumentExtractionExpressions = + method + .GetParameters() + .Select(parameter => + Expression.Convert( + Expression.ArrayAccess( + argumentsParameterExpression, + Expression.Constant(index++) + ), + parameter.ParameterType + ) + ).ToList(); + + var callExpression = method.IsStatic + ? Expression.Call(method, argumentExtractionExpressions) + : Expression.Call( + Expression.Convert( + instanceParameterExpression, + method.DeclaringType + ), + method, + argumentExtractionExpressions + ); + + var endLabel = Expression.Label(typeof(object)); + + var finalExpression = method.ReturnType == typeof(void) + ? (Expression)Expression.Block( + callExpression, + Expression.Return(endLabel, Expression.Constant(null)), + Expression.Label(endLabel, Expression.Constant(null)) + ) + : Expression.Convert(callExpression, typeof(object)); + + var lambdaExpression = Expression.Lambda>( + finalExpression, + instanceParameterExpression, + argumentsParameterExpression + ); + + var lamdba = lambdaExpression.Compile(); + return lamdba; + } + + /// + /// Invokes a method using the specified object and parameter instances. + /// + /// The objectinstance + /// Method name + /// Argument types of the matching method overload (in exact order) + /// Parameter instances to pass to invocation + /// The method invocation result. + public static object Invoke(object obj, string methodName, Type[] parameterTypes, object[] parameters) + { + Guard.NotNull(obj, nameof(obj)); + + FastInvoker invoker; + + if (parameterTypes == null || parameterTypes.Length == 0) + { + invoker = GetInvoker(obj.GetType(), methodName); + } + else + { + invoker = GetInvoker(obj.GetType(), methodName, parameterTypes); + } + + //var hasAnyNullParam = parameters.Any(x => x == null); + //if (hasAnyNullParam) + //{ + // throw new ArgumentException("When invoking a method with parameter instances, no instance must be null.", nameof(parameters)); + //} + + return invoker.Invoke(obj, parameters ?? Array.Empty()); + } + + /// + /// Creates and caches a fast method invoker. + /// + /// Name of method to create an invoker for. + /// Argument types of method to create an invoker for. + /// The fast method invoker. + public static FastInvoker GetInvoker(string methodName, params Type[] argTypes) + { + return GetInvoker(typeof(T), methodName, argTypes); + } + + /// + /// Creates and caches a fast method invoker. + /// + /// The type to extract fast method invoker for. + /// Name of method to create an invoker for. + /// Argument types of method to create an invoker for. + /// The fast method invoker. + public static FastInvoker GetInvoker(Type type, string methodName, params Type[] argTypes) + { + Guard.NotNull(type, nameof(type)); + Guard.NotEmpty(methodName, nameof(methodName)); + + var cacheKey = MethodKey.Create(type, methodName, argTypes); + + if (!_invokersCache.TryGetValue(cacheKey, out var invoker)) + { + var method = FindMatchingMethod(type, methodName, argTypes); + + if (method == null) + { + throw new MethodAccessException("Could not find a matching method '{0}' in type {1}.".FormatInvariant(methodName, type)); + } + + invoker = new FastInvoker(method); + _invokersCache.TryAdd(cacheKey, invoker); + } + + return invoker; + } + + /// + /// Creates and caches a fast method invoker. + /// + /// Method info instance to create an invoker for. + /// The fast method invoker. + public static FastInvoker GetInvoker(MethodInfo method) + { + Guard.NotNull(method, nameof(method)); + + var cacheKey = MethodKey.Create(method); + + if (!_invokersCache.TryGetValue(cacheKey, out var invoker)) + { + invoker = new FastInvoker(method); + _invokersCache.TryAdd(cacheKey, invoker); + } + + return invoker; + } + + private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[] argTypes) + { + var method = argTypes == null || argTypes.Length == 0 + ? type.GetMethod(methodName) + : type.GetMethod( + methodName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + null, + argTypes ?? new Type[0], + null); + + return method; + } + + #endregion + + abstract class MethodKey + { + public override bool Equals(object obj) + { + throw new NotImplementedException(); + } + + public override int GetHashCode() + { + throw new NotImplementedException(); + } + + public static bool operator ==(MethodKey left, MethodKey right) + { + return object.Equals(left, right); + } + + public static bool operator !=(MethodKey left, MethodKey right) + { + return !(left == right); + } + + internal static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes) + { + return new HashMethodKey(type, methodName, parameterTypes); + } + + internal static MethodKey Create(MethodInfo method) + { + return new MethodInfoKey(method); + } + } + + class HashMethodKey : MethodKey, IEquatable + { + private readonly int _hash; + + public HashMethodKey(Type type, string methodName, IEnumerable parameterTypes) + { + _hash = HashCodeCombiner.Start().Add(type).Add(methodName).Add(parameterTypes).CombinedHash; + } + + public override bool Equals(object obj) + { + return this.Equals(obj as HashMethodKey); + } + + public bool Equals(HashMethodKey other) + { + if (other == null) + return false; + + return this._hash == other._hash; + } + + public override int GetHashCode() + { + return _hash; + } + } + + class MethodInfoKey : MethodKey, IEquatable + { + private readonly MethodInfo _method; + + public MethodInfoKey(MethodInfo method) + { + _method = method; + } + + public override bool Equals(object obj) + { + return this.Equals(obj as MethodInfoKey); + } + + public bool Equals(MethodInfoKey other) + { + if (other == null) + return false; + + return this._method == other._method; + } + + public override int GetHashCode() + { + return _method.GetHashCode(); + } + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs index f0462bb9e1..2f5301595a 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs @@ -10,52 +10,50 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using SmartStore.Utilities.Threading; namespace SmartStore.ComponentModel { - public enum PropertyCachingStrategy - { - /// - /// Don't cache FastProperty instances - /// - Uncached = 0, - /// - /// Always cache FastProperty instances - /// - Cached = 1, - /// - /// Always cache FastProperty instances. PLUS cache all other properties of the declaring type. - /// - EagerCached = 2 - } - - public abstract class FastProperty - { - private static readonly ConcurrentDictionary _singlePropertiesCache = new ConcurrentDictionary(); - - // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. - private static readonly ConcurrentDictionary> _propertiesCache = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary> _visiblePropertiesCache = new ConcurrentDictionary>(); + public enum PropertyCachingStrategy + { + /// + /// Don't cache FastProperty instances + /// + Uncached = 0, + /// + /// Always cache FastProperty instances + /// + Cached = 1, + /// + /// Always cache FastProperty instances. PLUS cache all other properties of the declaring type. + /// + EagerCached = 2 + } + + public abstract class FastProperty + { + private static readonly ConcurrentDictionary _singlePropertiesCache = new ConcurrentDictionary(); + + // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. + private static readonly ConcurrentDictionary> _propertiesCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> _visiblePropertiesCache = new ConcurrentDictionary>(); private Func _valueGetter; private Action _valueSetter; - private bool? _isPublicSettable; - private bool? _isSequenceType; - - /// - /// Initializes a . - /// This constructor does not cache the helper. For caching, use . - /// - [SuppressMessage("ReSharper", "VirtualMemberCallInContructor")] - protected FastProperty(PropertyInfo property) - { - Guard.NotNull(property, nameof(property)); - - Property = property; - Name = property.Name; - } + private bool? _isPublicSettable; + private bool? _isSequenceType; + + /// + /// Initializes a . + /// This constructor does not cache the helper. For caching, use . + /// + [SuppressMessage("ReSharper", "VirtualMemberCallInContructor")] + protected FastProperty(PropertyInfo property) + { + Guard.NotNull(property, nameof(property)); + + Property = property; + Name = property.Name; + } /// /// Gets the property value getter. @@ -72,10 +70,7 @@ public Func ValueGetter return _valueGetter; } - private set - { - _valueGetter = value; - } + private set => _valueGetter = value; } /// @@ -93,10 +88,7 @@ public Action ValueSetter return _valueSetter; } - private set - { - _valueSetter = value; - } + private set => _valueSetter = value; } protected abstract Func MakePropertyGetter(PropertyInfo propertyInfo); @@ -108,34 +100,34 @@ private set /// public PropertyInfo Property { get; private set; } - /// - /// Gets (or sets in derived types) the property name. - /// - public virtual string Name { get; protected set; } - - public bool IsPublicSettable - { - get - { - if (!_isPublicSettable.HasValue) - { - _isPublicSettable = Property.CanWrite && Property.GetSetMethod(false) != null; - } - return _isPublicSettable.Value; - } - } - - public bool IsSequenceType - { - get - { - if (!_isSequenceType.HasValue) - { - _isSequenceType = Property.PropertyType != typeof(string) && Property.PropertyType.IsSubClass(typeof(IEnumerable<>)); - } - return _isSequenceType.Value; - } - } + /// + /// Gets (or sets in derived types) the property name. + /// + public virtual string Name { get; protected set; } + + public bool IsPublicSettable + { + get + { + if (!_isPublicSettable.HasValue) + { + _isPublicSettable = Property.CanWrite && Property.GetSetMethod(false) != null; + } + return _isPublicSettable.Value; + } + } + + public bool IsSequenceType + { + get + { + if (!_isSequenceType.HasValue) + { + _isSequenceType = Property.PropertyType != typeof(string) && Property.PropertyType.IsSubClass(typeof(IEnumerable<>)); + } + return _isSequenceType.Value; + } + } /// /// Returns the property value for the specified . @@ -144,9 +136,9 @@ public bool IsSequenceType /// The property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public object GetValue(object instance) - { - return ValueGetter(instance); - } + { + return ValueGetter(instance); + } /// /// Sets the property value for the specified . @@ -155,94 +147,94 @@ public object GetValue(object instance) /// The property value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetValue(object instance, object value) - { - ValueSetter(instance, value); - } + { + ValueSetter(instance, value); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FastProperty GetProperty( - Expression> property, - PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) - { - return GetProperty(property.ExtractPropertyInfo(), cachingStrategy); - } - - public static FastProperty GetProperty( - Type type, - string propertyName, - PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) - { - Guard.NotNull(type, nameof(type)); - Guard.NotEmpty(propertyName, nameof(propertyName)); - - if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty)) - { - return fastProperty; - } - - var key = new PropertyKey(type, propertyName); - if (!_singlePropertiesCache.TryGetValue(key, out fastProperty)) - { - var pi = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (pi != null) - { - fastProperty = Create(pi); - if (cachingStrategy > PropertyCachingStrategy.Uncached) - { - _singlePropertiesCache.TryAdd(key, fastProperty); - } - } - } - - return fastProperty; - } - - public static FastProperty GetProperty( - PropertyInfo propertyInfo, - PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) - { - Guard.NotNull(propertyInfo, nameof(propertyInfo)); - - if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty)) - { - return fastProperty; - } - - var key = new PropertyKey(propertyInfo.ReflectedType, propertyInfo.Name); - if (!_singlePropertiesCache.TryGetValue(key, out fastProperty)) - { - fastProperty = Create(propertyInfo); - if (cachingStrategy > PropertyCachingStrategy.Uncached) - { - _singlePropertiesCache.TryAdd(key, fastProperty); - } - } - - return fastProperty; - } - - private static bool TryGetCachedProperty( - Type type, - string propertyName, - bool eagerCached, - out FastProperty fastProperty) - { - fastProperty = null; - IDictionary allProperties; - - if (eagerCached) - { - allProperties = (IDictionary)GetProperties(type); - allProperties.TryGetValue(propertyName, out fastProperty); - } - - if (fastProperty == null && _propertiesCache.TryGetValue(type, out allProperties)) - { - allProperties.TryGetValue(propertyName, out fastProperty); - } - - return fastProperty != null; - } + Expression> property, + PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + return GetProperty(property.ExtractPropertyInfo(), cachingStrategy); + } + + public static FastProperty GetProperty( + Type type, + string propertyName, + PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + Guard.NotNull(type, nameof(type)); + Guard.NotEmpty(propertyName, nameof(propertyName)); + + if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty)) + { + return fastProperty; + } + + var key = new PropertyKey(type, propertyName); + if (!_singlePropertiesCache.TryGetValue(key, out fastProperty)) + { + var pi = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (pi != null) + { + fastProperty = Create(pi); + if (cachingStrategy > PropertyCachingStrategy.Uncached) + { + _singlePropertiesCache.TryAdd(key, fastProperty); + } + } + } + + return fastProperty; + } + + public static FastProperty GetProperty( + PropertyInfo propertyInfo, + PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + Guard.NotNull(propertyInfo, nameof(propertyInfo)); + + if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty)) + { + return fastProperty; + } + + var key = new PropertyKey(propertyInfo.ReflectedType, propertyInfo.Name); + if (!_singlePropertiesCache.TryGetValue(key, out fastProperty)) + { + fastProperty = Create(propertyInfo); + if (cachingStrategy > PropertyCachingStrategy.Uncached) + { + _singlePropertiesCache.TryAdd(key, fastProperty); + } + } + + return fastProperty; + } + + private static bool TryGetCachedProperty( + Type type, + string propertyName, + bool eagerCached, + out FastProperty fastProperty) + { + fastProperty = null; + IDictionary allProperties; + + if (eagerCached) + { + allProperties = (IDictionary)GetProperties(type); + allProperties.TryGetValue(propertyName, out fastProperty); + } + + if (fastProperty == null && _propertiesCache.TryGetValue(type, out allProperties)) + { + allProperties.TryGetValue(propertyName, out fastProperty); + } + + return fastProperty != null; + } /// /// Given an object, adds each instance property with a public get method as a key and its @@ -261,38 +253,38 @@ private static bool TryGetCachedProperty( /// faster when the the same type is used multiple times with ObjectToDictionary. /// public static IDictionary ObjectToDictionary(object value, Func keySelector = null, bool deep = false) - { - if (value is IDictionary dictionary) - { - return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); - } + { + if (value is IDictionary dictionary) + { + return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } - dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (value != null) - { - keySelector = keySelector ?? new Func(key => key); + if (value != null) + { + keySelector = keySelector ?? new Func(key => key); - foreach (var prop in GetProperties(value.GetType()).Values) - { - var propValue = prop.GetValue(value); - if (deep && propValue != null && prop.Property.PropertyType.IsPlainObjectType()) - { - propValue = ObjectToDictionary(propValue, deep: true); - } + foreach (var prop in GetProperties(value.GetType()).Values) + { + var propValue = prop.GetValue(value); + if (deep && propValue != null && prop.Property.PropertyType.IsPlainObjectType()) + { + propValue = ObjectToDictionary(propValue, deep: true); + } - dictionary[keySelector(prop.Name)] = propValue; - } - } + dictionary[keySelector(prop.Name)] = propValue; + } + } - return dictionary; - } + return dictionary; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FastProperty Create(PropertyInfo property) - { - return new DelegatedAccessor(property); - } + { + return new DelegatedAccessor(property); + } /// @@ -437,46 +429,46 @@ private static void CleanDuplicates(Type type, IEnumerable propNames) } } - internal static IEnumerable GetCandidateProperties(Type type) - { - // We avoid loading indexed properties using the Where statement. - var properties = type.GetRuntimeProperties().Where(IsCandidateProperty); - - var typeInfo = type.GetTypeInfo(); - if (typeInfo.IsInterface) - { - // Reflection does not return information about inherited properties on the interface itself. - properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany( - interfaceType => interfaceType.GetRuntimeProperties().Where(IsCandidateProperty))); - } - - return properties; - } - - // Indexed properties are not useful (or valid) for grabbing properties off an object. - private static bool IsCandidateProperty(PropertyInfo property) - { - return property.GetIndexParameters().Length == 0 && - property.GetMethod != null && - property.GetMethod.IsPublic && - !property.GetMethod.IsStatic; - } - - private static ConcurrentDictionary> CreateVolatileCache() - { - return new ConcurrentDictionary>(); - } - - class PropertyKey : Tuple - { - public PropertyKey(Type type, string propertyName) - : base(type, propertyName) - { - } - public Type Type { get { return base.Item1; } } - public string PropertyName { get { return base.Item2; } } - } - } + internal static IEnumerable GetCandidateProperties(Type type) + { + // We avoid loading indexed properties using the Where statement. + var properties = type.GetRuntimeProperties().Where(IsCandidateProperty); + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsInterface) + { + // Reflection does not return information about inherited properties on the interface itself. + properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany( + interfaceType => interfaceType.GetRuntimeProperties().Where(IsCandidateProperty))); + } + + return properties; + } + + // Indexed properties are not useful (or valid) for grabbing properties off an object. + private static bool IsCandidateProperty(PropertyInfo property) + { + return property.GetIndexParameters().Length == 0 && + property.GetMethod != null && + property.GetMethod.IsPublic && + !property.GetMethod.IsStatic; + } + + private static ConcurrentDictionary> CreateVolatileCache() + { + return new ConcurrentDictionary>(); + } + + class PropertyKey : Tuple + { + public PropertyKey(Type type, string propertyName) + : base(type, propertyName) + { + } + public Type Type => base.Item1; + public string PropertyName => base.Item2; + } + } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastPropertyValueProvider.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastPropertyValueProvider.cs index afa4c9a0d2..2a3a22d7f7 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastPropertyValueProvider.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastPropertyValueProvider.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json.Serialization; namespace SmartStore.ComponentModel @@ -32,7 +28,7 @@ public object GetValue(object target) { return _inner.GetValue(target); } - + if (_getter == null) { try diff --git a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs index 78ebbdc562..0bb0f7142d 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs @@ -32,22 +32,22 @@ #endregion using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Dynamic; +using System.Linq; using System.Reflection; -using System.Collections; using SmartStore.Utilities; namespace SmartStore.ComponentModel { public enum MemberOptMethod - { - Allow, - Disallow - } - - /// + { + Allow, + Disallow + } + + /// /// Class that provides extensible properties and methods to an /// existing object when cast to dynamic. This /// dynamic object stores 'extra' properties in a dictionary or @@ -78,33 +78,33 @@ public class HybridExpando : DynamicObject, IDictionary /// private Type _instanceType; - /// - /// Adjusted property list for the wrapped instance type after white/black-list members has been applied. - /// - private IDictionary _instanceProps; - private readonly static IDictionary EmptyProps = new Dictionary(); - - /// - /// String Dictionary that contains the extra dynamic values - /// stored on this object/instance - /// - /// Using PropertyBag to support XML Serialization of the dictionary - public PropertyBag Properties = new PropertyBag(); - - private readonly HashSet _optMembers; - private readonly MemberOptMethod _optMethod; - - private readonly bool _returnNullWhenFalsy; - - /// - /// This constructor just works off the internal dictionary and any - /// public properties of this object. - /// - /// Note you can subclass HybridExpando. - /// - public HybridExpando(bool returnNullWhenFalsy = false) + /// + /// Adjusted property list for the wrapped instance type after white/black-list members has been applied. + /// + private IDictionary _instanceProps; + private readonly static IDictionary EmptyProps = new Dictionary(); + + /// + /// String Dictionary that contains the extra dynamic values + /// stored on this object/instance + /// + /// Using PropertyBag to support XML Serialization of the dictionary + public PropertyBag Properties = new PropertyBag(); + + private readonly HashSet _optMembers; + private readonly MemberOptMethod _optMethod; + + private readonly bool _returnNullWhenFalsy; + + /// + /// This constructor just works off the internal dictionary and any + /// public properties of this object. + /// + /// Note you can subclass HybridExpando. + /// + public HybridExpando(bool returnNullWhenFalsy = false) { - _returnNullWhenFalsy = returnNullWhenFalsy; + _returnNullWhenFalsy = returnNullWhenFalsy; } /// @@ -117,169 +117,166 @@ public HybridExpando(bool returnNullWhenFalsy = false) /// public HybridExpando(object instance, bool returnNullWhenFalsy = false) { - Guard.NotNull(instance, nameof(instance)); + Guard.NotNull(instance, nameof(instance)); - _returnNullWhenFalsy = returnNullWhenFalsy; - Initialize(instance); + _returnNullWhenFalsy = returnNullWhenFalsy; + Initialize(instance); } - /// - /// Allows passing in an existing instance variable to 'extend' - /// along with a list of member names to allow or disallow. - /// - /// - public HybridExpando(object instance, IEnumerable optMembers, MemberOptMethod optMethod, bool returnNullWhenFalsy = false) - { - Guard.NotNull(instance, nameof(instance)); - - _returnNullWhenFalsy = returnNullWhenFalsy; - Initialize(instance); - - _optMethod = optMethod; - - if (optMembers is HashSet h) - { - _optMembers = h; - } - else - { - _optMembers = new HashSet(optMembers); - } - } - - protected void Initialize(object instance) + /// + /// Allows passing in an existing instance variable to 'extend' + /// along with a list of member names to allow or disallow. + /// + /// + public HybridExpando(object instance, IEnumerable optMembers, MemberOptMethod optMethod, bool returnNullWhenFalsy = false) + { + Guard.NotNull(instance, nameof(instance)); + + _returnNullWhenFalsy = returnNullWhenFalsy; + Initialize(instance); + + _optMethod = optMethod; + + if (optMembers is HashSet h) + { + _optMembers = h; + } + else + { + _optMembers = new HashSet(optMembers); + } + } + + protected void Initialize(object instance) { _instance = instance; - _instanceType = instance?.GetType(); + _instanceType = instance?.GetType(); } - protected object WrappedObject - { - get { return _instance; } - } + protected object WrappedObject => _instance; - public override IEnumerable GetDynamicMemberNames() + public override IEnumerable GetDynamicMemberNames() { - foreach (var kvp in this.Properties.Keys) - { - yield return kvp; - } - - foreach (var kvp in GetInstanceProperties()) - { - if (!this.Properties.ContainsKey(kvp.Key)) - { - yield return kvp.Key; - } - } - } - - - /// - /// Try to retrieve a member by name first from instance properties - /// followed by the collection entries. - /// - /// - /// - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) + foreach (var kvp in this.Properties.Keys) + { + yield return kvp; + } + + foreach (var kvp in GetInstanceProperties()) + { + if (!this.Properties.ContainsKey(kvp.Key)) + { + yield return kvp.Key; + } + } + } + + + /// + /// Try to retrieve a member by name first from instance properties + /// followed by the collection entries. + /// + /// + /// + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) { - return TryGetMemberCore(binder.Name, out result); + return TryGetMemberCore(binder.Name, out result); } - protected virtual bool TryGetMemberCore(string name, out object result) - { - result = null; - bool exists = false; - - // first check the Properties collection for member - if (Properties.ContainsKey(name)) - { - result = Properties[name]; - exists = true; - } - - // Next check for public properties via Reflection - if (!exists && _instance != null) - { - try - { - exists = GetProperty(_instance, name, out result); - } - catch { } - } - - // Falsy check - if (_returnNullWhenFalsy && result != null && !CommonHelper.IsTruthy(result)) - { - result = null; - } - - // failed to retrieve a property - return exists; - } - - - /// - /// Property setter implementation tries to retrieve value from instance - /// first then into this object - /// - /// - /// - /// - public override bool TrySetMember(SetMemberBinder binder, object value) + protected virtual bool TryGetMemberCore(string name, out object result) { - return TrySetMemberCore(binder.Name, value); + result = null; + bool exists = false; + + // first check the Properties collection for member + if (Properties.ContainsKey(name)) + { + result = Properties[name]; + exists = true; + } + + // Next check for public properties via Reflection + if (!exists && _instance != null) + { + try + { + exists = GetProperty(_instance, name, out result); + } + catch { } + } + + // Falsy check + if (_returnNullWhenFalsy && result != null && !CommonHelper.IsTruthy(result)) + { + result = null; + } + + // failed to retrieve a property + return exists; + } + + + /// + /// Property setter implementation tries to retrieve value from instance + /// first then into this object + /// + /// + /// + /// + public override bool TrySetMember(SetMemberBinder binder, object value) + { + return TrySetMemberCore(binder.Name, value); + } + + protected virtual bool TrySetMemberCore(string name, object value) + { + // first check to see if there's a dictionary entry to set + if (Properties.ContainsKey(name)) + { + Properties[name] = value; + return true; + } + + // Check to see if there's a native property to set + if (_instance != null) + { + try + { + bool result = SetProperty(_instance, name, value); + if (result) + return true; + } + catch { } + } + + // no match - set or add to dictionary + Properties[name] = value; + return true; } - protected virtual bool TrySetMemberCore(string name, object value) - { - // first check to see if there's a dictionary entry to set - if (Properties.ContainsKey(name)) - { - Properties[name] = value; - return true; - } - - // Check to see if there's a native property to set - if (_instance != null) - { - try - { - bool result = SetProperty(_instance, name, value); - if (result) - return true; - } - catch { } - } - - // no match - set or add to dictionary - Properties[name] = value; - return true; - } - - /// - /// Dynamic invocation method. Currently allows only for Reflection based - /// operation (no ability to add methods dynamically). - /// - /// - /// - /// - public void Override(string name, object value = null) - { - Guard.NotEmpty(name, nameof(name)); - Properties[name] = value; - } - - /// - /// Dynamic invocation method. Currently allows only for Reflection based - /// operation (no ability to add methods dynamically). - /// - /// - /// - /// - /// - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + /// + /// Dynamic invocation method. Currently allows only for Reflection based + /// operation (no ability to add methods dynamically). + /// + /// + /// + /// + public void Override(string name, object value = null) + { + Guard.NotEmpty(name, nameof(name)); + Properties[name] = value; + } + + /// + /// Dynamic invocation method. Currently allows only for Reflection based + /// operation (no ability to add methods dynamically). + /// + /// + /// + /// + /// + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { if (_instance != null) { @@ -306,15 +303,15 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o /// protected bool GetProperty(object instance, string name, out object result) { - if (GetInstanceProperties().TryGetValue(name, out var fastProp)) - { - result = fastProp.GetValue(instance ?? this); - return true; - } + if (GetInstanceProperties().TryGetValue(name, out var fastProp)) + { + result = fastProp.GetValue(instance ?? this); + return true; + } - result = null; + result = null; return false; - } + } /// /// Reflection helper method to set a property value @@ -325,13 +322,13 @@ protected bool GetProperty(object instance, string name, out object result) /// protected bool SetProperty(object instance, string name, object value) { - if (GetInstanceProperties().TryGetValue(name, out var fastProp)) - { - fastProp.SetValue(instance ?? this, value); - return true; - } + if (GetInstanceProperties().TryGetValue(name, out var fastProp)) + { + fastProp.SetValue(instance ?? this, value); + return true; + } - return false; + return false; } /// @@ -349,7 +346,7 @@ protected bool InvokeMethod(object instance, string name, object[] args, out obj if (mi != null) { result = mi.Invoke(instance ?? this, args); - return true; + return true; } result = null; @@ -357,90 +354,87 @@ protected bool InvokeMethod(object instance, string name, object[] args, out obj } - /// - /// Convenience method that provides a string Indexer - /// to the Properties collection AND the strongly typed - /// properties of the object by name. - /// - /// // dynamic - /// exp["Address"] = "112 nowhere lane"; - /// // strong - /// var name = exp["StronglyTypedProperty"] as string; - /// - /// - /// The getter checks the Properties dictionary first - /// then looks in PropertyInfo for properties. - /// The setter checks the instance properties before - /// checking the Properties dictionary. - /// - /// - /// - /// - public object this[string key] - { - get - { - if (!TryGetMemberCore(key, out var result)) - { - throw new KeyNotFoundException(); - } - - return result; - } - set - { - TrySetMemberCore(key, value); - } - } - - - /// - /// Returns all properties - /// - /// - /// - public IEnumerable> GetProperties(bool includeInstanceProperties = false) + /// + /// Convenience method that provides a string Indexer + /// to the Properties collection AND the strongly typed + /// properties of the object by name. + /// + /// // dynamic + /// exp["Address"] = "112 nowhere lane"; + /// // strong + /// var name = exp["StronglyTypedProperty"] as string; + /// + /// + /// The getter checks the Properties dictionary first + /// then looks in PropertyInfo for properties. + /// The setter checks the instance properties before + /// checking the Properties dictionary. + /// + /// + /// + /// + public object this[string key] { - foreach (var kvp in this.Properties) - { - yield return kvp; - } - - if (includeInstanceProperties) + get + { + if (!TryGetMemberCore(key, out var result)) + { + throw new KeyNotFoundException(); + } + + return result; + } + set => TrySetMemberCore(key, value); + } + + + /// + /// Returns all properties + /// + /// + /// + public IEnumerable> GetProperties(bool includeInstanceProperties = false) + { + foreach (var kvp in this.Properties) + { + yield return kvp; + } + + if (includeInstanceProperties) { foreach (var prop in GetInstanceProperties().Values) - { - if (!this.Properties.ContainsKey(prop.Name)) - { - yield return new KeyValuePair(prop.Name, prop.GetValue(_instance)); - } - } + { + if (!this.Properties.ContainsKey(prop.Name)) + { + yield return new KeyValuePair(prop.Name, prop.GetValue(_instance)); + } + } } } - private IDictionary GetInstanceProperties() - { - if (_instance == null) - { - return EmptyProps; - } + private IDictionary GetInstanceProperties() + { + if (_instance == null) + { + return EmptyProps; + } - if (_instanceProps == null) - { - var props = FastProperty.GetProperties(_instance.GetType()) as IDictionary; + if (_instanceProps == null) + { + var props = FastProperty.GetProperties(_instance.GetType()) as IDictionary; - if (_optMembers != null) - { - props = props - .Where(x => _optMethod == MemberOptMethod.Allow ? _optMembers.Contains(x.Key) : !_optMembers.Contains(x.Key)) - .ToDictionary(x => x.Key, x => x.Value); - } + if (_optMembers != null) + { + props = props + .Where(x => _optMethod == MemberOptMethod.Allow ? _optMembers.Contains(x.Key) : !_optMembers.Contains(x.Key)) + .ToDictionary(x => x.Key, x => x.Value); + } - _instanceProps = props; - } + _instanceProps = props; + } - return _instanceProps; - } + return _instanceProps; + } /// /// Checks whether a property exists in the Property collection @@ -463,128 +457,98 @@ public bool Contains(KeyValuePair item, bool includeInstanceProp public bool Contains(string propertyName, bool includeInstanceProperties = false) { if (Properties.ContainsKey(propertyName)) - { - return true; - } + { + return true; + } if (includeInstanceProperties) { - return GetInstanceProperties().ContainsKey(propertyName); - } + return GetInstanceProperties().ContainsKey(propertyName); + } return false; } - #region IDictionary - - ICollection IDictionary.Keys - { - get - { - return GetProperties(true).Select(x => x.Key).AsReadOnly(); - } - } - - ICollection IDictionary.Values - { - get - { - return GetProperties(true).Select(x => x.Value).AsReadOnly(); - } - } - - int ICollection>.Count - { - get - { - return GetDynamicMemberNames().Count(); - } - } - - bool ICollection>.IsReadOnly - { - get - { - return false; - } - } - - object IDictionary.this[string key] - { - get - { - return this[key]; - } - - set - { - this[key] = value; - } - } - - bool IDictionary.ContainsKey(string key) - { - return Contains(key, true); - } - - void IDictionary.Add(string key, object value) - { - throw new NotImplementedException(); - } - - bool IDictionary.Remove(string key) - { - throw new NotImplementedException(); - } - - public bool TryGetValue(string key, out object value) - { - value = null; - - if (this.Contains(key, true)) - { - value = this[key]; - return true; - } - - return false; - } - - void ICollection>.Add(KeyValuePair item) - { - throw new NotImplementedException(); - } - - void ICollection>.Clear() - { - throw new NotImplementedException(); - } - - bool ICollection>.Contains(KeyValuePair item) - { - return Contains(item.Key, true); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection>.Remove(KeyValuePair item) - { - throw new NotImplementedException(); - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - return GetProperties(true).GetEnumerator(); + #region IDictionary + + ICollection IDictionary.Keys => GetProperties(true).Select(x => x.Key).AsReadOnly(); + + ICollection IDictionary.Values => GetProperties(true).Select(x => x.Value).AsReadOnly(); + + int ICollection>.Count => GetDynamicMemberNames().Count(); + + bool ICollection>.IsReadOnly => false; + + object IDictionary.this[string key] + { + get => this[key]; + + set => this[key] = value; + } + + bool IDictionary.ContainsKey(string key) + { + return Contains(key, true); + } + + void IDictionary.Add(string key, object value) + { + throw new NotImplementedException(); + } + + bool IDictionary.Remove(string key) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, out object value) + { + value = null; + + if (this.Contains(key, true)) + { + value = this[key]; + return true; + } + + return false; + } + + void ICollection>.Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void ICollection>.Clear() + { + throw new NotImplementedException(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return Contains(item.Key, true); } - IEnumerator IEnumerable.GetEnumerator() - { - return GetProperties(true).GetEnumerator(); - } + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetProperties(true).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetProperties(true).GetEnumerator(); + } - #endregion - } + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/ComponentModel/IMapper.cs b/src/Libraries/SmartStore.Core/ComponentModel/IMapper.cs index 8be7514677..5b56d46ef9 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/IMapper.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/IMapper.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.ComponentModel { diff --git a/src/Libraries/SmartStore.Core/ComponentModel/MapperFactory.cs b/src/Libraries/SmartStore.Core/ComponentModel/MapperFactory.cs index b1cd11add9..a198bd7d0c 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/MapperFactory.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/MapperFactory.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; using SmartStore.Core.Infrastructure; namespace SmartStore.ComponentModel @@ -12,7 +10,7 @@ public static class MapperFactory { private static IDictionary _mapperTypes = null; private readonly static object _lock = new object(); - + private static void EnsureInitialized() { if (_mapperTypes == null) @@ -31,7 +29,7 @@ private static void EnsureInitialized() } } } - + /// /// For testing purposes /// @@ -95,8 +93,8 @@ public TypePair(Type fromType, Type toType) { } - public Type FromType { get => base.Item1; } - public Type ToType { get => base.Item2; } + public Type FromType => base.Item1; + public Type ToType => base.Item2; } class GenericMapper : IMapper diff --git a/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs b/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs index d2c3c178ab..ad3c8cc012 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs @@ -1,27 +1,23 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Reflection; using System.Globalization; -using SmartStore.Utilities; +using System.Linq; using SmartStore.Core; +using SmartStore.Utilities; namespace SmartStore.ComponentModel { - /// - /// A very simple object mapper utility which tries to map properties of the same name between two objects. - /// If matched properties has different types, the mapper tries to convert them. - /// If conversion fails, the property is skipped (no exception is thrown). - /// MiniMapper cannot handle sequence and predefined types. - /// - public static class MiniMapper - { - public static TTo Map(TFrom from, CultureInfo culture = null) - where TFrom : class - where TTo : class, new() - { + /// + /// A very simple object mapper utility which tries to map properties of the same name between two objects. + /// If matched properties has different types, the mapper tries to convert them. + /// If conversion fails, the property is skipped (no exception is thrown). + /// MiniMapper cannot handle sequence and predefined types. + /// + public static class MiniMapper + { + public static TTo Map(TFrom from, CultureInfo culture = null) + where TFrom : class + where TTo : class, new() + { Guard.NotNull(from, nameof(from)); if (TryMap(from, from.GetType(), typeof(TTo), culture, out var result)) @@ -30,29 +26,29 @@ public static TTo Map(TFrom from, CultureInfo culture = null) } return default(TTo); - } + } - public static object Map(TFrom from, Type toType, CultureInfo culture = null) - where TFrom : class - { + public static object Map(TFrom from, Type toType, CultureInfo culture = null) + where TFrom : class + { Guard.NotNull(from, nameof(from)); Guard.NotNull(toType, nameof(toType)); - Guard.HasDefaultConstructor(toType); + Guard.HasDefaultConstructor(toType); TryMap(from, from.GetType(), toType, culture, out var result); return result; - } + } - public static void Map(TFrom from, TTo to, CultureInfo culture = null) - where TFrom : class - where TTo : class - { - Guard.NotNull(from, nameof(from)); - Guard.NotNull(to, nameof(to)); + public static void Map(TFrom from, TTo to, CultureInfo culture = null) + where TFrom : class + where TTo : class + { + Guard.NotNull(from, nameof(from)); + Guard.NotNull(to, nameof(to)); MapComplex(from, to, from.GetType(), culture ?? CultureInfo.CurrentCulture); - } + } private static bool TryMap(object from, Type fromType, Type toType, CultureInfo culture, out object result) { @@ -111,12 +107,12 @@ private static void MapComplex(object from, object to, Type fromType, CultureInf } } - private static FastProperty[] GetFastPropertiesFor(Type type) - { + private static FastProperty[] GetFastPropertiesFor(Type type) + { return FastProperty.GetProperties(type, PropertyCachingStrategy.Uncached) .Values .Where(pi => pi.IsPublicSettable) .ToArray(); - } - } + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs index 328ef76ce0..8b5d5e0db9 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Xml.Serialization; -using System.Xml; using System.Reflection; +using System.Xml; +using System.Xml.Serialization; namespace SmartStore.ComponentModel { diff --git a/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs b/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs index e36995facd..5228dbcd56 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs @@ -32,12 +32,12 @@ #endregion using System; +using System.Diagnostics; using System.IO; +using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Xml; using System.Xml.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using System.Diagnostics; namespace SmartStore.ComponentModel { @@ -187,25 +187,25 @@ public static bool SerializeObject(object instance, out string xmlResultString, return true; } - /// - /// Serializes an object instance to a file. - /// - /// the object instance to serialize - /// - /// - /// - public static bool SerializeObject(object instance, out byte[] resultBuffer, bool throwExceptions = false) + /// + /// Serializes an object instance to a file. + /// + /// the object instance to serialize + /// + /// + /// + public static bool SerializeObject(object instance, out byte[] resultBuffer, bool throwExceptions = false) { bool retVal = true; - resultBuffer = null; + resultBuffer = null; - var ms = new MemoryStream(); + var ms = new MemoryStream(); try { var serializer = new BinaryFormatter(); serializer.Serialize(ms, instance); - resultBuffer = ms.ToArray(); - } + resultBuffer = ms.ToArray(); + } catch (Exception ex) { Debug.Write("SerializeObject failed with : " + ex.GetBaseException().Message, "West Wind"); @@ -216,7 +216,7 @@ public static bool SerializeObject(object instance, out byte[] resultBuffer, boo } finally { - ms.Close(); + ms.Close(); } return retVal; diff --git a/src/Libraries/SmartStore.Core/ComponentModel/SmartContractResolver.cs b/src/Libraries/SmartStore.Core/ComponentModel/SmartContractResolver.cs index 64f17f7f8c..1a4d448f7f 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/SmartContractResolver.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/SmartContractResolver.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; using Newtonsoft.Json.Serialization; namespace SmartStore.ComponentModel diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs index a269a9f92d..94b39225b3 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs @@ -2,83 +2,74 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] - public class BooleanConverter : DefaultTypeConverter - { - private readonly HashSet _trueValues; - private readonly HashSet _falseValues; + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class BooleanConverter : DefaultTypeConverter + { + private readonly HashSet _trueValues; + private readonly HashSet _falseValues; - public BooleanConverter(string[] trueValues, string[] falseValues) - : base(typeof(bool)) - { - _trueValues = new HashSet(trueValues, StringComparer.OrdinalIgnoreCase); - _falseValues = new HashSet(falseValues, StringComparer.OrdinalIgnoreCase); - } + public BooleanConverter(string[] trueValues, string[] falseValues) + : base(typeof(bool)) + { + _trueValues = new HashSet(trueValues, StringComparer.OrdinalIgnoreCase); + _falseValues = new HashSet(falseValues, StringComparer.OrdinalIgnoreCase); + } - public ICollection TrueValues - { - get { return _trueValues; } - } + public ICollection TrueValues => _trueValues; - public ICollection FalseValues - { - get { return _falseValues; } - } + public ICollection FalseValues => _falseValues; - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is short) - { - if ((short)value == 0) - { - return false; - } - if ((short)value == 1) - { - return true; - } - } + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is short) + { + if ((short)value == 0) + { + return false; + } + if ((short)value == 1) + { + return true; + } + } - if (value is string) - { - var str = (string)value; + if (value is string) + { + var str = (string)value; - if (bool.TryParse(str, out var b)) - { - return b; - } + if (bool.TryParse(str, out var b)) + { + return b; + } - if (short.TryParse(str, out var sh)) - { - if (sh == 0) - { - return false; - } - if (sh == 1) - { - return true; - } - } + if (short.TryParse(str, out var sh)) + { + if (sh == 0) + { + return false; + } + if (sh == 1) + { + return true; + } + } - str = (str.NullEmpty() ?? string.Empty).Trim(); - if (_trueValues.Contains(str)) - { - return true; - } + str = (str.NullEmpty() ?? string.Empty).Trim(); + if (_trueValues.Contains(str)) + { + return true; + } - if (_falseValues.Contains(str)) - { - return false; - } - } + if (_falseValues.Contains(str)) + { + return false; + } + } - return base.ConvertFrom(culture, value); - } - } + return base.ConvertFrom(culture, value); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs index c3184362c9..56eba9b9b2 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs @@ -1,110 +1,105 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] - public class DateTimeConverter : DefaultTypeConverter - { - public DateTimeConverter() - : base(typeof(DateTime)) - { - } - - public override bool CanConvertFrom(Type type) - { - return type == typeof(string) - || type == typeof(long) - || type == typeof(double) - || type == typeof(TimeSpan) - || base.CanConvertFrom(type); - } - - public override bool CanConvertTo(Type type) - { - return type == typeof(string) - || type == typeof(long) - || type == typeof(double) - || type == typeof(DateTimeOffset) - || type == typeof(TimeSpan) - || base.CanConvertTo(type); - } - - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is TimeSpan) - { - var span = (TimeSpan)value; - return new DateTime(span.Ticks); - } - - if (value is string) - { - var str = (string)value; - - DateTime time; - if (DateTime.TryParse(str, culture, DateTimeStyles.None, out time)) - { - return time; - } - - long lng; - if (long.TryParse(str, NumberStyles.None, culture, out lng)) - { - return lng.FromUnixTime(); - } - - double dbl; - if (double.TryParse(str, NumberStyles.AllowDecimalPoint, culture, out dbl)) - { - return DateTime.FromOADate(dbl); - } + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class DateTimeConverter : DefaultTypeConverter + { + public DateTimeConverter() + : base(typeof(DateTime)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string) + || type == typeof(long) + || type == typeof(double) + || type == typeof(TimeSpan) + || base.CanConvertFrom(type); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string) + || type == typeof(long) + || type == typeof(double) + || type == typeof(DateTimeOffset) + || type == typeof(TimeSpan) + || base.CanConvertTo(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is TimeSpan) + { + var span = (TimeSpan)value; + return new DateTime(span.Ticks); } - if (value is long) - { - return ((long)value).FromUnixTime(); - } + if (value is string) + { + var str = (string)value; + + DateTime time; + if (DateTime.TryParse(str, culture, DateTimeStyles.None, out time)) + { + return time; + } + + long lng; + if (long.TryParse(str, NumberStyles.None, culture, out lng)) + { + return lng.FromUnixTime(); + } + + double dbl; + if (double.TryParse(str, NumberStyles.AllowDecimalPoint, culture, out dbl)) + { + return DateTime.FromOADate(dbl); + } + } - if (value is double) - { - return DateTime.FromOADate((double)value); - } + if (value is long) + { + return ((long)value).FromUnixTime(); + } - return base.ConvertFrom(culture, value); - } + if (value is double) + { + return DateTime.FromOADate((double)value); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - var time = (DateTime)value; + return base.ConvertFrom(culture, value); + } - if (to == typeof(DateTimeOffset)) - { - return new DateTimeOffset(time); - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + var time = (DateTime)value; - if (to == typeof(TimeSpan)) - { - return new TimeSpan(time.Ticks); - } + if (to == typeof(DateTimeOffset)) + { + return new DateTimeOffset(time); + } - if (to == typeof(double)) - { - return time.ToOADate(); - } + if (to == typeof(TimeSpan)) + { + return new TimeSpan(time.Ticks); + } - if (to == typeof(long)) - { - return time.ToUnixTime(); - } + if (to == typeof(double)) + { + return time.ToOADate(); + } + + if (to == typeof(long)) + { + return time.ToUnixTime(); + } - return base.ConvertTo(culture, format, value, to); + return base.ConvertTo(culture, format, value, to); } - } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DefaultTypeConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DefaultTypeConverter.cs index 27d789e285..a2ca535d1f 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DefaultTypeConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DefaultTypeConverter.cs @@ -5,37 +5,37 @@ namespace SmartStore.ComponentModel { public class DefaultTypeConverter : ITypeConverter - { - private readonly Lazy _systemConverter; - private readonly Type _type; + { + private readonly Lazy _systemConverter; + private readonly Type _type; private readonly bool _typeIsConvertible; private readonly bool _typeIsEnum; public DefaultTypeConverter(Type type) - { - Guard.NotNull(type, nameof(type)); + { + Guard.NotNull(type, nameof(type)); - _type = type; + _type = type; _typeIsConvertible = typeof(IConvertible).IsAssignableFrom(type); _typeIsEnum = type.IsEnum; _systemConverter = new Lazy(() => TypeDescriptor.GetConverter(type), true); - } - - public TypeConverter SystemConverter - { - get - { - if (_type == typeof(object)) - { - return null; - } - - return _systemConverter.Value; - } - } - - public virtual bool CanConvertFrom(Type type) - { + } + + public TypeConverter SystemConverter + { + get + { + if (_type == typeof(object)) + { + return null; + } + + return _systemConverter.Value; + } + } + + public virtual bool CanConvertFrom(Type type) + { // Use Convert.ChangeType if both types are IConvertible if (_typeIsConvertible && typeof(IConvertible).IsAssignableFrom(type)) { @@ -48,10 +48,10 @@ public virtual bool CanConvertFrom(Type type) } return false; - } + } - public virtual bool CanConvertTo(Type type) - { + public virtual bool CanConvertTo(Type type) + { // Use Convert.ChangeType if both types are IConvertible if (_typeIsConvertible && typeof(IConvertible).IsAssignableFrom(type)) { @@ -71,8 +71,8 @@ public virtual bool CanConvertTo(Type type) return false; } - public virtual object ConvertFrom(CultureInfo culture, object value) - { + public virtual object ConvertFrom(CultureInfo culture, object value) + { // Use Convert.ChangeType if both types are IConvertible if (!_typeIsEnum && _typeIsConvertible && value is IConvertible) { @@ -80,15 +80,15 @@ public virtual object ConvertFrom(CultureInfo culture, object value) } if (SystemConverter != null) - { - return SystemConverter.ConvertFrom(null, culture, value); - } + { + return SystemConverter.ConvertFrom(null, culture, value); + } throw Error.InvalidCast(value.GetType(), _type); - } + } - public virtual object ConvertTo(CultureInfo culture, string format, object value, Type to) - { + public virtual object ConvertTo(CultureInfo culture, string format, object value, Type to) + { // Use Convert.ChangeType if both types are IConvertible if (!_typeIsEnum && _typeIsConvertible && value != null && typeof(IConvertible).IsAssignableFrom(to)) { @@ -96,16 +96,16 @@ public virtual object ConvertTo(CultureInfo culture, string format, object value } if (SystemConverter != null) - { - return SystemConverter.ConvertTo(null, culture, value, to); - } + { + return SystemConverter.ConvertTo(null, culture, value, to); + } if (value == null) - { - return string.Empty; - } + { + return string.Empty; + } - return value.ToString(); - } - } + return value.ToString(); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs index 110c20424b..d94b7933e7 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs @@ -7,43 +7,43 @@ namespace SmartStore.ComponentModel { - public class DictionaryTypeConverter : DefaultTypeConverter where T : IDictionary - { - public DictionaryTypeConverter() - : base(typeof(object)) - { - } + public class DictionaryTypeConverter : DefaultTypeConverter where T : IDictionary + { + public DictionaryTypeConverter() + : base(typeof(object)) + { + } - public override bool CanConvertFrom(Type type) - { + public override bool CanConvertFrom(Type type) + { return type.IsPlainObjectType() || type.IsAnonymous(); - } + } - public override bool CanConvertTo(Type type) - { - return DictionaryConverter.CanCreateType(type); - } + public override bool CanConvertTo(Type type) + { + return DictionaryConverter.CanCreateType(type); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - // Obj > Dict - var dict = CommonHelper.ObjectToDictionary(value); - var to = typeof(T); + public override object ConvertFrom(CultureInfo culture, object value) + { + // Obj > Dict + var dict = CommonHelper.ObjectToDictionary(value); + var to = typeof(T); - if (to == typeof(RouteValueDictionary)) - { - return new RouteValueDictionary(dict); - } - else if (to == typeof(Dictionary)) - { - return (Dictionary)dict; - } - else if (to == typeof(ExpandoObject)) - { - var expando = new ExpandoObject(); - expando.Merge(dict); - return expando; - } + if (to == typeof(RouteValueDictionary)) + { + return new RouteValueDictionary(dict); + } + else if (to == typeof(Dictionary)) + { + return (Dictionary)dict; + } + else if (to == typeof(ExpandoObject)) + { + var expando = new ExpandoObject(); + expando.Merge(dict); + return expando; + } else if (to == typeof(HybridExpando)) { var expando = new HybridExpando(); @@ -51,20 +51,20 @@ public override object ConvertFrom(CultureInfo culture, object value) return expando; } else - { - return dict; - } - } + { + return dict; + } + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - // Dict > Obj - if (value is IDictionary dict) - { - return DictionaryConverter.CreateAndPopulate(to, dict, out _); - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + // Dict > Obj + if (value is IDictionary dict) + { + return DictionaryConverter.CreateAndPopulate(to, dict, out _); + } - return base.ConvertTo(culture, format, value, to); - } - } + return base.ConvertTo(culture, format, value, to); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs index 6eac8befdf..ca5bf0f744 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs @@ -4,40 +4,40 @@ namespace SmartStore.ComponentModel { - public class EmailAddressConverter : DefaultTypeConverter - { - public EmailAddressConverter() : base(typeof(object)) - { - } + public class EmailAddressConverter : DefaultTypeConverter + { + public EmailAddressConverter() : base(typeof(object)) + { + } - public override bool CanConvertFrom(Type type) - { - return type == typeof(string); - } + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } - public override bool CanConvertTo(Type type) - { - return type == typeof(string); - } + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is string str && str.HasValue()) - { - return new EmailAddress(str); - } + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string str && str.HasValue()) + { + return new EmailAddress(str); + } - return base.ConvertFrom(culture, value); - } + return base.ConvertFrom(culture, value); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (to == typeof(string) && value is EmailAddress address) - { - return address.ToString(); - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string) && value is EmailAddress address) + { + return address.ToString(); + } - return base.ConvertTo(culture, format, value, to); - } - } + return base.ConvertTo(culture, format, value, to); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs index 7cc030c658..4f0e17c8ac 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs @@ -6,110 +6,109 @@ using System.Globalization; using System.Linq; using SmartStore.Collections; -using SmartStore.Core.Caching; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] - public class EnumerableConverter : DefaultTypeConverter - { - private readonly Func, object> _activator; + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class EnumerableConverter : DefaultTypeConverter + { + private readonly Func, object> _activator; private readonly ITypeConverter _elementTypeConverter; public EnumerableConverter(Type sequenceType) - : base(typeof(object)) - { + : base(typeof(object)) + { _elementTypeConverter = TypeConverterFactory.GetConverter(); - if (_elementTypeConverter == null) - throw new InvalidOperationException("No type converter exists for type " + typeof(T).FullName); + if (_elementTypeConverter == null) + throw new InvalidOperationException("No type converter exists for type " + typeof(T).FullName); - _activator = CreateSequenceActivator(sequenceType); + _activator = CreateSequenceActivator(sequenceType); } - [SuppressMessage("ReSharper", "RedundantLambdaSignatureParentheses")] - private static Func, object> CreateSequenceActivator(Type sequenceType) - { - // Default is IEnumerable - Func, object> activator = null; + [SuppressMessage("ReSharper", "RedundantLambdaSignatureParentheses")] + private static Func, object> CreateSequenceActivator(Type sequenceType) + { + // Default is IEnumerable + Func, object> activator = null; - var t = sequenceType; + var t = sequenceType; - if (t == typeof(IEnumerable)) - { - activator = (x) => x; - } + if (t == typeof(IEnumerable)) + { + activator = (x) => x; + } else if (t == typeof(T[])) { activator = (x) => x.ToArray(); } else if (t == (typeof(IReadOnlyCollection)) || t == (typeof(IReadOnlyList))) - { - activator = (x) => x.AsReadOnly(); - } - else if (t.IsAssignableFrom(typeof(List))) - { - activator = (x) => x.ToList(); - } - else if (t.IsAssignableFrom(typeof(HashSet))) - { - activator = (x) => new HashSet(x); - } + { + activator = (x) => x.AsReadOnly(); + } + else if (t.IsAssignableFrom(typeof(List))) + { + activator = (x) => x.ToList(); + } + else if (t.IsAssignableFrom(typeof(HashSet))) + { + activator = (x) => new HashSet(x); + } else if (t.IsAssignableFrom(typeof(Queue))) - { - activator = (x) => new Queue(x); - } - else if (t.IsAssignableFrom(typeof(Stack))) - { - activator = (x) => new Stack(x); - } - else if (t.IsAssignableFrom(typeof(LinkedList))) - { - activator = (x) => new LinkedList(x); - } - else if (t.IsAssignableFrom(typeof(ConcurrentBag))) - { - activator = (x) => new ConcurrentBag(x); - } + { + activator = (x) => new Queue(x); + } + else if (t.IsAssignableFrom(typeof(Stack))) + { + activator = (x) => new Stack(x); + } + else if (t.IsAssignableFrom(typeof(LinkedList))) + { + activator = (x) => new LinkedList(x); + } + else if (t.IsAssignableFrom(typeof(ConcurrentBag))) + { + activator = (x) => new ConcurrentBag(x); + } else if (t.IsAssignableFrom(typeof(SyncedCollection))) { activator = (x) => new SyncedCollection(new List(x)); } else if (t.IsAssignableFrom(typeof(ArraySegment))) - { - activator = (x) => new ArraySegment(x.ToArray()); - } + { + activator = (x) => new ArraySegment(x.ToArray()); + } - if (activator == null) - { - throw new InvalidOperationException("'{0}' is not a valid type for enumerable conversion.".FormatInvariant(sequenceType.FullName)); - } + if (activator == null) + { + throw new InvalidOperationException("'{0}' is not a valid type for enumerable conversion.".FormatInvariant(sequenceType.FullName)); + } - return activator; - } + return activator; + } - public override bool CanConvertFrom(Type type) - { + public override bool CanConvertFrom(Type type) + { if (type.IsSequenceType(out var elementType)) { return elementType.IsAssignableFrom(typeof(T)) - || _elementTypeConverter.CanConvertFrom(elementType) + || _elementTypeConverter.CanConvertFrom(elementType) || TypeConverterFactory.GetConverter(elementType).CanConvertTo(typeof(T)); } return type == typeof(string) || typeof(IConvertible).IsAssignableFrom(type); } - public override bool CanConvertTo(Type type) - { + public override bool CanConvertTo(Type type) + { return type == typeof(string) && _elementTypeConverter.CanConvertTo(type); } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value == null) - { - return _activator(Enumerable.Empty()); - } + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value == null) + { + return _activator(Enumerable.Empty()); + } var items = value as IEnumerable; @@ -140,14 +139,14 @@ public override object ConvertFrom(CultureInfo culture, object value) var result = items .Cast() - .Select(x => + .Select(x => { if (isAssignable) return x; - - return !isOtherConverter - ? elementConverter.ConvertFrom(culture, x) - : elementConverter.ConvertTo(culture, null, x, elementType); + + return !isOtherConverter + ? elementConverter.ConvertFrom(culture, x) + : elementConverter.ConvertTo(culture, null, x, elementType); }) .Where(x => x != null) .Cast(); @@ -156,37 +155,37 @@ public override object ConvertFrom(CultureInfo culture, object value) } return base.ConvertFrom(culture, value); - } - - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (to == typeof(string)) - { - string result = string.Empty; - - if (value is IEnumerable enumerable) - { - // We don't use string.Join() because it doesn't support invariant culture - foreach (var token in enumerable) - { - var str = _elementTypeConverter.ConvertTo(culture, format, token, typeof(string)); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + string result = string.Empty; + + if (value is IEnumerable enumerable) + { + // We don't use string.Join() because it doesn't support invariant culture + foreach (var token in enumerable) + { + var str = _elementTypeConverter.ConvertTo(culture, format, token, typeof(string)); result += str + ","; - } + } - result = result.TrimEnd(','); - } + result = result.TrimEnd(','); + } - return result; - } + return result; + } - return base.ConvertTo(culture, format, value, to); - } + return base.ConvertTo(culture, format, value, to); + } - protected virtual string[] GetStringArray(string input) - { - var result = input.SplitSafe(null); - Array.ForEach(result, s => s.Trim()); - return result; - } - } + protected virtual string[] GetStringArray(string input) + { + var result = input.SplitSafe(null); + Array.ForEach(result, s => s.Trim()); + return result; + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs index c76ecba218..9323aa70b0 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs @@ -3,81 +3,81 @@ namespace SmartStore.ComponentModel { - /// - /// Converts objects. - /// - public interface ITypeConverter - { - /// - /// Returns whether this converter can convert an object of the given type to the type of this converter. - /// - /// A Type that represents the type you want to convert from. - /// true if this converter can perform the conversion; otherwise, false. - bool CanConvertFrom(Type type); + /// + /// Converts objects. + /// + public interface ITypeConverter + { + /// + /// Returns whether this converter can convert an object of the given type to the type of this converter. + /// + /// A Type that represents the type you want to convert from. + /// true if this converter can perform the conversion; otherwise, false. + bool CanConvertFrom(Type type); - /// - /// Returns whether this converter can convert the object to the specified type. - /// - /// A Type that represents the type you want to convert to. - /// true if this converter can perform the conversion; otherwise, false. - bool CanConvertTo(Type type); + /// + /// Returns whether this converter can convert the object to the specified type. + /// + /// A Type that represents the type you want to convert to. + /// true if this converter can perform the conversion; otherwise, false. + bool CanConvertTo(Type type); - /// - /// Converts the given value to the type of this converter. - /// - /// The to use as the current culture. If null is passed, the invariant culture is assumed. - /// The object to convert. - /// An object that represents the converted value. - object ConvertFrom(CultureInfo culture, object value); + /// + /// Converts the given value to the type of this converter. + /// + /// The to use as the current culture. If null is passed, the invariant culture is assumed. + /// The object to convert. + /// An object that represents the converted value. + object ConvertFrom(CultureInfo culture, object value); - /// - /// Converts the given value object to the specified type, using the arguments. - /// - /// The to use as the current culture. If null is passed, the invariant culture is assumed. - /// A standard or custom format expression. - /// The object to convert. - /// The type to convert the value parameter to. - /// An Object that represents the converted value. - object ConvertTo(CultureInfo culture, string format, object value, Type to); - } + /// + /// Converts the given value object to the specified type, using the arguments. + /// + /// The to use as the current culture. If null is passed, the invariant culture is assumed. + /// A standard or custom format expression. + /// The object to convert. + /// The type to convert the value parameter to. + /// An Object that represents the converted value. + object ConvertTo(CultureInfo culture, string format, object value, Type to); + } - public static class ITypeConverterExtensions - { - public static object ConvertFrom(this ITypeConverter converter, object value) - { - return converter.ConvertFrom(CultureInfo.InvariantCulture, value); - } + public static class ITypeConverterExtensions + { + public static object ConvertFrom(this ITypeConverter converter, object value) + { + return converter.ConvertFrom(CultureInfo.InvariantCulture, value); + } - public static object ConvertTo(this ITypeConverter converter, object value, Type to) - { - return converter.ConvertTo(CultureInfo.InvariantCulture, null, value, to); - } + public static object ConvertTo(this ITypeConverter converter, object value, Type to) + { + return converter.ConvertTo(CultureInfo.InvariantCulture, null, value, to); + } - public static object SafeConvert(this ITypeConverter converter, string value) - { - try - { - if (converter != null && value.HasValue() && converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFrom(value); - } - } - catch (Exception exc) - { - exc.Dump(); - } + public static object SafeConvert(this ITypeConverter converter, string value) + { + try + { + if (converter != null && value.HasValue() && converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(value); + } + } + catch (Exception exc) + { + exc.Dump(); + } - return null; - } + return null; + } - public static bool IsEqual(this ITypeConverter converter, string value, object compareWith) - { - object convertedObject = converter.SafeConvert(value); + public static bool IsEqual(this ITypeConverter converter, string value, object compareWith) + { + object convertedObject = converter.SafeConvert(value); - if (convertedObject != null && compareWith != null) - return convertedObject.Equals(compareWith); + if (convertedObject != null && compareWith != null) + return convertedObject.Equals(compareWith); - return false; - } - } + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/JObjectConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/JObjectConverter.cs index 3613e06b03..8ab85e5724 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/JObjectConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/JObjectConverter.cs @@ -1,60 +1,55 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] - public class JObjectConverter : DefaultTypeConverter - { - public JObjectConverter() - : base(typeof(JObject)) - { + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class JObjectConverter : DefaultTypeConverter + { + public JObjectConverter() + : base(typeof(JObject)) + { } - public override bool CanConvertFrom(Type type) - { - return type == typeof(string) - || type.IsPlainObjectType() - || type.IsAnonymous(); + public override bool CanConvertFrom(Type type) + { + return type == typeof(string) + || type.IsPlainObjectType() + || type.IsAnonymous(); - } + } - public override bool CanConvertTo(Type type) - { - return type == typeof(string); - } + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is string json) - { + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string json) + { return JObject.Parse(json); - } + } if (value != null) { return JObject.FromObject(value); } - return base.ConvertFrom(culture, value); - } + return base.ConvertFrom(culture, value); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (value is JObject jobj && to == typeof(string)) - { - return jobj.ToString(Formatting.Indented); - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (value is JObject jobj && to == typeof(string)) + { + return jobj.ToString(Formatting.Indented); + } - return base.ConvertTo(culture, format, value, to); + return base.ConvertTo(culture, format, value, to); } - } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs index 97f0d0ce63..ee97942c56 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs @@ -1,117 +1,116 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] - public class NullableConverter : DefaultTypeConverter - { - private readonly bool _elementTypeIsConvertible; - - internal NullableConverter(Type type, Type elementType) - : base(type) - { - NullableType = type; - ElementType = elementType ?? Nullable.GetUnderlyingType(type); - - if (ElementType == null) - { - throw Error.Argument("type", "Type is not a nullable type."); - } - - _elementTypeIsConvertible = typeof(IConvertible).IsAssignableFrom(ElementType) && !ElementType.IsEnum; - ElementTypeConverter = TypeConverterFactory.GetConverter(ElementType); - } - - public Type NullableType - { - get; - private set; - } - - public Type ElementType - { - get; - private set; - } - - public ITypeConverter ElementTypeConverter - { - get; - private set; - } - - public override bool CanConvertFrom(Type type) - { - if (type == this.ElementType) - { - return true; - } - - if (ElementTypeConverter.CanConvertFrom(type)) - { - return true; - } - - if (_elementTypeIsConvertible && type != typeof(string) && typeof(IConvertible).IsAssignableFrom(type)) - { - return true; - } - - return false; - } - - public override bool CanConvertTo(Type type) - { - //Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type))); - - if (type == this.ElementType) - { - return true; - } - - return ElementTypeConverter.CanConvertTo(type); - } - - public override object ConvertFrom(CultureInfo culture, object value) - { - if ((value == null) || (value.GetType() == this.ElementType)) - { - return value; - } - - if ((value is string) && string.IsNullOrEmpty(value as string)) - { - return null; - } - - if (_elementTypeIsConvertible && !(value is string) && value is IConvertible) - { - // num > num? - return Convert.ChangeType(value, ElementType, culture); - } - - return ElementTypeConverter.ConvertFrom(culture, value); - } - - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if ((to == this.ElementType) && this.NullableType.IsInstanceOfType(value)) - { - return value; - } - - if (value == null) - { - if (to == typeof(string)) - { - return string.Empty; - } - } - - return ElementTypeConverter.ConvertTo(culture, format, value, to); - } - } + [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] + public class NullableConverter : DefaultTypeConverter + { + private readonly bool _elementTypeIsConvertible; + + internal NullableConverter(Type type, Type elementType) + : base(type) + { + NullableType = type; + ElementType = elementType ?? Nullable.GetUnderlyingType(type); + + if (ElementType == null) + { + throw Error.Argument("type", "Type is not a nullable type."); + } + + _elementTypeIsConvertible = typeof(IConvertible).IsAssignableFrom(ElementType) && !ElementType.IsEnum; + ElementTypeConverter = TypeConverterFactory.GetConverter(ElementType); + } + + public Type NullableType + { + get; + private set; + } + + public Type ElementType + { + get; + private set; + } + + public ITypeConverter ElementTypeConverter + { + get; + private set; + } + + public override bool CanConvertFrom(Type type) + { + if (type == this.ElementType) + { + return true; + } + + if (ElementTypeConverter.CanConvertFrom(type)) + { + return true; + } + + if (_elementTypeIsConvertible && type != typeof(string) && typeof(IConvertible).IsAssignableFrom(type)) + { + return true; + } + + return false; + } + + public override bool CanConvertTo(Type type) + { + //Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type))); + + if (type == this.ElementType) + { + return true; + } + + return ElementTypeConverter.CanConvertTo(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if ((value == null) || (value.GetType() == this.ElementType)) + { + return value; + } + + if ((value is string) && string.IsNullOrEmpty(value as string)) + { + return null; + } + + if (_elementTypeIsConvertible && !(value is string) && value is IConvertible) + { + // num > num? + return Convert.ChangeType(value, ElementType, culture); + } + + return ElementTypeConverter.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if ((to == this.ElementType) && this.NullableType.IsInstanceOfType(value)) + { + return value; + } + + if (value == null) + { + if (to == typeof(string)) + { + return string.Empty; + } + } + + return ElementTypeConverter.ConvertTo(culture, format, value, to); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs index d4d3974214..e091abe8af 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs @@ -3,83 +3,82 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Linq; using System.Text; using System.Xml.Serialization; using SmartStore.Core.Domain.Catalog; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] - public class ProductBundleDataConverter : DefaultTypeConverter - { - private readonly bool _forList; + [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] + public class ProductBundleDataConverter : DefaultTypeConverter + { + private readonly bool _forList; - public ProductBundleDataConverter(bool forList) - : base(typeof(object)) - { - _forList = forList; - } + public ProductBundleDataConverter(bool forList) + : base(typeof(object)) + { + _forList = forList; + } - public override bool CanConvertFrom(Type type) - { - return type == typeof(string); - } + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } - public override bool CanConvertTo(Type type) - { - return type == typeof(string); - } + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is string) - { - object result = null; - string str = value as string; - if (!string.IsNullOrEmpty(str)) - { - try - { - using (var tr = new StringReader(str)) - { - var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ProductBundleItemOrderData)); - result = serializer.Deserialize(tr); - } - } - catch - { - // xml error - } - } + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + object result = null; + string str = value as string; + if (!string.IsNullOrEmpty(str)) + { + try + { + using (var tr = new StringReader(str)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ProductBundleItemOrderData)); + result = serializer.Deserialize(tr); + } + } + catch + { + // xml error + } + } - return result; - } + return result; + } - return base.ConvertFrom(culture, value); - } + return base.ConvertFrom(culture, value); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (to == typeof(string)) - { - if (value != null && (value is ProductBundleItemOrderData || value is IList)) - { - var sb = new StringBuilder(); - using (var tw = new StringWriter(sb)) - { - var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ProductBundleItemOrderData)); - serializer.Serialize(tw, value); - return sb.ToString(); - } - } - else - { - return string.Empty; - } - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value != null && (value is ProductBundleItemOrderData || value is IList)) + { + var sb = new StringBuilder(); + using (var tw = new StringWriter(sb)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ProductBundleItemOrderData)); + serializer.Serialize(tw, value); + return sb.ToString(); + } + } + else + { + return string.Empty; + } + } - return base.ConvertTo(culture, format, value, to); - } - } + return base.ConvertTo(culture, format, value, to); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs index 0699dbe7c3..dbb6b5f094 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs @@ -9,75 +9,75 @@ namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] - public class ShippingOptionConverter : DefaultTypeConverter - { - private readonly bool _forList; + [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] + public class ShippingOptionConverter : DefaultTypeConverter + { + private readonly bool _forList; - public ShippingOptionConverter(bool forList) - : base(typeof(object)) - { - _forList = forList; + public ShippingOptionConverter(bool forList) + : base(typeof(object)) + { + _forList = forList; } - public override bool CanConvertFrom(Type type) - { - return type == typeof(string); - } + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } - public override bool CanConvertTo(Type type) - { - return type == typeof(string); - } + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is string str) - { - object result = null; - if (!String.IsNullOrEmpty(str)) - { - try - { - using (var tr = new StringReader(str)) - { - var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ShippingOption)); - result = serializer.Deserialize(tr); - } - } - catch - { - // xml error - } - } + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string str) + { + object result = null; + if (!String.IsNullOrEmpty(str)) + { + try + { + using (var tr = new StringReader(str)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ShippingOption)); + result = serializer.Deserialize(tr); + } + } + catch + { + // xml error + } + } - return result; - } + return result; + } - return base.ConvertFrom(culture, value); - } + return base.ConvertFrom(culture, value); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (to == typeof(string)) - { - if (value != null && (value is ShippingOption || value is IList)) - { - var sb = new StringBuilder(); - using (var tw = new StringWriter(sb)) - { - var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ShippingOption)); - serializer.Serialize(tw, value); - return sb.ToString(); - } - } - else - { - return string.Empty; - } - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value != null && (value is ShippingOption || value is IList)) + { + var sb = new StringBuilder(); + using (var tw = new StringWriter(sb)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ShippingOption)); + serializer.Serialize(tw, value); + return sb.ToString(); + } + } + else + { + return string.Empty; + } + } - return base.ConvertTo(culture, format, value, to); - } - } + return base.ConvertTo(culture, format, value, to); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs index a6ccc13470..c6ec04e190 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs @@ -1,67 +1,62 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.ComponentModel { - [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] - public class TimeSpanConverter : DefaultTypeConverter - { - public TimeSpanConverter() - : base(typeof(TimeSpan)) - { - } - - public override bool CanConvertFrom(Type type) - { - return type == typeof(string) - || type == typeof(DateTime) - || base.CanConvertFrom(type); - } - - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is DateTime) - { - var time = (DateTime)value; - return new TimeSpan(time.Ticks); - } - - if (value is string) - { - var str = (string)value; - - TimeSpan span; - if (TimeSpan.TryParse(str, culture, out span)) - { - return span; - } - - long lng; - if (long.TryParse(str, NumberStyles.None, culture, out lng)) - { - return new TimeSpan(lng.FromUnixTime().Ticks); - } - - double dbl; - if (double.TryParse(str, NumberStyles.None, culture, out dbl)) - { - return new TimeSpan(DateTime.FromOADate(dbl).Ticks); - } - } - - try - { - return (TimeSpan)System.Convert.ChangeType(value, typeof(TimeSpan), culture); - } - catch { } - - return base.ConvertFrom(culture, value); - } - } + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class TimeSpanConverter : DefaultTypeConverter + { + public TimeSpanConverter() + : base(typeof(TimeSpan)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string) + || type == typeof(DateTime) + || base.CanConvertFrom(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is DateTime) + { + var time = (DateTime)value; + return new TimeSpan(time.Ticks); + } + + if (value is string) + { + var str = (string)value; + + TimeSpan span; + if (TimeSpan.TryParse(str, culture, out span)) + { + return span; + } + + long lng; + if (long.TryParse(str, NumberStyles.None, culture, out lng)) + { + return new TimeSpan(lng.FromUnixTime().Ticks); + } + + double dbl; + if (double.TryParse(str, NumberStyles.None, culture, out dbl)) + { + return new TimeSpan(DateTime.FromOADate(dbl).Ticks); + } + } + + try + { + return (TimeSpan)System.Convert.ChangeType(value, typeof(TimeSpan), culture); + } + catch { } + + return base.ConvertFrom(culture, value); + } + } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs index de9898639d..4db9fd8d90 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; using System.Dynamic; using System.Web.Routing; using Newtonsoft.Json.Linq; @@ -11,89 +10,86 @@ namespace SmartStore.ComponentModel { - public static class TypeConverterFactory - { - private static readonly ConcurrentDictionary _typeConverters = new ConcurrentDictionary(); - - static TypeConverterFactory() - { - CreateDefaultConverters(); - } - - private static void CreateDefaultConverters() - { - _typeConverters.TryAdd(typeof(DateTime), new DateTimeConverter()); - _typeConverters.TryAdd(typeof(TimeSpan), new TimeSpanConverter()); - _typeConverters.TryAdd(typeof(bool), new BooleanConverter( - new [] { "yes", "y", "on", "wahr" }, - new [] { "no", "n", "off", "falsch" })); - - ITypeConverter converter = new ShippingOptionConverter(true); - _typeConverters.TryAdd(typeof(IList), converter); - _typeConverters.TryAdd(typeof(List), converter); - _typeConverters.TryAdd(typeof(ShippingOption), new ShippingOptionConverter(false)); - - converter = new ProductBundleDataConverter(true); - _typeConverters.TryAdd(typeof(IList), converter); - _typeConverters.TryAdd(typeof(List), converter); - _typeConverters.TryAdd(typeof(ProductBundleItemOrderData), new ProductBundleDataConverter(false)); - - converter = new DictionaryTypeConverter>(); - _typeConverters.TryAdd(typeof(IDictionary), converter); - _typeConverters.TryAdd(typeof(Dictionary), converter); - _typeConverters.TryAdd(typeof(RouteValueDictionary), new DictionaryTypeConverter()); - _typeConverters.TryAdd(typeof(ExpandoObject), new DictionaryTypeConverter()); + public static class TypeConverterFactory + { + private static readonly ConcurrentDictionary _typeConverters = new ConcurrentDictionary(); + + static TypeConverterFactory() + { + CreateDefaultConverters(); + } + + private static void CreateDefaultConverters() + { + _typeConverters.TryAdd(typeof(DateTime), new DateTimeConverter()); + _typeConverters.TryAdd(typeof(TimeSpan), new TimeSpanConverter()); + _typeConverters.TryAdd(typeof(bool), new BooleanConverter( + new[] { "yes", "y", "on", "wahr" }, + new[] { "no", "n", "off", "falsch" })); + + ITypeConverter converter = new ShippingOptionConverter(true); + _typeConverters.TryAdd(typeof(IList), converter); + _typeConverters.TryAdd(typeof(List), converter); + _typeConverters.TryAdd(typeof(ShippingOption), new ShippingOptionConverter(false)); + + converter = new ProductBundleDataConverter(true); + _typeConverters.TryAdd(typeof(IList), converter); + _typeConverters.TryAdd(typeof(List), converter); + _typeConverters.TryAdd(typeof(ProductBundleItemOrderData), new ProductBundleDataConverter(false)); + + converter = new DictionaryTypeConverter>(); + _typeConverters.TryAdd(typeof(IDictionary), converter); + _typeConverters.TryAdd(typeof(Dictionary), converter); + _typeConverters.TryAdd(typeof(RouteValueDictionary), new DictionaryTypeConverter()); + _typeConverters.TryAdd(typeof(ExpandoObject), new DictionaryTypeConverter()); _typeConverters.TryAdd(typeof(HybridExpando), new DictionaryTypeConverter()); _typeConverters.TryAdd(typeof(EmailAddress), new EmailAddressConverter()); _typeConverters.TryAdd(typeof(JObject), new JObjectConverter()); } - public static IReadOnlyCollection Converters - { - get { return _typeConverters.Values.AsReadOnly(); } - } + public static IReadOnlyCollection Converters => _typeConverters.Values.AsReadOnly(); public static void RegisterConverter(ITypeConverter typeConverter) - { - RegisterConverter(typeof(T), typeConverter); - } + { + RegisterConverter(typeof(T), typeConverter); + } - public static void RegisterConverter(Type type, ITypeConverter typeConverter) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(typeConverter, nameof(typeConverter)); + public static void RegisterConverter(Type type, ITypeConverter typeConverter) + { + Guard.NotNull(type, nameof(type)); + Guard.NotNull(typeConverter, nameof(typeConverter)); - _typeConverters.TryAdd(type, typeConverter); + _typeConverters.TryAdd(type, typeConverter); } - public static ITypeConverter RemoveConverter(ITypeConverter typeConverter) - { - return RemoveConverter(typeof(T)); - } + public static ITypeConverter RemoveConverter(ITypeConverter typeConverter) + { + return RemoveConverter(typeof(T)); + } public static ITypeConverter RemoveConverter(Type type) - { - Guard.NotNull(type, nameof(type)); + { + Guard.NotNull(type, nameof(type)); - _typeConverters.TryRemove(type, out var converter); - return converter; - } + _typeConverters.TryRemove(type, out var converter); + return converter; + } - public static ITypeConverter GetConverter() - { - return GetConverter(typeof(T)); - } + public static ITypeConverter GetConverter() + { + return GetConverter(typeof(T)); + } - public static ITypeConverter GetConverter(object component) - { - Guard.NotNull(component, nameof(component)); + public static ITypeConverter GetConverter(object component) + { + Guard.NotNull(component, nameof(component)); - return GetConverter(component.GetType()); - } + return GetConverter(component.GetType()); + } - public static ITypeConverter GetConverter(Type type) - { + public static ITypeConverter GetConverter(Type type) + { Guard.NotNull(type, nameof(type)); return _typeConverters.GetOrAdd(type, Get); @@ -116,6 +112,6 @@ ITypeConverter Get(Type t) // Default fallback return new DefaultTypeConverter(type); } - } - } + } + } } diff --git a/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs b/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs index c2d4fab7c4..3ab801f744 100644 --- a/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs +++ b/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs @@ -3,156 +3,155 @@ using System.Linq.Expressions; using SmartStore.Core.Configuration; using SmartStore.Core.Domain.Configuration; -using SmartStore.Core.Plugins; namespace SmartStore.Services.Configuration { - public enum SaveSettingResult - { - Unchanged, - Modified, - Inserted, - Deleted - } - - /// - /// Setting service interface - /// - public partial interface ISettingService + public enum SaveSettingResult { - /// - /// Creates a unit of work in which cache eviction is suppressed - /// - /// Specifies whether the cache should be evicted completely on batch disposal - /// A disposable unit of work - IDisposable BeginScope(bool clearCache = true); - - /// - /// Gets a value indicating whether settings have changed during a request, making cache eviction necessary. - /// - /// - /// Cache eviction sets this member to false - /// - bool HasChanges { get; } - - /// + Unchanged, + Modified, + Inserted, + Deleted + } + + /// + /// Setting service interface + /// + public partial interface ISettingService + { + /// + /// Creates a unit of work in which cache eviction is suppressed + /// + /// Specifies whether the cache should be evicted completely on batch disposal + /// A disposable unit of work + IDisposable BeginScope(bool clearCache = true); + + /// + /// Gets a value indicating whether settings have changed during a request, making cache eviction necessary. + /// + /// + /// Cache eviction sets this member to false + /// + bool HasChanges { get; } + + /// /// Gets a setting by identifier /// /// Setting identifier /// Setting Setting GetSettingById(int settingId); - /// - /// Gets a setting by key - /// - /// Unique setting key - /// Store identifier - /// Setting - Setting GetSettingEntityByKey(string key, int storeId = 0); - - /// - /// Get setting value by key - /// - /// Type - /// Key - /// Default value - /// Store identifier - /// A value indicating whether a shared (for all stores) value should be loaded if a value specific for a certain store is not found - /// Setting value - T GetSettingByKey(string key, T defaultValue = default(T), int storeId = 0, bool loadSharedValueIfNotFound = false); - - /// - /// Gets all settings - /// - /// Settings - IList GetAllSettings(); - - /// - /// Determines whether a setting exists - /// - /// Entity type - /// Property type - /// Settings - /// Key selector - /// Store identifier - /// true -setting exists; false - does not exist - bool SettingExists(T settings, - Expression> keySelector, int storeId = 0) - where T : ISettings, new(); - - /// - /// Load settings - /// - /// Type - /// Store identifier for which settigns should be loaded - T LoadSetting(int storeId = 0) where T : ISettings, new(); - - /// - /// Load settings - /// - /// Setting class type - /// Store identifier for which settigns should be loaded - ISettings LoadSetting(Type settingType, int storeId = 0); - - /// - /// Set setting value - /// - /// Type - /// Key - /// Value - /// Store identifier - /// A value indicating whether to clear cache after setting update - SaveSettingResult SetSetting(string key, T value, int storeId = 0, bool clearCache = true); - - /// - /// Save settings object - /// - /// Type - /// Setting instance - /// Store identifier - /// true when any setting property has been modified. - bool SaveSetting(T settings, int storeId = 0) where T : ISettings, new(); - - /// - /// Save settings object - /// - /// Entity type - /// Property type - /// Settings - /// Key selector - /// Store ID - /// A value indicating whether to clear cache after setting update - /// true when the setting property has been modified. - SaveSettingResult SaveSetting( - T settings, - Expression> keySelector, - int storeId = 0, - bool clearCache = true) where T : ISettings, new(); - - /// - /// Updates a setting property - /// - /// Entity type - /// Property type - /// Settings - /// Key selector - /// Store ID - /// true when the setting property has been modified. - SaveSettingResult UpdateSetting( - T settings, - Expression> keySelector, - bool overrideForStore, - int storeId = 0) where T : ISettings, new(); - - void InsertSetting(Setting setting, bool clearCache = true); - - void UpdateSetting(Setting setting, bool clearCache = true); - - /// - /// Deletes a setting - /// - /// Setting - void DeleteSetting(Setting setting); + /// + /// Gets a setting by key + /// + /// Unique setting key + /// Store identifier + /// Setting + Setting GetSettingEntityByKey(string key, int storeId = 0); + + /// + /// Get setting value by key + /// + /// Type + /// Key + /// Default value + /// Store identifier + /// A value indicating whether a shared (for all stores) value should be loaded if a value specific for a certain store is not found + /// Setting value + T GetSettingByKey(string key, T defaultValue = default(T), int storeId = 0, bool loadSharedValueIfNotFound = false); + + /// + /// Gets all settings + /// + /// Settings + IList GetAllSettings(); + + /// + /// Determines whether a setting exists + /// + /// Entity type + /// Property type + /// Settings + /// Key selector + /// Store identifier + /// true -setting exists; false - does not exist + bool SettingExists(T settings, + Expression> keySelector, int storeId = 0) + where T : ISettings, new(); + + /// + /// Load settings + /// + /// Type + /// Store identifier for which settigns should be loaded + T LoadSetting(int storeId = 0) where T : ISettings, new(); + + /// + /// Load settings + /// + /// Setting class type + /// Store identifier for which settigns should be loaded + ISettings LoadSetting(Type settingType, int storeId = 0); + + /// + /// Set setting value + /// + /// Type + /// Key + /// Value + /// Store identifier + /// A value indicating whether to clear cache after setting update + SaveSettingResult SetSetting(string key, T value, int storeId = 0, bool clearCache = true); + + /// + /// Save settings object + /// + /// Type + /// Setting instance + /// Store identifier + /// true when any setting property has been modified. + bool SaveSetting(T settings, int storeId = 0) where T : ISettings, new(); + + /// + /// Save settings object + /// + /// Entity type + /// Property type + /// Settings + /// Key selector + /// Store ID + /// A value indicating whether to clear cache after setting update + /// true when the setting property has been modified. + SaveSettingResult SaveSetting( + T settings, + Expression> keySelector, + int storeId = 0, + bool clearCache = true) where T : ISettings, new(); + + /// + /// Updates a setting property + /// + /// Entity type + /// Property type + /// Settings + /// Key selector + /// Store ID + /// true when the setting property has been modified. + SaveSettingResult UpdateSetting( + T settings, + Expression> keySelector, + bool overrideForStore, + int storeId = 0) where T : ISettings, new(); + + void InsertSetting(Setting setting, bool clearCache = true); + + void UpdateSetting(Setting setting, bool clearCache = true); + + /// + /// Deletes a setting + /// + /// Setting + void DeleteSetting(Setting setting); /// /// Delete all settings @@ -160,32 +159,32 @@ SaveSettingResult UpdateSetting( /// Type void DeleteSetting() where T : ISettings, new(); - /// - /// Delete a settings property from storage - /// - /// Entity type - /// Property type - /// Settings - /// Key selector - /// Store ID - /// true when the setting existed and has been deleted - bool DeleteSetting( - T settings, - Expression> keySelector, - int storeId = 0) where T : ISettings, new(); - - /// - /// Delete a settings property from storage - /// - /// true when the setting existed and has been deleted - bool DeleteSetting(string key, int storeId = 0); - - /// - /// Deletes all settings with its key beginning with rootKey. - /// - /// codehint: sm-add - /// Number of deleted settings - int DeleteSettings(string rootKey); + /// + /// Delete a settings property from storage + /// + /// Entity type + /// Property type + /// Settings + /// Key selector + /// Store ID + /// true when the setting existed and has been deleted + bool DeleteSetting( + T settings, + Expression> keySelector, + int storeId = 0) where T : ISettings, new(); + + /// + /// Delete a settings property from storage + /// + /// true when the setting existed and has been deleted + bool DeleteSetting(string key, int storeId = 0); + + /// + /// Deletes all settings with its key beginning with rootKey. + /// + /// codehint: sm-add + /// Number of deleted settings + int DeleteSettings(string rootKey); /// /// Clear cache diff --git a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs index 00d96fe9ba..35539a8293 100644 --- a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs +++ b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs @@ -1,17 +1,14 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace SmartStore.Core.Configuration -{ +{ /// /// Marker attribute. Indicates that the settings should /// be persisted as a JSON string rather than splitted /// into single properties. /// [AttributeUsage(AttributeTargets.Class)] - [Obsolete] + [Obsolete] public class JsonPersistAttribute : Attribute { } diff --git a/src/Libraries/SmartStore.Core/Data/DataProviderFactory.cs b/src/Libraries/SmartStore.Core/Data/DataProviderFactory.cs index 34d423474f..368bebf752 100644 --- a/src/Libraries/SmartStore.Core/Data/DataProviderFactory.cs +++ b/src/Libraries/SmartStore.Core/Data/DataProviderFactory.cs @@ -1,20 +1,18 @@ -using System; - -namespace SmartStore.Core.Data +namespace SmartStore.Core.Data { public abstract class DataProviderFactory { protected DataProviderFactory(DataSettings settings) { - Guard.NotNull(settings, nameof(settings)); + Guard.NotNull(settings, nameof(settings)); this.Settings = settings; } - protected DataSettings Settings - { - get; - private set; - } + protected DataSettings Settings + { + get; + private set; + } public abstract IDataProvider LoadDataProvider(); } diff --git a/src/Libraries/SmartStore.Core/Data/DataSettings.cs b/src/Libraries/SmartStore.Core/Data/DataSettings.cs index 146cc96bcb..3f9a7e5b82 100644 --- a/src/Libraries/SmartStore.Core/Data/DataSettings.cs +++ b/src/Libraries/SmartStore.Core/Data/DataSettings.cs @@ -5,362 +5,352 @@ using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; -using System.Web.Hosting; using SmartStore.Utilities; using SmartStore.Utilities.Threading; namespace SmartStore.Core.Data -{ - public partial class DataSettings +{ + public partial class DataSettings { - private static readonly ReaderWriterLockSlim s_rwLock = new ReaderWriterLockSlim(); - private static DataSettings s_current = null; - private static Func s_settingsFactory = new Func(() => new DataSettings()); - private static bool? s_installed = null; - private static bool s_TestMode = false; + private static readonly ReaderWriterLockSlim s_rwLock = new ReaderWriterLockSlim(); + private static DataSettings s_current = null; + private static Func s_settingsFactory = new Func(() => new DataSettings()); + private static bool? s_installed = null; + private static bool s_TestMode = false; - protected const char SEPARATOR = ':'; - protected const string FILENAME = "Settings.txt"; + protected const char SEPARATOR = ':'; + protected const string FILENAME = "Settings.txt"; - private DataSettings() + private DataSettings() { RawDataSettings = new Dictionary(); } - #region Static members - - public static void SetDefaultFactory(Func factory) - { - Guard.NotNull(factory, nameof(factory)); - - lock (s_rwLock.GetWriteLock()) - { - s_settingsFactory = factory; - } - } - - public static DataSettings Current - { - get - { - using (s_rwLock.GetUpgradeableReadLock()) - { - if (s_current == null) - { - using (s_rwLock.GetWriteLock()) - { - if (s_current == null) - { - s_current = s_settingsFactory(); - s_current.Load(); - } - } - } - } - - return s_current; - } - } - - public static bool DatabaseIsInstalled() - { - if (s_TestMode) - return false; - - if (!s_installed.HasValue) - { - s_installed = Current.IsValid(); - } - - return s_installed.Value; - } - - internal static void SetTestMode(bool isTestMode) - { - s_TestMode = isTestMode; - } - - public static void Reload() - { - using (s_rwLock.GetWriteLock()) - { - s_current = null; - s_installed = null; - } - } - - public static void Delete() - { - if (s_current?.TenantPath != null) - { - using (s_rwLock.GetWriteLock()) - { - string filePath = Path.Combine(CommonHelper.MapPath(s_current.TenantPath), FILENAME); - try - { - File.Delete(filePath); - } - finally - { - s_current = null; - s_installed = null; - } - } - } - } - - #endregion - - #region Instance members - - public string TenantName - { - get; - private set; - } - - public string TenantPath - { - get; - private set; - } - - public Version AppVersion - { - get; - set; - } - - public string DataProvider - { - get; - set; - } - - public string ProviderInvariantName - { - get - { - if (this.DataProvider.HasValue() && this.DataProvider.IsCaseInsensitiveEqual("sqlserver")) - return "System.Data.SqlClient"; - - // SqlCe should always be the default provider - return "System.Data.SqlServerCe.4.0"; - } - } - - public string ProviderFriendlyName - { - get - { - if (this.DataProvider.HasValue() && this.DataProvider.IsCaseInsensitiveEqual("sqlserver")) - return "SQL Server"; - - // SqlCe should always be the default provider - return "SQL Server Compact (SQL CE)"; - } - } - - public bool IsSqlServer - { - get - { - return this.DataProvider.HasValue() && this.DataProvider.IsCaseInsensitiveEqual("sqlserver"); - } - } - - public string DataConnectionString - { - get; - set; - } - - public string DataConnectionType - { - get; - set; - } - - public IDictionary RawDataSettings - { - get; - private set; - } + #region Static members + + public static void SetDefaultFactory(Func factory) + { + Guard.NotNull(factory, nameof(factory)); + + lock (s_rwLock.GetWriteLock()) + { + s_settingsFactory = factory; + } + } + + public static DataSettings Current + { + get + { + using (s_rwLock.GetUpgradeableReadLock()) + { + if (s_current == null) + { + using (s_rwLock.GetWriteLock()) + { + if (s_current == null) + { + s_current = s_settingsFactory(); + s_current.Load(); + } + } + } + } + + return s_current; + } + } + + public static bool DatabaseIsInstalled() + { + if (s_TestMode) + return false; + + if (!s_installed.HasValue) + { + s_installed = Current.IsValid(); + } + + return s_installed.Value; + } + + internal static void SetTestMode(bool isTestMode) + { + s_TestMode = isTestMode; + } + + public static void Reload() + { + using (s_rwLock.GetWriteLock()) + { + s_current = null; + s_installed = null; + } + } + + public static void Delete() + { + if (s_current?.TenantPath != null) + { + using (s_rwLock.GetWriteLock()) + { + string filePath = Path.Combine(CommonHelper.MapPath(s_current.TenantPath), FILENAME); + try + { + File.Delete(filePath); + } + finally + { + s_current = null; + s_installed = null; + } + } + } + } + + #endregion + + #region Instance members + + public string TenantName + { + get; + private set; + } + + public string TenantPath + { + get; + private set; + } + + public Version AppVersion + { + get; + set; + } + + public string DataProvider + { + get; + set; + } + + public string ProviderInvariantName + { + get + { + if (this.DataProvider.HasValue() && this.DataProvider.IsCaseInsensitiveEqual("sqlserver")) + return "System.Data.SqlClient"; + + // SqlCe should always be the default provider + return "System.Data.SqlServerCe.4.0"; + } + } + + public string ProviderFriendlyName + { + get + { + if (this.DataProvider.HasValue() && this.DataProvider.IsCaseInsensitiveEqual("sqlserver")) + return "SQL Server"; + + // SqlCe should always be the default provider + return "SQL Server Compact (SQL CE)"; + } + } + + public bool IsSqlServer => this.DataProvider.HasValue() && this.DataProvider.IsCaseInsensitiveEqual("sqlserver"); + + public string DataConnectionString + { + get; + set; + } + + public string DataConnectionType + { + get; + set; + } + + public IDictionary RawDataSettings + { + get; + private set; + } public bool IsValid() { return this.DataProvider.HasValue() && this.DataConnectionString.HasValue(); - } - - public virtual bool Load() - { - using (s_rwLock.GetWriteLock()) - { - this.Reset(); - - var curTenant = ResolveTenant(); - var tenantPath = "~/App_Data/Tenants/" + curTenant; - string filePath = Path.Combine(CommonHelper.MapPath(tenantPath), FILENAME); - - this.TenantName = curTenant; - this.TenantPath = tenantPath; - - if (File.Exists(filePath) && !s_TestMode) - { - string text = File.ReadAllText(filePath); - var settings = ParseSettings(text); - if (settings.Any()) - { - this.RawDataSettings.AddRange(settings); - if (settings.ContainsKey("AppVersion")) - { - this.AppVersion = new Version(settings["AppVersion"]); - } - if (settings.ContainsKey("DataProvider")) - { - this.DataProvider = settings["DataProvider"]; - this.DataConnectionType = this.IsSqlServer - ? typeof(SqlConnection).AssemblyQualifiedName - : typeof(SqlCeConnection).AssemblyQualifiedName; - } - if (settings.ContainsKey("DataConnectionString")) - { - this.DataConnectionString = settings["DataConnectionString"]; - } - - return this.IsValid(); - } - } - - return false; - } - } - - public void Reset() - { - using (s_rwLock.GetWriteLock()) - { - this.RawDataSettings.Clear(); - this.TenantName = null; - this.TenantPath = null; - this.AppVersion = null; - this.DataProvider = null; - this.DataConnectionString = null; - this.DataConnectionType = null; - - s_installed = null; - } - } - - public virtual bool Save() - { - if (!this.IsValid()) - return false; - - using (s_rwLock.GetWriteLock()) - { - string filePath = Path.Combine(CommonHelper.MapPath(this.TenantPath), FILENAME); - if (!File.Exists(filePath)) - { - using (File.Create(filePath)) - { - // we use 'using' to close the file after it's created - } - } - - var text = SerializeSettings(); - File.WriteAllText(filePath, text); - - return true; - } - } - - #endregion - - #region Instance helpers - - protected virtual string ResolveTenant() - { - var tenantsBaseDir = CommonHelper.MapPath("~/App_Data/Tenants"); - var curTenantFile = Path.Combine(tenantsBaseDir, "current.txt"); - - string curTenant = null; - - if (File.Exists(curTenantFile)) - { - curTenant = File.ReadAllText(curTenantFile).EmptyNull().Trim().NullEmpty(); - if (curTenant != curTenant.EmptyNull().ToValidPath()) - { - // File contains invalid path string - curTenant = null; - } - - if (curTenant != null && !Directory.Exists(Path.Combine(tenantsBaseDir, curTenant))) - { - // Specified Tenant directory does not exist - curTenant = null; - } - } - - curTenant = curTenant ?? "Default"; - - var tenantPath = Path.Combine(tenantsBaseDir, curTenant); - - if (curTenant.IsCaseInsensitiveEqual("Default") && !Directory.Exists(tenantPath)) - { - // The Default tenant dir should be created if it doesn't exist - Directory.CreateDirectory(tenantPath); - } - - return curTenant.TrimEnd('/'); - } - - protected virtual IDictionary ParseSettings(string text) - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (text.IsEmpty()) - return result; - - var settings = new List(); - using (var reader = new StringReader(text)) - { - string str; - while ((str = reader.ReadLine()) != null) - settings.Add(str); - } - - foreach (var setting in settings) - { - var separatorIndex = setting.IndexOf(SEPARATOR); - if (separatorIndex == -1) - { - continue; - } - string key = setting.Substring(0, separatorIndex).Trim(); - string value = setting.Substring(separatorIndex + 1).Trim(); - - if (key.HasValue() && value.HasValue()) - { - result.Add(key, value); - } - } - - return result; - } - - protected virtual string SerializeSettings() - { - return string.Format("AppVersion: {0}{3}DataProvider: {1}{3}DataConnectionString: {2}{3}", - this.AppVersion.ToString(), - this.DataProvider, - this.DataConnectionString, - Environment.NewLine); - } - - #endregion - - } + } + + public virtual bool Load() + { + using (s_rwLock.GetWriteLock()) + { + this.Reset(); + + var curTenant = ResolveTenant(); + var tenantPath = "~/App_Data/Tenants/" + curTenant; + string filePath = Path.Combine(CommonHelper.MapPath(tenantPath), FILENAME); + + this.TenantName = curTenant; + this.TenantPath = tenantPath; + + if (File.Exists(filePath) && !s_TestMode) + { + string text = File.ReadAllText(filePath); + var settings = ParseSettings(text); + if (settings.Any()) + { + this.RawDataSettings.AddRange(settings); + if (settings.ContainsKey("AppVersion")) + { + this.AppVersion = new Version(settings["AppVersion"]); + } + if (settings.ContainsKey("DataProvider")) + { + this.DataProvider = settings["DataProvider"]; + this.DataConnectionType = this.IsSqlServer + ? typeof(SqlConnection).AssemblyQualifiedName + : typeof(SqlCeConnection).AssemblyQualifiedName; + } + if (settings.ContainsKey("DataConnectionString")) + { + this.DataConnectionString = settings["DataConnectionString"]; + } + + return this.IsValid(); + } + } + + return false; + } + } + + public void Reset() + { + using (s_rwLock.GetWriteLock()) + { + this.RawDataSettings.Clear(); + this.TenantName = null; + this.TenantPath = null; + this.AppVersion = null; + this.DataProvider = null; + this.DataConnectionString = null; + this.DataConnectionType = null; + + s_installed = null; + } + } + + public virtual bool Save() + { + if (!this.IsValid()) + return false; + + using (s_rwLock.GetWriteLock()) + { + string filePath = Path.Combine(CommonHelper.MapPath(this.TenantPath), FILENAME); + if (!File.Exists(filePath)) + { + using (File.Create(filePath)) + { + // we use 'using' to close the file after it's created + } + } + + var text = SerializeSettings(); + File.WriteAllText(filePath, text); + + return true; + } + } + + #endregion + + #region Instance helpers + + protected virtual string ResolveTenant() + { + var tenantsBaseDir = CommonHelper.MapPath("~/App_Data/Tenants"); + var curTenantFile = Path.Combine(tenantsBaseDir, "current.txt"); + + string curTenant = null; + + if (File.Exists(curTenantFile)) + { + curTenant = File.ReadAllText(curTenantFile).EmptyNull().Trim().NullEmpty(); + if (curTenant != curTenant.EmptyNull().ToValidPath()) + { + // File contains invalid path string + curTenant = null; + } + + if (curTenant != null && !Directory.Exists(Path.Combine(tenantsBaseDir, curTenant))) + { + // Specified Tenant directory does not exist + curTenant = null; + } + } + + curTenant = curTenant ?? "Default"; + + var tenantPath = Path.Combine(tenantsBaseDir, curTenant); + + if (curTenant.IsCaseInsensitiveEqual("Default") && !Directory.Exists(tenantPath)) + { + // The Default tenant dir should be created if it doesn't exist + Directory.CreateDirectory(tenantPath); + } + + return curTenant.TrimEnd('/'); + } + + protected virtual IDictionary ParseSettings(string text) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (text.IsEmpty()) + return result; + + var settings = new List(); + using (var reader = new StringReader(text)) + { + string str; + while ((str = reader.ReadLine()) != null) + settings.Add(str); + } + + foreach (var setting in settings) + { + var separatorIndex = setting.IndexOf(SEPARATOR); + if (separatorIndex == -1) + { + continue; + } + string key = setting.Substring(0, separatorIndex).Trim(); + string value = setting.Substring(separatorIndex + 1).Trim(); + + if (key.HasValue() && value.HasValue()) + { + result.Add(key, value); + } + } + + return result; + } + + protected virtual string SerializeSettings() + { + return string.Format("AppVersion: {0}{3}DataProvider: {1}{3}DataConnectionString: {2}{3}", + this.AppVersion.ToString(), + this.DataProvider, + this.DataConnectionString, + Environment.NewLine); + } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs index 7d3b433e8e..603bdbde8a 100644 --- a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs +++ b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs @@ -1,110 +1,106 @@ using System; -using SmartStore.Core.Infrastructure; -using System.Linq.Expressions; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; +using SmartStore.Core.Infrastructure; namespace SmartStore.Core.Data { public class DbContextScope : IDisposable { - private readonly IDbContext _ctx; - private readonly bool _autoDetectChangesEnabled; + private readonly IDbContext _ctx; + private readonly bool _autoDetectChangesEnabled; private readonly bool _proxyCreationEnabled; private readonly bool _validateOnSaveEnabled; - private readonly bool _forceNoTracking; - private readonly bool _hooksEnabled; - private readonly bool _autoCommit; - private readonly bool _lazyLoading; - - - public DbContextScope(IDbContext ctx = null, - bool? autoDetectChanges = null, - bool? proxyCreation = null, - bool? validateOnSave = null, - bool? forceNoTracking = null, - bool? hooksEnabled = null, - bool? autoCommit = null, - bool? lazyLoading = null) + private readonly bool _forceNoTracking; + private readonly bool _hooksEnabled; + private readonly bool _autoCommit; + private readonly bool _lazyLoading; + + + public DbContextScope(IDbContext ctx = null, + bool? autoDetectChanges = null, + bool? proxyCreation = null, + bool? validateOnSave = null, + bool? forceNoTracking = null, + bool? hooksEnabled = null, + bool? autoCommit = null, + bool? lazyLoading = null) { - _ctx = ctx ?? EngineContext.Current.Resolve(); - _autoDetectChangesEnabled = _ctx.AutoDetectChangesEnabled; - _proxyCreationEnabled = _ctx.ProxyCreationEnabled; - _validateOnSaveEnabled = _ctx.ValidateOnSaveEnabled; - _forceNoTracking = _ctx.ForceNoTracking; - _hooksEnabled = _ctx.HooksEnabled; - _autoCommit = _ctx.AutoCommitEnabled; - _lazyLoading = _ctx.LazyLoadingEnabled; - + _ctx = ctx ?? EngineContext.Current.Resolve(); + _autoDetectChangesEnabled = _ctx.AutoDetectChangesEnabled; + _proxyCreationEnabled = _ctx.ProxyCreationEnabled; + _validateOnSaveEnabled = _ctx.ValidateOnSaveEnabled; + _forceNoTracking = _ctx.ForceNoTracking; + _hooksEnabled = _ctx.HooksEnabled; + _autoCommit = _ctx.AutoCommitEnabled; + _lazyLoading = _ctx.LazyLoadingEnabled; + if (autoDetectChanges.HasValue) - _ctx.AutoDetectChangesEnabled = autoDetectChanges.Value; + _ctx.AutoDetectChangesEnabled = autoDetectChanges.Value; if (proxyCreation.HasValue) - _ctx.ProxyCreationEnabled = proxyCreation.Value; + _ctx.ProxyCreationEnabled = proxyCreation.Value; if (validateOnSave.HasValue) - _ctx.ValidateOnSaveEnabled = validateOnSave.Value; - - if (forceNoTracking.HasValue) - _ctx.ForceNoTracking = forceNoTracking.Value; - - if (hooksEnabled.HasValue) - _ctx.HooksEnabled = hooksEnabled.Value; - - if (autoCommit.HasValue) - _ctx.AutoCommitEnabled = autoCommit.Value; - - if (lazyLoading.HasValue) - _ctx.LazyLoadingEnabled = lazyLoading.Value; - } - - public IDbContext DbContext - { - get { return _ctx; } - } - - public void LoadCollection( - TEntity entity, - Expression>> navigationProperty, - bool force = false, - Func, IQueryable> queryAction = null) - where TEntity : BaseEntity - where TCollection : BaseEntity - { - _ctx.LoadCollection(entity, navigationProperty, force, queryAction); - } - - public void LoadReference( - TEntity entity, - Expression> navigationProperty, - bool force = false) - where TEntity : BaseEntity - where TProperty : BaseEntity - { - _ctx.LoadReference(entity, navigationProperty, force); - } - - public int Commit() - { - return _ctx.SaveChanges(); - } - - public async Task CommitAsync() - { - return await _ctx.SaveChangesAsync(); - } - - public void Dispose() + _ctx.ValidateOnSaveEnabled = validateOnSave.Value; + + if (forceNoTracking.HasValue) + _ctx.ForceNoTracking = forceNoTracking.Value; + + if (hooksEnabled.HasValue) + _ctx.HooksEnabled = hooksEnabled.Value; + + if (autoCommit.HasValue) + _ctx.AutoCommitEnabled = autoCommit.Value; + + if (lazyLoading.HasValue) + _ctx.LazyLoadingEnabled = lazyLoading.Value; + } + + public IDbContext DbContext => _ctx; + + public void LoadCollection( + TEntity entity, + Expression>> navigationProperty, + bool force = false, + Func, IQueryable> queryAction = null) + where TEntity : BaseEntity + where TCollection : BaseEntity + { + _ctx.LoadCollection(entity, navigationProperty, force, queryAction); + } + + public void LoadReference( + TEntity entity, + Expression> navigationProperty, + bool force = false) + where TEntity : BaseEntity + where TProperty : BaseEntity + { + _ctx.LoadReference(entity, navigationProperty, force); + } + + public int Commit() + { + return _ctx.SaveChanges(); + } + + public async Task CommitAsync() { - _ctx.AutoDetectChangesEnabled = _autoDetectChangesEnabled; - _ctx.ProxyCreationEnabled = _proxyCreationEnabled; - _ctx.ValidateOnSaveEnabled = _validateOnSaveEnabled; - _ctx.ForceNoTracking = _forceNoTracking; - _ctx.HooksEnabled = _hooksEnabled; - _ctx.AutoCommitEnabled = _autoCommit; - _ctx.LazyLoadingEnabled = _lazyLoading; + return await _ctx.SaveChangesAsync(); } + public void Dispose() + { + _ctx.AutoDetectChangesEnabled = _autoDetectChangesEnabled; + _ctx.ProxyCreationEnabled = _proxyCreationEnabled; + _ctx.ValidateOnSaveEnabled = _validateOnSaveEnabled; + _ctx.ForceNoTracking = _forceNoTracking; + _ctx.HooksEnabled = _hooksEnabled; + _ctx.AutoCommitEnabled = _autoCommit; + _ctx.LazyLoadingEnabled = _lazyLoading; + } } } diff --git a/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs b/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs index 3e2318e4f0..3970eeecf2 100644 --- a/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs +++ b/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs @@ -1,18 +1,16 @@ -using System; - -namespace SmartStore.Core.Data +namespace SmartStore.Core.Data { - public class DbQuerySettings - { - public DbQuerySettings(bool ignoreAcl, bool ignoreMultiStore) - { - this.IgnoreAcl = ignoreAcl; - this.IgnoreMultiStore = ignoreMultiStore; - } + public class DbQuerySettings + { + public DbQuerySettings(bool ignoreAcl, bool ignoreMultiStore) + { + this.IgnoreAcl = ignoreAcl; + this.IgnoreMultiStore = ignoreMultiStore; + } - public bool IgnoreAcl { get; private set; } - public bool IgnoreMultiStore { get; private set; } + public bool IgnoreAcl { get; private set; } + public bool IgnoreMultiStore { get; private set; } - public static DbQuerySettings Default { get; } = new DbQuerySettings(false, false); - } + public static DbQuerySettings Default { get; } = new DbQuerySettings(false, false); + } } diff --git a/src/Libraries/SmartStore.Core/Data/EntityState.cs b/src/Libraries/SmartStore.Core/Data/EntityState.cs index 36f55a4c06..0cab007410 100644 --- a/src/Libraries/SmartStore.Core/Data/EntityState.cs +++ b/src/Libraries/SmartStore.Core/Data/EntityState.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Core.Data { diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs index 09b74716ac..e38efb2620 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs @@ -2,78 +2,80 @@ namespace SmartStore.Core.Data.Hooks { - public abstract class DbSaveHook : IDbSaveHook where TEntity : class - { - public virtual void OnBeforeSave(IHookedEntity entry) - { - var entity = entry.Entity as TEntity; - switch (entry.InitialState) - { - case EntityState.Added: - OnInserting(entity, entry); - break; - case EntityState.Modified: - OnUpdating(entity, entry); - break; - case EntityState.Deleted: - OnDeleting(entity, entry); - break; - } - } + public abstract class DbSaveHook : IDbSaveHook + where TContext : IDbContext + where TEntity : class + { + public virtual void OnBeforeSave(IHookedEntity entry) + { + var entity = entry.Entity as TEntity; + switch (entry.InitialState) + { + case EntityState.Added: + OnInserting(entity, entry); + break; + case EntityState.Modified: + OnUpdating(entity, entry); + break; + case EntityState.Deleted: + OnDeleting(entity, entry); + break; + } + } - protected virtual void OnInserting(TEntity entity, IHookedEntity entry) - { - throw new NotImplementedException(); - } + protected virtual void OnInserting(TEntity entity, IHookedEntity entry) + { + throw new NotImplementedException(); + } - protected virtual void OnUpdating(TEntity entity, IHookedEntity entry) - { - throw new NotImplementedException(); - } + protected virtual void OnUpdating(TEntity entity, IHookedEntity entry) + { + throw new NotImplementedException(); + } - protected virtual void OnDeleting(TEntity entity, IHookedEntity entry) - { - throw new NotImplementedException(); - } + protected virtual void OnDeleting(TEntity entity, IHookedEntity entry) + { + throw new NotImplementedException(); + } - public virtual void OnBeforeSaveCompleted() - { - } + public virtual void OnBeforeSaveCompleted() + { + } - public virtual void OnAfterSave(IHookedEntity entry) - { - var entity = entry.Entity as TEntity; - switch (entry.InitialState) - { - case EntityState.Added: - OnInserted(entity, entry); - break; - case EntityState.Modified: - OnUpdated(entity, entry); - break; - case EntityState.Deleted: - OnDeleted(entity, entry); - break; - } - } + public virtual void OnAfterSave(IHookedEntity entry) + { + var entity = entry.Entity as TEntity; + switch (entry.InitialState) + { + case EntityState.Added: + OnInserted(entity, entry); + break; + case EntityState.Modified: + OnUpdated(entity, entry); + break; + case EntityState.Deleted: + OnDeleted(entity, entry); + break; + } + } - protected virtual void OnInserted(TEntity entity, IHookedEntity entry) - { - throw new NotImplementedException(); - } + protected virtual void OnInserted(TEntity entity, IHookedEntity entry) + { + throw new NotImplementedException(); + } - protected virtual void OnUpdated(TEntity entity, IHookedEntity entry) - { - throw new NotImplementedException(); - } + protected virtual void OnUpdated(TEntity entity, IHookedEntity entry) + { + throw new NotImplementedException(); + } - protected virtual void OnDeleted(TEntity entity, IHookedEntity entry) - { - throw new NotImplementedException(); - } + protected virtual void OnDeleted(TEntity entity, IHookedEntity entry) + { + throw new NotImplementedException(); + } - public virtual void OnAfterSaveCompleted() - { - } - } + public virtual void OnAfterSaveCompleted() + { + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs index 46e27889a4..66b71ad411 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs @@ -7,268 +7,251 @@ namespace SmartStore.Core.Data.Hooks { - public class DefaultDbHookHandler : IDbHookHandler - { - private readonly IList> _saveHooks; - - private readonly Multimap _hooksRequestCache = new Multimap(); - - // Prevents repetitive hooking of the same entity/state/[pre|post] combination within a single request - private readonly HashSet _hookedEntities = new HashSet(); - - private static HashSet _importantSaveHookTypes; - private readonly static object _lock = new object(); - - // Contains all IDbHook/EntityType/State/Stage combinations in which - // the implementor threw either NotImplementedException or NotSupportedException. - // This boosts performance because these VOID combinations are not processed again - // and frees us mostly from the obligation always to detect changes. - private readonly static HashSet _voidHooks = new HashSet(); - - public DefaultDbHookHandler(IEnumerable> hooks) - { - _saveHooks = hooks.Where(x => x.Metadata.IsLoadHook == false).ToList(); - } - - public ILogger Logger - { - get; - set; - } = NullLogger.Instance; - - public bool HasImportantSaveHooks() - { - if (_importantSaveHookTypes == null) - { - lock (_lock) - { - if (_importantSaveHookTypes == null) - { - _importantSaveHookTypes = new HashSet(); - _importantSaveHookTypes.AddRange(_saveHooks.Where(x => x.Metadata.Important).Select(x => x.Metadata.ImplType)); - } - } - } - - return _importantSaveHookTypes.Any(); - } - - public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged) - { - Guard.NotNull(entries, nameof(entries)); - - anyStateChanged = false; - - var processedHooks = new HashSet(); - - if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) - return processedHooks; - - foreach (var entry in entries) - { - var e = entry; // Prevents access to modified closure - - if (HandledAlready(e, HookStage.PreSave)) - { - // Prevent repetitive hooking of the same entity/state/pre combination within a single request - continue; - } - - var hooks = GetSaveHookInstancesFor(e, HookStage.PreSave, importantHooksOnly); - - foreach (var hook in hooks) - { - // call hook - try - { - //Logger.DebugFormat("PRE save hook: {0}, State: {1}, Entity: {2}", hook.GetType().Name, e.InitialState, e.Entity.GetUnproxiedType().Name); - hook.OnBeforeSave(e); - processedHooks.Add(hook); - } - catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) - { - RegisterVoidHook(hook, e.EntityType, e.InitialState, HookStage.PreSave); - } - catch (Exception ex) - { - Logger.ErrorFormat(ex, "PreSaveHook exception ({0})", hook.GetType().FullName); - } - - // change state if applicable - if (e.HasStateChanged) - { - e.InitialState = e.State; - anyStateChanged = true; - } - } - } - - processedHooks.Each(x => x.OnBeforeSaveCompleted()); - - return processedHooks; - } - - public IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) - { - Guard.NotNull(entries, nameof(entries)); - - var processedHooks = new HashSet(); - - if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) - return processedHooks; - - foreach (var entry in entries) - { - var e = entry; // Prevents access to modified closure - - if (HandledAlready(e, HookStage.PostSave)) - { - // Prevent repetitive hooking of the same entity/state/post combination within a single request - continue; - } - - var hooks = GetSaveHookInstancesFor(e, HookStage.PostSave, importantHooksOnly); - - foreach (var hook in hooks) - { - // call hook - try - { - //Logger.DebugFormat("POST save hook: {0}, State: {1}, Entity: {2}", hook.GetType().Name, e.InitialState, e.Entity.GetUnproxiedType().Name); - hook.OnAfterSave(e); - processedHooks.Add(hook); - } - catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) - { - RegisterVoidHook(hook, e.EntityType, e.InitialState, HookStage.PostSave); - } - catch (Exception ex) - { - Logger.ErrorFormat(ex, "PostSaveHook exception ({0})", hook.GetType().FullName); - } - } - } - - processedHooks.Each(x => x.OnAfterSaveCompleted()); - - return processedHooks; - } + public class DefaultDbHookHandler : IDbHookHandler + { + private readonly IEnumerable> _saveHooks; + + private readonly Multimap _hooksRequestCache = new Multimap(); + + // Prevents repetitive hooking of the same entity/state/[pre|post] combination within a single request + private readonly HashSet _hookedEntities = new HashSet(); + + private static HashSet _importantSaveHookTypes; + private readonly static object _lock = new object(); + + // Contains all IDbSaveHook/EntityType/State/Stage combinations in which + // the implementor threw either NotImplementedException or NotSupportedException. + // This boosts performance because these VOID combinations are not processed again + // and frees us mostly from the obligation always to detect changes. + private readonly static HashSet _voidHooks = new HashSet(); + + public DefaultDbHookHandler(IEnumerable> hooks) + { + _saveHooks = hooks; + } + + public ILogger Logger + { + get; + set; + } = NullLogger.Instance; + + public bool HasImportantSaveHooks() + { + if (_importantSaveHookTypes == null) + { + lock (_lock) + { + if (_importantSaveHookTypes == null) + { + _importantSaveHookTypes = new HashSet(); + _importantSaveHookTypes.AddRange(_saveHooks.Where(x => x.Metadata.Important).Select(x => x.Metadata.ImplType)); + } + } + } + + return _importantSaveHookTypes.Any(); + } + + public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged) + { + Guard.NotNull(entries, nameof(entries)); + + anyStateChanged = false; + + var processedHooks = new HashSet(); + + if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) + return processedHooks; + + foreach (var entry in entries) + { + var e = entry; // Prevents access to modified closure + + if (HandledAlready(e, HookStage.PreSave)) + { + // Prevent repetitive hooking of the same entity/state/pre combination within a single request + continue; + } + + var hooks = GetSaveHookInstancesFor(e, HookStage.PreSave, importantHooksOnly); + + foreach (var hook in hooks) + { + // call hook + try + { + //Logger.DebugFormat("PRE save hook: {0}, State: {1}, Entity: {2}", hook.GetType().Name, e.InitialState, e.Entity.GetUnproxiedType().Name); + hook.OnBeforeSave(e); + processedHooks.Add(hook); + } + catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) + { + RegisterVoidHook(hook, e, HookStage.PreSave); + } + catch (Exception ex) + { + Logger.ErrorFormat(ex, "PreSaveHook exception ({0})", hook.GetType().FullName); + } + + // change state if applicable + if (e.HasStateChanged) + { + e.InitialState = e.State; + anyStateChanged = true; + } + } + } + + processedHooks.Each(x => x.OnBeforeSaveCompleted()); + + return processedHooks; + } + + public IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) + { + Guard.NotNull(entries, nameof(entries)); + + var processedHooks = new HashSet(); + + if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) + return processedHooks; + + foreach (var entry in entries) + { + var e = entry; // Prevents access to modified closure + + if (HandledAlready(e, HookStage.PostSave)) + { + // Prevent repetitive hooking of the same entity/state/post combination within a single request + continue; + } + + var hooks = GetSaveHookInstancesFor(e, HookStage.PostSave, importantHooksOnly); + + foreach (var hook in hooks) + { + // call hook + try + { + //Logger.DebugFormat("POST save hook: {0}, State: {1}, Entity: {2}", hook.GetType().Name, e.InitialState, e.Entity.GetUnproxiedType().Name); + hook.OnAfterSave(e); + processedHooks.Add(hook); + } + catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) + { + RegisterVoidHook(hook, e, HookStage.PostSave); + } + catch (Exception ex) + { + Logger.ErrorFormat(ex, "PostSaveHook exception ({0})", hook.GetType().FullName); + } + } + } + + processedHooks.Each(x => x.OnAfterSaveCompleted()); + + return processedHooks; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private IEnumerable GetSaveHookInstancesFor(IHookedEntity entry, HookStage stage, bool importantOnly) - { - return GetHookInstancesFor( - entry.EntityType, - entry.InitialState, - stage, - importantOnly, - _saveHooks, - _importantSaveHookTypes); - } - - private IEnumerable GetHookInstancesFor( - Type entityType, - EntityState entityState, - HookStage stage, - bool importantOnly, - IList> hookList, - HashSet importantHookTypes) where THook : IDbHook - { - IEnumerable hooks; - - if (entityType == null) - { - return Enumerable.Empty(); - } - - // For request cache lookup - var requestKey = new RequestHookKey(entityType, entityState, stage, importantOnly); - - if (_hooksRequestCache.ContainsKey(requestKey)) - { - hooks = _hooksRequestCache[requestKey]; - } - else - { - hooks = hookList - // Reduce by entity types which can be processed by this hook - .Where(x => x.Metadata.HookedType.IsAssignableFrom(entityType)) - // When importantOnly, only include hook types with [ImportantAttribute] - .Where(x => !importantOnly || importantHookTypes.Contains(x.Metadata.ImplType)) - // Exclude void hooks (hooks known to be useless for the current EntityType/State/Stage combination) - .Where(x => !_voidHooks.Contains(new HookKey(x.Metadata.ImplType, entityType, entityState, stage))) - .Select(x => x.Value) - .ToArray(); - - _hooksRequestCache.AddRange(requestKey, hooks); - } - - return hooks.Cast(); - } + { + if (entry.EntityType == null) + { + return Enumerable.Empty(); + } + + IEnumerable hooks; + + // For request cache lookup + var requestKey = new RequestHookKey(entry, stage, importantOnly); + + if (_hooksRequestCache.ContainsKey(requestKey)) + { + hooks = _hooksRequestCache[requestKey].ToArray(); + } + else + { + hooks = _saveHooks + // Reduce by data context types + .Where(x => x.Metadata.DbContextType.IsAssignableFrom(entry.ContextType)) + // Reduce by entity types which can be processed by this hook + .Where(x => x.Metadata.HookedType.IsAssignableFrom(entry.EntityType)) + // When importantOnly, only include hook types with [ImportantAttribute] + .Where(x => !importantOnly || _importantSaveHookTypes.Contains(x.Metadata.ImplType)) + // Exclude void hooks (hooks known to be useless for the current EntityType/State/Stage combination) + .Where(x => !_voidHooks.Contains(new HookKey(x.Metadata.ImplType, entry, stage))) + .Select(x => x.Value) + .ToArray(); + + _hooksRequestCache.AddRange(requestKey, hooks); + } + + return hooks; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool HandledAlready(IHookedEntity entry, HookStage stage) - { - var entity = entry.Entity; + private bool HandledAlready(IHookedEntity entry, HookStage stage) + { + var entity = entry.Entity; - if (entity == null || entity.IsTransientRecord()) - return false; + if (entity == null || entity.IsTransientRecord()) + return false; - var key = new HookedEntityKey(entry.EntityType, entity.Id, entry.InitialState, stage); - if (_hookedEntities.Contains(key)) - { - return true; - } + var key = new HookedEntityKey(entry, stage, entity.Id); + if (_hookedEntities.Contains(key)) + { + return true; + } - _hookedEntities.Add(key); - return false; - } + _hookedEntities.Add(key); + return false; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RegisterVoidHook(IDbHook hook, Type entityType, EntityState entityState, HookStage stage) - { - var hookType = hook.GetType(); - - // Unregister from request cache (if cached) - _hooksRequestCache.Remove(new RequestHookKey(entityType, entityState, stage, false), hook); - _hooksRequestCache.Remove(new RequestHookKey(entityType, entityState, stage, true), hook); - - lock (_lock) - { - // Add to static void hooks set - _voidHooks.Add(new HookKey(hookType, entityType, entityState, stage)); - } - } - - enum HookStage - { - Load, - PreSave, - PostSave - } - - class HookedEntityKey : Tuple - { - public HookedEntityKey(Type entityType, int entityId, EntityState initialState, HookStage stage) - : base(entityType, entityId, initialState, stage) - { - } - } - - class RequestHookKey : Tuple - { - public RequestHookKey(Type entityType, EntityState entityState, HookStage stage, bool importantOnly) - : base(entityType, entityState, stage, importantOnly) - { - } - } - - class HookKey : Tuple - { - public HookKey(Type hookType, Type entityType, EntityState entityState, HookStage stage) - : base(hookType, entityType, entityState, stage) - { - } - } - } + private void RegisterVoidHook(IDbSaveHook hook, IHookedEntity entry, HookStage stage) + { + var hookType = hook.GetType(); + + // Unregister from request cache (if cached) + _hooksRequestCache.Remove(new RequestHookKey(entry, stage, false), hook); + _hooksRequestCache.Remove(new RequestHookKey(entry, stage, true), hook); + + lock (_lock) + { + // Add to static void hooks set + _voidHooks.Add(new HookKey(hookType, entry, stage)); + } + } + + enum HookStage + { + PreSave, + PostSave + } + + class HookedEntityKey : Tuple + { + public HookedEntityKey(IHookedEntity entry, HookStage stage, int entityId) + : base(entry.ContextType, entry.EntityType, entityId, entry.InitialState, stage) + { + } + } + + class RequestHookKey : Tuple + { + public RequestHookKey(IHookedEntity entry, HookStage stage, bool importantOnly) + : base(entry.ContextType, entry.EntityType, entry.InitialState, stage, importantOnly) + { + } + } + + class HookKey : Tuple + { + public HookKey(Type hookType, IHookedEntity entry, HookStage stage) + : base(hookType, entry.EntityType, entry.InitialState, stage) + { + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs b/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs index f1f81574a2..855787040e 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs @@ -2,35 +2,41 @@ namespace SmartStore.Core.Data.Hooks { - public class HookMetadata - { - /// - /// The type of entity - /// - public Type HookedType { get; set; } + public class HookMetadata + { + /// + /// The type of entity + /// + public Type HookedType { get; set; } - /// - /// The type of the hook class itself - /// - public Type ImplType { get; set; } + /// + /// The type of the hook class itself + /// + public Type ImplType { get; set; } - public bool IsLoadHook { get; set; } + /// + /// The impl type of to which the hook belongs to. + /// + public Type DbContextType { get; set; } - /// - /// Whether the hook should run in any case, even if hooking has been turned off. - /// - public bool Important { get; set; } + /// + /// Whether the hook should run in any case, even if hooking has been turned off. + /// + public bool Important { get; set; } - public static HookMetadata Create(Type hookedType, bool important = false) where THook : IDbHook - { - Guard.NotNull(hookedType, nameof(hookedType)); + public static HookMetadata Create(Type hookedType, bool important = false) + where THook : IDbSaveHook + where TContext : IDbContext + { + Guard.NotNull(hookedType, nameof(hookedType)); - return new HookMetadata - { - ImplType = typeof(THook), - HookedType = hookedType, - Important = important - }; - } - } + return new HookMetadata + { + ImplType = typeof(THook), + DbContextType = typeof(TContext), + HookedType = hookedType, + Important = important + }; + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/HookableAttribute.cs b/src/Libraries/SmartStore.Core/Data/Hooks/HookableAttribute.cs index ed20b256eb..ac757a4c1c 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/HookableAttribute.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/HookableAttribute.cs @@ -2,21 +2,21 @@ namespace SmartStore.Core.Data.Hooks { - /// - /// Turns hooking for a specific entity type explicitly on or off. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public sealed class HookableAttribute : Attribute - { - public HookableAttribute(bool isHookable) - { - IsHookable = IsHookable; - } + /// + /// Turns hooking for a specific entity type explicitly on or off. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class HookableAttribute : Attribute + { + public HookableAttribute(bool isHookable) + { + IsHookable = IsHookable; + } - public bool IsHookable - { - get; - private set; - } - } + public bool IsHookable + { + get; + private set; + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs b/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs index 2175d189d9..11a9944a32 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs @@ -4,135 +4,135 @@ namespace SmartStore.Core.Data.Hooks { - public interface IHookedEntity - { - /// - /// Gets the hooked entity entry - /// - DbEntityEntry Entry { get; } - - BaseEntity Entity { get; } - - Type EntityType { get; } - - /// - /// Gets or sets the initial (presave) state of the hooked entity. - /// The setter is for internal use only, don't invoke! - /// - EntityState InitialState { get; set; } - - /// - /// Gets or sets the current state of the hooked entity - /// - EntityState State { get; set; } - - /// - /// Gets a value indicating whether the entity state has changed during hooking. - /// - bool HasStateChanged { get; } - - /// - /// Gets a value indicating whether a property has been modified. - /// - /// Name of the property - bool IsPropertyModified(string propertyName); - - /// - /// Gets a value indicating whether the entity is in soft deleted state. - /// This is the case when the entity is an instance of - /// and the value of its Deleted property is true AND has changed since tracking. - /// But when the entity is not in modified state the snapshot comparison is omitted. - /// - bool IsSoftDeleted { get; } - } - - public class HookedEntity : IHookedEntity - { - private Type _entityType; - - public HookedEntity(DbEntityEntry entry) - { - Entry = entry; - InitialState = (EntityState)entry.State; - } - - public DbEntityEntry Entry - { - get; - private set; - } - - public BaseEntity Entity - { - get { return Entry.Entity as BaseEntity; } - } - - public Type EntityType - { - get - { - return _entityType ?? (_entityType = this.Entity?.GetUnproxiedType()); - } - } - - public EntityState InitialState - { - get; - set; - } - - public EntityState State - { - get - { - return (EntityState)Entry.State; - } - set - { - Entry.State = (EfState)((int)value); - } - } - - public bool HasStateChanged - { - get - { - return InitialState != State; - } - } - - public bool IsPropertyModified(string propertyName) - { - Guard.NotEmpty(propertyName, nameof(propertyName)); - - if (State == EntityState.Modified) - { - var prop = Entry.Property(propertyName); - if (prop == null) - { - throw new SmartException($"An entity property '{propertyName}' does not exist."); - } - - return prop.CurrentValue != null && !prop.CurrentValue.Equals(prop.OriginalValue); - } - - return false; - } - - public bool IsSoftDeleted - { - get - { - var entity = Entry.Entity as ISoftDeletable; - if (entity != null) - { - return Entry.State == EfState.Modified - ? entity.Deleted && IsPropertyModified("Deleted") - : entity.Deleted; - } - - return false; - } - } - } + public interface IHookedEntity + { + /// + /// Gets the impl type of the data context that triggered the hook. + /// + Type ContextType { get; } + + /// + /// Gets the hooked entity entry + /// + DbEntityEntry Entry { get; } + + /// + /// Gets the hooked entity instance + /// + BaseEntity Entity { get; } + + /// + /// Gets the unproxied type of the hooked entity instance. + /// + Type EntityType { get; } + + /// + /// Gets or sets the initial (presave) state of the hooked entity. + /// The setter is for internal use only, don't invoke! + /// + EntityState InitialState { get; set; } + + /// + /// Gets or sets the current state of the hooked entity + /// + EntityState State { get; set; } + + /// + /// Gets a value indicating whether the entity state has changed during hooking. + /// + bool HasStateChanged { get; } + + /// + /// Gets a value indicating whether a property has been modified. + /// + /// Name of the property + bool IsPropertyModified(string propertyName); + + /// + /// Gets a value indicating whether the entity is in soft deleted state. + /// This is the case when the entity is an instance of + /// and the value of its Deleted property is true AND has changed since tracking. + /// But when the entity is not in modified state the snapshot comparison is omitted. + /// + bool IsSoftDeleted { get; } + } + + public class HookedEntity : IHookedEntity + { + private Type _entityType; + + public HookedEntity(IDbContext context, DbEntityEntry entry) + : this(context.GetType(), entry) + { + } + + internal HookedEntity(Type contextType, DbEntityEntry entry) + { + ContextType = contextType; + Entry = entry; + InitialState = (EntityState)entry.State; + } + + public Type ContextType + { + get; + } + + public DbEntityEntry Entry + { + get; + } + + public BaseEntity Entity => Entry.Entity as BaseEntity; + + public Type EntityType => _entityType ?? (_entityType = this.Entity?.GetUnproxiedType()); + + public EntityState InitialState + { + get; + set; + } + + public EntityState State + { + get => (EntityState)Entry.State; + set => Entry.State = (EfState)((int)value); + } + + public bool HasStateChanged => InitialState != State; + + public bool IsPropertyModified(string propertyName) + { + Guard.NotEmpty(propertyName, nameof(propertyName)); + + if (State == EntityState.Modified) + { + var prop = Entry.Property(propertyName); + if (prop == null) + { + throw new SmartException($"An entity property '{propertyName}' does not exist."); + } + + return prop.CurrentValue != null && !prop.CurrentValue.Equals(prop.OriginalValue); + } + + return false; + } + + public bool IsSoftDeleted + { + get + { + var entity = Entry.Entity as ISoftDeletable; + if (entity != null) + { + return Entry.State == EfState.Modified + ? entity.Deleted && IsPropertyModified("Deleted") + : entity.Deleted; + } + + return false; + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHook.cs deleted file mode 100644 index 14194258c8..0000000000 --- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHook.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SmartStore.Core.Data.Hooks -{ - /// - /// Marker interface for the hooking framework - /// - public interface IDbHook - { - } -} diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs index 9e46634c86..a79af78c99 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs @@ -1,54 +1,50 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace SmartStore.Core.Data.Hooks { - public interface IDbHookHandler - { - bool HasImportantSaveHooks(); - - /// - /// Triggers all pre action hooks - /// - /// Entries - /// Whether to trigger only hooks marked with the attribute - /// true if the state of any entry changed - /// The list of actually processed hook instances - IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged); - - /// - /// Triggers all post action hooks - /// - /// Entries - /// Whether to trigger only hooks marked with the attribute - /// The list of actually processed hook instances - IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly); - } - - public sealed class NullDbHookHandler : IDbHookHandler - { - private readonly static IDbHookHandler s_instance = new NullDbHookHandler(); - - public static IDbHookHandler Instance - { - get { return s_instance; } - } - - public bool HasImportantSaveHooks() - { - return false; - } - - public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged) - { - anyStateChanged = false; - return Enumerable.Empty(); - } - - public IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) - { - return Enumerable.Empty(); - } - } + public interface IDbHookHandler + { + bool HasImportantSaveHooks(); + + /// + /// Triggers all pre action hooks + /// + /// Entries + /// Whether to trigger only hooks marked with the attribute + /// true if the state of any entry changed + /// The list of actually processed hook instances + IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged); + + /// + /// Triggers all post action hooks + /// + /// Entries + /// Whether to trigger only hooks marked with the attribute + /// The list of actually processed hook instances + IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly); + } + + public sealed class NullDbHookHandler : IDbHookHandler + { + private readonly static IDbHookHandler s_instance = new NullDbHookHandler(); + + public static IDbHookHandler Instance => s_instance; + + public bool HasImportantSaveHooks() + { + return false; + } + + public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged) + { + anyStateChanged = false; + return Enumerable.Empty(); + } + + public IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) + { + return Enumerable.Empty(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs deleted file mode 100644 index 669ffc9dec..0000000000 --- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace SmartStore.Core.Data.Hooks -{ - /// - /// A hook that is executed right after an entity has been loaded from database and materialized. - /// - [Obsolete("Not called anymore for performance reasons.")] - public interface IDbLoadHook : IDbHook - { - void OnLoaded(BaseEntity entity); - } -} diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs index 3a1161aa39..f68c7ceab3 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs @@ -2,27 +2,38 @@ namespace SmartStore.Core.Data.Hooks { - /// - /// A hook that is executed before and after a database save operation. - /// An implementor should raise or - /// to signal the hook handler - /// that it never should process the hook again for the current - /// EntityType/State/Stage combination. - /// - public interface IDbSaveHook : IDbHook - { - void OnBeforeSave(IHookedEntity entry); + /// + /// A hook that is executed before and after a database save operation. + /// An implementor should raise or + /// to signal the hook handler + /// that it never should process the hook again for the current + /// EntityType/State/Stage combination. + /// + public interface IDbSaveHook + { + void OnBeforeSave(IHookedEntity entry); - void OnAfterSave(IHookedEntity entry); + void OnAfterSave(IHookedEntity entry); - /// - /// Called after all entities in the current unit of work has been handled right before saving changes to the database - /// - void OnBeforeSaveCompleted(); + /// + /// Called after all entities in the current unit of work has been handled right before saving changes to the database + /// + void OnBeforeSaveCompleted(); - /// - /// Called after all entities in the current unit of work has been handled after saving changes to the database - /// - void OnAfterSaveCompleted(); - } + /// + /// Called after all entities in the current unit of work has been handled after saving changes to the database + /// + void OnAfterSaveCompleted(); + } + + /// + /// + /// Restricts the hook to the specified data context implementation type. + /// To restrict to the core data context, implement the parameterless instead. + /// Abstract base types can also be specified in order to bypass restrictions. + /// + public interface IDbSaveHook : IDbSaveHook + where TContext : IDbContext + { + } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs b/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs index 585988ee7c..d16b434a2a 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs @@ -2,11 +2,11 @@ namespace SmartStore.Core.Data.Hooks { - /// - /// Indicates that a hook instance should run in any case, even when hooking has been turned off. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ImportantAttribute : Attribute - { - } + /// + /// Indicates that a hook instance should run in any case, even when hooking has been turned off. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class ImportantAttribute : Attribute + { + } } diff --git a/src/Libraries/SmartStore.Core/Data/IDataProvider.cs b/src/Libraries/SmartStore.Core/Data/IDataProvider.cs index 7512eef491..81fd7d9108 100644 --- a/src/Libraries/SmartStore.Core/Data/IDataProvider.cs +++ b/src/Libraries/SmartStore.Core/Data/IDataProvider.cs @@ -18,9 +18,9 @@ public interface IDataProvider /// Parameter DbParameter GetParameter(); - /// - /// Gets the db provider invariant name (e.g. System.Data.SqlClient) - /// - string ProviderInvariantName { get; } + /// + /// Gets the db provider invariant name (e.g. System.Data.SqlClient) + /// + string ProviderInvariantName { get; } } } diff --git a/src/Libraries/SmartStore.Core/Data/IDbContext.cs b/src/Libraries/SmartStore.Core/Data/IDbContext.cs index 8f42a159bb..95224e0308 100644 --- a/src/Libraries/SmartStore.Core/Data/IDbContext.cs +++ b/src/Libraries/SmartStore.Core/Data/IDbContext.cs @@ -3,20 +3,18 @@ using System.Data; using System.Data.Common; using System.Data.Entity; -using System.Linq; -using System.Linq.Expressions; using System.Threading.Tasks; namespace SmartStore.Core.Data { - public interface IDbContext + public interface IDbContext { DbSet Set() where TEntity : BaseEntity; int SaveChanges(); - Task SaveChangesAsync(); + Task SaveChangesAsync(); - IList ExecuteStoredProcedureList(string commandText, params object[] parameters) + IList ExecuteStoredProcedureList(string commandText, params object[] parameters) where TEntity : BaseEntity, new(); /// @@ -27,7 +25,7 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// The parameters to apply to the SQL query string. /// Result IEnumerable SqlQuery(string sql, params object[] parameters); - + /// /// Executes the given DDL/DML command against the database. /// @@ -41,70 +39,70 @@ IList ExecuteStoredProcedureList(string commandText, params ob // Increasing performance on bulk operations bool ProxyCreationEnabled { get; set; } - bool LazyLoadingEnabled { get; set; } - bool AutoDetectChangesEnabled { get; set; } + bool LazyLoadingEnabled { get; set; } + bool AutoDetectChangesEnabled { get; set; } bool ValidateOnSaveEnabled { get; set; } - bool HooksEnabled { get; set; } + bool HooksEnabled { get; set; } bool HasChanges { get; } - /// - /// Gets or sets a value indicating whether entities returned from queries - /// or created from stored procedures - /// should automatically be attached to the DbContext. - /// - /// - /// Set this to true only during long running processes (like export) - /// - bool ForceNoTracking { get; set; } - - /// - /// Gets or sets a value indicating whether database write operations - /// originating from repositories should be committed immediately. - /// - bool AutoCommitEnabled { get; set; } - - /// - /// Detects changes made to the properties and relationships of POCO entities. - /// Please note that normally DetectChanges is called automatically by many of the methods of - /// DbContext and its related classes such that it is rare that this method will need to be called explicitly. - /// However, it may be desirable, usually for performance reasons, to turn off this automatic - /// calling of DetectChanges using the AutoDetectChangesEnabled flag. - /// - void DetectChanges(); - - /// - /// Checks whether the underlying ORM mapper is currently in the process of detecting changes. - /// - /// - bool IsDetectingChanges(); - - /// - /// Gets a value indicating whether the given entity was modified since it has been attached to the context - /// - /// The entity to check - /// true if the entity was modified, false otherwise - bool IsModified(BaseEntity entity); - - /// - /// Determines whether an entity property has changed since it was attached. - /// - /// Entity - /// The property name to check - /// The previous/original property value if change was detected - /// true if property has changed, false otherwise - bool TryGetModifiedProperty(BaseEntity entity, string propertyName, out object originalValue); - - /// - /// Gets a list of modified properties for the specified entity - /// - /// The entity instance for which to get modified properties for - /// - /// A dictionary, where the key is the name of the modified property - /// and the value is its ORIGINAL value (which was tracked when the entity - /// was attached to the context the first time) - /// Returns an empty dictionary if no modification could be detected. - /// - IDictionary GetModifiedProperties(BaseEntity entity); + /// + /// Gets or sets a value indicating whether entities returned from queries + /// or created from stored procedures + /// should automatically be attached to the DbContext. + /// + /// + /// Set this to true only during long running processes (like export) + /// + bool ForceNoTracking { get; set; } + + /// + /// Gets or sets a value indicating whether database write operations + /// originating from repositories should be committed immediately. + /// + bool AutoCommitEnabled { get; set; } + + /// + /// Detects changes made to the properties and relationships of POCO entities. + /// Please note that normally DetectChanges is called automatically by many of the methods of + /// DbContext and its related classes such that it is rare that this method will need to be called explicitly. + /// However, it may be desirable, usually for performance reasons, to turn off this automatic + /// calling of DetectChanges using the AutoDetectChangesEnabled flag. + /// + void DetectChanges(); + + /// + /// Checks whether the underlying ORM mapper is currently in the process of detecting changes. + /// + /// + bool IsDetectingChanges(); + + /// + /// Gets a value indicating whether the given entity was modified since it has been attached to the context + /// + /// The entity to check + /// true if the entity was modified, false otherwise + bool IsModified(BaseEntity entity); + + /// + /// Determines whether an entity property has changed since it was attached. + /// + /// Entity + /// The property name to check + /// The previous/original property value if change was detected + /// true if property has changed, false otherwise + bool TryGetModifiedProperty(BaseEntity entity, string propertyName, out object originalValue); + + /// + /// Gets a list of modified properties for the specified entity + /// + /// The entity instance for which to get modified properties for + /// + /// A dictionary, where the key is the name of the modified property + /// and the value is its ORIGINAL value (which was tracked when the entity + /// was attached to the context the first time) + /// Returns an empty dictionary if no modification could be detected. + /// + IDictionary GetModifiedProperties(BaseEntity entity); /// /// Determines whether the given entity is already attached to the current object context @@ -114,67 +112,67 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// true when the entity is attched already, false otherwise bool IsAttached(TEntity entity) where TEntity : BaseEntity; - /// - /// Attaches an entity to the context or returns an already attached entity (if it was already attached) - /// - /// Type of entity - /// Entity - /// Attached entity - TEntity Attach(TEntity entity) where TEntity : BaseEntity; - - /// - /// Detaches an entity from the current object context if it's attached - /// - /// Type of entity - /// The entity instance to detach - /// Whether to scan all navigation properties and detach them recursively also. - /// The count of detached entities - int DetachEntity(TEntity entity, bool deep = false) where TEntity : BaseEntity; - - /// - /// Detaches all entities of type TEntity from the current object context - /// - /// When true, only entities in unchanged state get detached. - /// Whether to scan all navigation properties and detach them recursively also. LazyLoading should be turned off when true. - /// The count of detached entities - int DetachEntities(bool unchangedEntitiesOnly = true, bool deep = false) where TEntity : BaseEntity; - - /// - /// Detaches all entities matching the passed from the current object context - /// - /// When true, only entities in unchanged state will be detached. - /// Whether to scan all navigation properties and detach them recursively also. - /// The count of detached entities - int DetachEntities(Func predicate, bool unchangedEntitiesOnly = true, bool deep = false); - - /// - /// Change the state of an entity object - /// - /// Type of entity - /// The entity instance - /// The requested new state - void ChangeState(TEntity entity, System.Data.Entity.EntityState requestedState) where TEntity : BaseEntity; - - /// - /// Reloads the entity from the database overwriting any property values with values from the database. - /// The entity will be in the Unchanged state after calling this method. - /// - /// Type of entity - /// The entity instance - void ReloadEntity(TEntity entity) where TEntity : BaseEntity; - - /// - /// Begins a transaction on the underlying store connection using the specified isolation level - /// - /// The database isolation level with which the underlying store transaction will be created - /// A transaction object wrapping access to the underlying store's transaction object - ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified); - - /// - /// Enables the user to pass in a database transaction created outside of the Database object if you want the Entity Framework to execute commands within that external transaction. Alternatively, pass in null to clear the framework's knowledge of that transaction. - /// - /// the external transaction - void UseTransaction(DbTransaction transaction); + /// + /// Attaches an entity to the context or returns an already attached entity (if it was already attached) + /// + /// Type of entity + /// Entity + /// Attached entity + TEntity Attach(TEntity entity) where TEntity : BaseEntity; + + /// + /// Detaches an entity from the current object context if it's attached + /// + /// Type of entity + /// The entity instance to detach + /// Whether to scan all navigation properties and detach them recursively also. + /// The count of detached entities + int DetachEntity(TEntity entity, bool deep = false) where TEntity : BaseEntity; + + /// + /// Detaches all entities of type TEntity from the current object context + /// + /// When true, only entities in unchanged state get detached. + /// Whether to scan all navigation properties and detach them recursively also. LazyLoading should be turned off when true. + /// The count of detached entities + int DetachEntities(bool unchangedEntitiesOnly = true, bool deep = false) where TEntity : BaseEntity; + + /// + /// Detaches all entities matching the passed from the current object context + /// + /// When true, only entities in unchanged state will be detached. + /// Whether to scan all navigation properties and detach them recursively also. + /// The count of detached entities + int DetachEntities(Func predicate, bool unchangedEntitiesOnly = true, bool deep = false); + + /// + /// Change the state of an entity object + /// + /// Type of entity + /// The entity instance + /// The requested new state + void ChangeState(TEntity entity, System.Data.Entity.EntityState requestedState) where TEntity : BaseEntity; + + /// + /// Reloads the entity from the database overwriting any property values with values from the database. + /// The entity will be in the Unchanged state after calling this method. + /// + /// Type of entity + /// The entity instance + void ReloadEntity(TEntity entity) where TEntity : BaseEntity; + + /// + /// Begins a transaction on the underlying store connection using the specified isolation level + /// + /// The database isolation level with which the underlying store transaction will be created + /// A transaction object wrapping access to the underlying store's transaction object + ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified); + + /// + /// Enables the user to pass in a database transaction created outside of the Database object if you want the Entity Framework to execute commands within that external transaction. Alternatively, pass in null to clear the framework's knowledge of that transaction. + /// + /// the external transaction + void UseTransaction(DbTransaction transaction); } } diff --git a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs index 7786f998eb..430f4eabee 100644 --- a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs @@ -1,208 +1,208 @@ using System; using System.Collections.Generic; -using SmartStore.Core; -using SmartStore.Core.Data; +using System.Data.Entity; using System.Linq; using System.Linq.Expressions; -using System.Data.Entity; +using SmartStore.Core; +using SmartStore.Core.Data; namespace SmartStore { - public static class IDbContextExtensions - { - /// - /// Detaches all entities from the current object context - /// - /// When true, only entities in unchanged state get detached. - /// The count of detached entities - public static int DetachAll(this IDbContext ctx, bool unchangedEntitiesOnly = true) - { - return ctx.DetachEntities(unchangedEntitiesOnly, false); - } - - public static void DetachEntities(this IDbContext ctx, IEnumerable entities, bool deep = false) where TEntity : BaseEntity - { - Guard.NotNull(ctx, nameof(ctx)); - - using (new DbContextScope(ctx, autoDetectChanges: false, lazyLoading: false)) - { - entities.Each(x => ctx.DetachEntity(x, deep)); - } - } - - /// - /// Changes the object state to unchanged - /// - /// Type of entity - /// - /// The entity instance - /// true on success, false on failure - public static bool SetToUnchanged(this IDbContext ctx, TEntity entity) where TEntity : BaseEntity - { - try - { - ctx.ChangeState(entity, System.Data.Entity.EntityState.Unchanged); - return true; - } - catch (Exception ex) - { - ex.Dump(); - return false; - } - } - - public static IQueryable QueryForCollection( - this IDbContext ctx, - TEntity entity, - Expression>> navigationProperty) - where TEntity : BaseEntity - where TCollection : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(navigationProperty, nameof(navigationProperty)); - - var dbContext = ctx as DbContext; - if (dbContext == null) - { - throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); - } - - return dbContext.Entry(entity).Collection(navigationProperty).Query(); - } - - public static IQueryable QueryForReference( - this IDbContext ctx, - TEntity entity, - Expression> navigationProperty) - where TEntity : BaseEntity - where TProperty : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(navigationProperty, nameof(navigationProperty)); - - var dbContext = ctx as DbContext; - if (dbContext == null) - { - throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); - } - - return dbContext.Entry(entity).Reference(navigationProperty).Query(); - } - - public static void LoadCollection( - this IDbContext ctx, - TEntity entity, - Expression>> navigationProperty, - bool force = false, - Func, IQueryable> queryAction = null) - where TEntity : BaseEntity - where TCollection : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(navigationProperty, nameof(navigationProperty)); - - var dbContext = ctx as DbContext; - if (dbContext == null) - { - throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); - } - - var entry = dbContext.Entry(entity); - var collection = entry.Collection(navigationProperty); - - // Avoid System.InvalidOperationException: Member 'IsLoaded' cannot be called for property... - if (entry.State == System.Data.Entity.EntityState.Detached) - { - ctx.Attach(entity); - } - - if (force) - { - collection.IsLoaded = false; - } - - if (!collection.IsLoaded) - { - if (queryAction != null || ctx.ForceNoTracking) - { - var query = !ctx.ForceNoTracking - ? collection.Query() - : collection.Query().AsNoTracking(); - - var myQuery = queryAction != null - ? queryAction(query) - : query; - - collection.CurrentValue = myQuery.ToList(); - } - else - { - collection.Load(); - } - - collection.IsLoaded = true; - } - } - - public static void LoadReference( - this IDbContext ctx, - TEntity entity, - Expression> navigationProperty, - bool force = false, - Func, IQueryable> queryAction = null) - where TEntity : BaseEntity - where TProperty : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(navigationProperty, nameof(navigationProperty)); - - var dbContext = ctx as DbContext; - if (dbContext == null) - { - throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); - } - - var entry = dbContext.Entry(entity); - var reference = entry.Reference(navigationProperty); - - // Avoid System.InvalidOperationException: Member 'IsLoaded' cannot be called for property... - if (entry.State == System.Data.Entity.EntityState.Detached) - { - ctx.Attach(entity); - } - - if (force) - { - reference.IsLoaded = false; - } - - if (!reference.IsLoaded) - { - if (queryAction != null || ctx.ForceNoTracking) - { - var query = !ctx.ForceNoTracking - ? reference.Query() - : reference.Query().AsNoTracking(); - - var myQuery = queryAction != null - ? queryAction(query) - : query; - - reference.CurrentValue = myQuery.FirstOrDefault(); - } - else - { - reference.Load(); - } - - reference.IsLoaded = true; - } - } - - public static void AttachRange(this IDbContext ctx, IEnumerable entities) where TEntity : BaseEntity - { - entities.Each(x => ctx.Attach(x)); - } - - } + public static class IDbContextExtensions + { + /// + /// Detaches all entities from the current object context + /// + /// When true, only entities in unchanged state get detached. + /// The count of detached entities + public static int DetachAll(this IDbContext ctx, bool unchangedEntitiesOnly = true) + { + return ctx.DetachEntities(unchangedEntitiesOnly, false); + } + + public static void DetachEntities(this IDbContext ctx, IEnumerable entities, bool deep = false) where TEntity : BaseEntity + { + Guard.NotNull(ctx, nameof(ctx)); + + using (new DbContextScope(ctx, autoDetectChanges: false, lazyLoading: false)) + { + entities.Each(x => ctx.DetachEntity(x, deep)); + } + } + + /// + /// Changes the object state to unchanged + /// + /// Type of entity + /// + /// The entity instance + /// true on success, false on failure + public static bool SetToUnchanged(this IDbContext ctx, TEntity entity) where TEntity : BaseEntity + { + try + { + ctx.ChangeState(entity, System.Data.Entity.EntityState.Unchanged); + return true; + } + catch (Exception ex) + { + ex.Dump(); + return false; + } + } + + public static IQueryable QueryForCollection( + this IDbContext ctx, + TEntity entity, + Expression>> navigationProperty) + where TEntity : BaseEntity + where TCollection : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(navigationProperty, nameof(navigationProperty)); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + return dbContext.Entry(entity).Collection(navigationProperty).Query(); + } + + public static IQueryable QueryForReference( + this IDbContext ctx, + TEntity entity, + Expression> navigationProperty) + where TEntity : BaseEntity + where TProperty : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(navigationProperty, nameof(navigationProperty)); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + return dbContext.Entry(entity).Reference(navigationProperty).Query(); + } + + public static void LoadCollection( + this IDbContext ctx, + TEntity entity, + Expression>> navigationProperty, + bool force = false, + Func, IQueryable> queryAction = null) + where TEntity : BaseEntity + where TCollection : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(navigationProperty, nameof(navigationProperty)); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + var entry = dbContext.Entry(entity); + var collection = entry.Collection(navigationProperty); + + // Avoid System.InvalidOperationException: Member 'IsLoaded' cannot be called for property... + if (entry.State == System.Data.Entity.EntityState.Detached) + { + ctx.Attach(entity); + } + + if (force) + { + collection.IsLoaded = false; + } + + if (!collection.IsLoaded) + { + if (queryAction != null || ctx.ForceNoTracking) + { + var query = !ctx.ForceNoTracking + ? collection.Query() + : collection.Query().AsNoTracking(); + + var myQuery = queryAction != null + ? queryAction(query) + : query; + + collection.CurrentValue = myQuery.ToList(); + } + else + { + collection.Load(); + } + + collection.IsLoaded = true; + } + } + + public static void LoadReference( + this IDbContext ctx, + TEntity entity, + Expression> navigationProperty, + bool force = false, + Func, IQueryable> queryAction = null) + where TEntity : BaseEntity + where TProperty : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(navigationProperty, nameof(navigationProperty)); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + var entry = dbContext.Entry(entity); + var reference = entry.Reference(navigationProperty); + + // Avoid System.InvalidOperationException: Member 'IsLoaded' cannot be called for property... + if (entry.State == System.Data.Entity.EntityState.Detached) + { + ctx.Attach(entity); + } + + if (force) + { + reference.IsLoaded = false; + } + + if (!reference.IsLoaded) + { + if (queryAction != null || ctx.ForceNoTracking) + { + var query = !ctx.ForceNoTracking + ? reference.Query() + : reference.Query().AsNoTracking(); + + var myQuery = queryAction != null + ? queryAction(query) + : query; + + reference.CurrentValue = myQuery.FirstOrDefault(); + } + else + { + reference.Load(); + } + + reference.IsLoaded = true; + } + } + + public static void AttachRange(this IDbContext ctx, IEnumerable entities) where TEntity : BaseEntity + { + entities.Each(x => ctx.Attach(x)); + } + + } } diff --git a/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs b/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs index d9534c06ea..dc5a3eed3e 100644 --- a/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs @@ -1,43 +1,42 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Data.Entity; +using System.Linq; using System.Linq.Expressions; using SmartStore.Core; namespace SmartStore { - public static class IQueryableExtensions - { - /// - /// Instructs the repository to eager load entities that may be in the type's association path. - /// - /// A previously created query object which the expansion should be applied to. - /// - /// The path of the child entities to eager load. - /// Deeper paths can be specified by separating the path with dots. - /// - /// A new query object to which the expansion was applied. - public static IQueryable Expand(this IQueryable query, string path) where T : BaseEntity - { - Guard.NotNull(query, "query"); - Guard.NotEmpty(path, "path"); + public static class IQueryableExtensions + { + /// + /// Instructs the repository to eager load entities that may be in the type's association path. + /// + /// A previously created query object which the expansion should be applied to. + /// + /// The path of the child entities to eager load. + /// Deeper paths can be specified by separating the path with dots. + /// + /// A new query object to which the expansion was applied. + public static IQueryable Expand(this IQueryable query, string path) where T : BaseEntity + { + Guard.NotNull(query, "query"); + Guard.NotEmpty(path, "path"); - return query.Include(path); - } + return query.Include(path); + } - /// - /// Instructs the repository to eager load entities that may be in the type's association path. - /// - /// A previously created query object which the expansion should be applied to. - /// The path of the child entities to eager load. - /// A new query object to which the expansion was applied. - public static IQueryable Expand(this IQueryable query, Expression> path) where T : BaseEntity - { - Guard.NotNull(query, "query"); - Guard.NotNull(path, "path"); + /// + /// Instructs the repository to eager load entities that may be in the type's association path. + /// + /// A previously created query object which the expansion should be applied to. + /// The path of the child entities to eager load. + /// A new query object to which the expansion was applied. + public static IQueryable Expand(this IQueryable query, Expression> path) where T : BaseEntity + { + Guard.NotNull(query, "query"); + Guard.NotNull(path, "path"); - return query.Include(path); - } - } + return query.Include(path); + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/IRepository.cs b/src/Libraries/SmartStore.Core/Data/IRepository.cs index 254b33044a..b555b731cf 100644 --- a/src/Libraries/SmartStore.Core/Data/IRepository.cs +++ b/src/Libraries/SmartStore.Core/Data/IRepository.cs @@ -22,10 +22,10 @@ public partial interface IRepository where T : BaseEntity /// IQueryable TableUntracked { get; } - /// - /// Provides access to the entities currently being tracked by the context and have not been marked as deleted - /// - ICollection Local { get; } + /// + /// Provides access to the entities currently being tracked by the context and have not been marked as deleted + /// + ICollection Local { get; } /// /// Creates a new instance of an entity of type {T} @@ -141,7 +141,7 @@ public partial interface IRepository where T : BaseEntity [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] IQueryable Expand(IQueryable query, string path); - [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] + [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] IQueryable Expand(IQueryable query, Expression> path); /// diff --git a/src/Libraries/SmartStore.Core/Data/ITransaction.cs b/src/Libraries/SmartStore.Core/Data/ITransaction.cs index e20bb41d17..54155bf572 100644 --- a/src/Libraries/SmartStore.Core/Data/ITransaction.cs +++ b/src/Libraries/SmartStore.Core/Data/ITransaction.cs @@ -2,9 +2,9 @@ namespace SmartStore.Core.Data { - public interface ITransaction : IDisposable - { - void Commit(); - void Rollback(); - } + public interface ITransaction : IDisposable + { + void Commit(); + void Rollback(); + } } diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs index 59ae3aa5c6..1fefc75eb2 100644 --- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; +using System.Data.Entity; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; -using System.Text; -using System.Data.Entity; using System.Threading.Tasks; namespace SmartStore.Core.Data -{ +{ public static class RepositoryExtensions { public static IEnumerable GetMany(this IRepository rs, IEnumerable ids) where T : BaseEntity @@ -24,140 +23,140 @@ public static IEnumerable GetMany(this IRepository rs, IEnumerable } } - public static async Task> GetManyAsync(this IRepository rs, IEnumerable ids) where T : BaseEntity - { - var result = new List(); - - foreach (var chunk in ids.Slice(128)) - { - var items = await rs.Table.Where(a => chunk.Contains(a.Id)).ToListAsync(); - result.AddRange(items); - } + public static async Task> GetManyAsync(this IRepository rs, IEnumerable ids) where T : BaseEntity + { + var result = new List(); - return result; - } + foreach (var chunk in ids.Slice(128)) + { + var items = await rs.Table.Where(a => chunk.Contains(a.Id)).ToListAsync(); + result.AddRange(items); + } - public static void Delete(this IRepository rs, int id) where T : BaseEntity + return result; + } + + public static void Delete(this IRepository rs, int id) where T : BaseEntity { - Guard.NotZero(id, nameof(id)); - - // Perf: work with stub entity - var entity = rs.Create(); - entity.Id = id; - - // must downcast 'cause of Rhino mocks stub - rs.Context.ChangeState((BaseEntity)entity, System.Data.Entity.EntityState.Deleted); + Guard.NotZero(id, nameof(id)); + + // Perf: work with stub entity + var entity = rs.Create(); + entity.Id = id; + + // must downcast 'cause of Rhino mocks stub + rs.Context.ChangeState((BaseEntity)entity, System.Data.Entity.EntityState.Deleted); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void DeleteRange(this IRepository rs, IEnumerable ids) where T : BaseEntity - { - Guard.NotNull(ids, nameof(ids)); - - ids.Each(id => rs.Delete(id)); - } - - /// - /// Truncates the table - /// - /// Entity type - /// The repository - /// An optional filter - /// - /// false: does not make any attempts to determine dependant entities, just deletes ONLY them (faster). - /// true: loads all entities into the context first and deletes them, along with their dependencies (slower). - /// - /// The total number of affected entities - /// - /// This method turns off auto detection, validation and hooking. - /// - [SuppressMessage("ReSharper", "UnusedVariable")] - public static int DeleteAll(this IRepository rs, Expression> predicate = null, bool cascade = false) where T : BaseEntity - { - var count = 0; - - using (var scope = new DbContextScope(ctx: rs.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false)) - { - var query = rs.Table; - if (predicate != null) - { - query = query.Where(predicate); - } - - if (cascade) - { - var records = query.ToList(); - foreach (var chunk in records.Slice(500)) - { - rs.DeleteRange(chunk.ToList()); - count += rs.Context.SaveChanges(); - } - } - else - { - var ids = query.Select(x => new { x.Id }).ToList(); - foreach (var chunk in ids.Slice(500)) - { - rs.DeleteRange(chunk.Select(x => x.Id)); - count += rs.Context.SaveChanges(); - } - } - } - - return count; - } - - /// - /// Truncates the table asynchronously. - /// - /// Entity type - /// The repository - /// An optional filter - /// - /// false: does not make any attempts to determine dependant entities, just deletes ONLY them (faster). - /// true: loads all entities into the context first and deletes them, along with their dependencies (slower). - /// - /// The total number of affected entities - /// - /// This method turns off auto detection, validation and hooking. - /// - [SuppressMessage("ReSharper", "UnusedVariable")] - public async static Task DeleteAllAsync(this IRepository rs, Expression> predicate = null, bool cascade = false) where T : BaseEntity - { - var count = 0; - - using (var scope = new DbContextScope(ctx: rs.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false)) - { - var query = rs.Table; - if (predicate != null) - { - query = query.Where(predicate); - } - - if (cascade) - { - var records = query.ToList(); - foreach (var chunk in records.Slice(500)) - { - rs.DeleteRange(chunk.ToList()); - count += await rs.Context.SaveChangesAsync(); - } - } - else - { - var ids = query.Select(x => new { x.Id }).ToList(); - foreach (var chunk in ids.Slice(500)) - { - rs.DeleteRange(chunk.Select(x => x.Id)); - count += await rs.Context.SaveChangesAsync(); - } - } - } - - return count; - } - - public static IQueryable Get( + { + Guard.NotNull(ids, nameof(ids)); + + ids.Each(id => rs.Delete(id)); + } + + /// + /// Truncates the table + /// + /// Entity type + /// The repository + /// An optional filter + /// + /// false: does not make any attempts to determine dependant entities, just deletes ONLY them (faster). + /// true: loads all entities into the context first and deletes them, along with their dependencies (slower). + /// + /// The total number of affected entities + /// + /// This method turns off auto detection, validation and hooking. + /// + [SuppressMessage("ReSharper", "UnusedVariable")] + public static int DeleteAll(this IRepository rs, Expression> predicate = null, bool cascade = false) where T : BaseEntity + { + var count = 0; + + using (var scope = new DbContextScope(ctx: rs.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var query = rs.Table; + if (predicate != null) + { + query = query.Where(predicate); + } + + if (cascade) + { + var records = query.ToList(); + foreach (var chunk in records.Slice(500)) + { + rs.DeleteRange(chunk.ToList()); + count += rs.Context.SaveChanges(); + } + } + else + { + var ids = query.Select(x => new { x.Id }).ToList(); + foreach (var chunk in ids.Slice(500)) + { + rs.DeleteRange(chunk.Select(x => x.Id)); + count += rs.Context.SaveChanges(); + } + } + } + + return count; + } + + /// + /// Truncates the table asynchronously. + /// + /// Entity type + /// The repository + /// An optional filter + /// + /// false: does not make any attempts to determine dependant entities, just deletes ONLY them (faster). + /// true: loads all entities into the context first and deletes them, along with their dependencies (slower). + /// + /// The total number of affected entities + /// + /// This method turns off auto detection, validation and hooking. + /// + [SuppressMessage("ReSharper", "UnusedVariable")] + public async static Task DeleteAllAsync(this IRepository rs, Expression> predicate = null, bool cascade = false) where T : BaseEntity + { + var count = 0; + + using (var scope = new DbContextScope(ctx: rs.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var query = rs.Table; + if (predicate != null) + { + query = query.Where(predicate); + } + + if (cascade) + { + var records = query.ToList(); + foreach (var chunk in records.Slice(500)) + { + rs.DeleteRange(chunk.ToList()); + count += await rs.Context.SaveChangesAsync(); + } + } + else + { + var ids = query.Select(x => new { x.Id }).ToList(); + foreach (var chunk in ids.Slice(500)) + { + rs.DeleteRange(chunk.Select(x => x.Id)); + count += await rs.Context.SaveChangesAsync(); + } + } + } + + return count; + } + + public static IQueryable Get( this IRepository rs, Expression> predicate = null, Func, IOrderedQueryable> orderBy = null, @@ -172,7 +171,7 @@ public static IQueryable Get( foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - query = query.Expand(includeProperty.Trim()); + query = query.Expand(includeProperty.Trim()); } if (orderBy != null) diff --git a/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs b/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs index 36f9284433..4c04b20bb2 100644 --- a/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs +++ b/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Reflection; using System.Text; using SmartStore.Utilities; @@ -10,89 +8,87 @@ namespace SmartStore.Core.Data { - - public class SqlFileTokenizer - { - - public SqlFileTokenizer(string fileName, Assembly assembly = null, string location = null) - { - Guard.NotEmpty(fileName, nameof(fileName)); - - this.FileName = fileName; - this.Assembly = assembly; - this.Location = location; - } - - public string FileName - { - get; - private set; - } - - public Assembly Assembly - { - get; - private set; - } - - public string Location - { - get; - private set; - } - - public IEnumerable Tokenize() - { - if (this.Assembly == null) - { - this.Assembly = Assembly.GetExecutingAssembly(); - } - - using (var reader = ReadSqlFile()) - { - string statement; - while ((statement = ReadNextSqlStatement(reader)) != null) - { - yield return statement.EmptyNull(); - } - } - } - - protected virtual StreamReader ReadSqlFile() - { - - var fileName = this.FileName; - - if (fileName.StartsWith("~") || fileName.StartsWith("/")) - { - string path = CommonHelper.MapPath(fileName); - if (!File.Exists(path)) - { - throw new FileNotFoundException("Sql file '{0}' not found".FormatInvariant(this.FileName)); - } - - return new StreamReader(File.OpenRead(path)); - } - - // SQL file is obviously an embedded resource - var assembly = this.Assembly; - var asmName = assembly.FullName.Substring(0, assembly.FullName.IndexOf(',')); - var location = this.Location ?? asmName + ".Sql"; - var name = String.Format("{0}.{1}", location, fileName); - - try - { - var stream = assembly.GetManifestResourceStream(name); - return new StreamReader(stream); - } - catch (Exception ex) - { - throw new FileLoadException("Error while loading embedded sql resource '{0}'".FormatInvariant(name), ex); - } - } - - private string ReadNextSqlStatement(TextReader reader) - { + public class SqlFileTokenizer + { + public SqlFileTokenizer(string fileName, Assembly assembly = null, string location = null) + { + Guard.NotEmpty(fileName, nameof(fileName)); + + this.FileName = fileName; + this.Assembly = assembly; + this.Location = location; + } + + public string FileName + { + get; + private set; + } + + public Assembly Assembly + { + get; + private set; + } + + public string Location + { + get; + private set; + } + + public IEnumerable Tokenize() + { + if (this.Assembly == null) + { + this.Assembly = Assembly.GetExecutingAssembly(); + } + + using (var reader = ReadSqlFile()) + { + string statement; + while ((statement = ReadNextSqlStatement(reader)) != null) + { + yield return statement.EmptyNull(); + } + } + } + + protected virtual StreamReader ReadSqlFile() + { + + var fileName = this.FileName; + + if (fileName.StartsWith("~") || fileName.StartsWith("/")) + { + string path = CommonHelper.MapPath(fileName); + if (!File.Exists(path)) + { + throw new FileNotFoundException("Sql file '{0}' not found".FormatInvariant(this.FileName)); + } + + return new StreamReader(File.OpenRead(path)); + } + + // SQL file is obviously an embedded resource + var assembly = this.Assembly; + var asmName = assembly.FullName.Substring(0, assembly.FullName.IndexOf(',')); + var location = this.Location ?? asmName + ".Sql"; + var name = String.Format("{0}.{1}", location, fileName); + + try + { + var stream = assembly.GetManifestResourceStream(name); + return new StreamReader(stream); + } + catch (Exception ex) + { + throw new FileLoadException("Error while loading embedded sql resource '{0}'".FormatInvariant(name), ex); + } + } + + private string ReadNextSqlStatement(TextReader reader) + { var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; @@ -116,8 +112,6 @@ private string ReadNextSqlStatement(TextReader reader) } return psb.ToStringAndReturn(); - } - - } - + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Affiliates/Affiliate.cs b/src/Libraries/SmartStore.Core/Domain/Affiliates/Affiliate.cs index c9cd75e4d1..d4a500cd4a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Affiliates/Affiliate.cs +++ b/src/Libraries/SmartStore.Core/Domain/Affiliates/Affiliate.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Orders; namespace SmartStore.Core.Domain.Affiliates { @@ -10,7 +7,7 @@ namespace SmartStore.Core.Domain.Affiliates /// Represents an affiliate /// [DataContract] - public partial class Affiliate : BaseEntity, ISoftDeletable + public partial class Affiliate : BaseEntity, ISoftDeletable { [DataMember] diff --git a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogComment.cs b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogComment.cs index fce406b0b9..22f323bc72 100644 --- a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogComment.cs +++ b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogComment.cs @@ -3,28 +3,28 @@ namespace SmartStore.Core.Domain.Blogs { - /// - /// Represents a blog comment - /// - [DataContract] - public partial class BlogComment : CustomerContent + /// + /// Represents a blog comment + /// + [DataContract] + public partial class BlogComment : CustomerContent { - /// - /// Gets or sets the comment text - /// - [DataMember] - public string CommentText { get; set; } + /// + /// Gets or sets the comment text + /// + [DataMember] + public string CommentText { get; set; } - /// - /// Gets or sets the blog post identifier - /// - [DataMember] - public int BlogPostId { get; set; } + /// + /// Gets or sets the blog post identifier + /// + [DataMember] + public int BlogPostId { get; set; } - /// - /// Gets or sets the blog post - /// - [DataMember] - public virtual BlogPost BlogPost { get; set; } + /// + /// Gets or sets the blog post + /// + [DataMember] + public virtual BlogPost BlogPost { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs index e5cbfc2f5c..1a626e9098 100644 --- a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs +++ b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs @@ -6,8 +6,8 @@ namespace SmartStore.Core.Domain.Blogs { public static class BlogExtensions { - [SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")] - public static string[] ParseTags(this BlogPost blogPost) + [SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")] + public static string[] ParseTags(this BlogPost blogPost) { if (blogPost == null) throw new ArgumentNullException("blogPost"); @@ -18,9 +18,9 @@ public static string[] ParseTags(this BlogPost blogPost) var tags2 = blogPost.Tags.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var tag2 in tags2) { - var tmp = tag2.Trim(); - if (!String.IsNullOrEmpty(tmp)) - parsedTags.Add(tmp); + var tmp = tag2.Trim(); + if (!String.IsNullOrEmpty(tmp)) + parsedTags.Add(tmp); } } return parsedTags.ToArray(); diff --git a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogPost.cs b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogPost.cs index 09d2269787..821d502963 100644 --- a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogPost.cs +++ b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogPost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; @@ -8,12 +9,29 @@ namespace SmartStore.Core.Domain.Blogs { - /// - /// Represents a blog post - /// - [DataContract] - public partial class BlogPost : BaseEntity, ISlugSupported, IStoreMappingSupported + /// + /// Represents a blog post + /// + [DataContract] + public partial class BlogPost : BaseEntity, ISlugSupported, IStoreMappingSupported, ILocalizedEntity { + #region static + + private static readonly List _visibilityAffectingProps = new List + { + nameof(BlogPost.IsPublished), + nameof(BlogPost.StartDateUtc), + nameof(BlogPost.EndDateUtc), + nameof(BlogPost.LimitedToStores) + }; + + public static IReadOnlyCollection GetVisibilityAffectingPropertyNames() + { + return _visibilityAffectingProps; + } + + #endregion + private ICollection _blogComments; /// @@ -23,16 +41,10 @@ public partial class BlogPost : BaseEntity, ISlugSupported, IStoreMappingSupport public bool IsPublished { get; set; } /// - /// Gets or sets the language identifier + /// Gets or sets the blog post title /// [DataMember] - public int LanguageId { get; set; } - - /// - /// Gets or sets the blog post title - /// - [DataMember] - public string Title { get; set; } + public string Title { get; set; } /// /// Defines the preview display type of the picture @@ -46,26 +58,26 @@ public partial class BlogPost : BaseEntity, ISlugSupported, IStoreMappingSupport [DataMember] public int? MediaFileId { get; set; } - /// - /// Gets or sets the media file. - /// - public virtual MediaFile MediaFile { get; set; } + /// + /// Gets or sets the media file. + /// + public virtual MediaFile MediaFile { get; set; } - /// - /// Gets or sets the preview media file identifier - /// - [DataMember] + /// + /// Gets or sets the preview media file identifier + /// + [DataMember] public int? PreviewMediaFileId { get; set; } - /// - /// Gets or sets the preview media file. - /// - public virtual MediaFile PreviewMediaFile { get; set; } + /// + /// Gets or sets the preview media file. + /// + public virtual MediaFile PreviewMediaFile { get; set; } - /// - /// Gets or sets background for the blog post - /// - [DataMember] + /// + /// Gets or sets background for the blog post + /// + [DataMember] public string SectionBg { get; set; } /// @@ -84,94 +96,101 @@ public partial class BlogPost : BaseEntity, ISlugSupported, IStoreMappingSupport /// Gets or sets the blog post title /// [DataMember] - public string Body { get; set; } + public string Body { get; set; } - /// - /// Gets or sets a value indicating whether the blog post comments are allowed - /// - [DataMember] - public bool AllowComments { get; set; } + /// + /// Gets or sets a value indicating whether the blog post comments are allowed + /// + [DataMember] + public bool AllowComments { get; set; } - /// - /// Gets or sets the total number of approved comments - /// The same as if we run newsItem.NewsComments.Where(n => n.IsApproved).Count() - /// We use this property for performance optimization (no SQL command executed) - /// - /// - [DataMember] - public int ApprovedCommentCount { get; set; } + /// + /// Gets or sets the total number of approved comments + /// The same as if we run newsItem.NewsComments.Where(n => n.IsApproved).Count() + /// We use this property for performance optimization (no SQL command executed) + /// + /// + [DataMember] + public int ApprovedCommentCount { get; set; } - /// - /// Gets or sets the total number of not approved comments - /// The same as if we run newsItem.NewsComments.Where(n => !n.IsApproved).Count() - /// We use this property for performance optimization (no SQL command executed) - /// - [DataMember] - public int NotApprovedCommentCount { get; set; } + /// + /// Gets or sets the total number of not approved comments + /// The same as if we run newsItem.NewsComments.Where(n => !n.IsApproved).Count() + /// We use this property for performance optimization (no SQL command executed) + /// + [DataMember] + public int NotApprovedCommentCount { get; set; } - /// - /// Gets or sets the blog tags - /// - [DataMember] - public string Tags { get; set; } + /// + /// Gets or sets the blog tags + /// + [DataMember] + public string Tags { get; set; } /// /// Gets or sets the blog post start date and time /// [DataMember] - public DateTime? StartDateUtc { get; set; } - - /// - /// Gets or sets the blog post end date and time - /// - [DataMember] - public DateTime? EndDateUtc { get; set; } - - /// - /// Gets or sets the meta keywords - /// - [DataMember] - public string MetaKeywords { get; set; } - - /// - /// Gets or sets the meta description - /// - [DataMember] - public string MetaDescription { get; set; } - - /// - /// Gets or sets the meta title - /// - [DataMember] - public string MetaTitle { get; set; } - - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } - - /// - /// Gets or sets the date and time of entity creation - /// - [DataMember] - public virtual DateTime CreatedOnUtc { get; set; } - - /// - /// Gets or sets the blog comments - /// - [DataMember] - public virtual ICollection BlogComments + public DateTime? StartDateUtc { get; set; } + + /// + /// Gets or sets the blog post end date and time + /// + [DataMember] + public DateTime? EndDateUtc { get; set; } + + /// + /// Gets or sets the meta keywords + /// + [DataMember] + public string MetaKeywords { get; set; } + + /// + /// Gets or sets the meta description + /// + [DataMember] + public string MetaDescription { get; set; } + + /// + /// Gets or sets the meta title + /// + [DataMember] + public string MetaTitle { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } + + /// + /// Gets or sets a language identifier for which the blog post should be displayed. + /// + [DataMember] + [Index] + public int? LanguageId { get; set; } + + /// + /// Gets or sets the date and time of entity creation + /// + [DataMember] + public virtual DateTime CreatedOnUtc { get; set; } + + /// + /// Gets or sets the language. + /// + [DataMember] + public virtual Language Language { get; set; } + + /// + /// Gets or sets the blog comments + /// + [DataMember] + public virtual ICollection BlogComments { - get { return _blogComments ?? (_blogComments = new HashSet()); } - protected set { _blogComments = value; } + get => _blogComments ?? (_blogComments = new HashSet()); + protected set => _blogComments = value; } - - /// - /// Gets or sets the language - /// - [DataMember] - public virtual Language Language { get; set; } } public enum PreviewDisplayType diff --git a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs index e0c7492934..1bba994e1b 100644 --- a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs @@ -5,29 +5,20 @@ namespace SmartStore.Core.Domain.Blogs { public class BlogSettings : ISettings { - public BlogSettings() - { - Enabled = true; - PostsPageSize = 10; - AllowNotRegisteredUsersToLeaveComments = true; - NumberOfTags = 15; - MaxAgeInDays = 180; - } - - /// + /// /// Gets or sets a value indicating whether blog is enabled /// - public bool Enabled { get; set; } + public bool Enabled { get; set; } = true; /// /// Gets or sets the page size for posts /// - public int PostsPageSize { get; set; } + public int PostsPageSize { get; set; } = 10; /// /// Gets or sets a value indicating whether not registered user can leave comments /// - public bool AllowNotRegisteredUsersToLeaveComments { get; set; } + public bool AllowNotRegisteredUsersToLeaveComments { get; set; } = true; /// /// Gets or sets a value indicating whether to notify about new blog comments @@ -37,16 +28,31 @@ public BlogSettings() /// /// Gets or sets a number of blog tags that appear in the tag cloud /// - public int NumberOfTags { get; set; } + public int NumberOfTags { get; set; } = 15; - /// - /// The maximum age of blog items (in days) for RSS feed - /// - public int MaxAgeInDays { get; set; } + /// + /// The maximum age of blog items (in days) for RSS feed + /// + public int MaxAgeInDays { get; set; } = 180; /// /// Enable the blog RSS feed link in customers browser address bar /// public bool ShowHeaderRssUrl { get; set; } + + /// + /// Gets or sets the meta title for blog index page + /// + public string MetaTitle { get; set; } + + /// + /// Gets or sets the meta description for blog index page + /// + public string MetaDescription { get; set; } + + /// + /// Gets or sets the meta keywords for blog index page + /// + public string MetaKeywords { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/AttributeChoiceBehaviour.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/AttributeChoiceBehaviour.cs new file mode 100644 index 0000000000..71a3edd7a5 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/AttributeChoiceBehaviour.cs @@ -0,0 +1,18 @@ +namespace SmartStore.Core.Domain.Catalog +{ + /// + /// Represents the behaviour when selecting product attributes. + /// + public enum AttributeChoiceBehaviour + { + /// + /// Gray out unavailable attributes. + /// + GrayOutUnavailable = 0, + + /// + /// No particular behaviour. + /// + None = 30 + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/BackInStockSubscription.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/BackInStockSubscription.cs index f3bddf1988..6b79d6e2b3 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/BackInStockSubscription.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/BackInStockSubscription.cs @@ -8,10 +8,10 @@ namespace SmartStore.Core.Domain.Catalog /// public partial class BackInStockSubscription : BaseEntity { - /// - /// Gets or sets the store identifier - /// - public int StoreId { get; set; } + /// + /// Gets or sets the store identifier + /// + public int StoreId { get; set; } /// /// Gets or sets the product identifier diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index 02376872ce..88de546eeb 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -1,47 +1,48 @@ using System.Collections.Generic; using SmartStore.Core.Configuration; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Core.Domain.Catalog { - public enum SubCategoryDisplayType - { - Hide = 0, - AboveProductList = 5, - Bottom = 10 - } - - public enum GridColumnSpan - { - Max2Cols = 2, - Max3Cols = 3, - Max4Cols = 4, - Max5Cols = 5, - Max6Cols = 6 - } + public enum SubCategoryDisplayType + { + Hide = 0, + AboveProductList = 5, + Bottom = 10 + } + + public enum GridColumnSpan + { + Max2Cols = 2, + Max3Cols = 3, + Max4Cols = 4, + Max5Cols = 5, + Max6Cols = 6 + } public class CatalogSettings : ISettings { public CatalogSettings() { FileUploadAllowedExtensions = new List(); - AllowProductSorting = true; + AllowProductSorting = true; DefaultSortOrder = ProductSortingEnum.Relevance; - AllowProductViewModeChanging = true; - DefaultViewMode = "grid"; - CategoryBreadcrumbEnabled = true; - ShowShareButton = true; - PageShareCode = "
"; - DefaultProductRatingValue = 5; - NotifyStoreOwnerAboutNewProductReviews = true; - EmailAFriendEnabled = true; - AskQuestionEnabled = true; - RecentlyViewedProductsNumber = 8; - RecentlyViewedProductsEnabled = true; - RecentlyAddedProductsNumber = 100; - RecentlyAddedProductsEnabled = true; - CompareProductsEnabled = true; - SubCategoryDisplayType = SubCategoryDisplayType.AboveProductList; - NumberOfBestsellersOnHomepage = 12; + AllowProductViewModeChanging = true; + DefaultViewMode = "grid"; + CategoryBreadcrumbEnabled = true; + ShowShareButton = true; + PageShareCode = "
"; + DefaultProductRatingValue = 5; + NotifyStoreOwnerAboutNewProductReviews = true; + EmailAFriendEnabled = true; + AskQuestionEnabled = true; + RecentlyViewedProductsNumber = 8; + RecentlyViewedProductsEnabled = true; + RecentlyAddedProductsNumber = 100; + RecentlyAddedProductsEnabled = true; + CompareProductsEnabled = true; + SubCategoryDisplayType = SubCategoryDisplayType.AboveProductList; + NumberOfBestsellersOnHomepage = 12; ShowPopularProductTagsOnHomepage = false; ShowManufacturersOnHomepage = true; ShowManufacturerPictures = true; @@ -50,38 +51,40 @@ public CatalogSettings() ManufacturerItemsToDisplayInOffcanvasMenu = 20; ShowManufacturerInProductDetail = true; ShowManufacturerPicturesInProductDetail = true; - ProductsAlsoPurchasedEnabled = true; - ProductsAlsoPurchasedNumber = 12; - NumberOfProductTags = 15; - DefaultProductListPageSize = 24; - UseSmallProductBoxOnHomePage = true; - DisplayTierPricesWithDiscounts = true; - DefaultPageSizeOptions = "12,24,36,48,72,120"; - AllowCustomersToSelectPageSize = true; - MaximumBackInStockSubscriptions = 200; - FileUploadMaximumSizeBytes = 1024 * 200; //200KB - DisplayAllImagesNumber = 6; - ShowShortDescriptionInGridStyleLists = true; - ShowManufacturerInGridStyleLists = true; - ShowManufacturerLogoInLists = false; - ShowProductOptionsInLists = true; - ShowColorSquaresInLists = true; - ShowDiscountSign = true; - ShowVariantCombinationPriceAdjustment = true; + ProductsAlsoPurchasedEnabled = true; + ProductsAlsoPurchasedNumber = 12; + NumberOfProductTags = 15; + DefaultProductListPageSize = 24; + UseSmallProductBoxOnHomePage = true; + DisplayTierPricesWithDiscounts = true; + DefaultPageSizeOptions = "12,24,36,48,72,120"; + AllowCustomersToSelectPageSize = true; + MaximumBackInStockSubscriptions = 200; + FileUploadMaximumSizeBytes = 1024 * 200; //200KB + DisplayAllImagesNumber = 6; + ShowShortDescriptionInGridStyleLists = true; + ShowManufacturerInGridStyleLists = true; + ShowManufacturerLogoInLists = false; + ShowProductOptionsInLists = true; + ShowColorSquaresInLists = true; + ShowDiscountSign = true; + ShowVariantCombinationPriceAdjustment = true; ShowLinkedAttributeValueImage = true; - EnableDynamicPriceUpdate = true; + EnableDynamicPriceUpdate = true; ShowProductReviewsInProductDetail = true; EnableHtmlTextCollapser = true; - HtmlTextCollapsedHeight = 260; - MostRecentlyUsedCategoriesMaxSize = 6; - MostRecentlyUsedManufacturersMaxSize = 4; - IncludeShortDescriptionInCompareProducts = true; - GridStyleListColumnSpan = GridColumnSpan.Max4Cols; - ShowProductsFromSubcategories = true; + HtmlTextCollapsedHeight = 260; + MostRecentlyUsedCategoriesMaxSize = 6; + MostRecentlyUsedManufacturersMaxSize = 4; + IncludeShortDescriptionInCompareProducts = true; + GridStyleListColumnSpan = GridColumnSpan.Max4Cols; + ShowProductsFromSubcategories = true; ApplyTierPricePercentageToAttributePriceAdjustments = false; AllowDifferingEmailAddressForEmailAFriend = false; - AllowAnonymousUsersToEmailAFriend = false; - AllowAnonymousUsersToReviewProduct = false; + AllowAnonymousUsersToEmailAFriend = false; + AllowAnonymousUsersToReviewProduct = false; + DeliveryTimesInLists = DeliveryTimesPresentation.DateOnly; + DeliveryTimesInProductDetail = DeliveryTimesPresentation.DateOnly; } /// @@ -110,14 +113,14 @@ public CatalogSettings() public bool ShowDimensions { get; set; } /// - /// Gets or sets a value indicating whether to display the delivery time of a product + /// Specifies the presentation of delivery times in product lists. /// - public bool ShowDeliveryTimesInProductLists { get; set; } + public DeliveryTimesPresentation DeliveryTimesInLists { get; set; } /// - /// Gets or sets a value indicating whether to display the delivery time of a product + /// Specifies the presentation of delivery times in product detail pages. /// - public bool ShowDeliveryTimesInProductDetail { get; set; } + public DeliveryTimesPresentation DeliveryTimesInProductDetail { get; set; } /// /// Gets or sets a value indicating whether to display the base price of a product @@ -139,10 +142,10 @@ public CatalogSettings() /// public bool ShowLinkedAttributeValueQuantity { get; set; } - /// - /// Gets or sets a value indicating whether to display the image of linked product at attribute values - /// - public bool ShowLinkedAttributeValueImage { get; set; } + /// + /// Gets or sets a value indicating whether to display the image of linked product at attribute values + /// + public bool ShowLinkedAttributeValueImage { get; set; } /// /// Gets or sets a value indicating how many menu items will be displayed @@ -188,26 +191,26 @@ public CatalogSettings() /// Gets or sets a value indicating whether category breadcrumb is enabled /// public bool CategoryBreadcrumbEnabled { get; set; } - - /// - /// Gets or sets a value indicating whether and where to display a list of subcategories - /// - public SubCategoryDisplayType SubCategoryDisplayType { get; set; } - /// - /// An option indicating whether sub pages should display the subcategories - /// - public bool ShowSubCategoriesInSubPages { get; set; } + /// + /// Gets or sets a value indicating whether and where to display a list of subcategories + /// + public SubCategoryDisplayType SubCategoryDisplayType { get; set; } - /// - /// An option indicating whether sub pages should display the category/manufacturer description - /// - public bool ShowDescriptionInSubPages { get; set; } + /// + /// An option indicating whether sub pages should display the subcategories + /// + public bool ShowSubCategoriesInSubPages { get; set; } - /// - /// Gets or sets a value indicating whether a 'Share button' is enabled - /// - public bool ShowShareButton { get; set; } + /// + /// An option indicating whether sub pages should display the category/manufacturer description + /// + public bool ShowDescriptionInSubPages { get; set; } + + /// + /// Gets or sets a value indicating whether a 'Share button' is enabled + /// + public bool ShowShareButton { get; set; } /// /// Gets or sets a share code (e.g. AddThis button code) @@ -253,7 +256,7 @@ public CatalogSettings() /// Gets or sets a value indicating whether 'ask product question' feature is enabled /// public bool AskQuestionEnabled { get; set; } - + /// /// Gets or sets a value indicating whether customers are allowed to enter a differing email address /// @@ -308,7 +311,7 @@ public CatalogSettings() /// Gets or sets a value indicating whether to show manufacturers in offcanvas menu /// public bool ShowManufacturersInOffCanvas { get; set; } - + /// /// Gets or sets the value indicating how many manufacturers on home page /// @@ -318,7 +321,7 @@ public CatalogSettings() /// Gets or sets the value indicating how many manufacturers in offcanvas menu ///
public int ManufacturerItemsToDisplayInOffcanvasMenu { get; set; } - + /// /// Gets or sets a value indicating whether to show manufacturer pictures or names on home page /// @@ -334,25 +337,25 @@ public CatalogSettings() ///
public bool ShowManufacturerPicturesInProductDetail { get; set; } - /// - /// Gets or sets a value indicating whether to hide manufacturer default pictures - /// - public bool HideManufacturerDefaultPictures { get; set; } + /// + /// Gets or sets a value indicating whether to hide manufacturer default pictures + /// + public bool HideManufacturerDefaultPictures { get; set; } /// /// Gets or sets a value indicating whether to hide manufacturer default pictures /// public bool SortManufacturersAlphabetically { get; set; } - + /// /// Gets or sets a value indicating whether to hide category default pictures /// public bool HideCategoryDefaultPictures { get; set; } - /// - /// Gets or sets a value indicating whether to hide product default pictures - /// - public bool HideProductDefaultPictures { get; set; } + /// + /// Gets or sets a value indicating whether to hide product default pictures + /// + public bool HideProductDefaultPictures { get; set; } /// /// Gets or sets a value whether to display the product condition. @@ -374,10 +377,10 @@ public CatalogSettings() /// public bool EnableDynamicPriceUpdate { get; set; } - /// - /// Gets or sets a value indicating whether base price should be rendered for bundle items - /// - public bool BundleItemShowBasePrice { get; set; } + /// + /// Gets or sets a value indicating whether base price should be rendered for bundle items + /// + public bool BundleItemShowBasePrice { get; set; } /// /// Gets or sets a number of product tags that appear in the tag cloud @@ -393,23 +396,23 @@ public CatalogSettings() /// Gets or sets a value indicating whether customers can select page size in product listings /// public bool AllowCustomersToSelectPageSize { get; set; } - - public int DisplayAllImagesNumber { get; set; } - public bool ShowManufacturerInGridStyleLists { get; set; } + public int DisplayAllImagesNumber { get; set; } - /// - /// Whether to show brand logo instead of textual name in product lists - /// - public bool ShowManufacturerLogoInLists { get; set; } + public bool ShowManufacturerInGridStyleLists { get; set; } + + /// + /// Whether to show brand logo instead of textual name in product lists + /// + public bool ShowManufacturerLogoInLists { get; set; } - public bool ShowShortDescriptionInGridStyleLists { get; set; } + public bool ShowShortDescriptionInGridStyleLists { get; set; } - public bool ShowProductOptionsInLists { get; set; } + public bool ShowProductOptionsInLists { get; set; } - public bool ShowColorSquaresInLists { get; set; } + public bool ShowColorSquaresInLists { get; set; } - public bool HideBuyButtonInLists { get; set; } + public bool HideBuyButtonInLists { get; set; } public int? LabelAsNewForMaxDays { get; set; } @@ -461,16 +464,16 @@ public CatalogSettings() ///
public bool IncludeFeaturedProductsInNormalLists { get; set; } - /// - /// An option indicating whether products on category and manufacturer pages should include featured products in sub pages as well - /// - public bool IncludeFeaturedProductsInSubPages { get; set; } + /// + /// An option indicating whether products on category and manufacturer pages should include featured products in sub pages as well + /// + public bool IncludeFeaturedProductsInSubPages { get; set; } + + /// + /// Gets or sets a value indicating whether tier prices should be displayed with applied discounts (if available) + /// + public bool DisplayTierPricesWithDiscounts { get; set; } - /// - /// Gets or sets a value indicating whether tier prices should be displayed with applied discounts (if available) - /// - public bool DisplayTierPricesWithDiscounts { get; set; } - /// /// Gets or sets a value indicating whether to ignore discounts (side-wide) /// @@ -500,36 +503,36 @@ public CatalogSettings() /// Gets or sets a list of allowed file extensions for customer uploaded files ///
public List FileUploadAllowedExtensions { get; set; } - - /// - /// Gets or sets a value indicating if html long text should be collapsed - /// - public bool EnableHtmlTextCollapser { get; set; } - /// - /// Gets or sets the height of collapsed text - /// - public int HtmlTextCollapsedHeight { get; set; } + /// + /// Gets or sets a value indicating if html long text should be collapsed + /// + public bool EnableHtmlTextCollapser { get; set; } - /// - /// Gets or sets an identifier for a delivery time dislayed when stock is empty - /// - public int? DeliveryTimeIdForEmptyStock { get; set; } + /// + /// Gets or sets the height of collapsed text + /// + public int HtmlTextCollapsedHeight { get; set; } - /// - /// Gets or sets how many items to display maximally in the most recently used category list - /// - public int MostRecentlyUsedCategoriesMaxSize { get; set; } + /// + /// Gets or sets an identifier for a delivery time dislayed when stock is empty + /// + public int? DeliveryTimeIdForEmptyStock { get; set; } - /// - /// Gets or sets how many items to display maximally in the most recently used manufacturer list - /// - public int MostRecentlyUsedManufacturersMaxSize { get; set; } + /// + /// Gets or sets how many items to display maximally in the most recently used category list + /// + public int MostRecentlyUsedCategoriesMaxSize { get; set; } - /// - /// Gets or sets how many columns per row should be displayed at most in grid style lists on largest screen resolution. - /// - public GridColumnSpan GridStyleListColumnSpan { get; set; } + /// + /// Gets or sets how many items to display maximally in the most recently used manufacturer list + /// + public int MostRecentlyUsedManufacturersMaxSize { get; set; } + + /// + /// Gets or sets how many columns per row should be displayed at most in grid style lists on largest screen resolution. + /// + public GridColumnSpan GridStyleListColumnSpan { get; set; } /// /// Gets or sets a value indicating whether percental values of tierprices should be applied to price adjustments of attributes diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs index 144821545d..f0364da897 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs @@ -14,8 +14,8 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a category /// [DataContract] - [DebuggerDisplay("{Id}: {Name} (Parent: {ParentCategoryId})")] - public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDeletable, IPagingOptions, IRulesContainer + [DebuggerDisplay("{Id}: {Name} (Parent: {ParentCategoryId})")] + public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDeletable, IPagingOptions, IRulesContainer { private ICollection _ruleSets; private ICollection _appliedDiscounts; @@ -26,11 +26,11 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele [DataMember] public string Name { get; set; } - /// - /// Gets or sets the full name (category page title) - /// - [DataMember] - public string FullName { get; set; } + /// + /// Gets or sets the full name (category page title) + /// + [DataMember] + public string FullName { get; set; } /// /// Gets or sets the description @@ -38,11 +38,11 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele [DataMember] public string Description { get; set; } - /// - /// Gets or sets a description displayed at the bottom of the category page - /// - [DataMember] - public string BottomDescription { get; set; } + /// + /// Gets or sets a description displayed at the bottom of the category page + /// + [DataMember] + public string BottomDescription { get; set; } /// /// Gets or sets the external link expression. If set, any category menu item will navigate to the specified link. @@ -67,7 +67,7 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele /// (an optional key for advanced customization) /// [DataMember] - public string Alias { get; set; } + public string Alias { get; set; } /// /// Gets or sets a value of used category template identifier @@ -105,42 +105,42 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele [DataMember] public int? MediaFileId { get; set; } - /// - /// Gets or sets the media file - /// - [DataMember] - public virtual MediaFile MediaFile { get; set; } + /// + /// Gets or sets the media file + /// + [DataMember] + public virtual MediaFile MediaFile { get; set; } /// /// Gets or sets the page size /// [DataMember] - public int? PageSize { get; set; } + public int? PageSize { get; set; } /// /// Gets or sets a value indicating whether customers can select the page size /// [DataMember] - public bool? AllowCustomersToSelectPageSize { get; set; } + public bool? AllowCustomersToSelectPageSize { get; set; } /// /// Gets or sets the available customer selectable page size options /// [DataMember] - public string PageSizeOptions { get; set; } + public string PageSizeOptions { get; set; } - /// - /// Gets or sets the available price ranges - /// - [Obsolete("Price ranges are calculated automatically since version 3")] - [StringLength(400)] - public string PriceRanges { get; set; } + /// + /// Gets or sets the available price ranges + /// + [Obsolete("Price ranges are calculated automatically since version 3")] + [StringLength(400)] + public string PriceRanges { get; set; } - /// - /// Gets or sets a value indicating whether to show the category on home page - /// - [DataMember] - public bool ShowOnHomePage { get; set; } + /// + /// Gets or sets a value indicating whether to show the category on home page + /// + [DataMember] + public bool ShowOnHomePage { get; set; } /// /// Gets or sets a value indicating whether this category has discounts applied @@ -150,19 +150,19 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele /// /// [DataMember] - public bool HasDiscountsApplied { get; set; } + public bool HasDiscountsApplied { get; set; } /// /// Gets or sets a value indicating whether the entity is subject to ACL /// [DataMember] - public bool SubjectToAcl { get; set; } + public bool SubjectToAcl { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } /// /// Gets or sets a value indicating whether the entity is published @@ -205,18 +205,18 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele /// public virtual ICollection RuleSets { - get { return _ruleSets ?? (_ruleSets = new HashSet()); } - protected set { _ruleSets = value; } + get => _ruleSets ?? (_ruleSets = new HashSet()); + protected set => _ruleSets = value; } /// /// Gets or sets the collection of applied discounts /// [DataMember] - public virtual ICollection AppliedDiscounts + public virtual ICollection AppliedDiscounts { - get { return _appliedDiscounts ?? (_appliedDiscounts = new HashSet()); } - protected set { _appliedDiscounts = value; } + get => _appliedDiscounts ?? (_appliedDiscounts = new HashSet()); + protected set => _appliedDiscounts = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs index e0c2ec17c8..19965ea78f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs @@ -6,35 +6,35 @@ namespace SmartStore.Core.Domain.Catalog { - public interface ICategoryNode : ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported - { - int ParentCategoryId { get; } - string Name { get; } + public interface ICategoryNode : ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported + { + int ParentCategoryId { get; } + string Name { get; } string ExternalLink { get; } string Alias { get; } - int? MediaFileId { get; } - bool Published { get; } - int DisplayOrder { get; } - DateTime UpdatedOnUtc { get; } - string BadgeText { get; } - int BadgeStyle { get; } - } + int? MediaFileId { get; } + bool Published { get; } + int DisplayOrder { get; } + DateTime UpdatedOnUtc { get; } + string BadgeText { get; } + int BadgeStyle { get; } + } - [Serializable] - public class CategoryNode : ICategoryNode - { - public int Id { get; set; } - public int ParentCategoryId { get; set; } - public string Name { get; set; } + [Serializable] + public class CategoryNode : ICategoryNode + { + public int Id { get; set; } + public int ParentCategoryId { get; set; } + public string Name { get; set; } public string ExternalLink { get; set; } public string Alias { get; set; } - public int? MediaFileId { get; set; } - public bool Published { get; set; } - public int DisplayOrder { get; set; } - public DateTime UpdatedOnUtc { get; set; } - public string BadgeText { get; set; } - public int BadgeStyle { get; set; } - public bool SubjectToAcl { get; set; } - public bool LimitedToStores { get; set; } - } + public int? MediaFileId { get; set; } + public bool Published { get; set; } + public int DisplayOrder { get; set; } + public DateTime UpdatedOnUtc { get; set; } + public string BadgeText { get; set; } + public int BadgeStyle { get; set; } + public bool SubjectToAcl { get; set; } + public bool LimitedToStores { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/IPagingOptions.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/IPagingOptions.cs index 3a4ce6b720..fb231147c8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/IPagingOptions.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/IPagingOptions.cs @@ -1,11 +1,9 @@ -using System; - -namespace SmartStore.Core.Domain.Catalog +namespace SmartStore.Core.Domain.Catalog { - public interface IPagingOptions - { - int? PageSize { get; } - bool? AllowCustomersToSelectPageSize { get; } - string PageSizeOptions { get; } - } + public interface IPagingOptions + { + int? PageSize { get; } + bool? AllowCustomersToSelectPageSize { get; } + string PageSizeOptions { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Manufacturer.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Manufacturer.cs index bdfc5bd503..68ec18111c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Manufacturer.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Manufacturer.cs @@ -12,25 +12,25 @@ namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a manufacturer - /// - [DataContract] - public partial class Manufacturer : BaseEntity, IAuditable, ISoftDeletable, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IPagingOptions - { - private ICollection _appliedDiscounts; - - /// - /// Gets or sets the name - /// - [DataMember] - public string Name { get; set; } + /// + /// Represents a manufacturer + /// + [DataContract] + public partial class Manufacturer : BaseEntity, IAuditable, ISoftDeletable, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IPagingOptions + { + private ICollection _appliedDiscounts; + + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } /// /// Gets or sets the description /// [DataMember] - public string Description { get; set; } + public string Description { get; set; } /// /// Gets or sets a description displayed at the bottom of the manufacturer page. @@ -42,68 +42,68 @@ public partial class Manufacturer : BaseEntity, IAuditable, ISoftDeletable, ILoc /// Gets or sets a value of used manufacturer template identifier /// [DataMember] - public int ManufacturerTemplateId { get; set; } + public int ManufacturerTemplateId { get; set; } /// /// Gets or sets the meta keywords /// [DataMember] - public string MetaKeywords { get; set; } + public string MetaKeywords { get; set; } /// /// Gets or sets the meta description /// [DataMember] - public string MetaDescription { get; set; } + public string MetaDescription { get; set; } /// /// Gets or sets the meta title /// [DataMember] - public string MetaTitle { get; set; } + public string MetaTitle { get; set; } /// /// Gets or sets the parent media file identifier /// [DataMember] - public int? MediaFileId { get; set; } + public int? MediaFileId { get; set; } - /// - /// Gets or sets the media file - /// - [DataMember] - public virtual MediaFile MediaFile { get; set; } + /// + /// Gets or sets the media file + /// + [DataMember] + public virtual MediaFile MediaFile { get; set; } /// /// Gets or sets the page size /// [DataMember] - public int? PageSize { get; set; } + public int? PageSize { get; set; } /// /// Gets or sets a value indicating whether customers can select the page size /// [DataMember] - public bool? AllowCustomersToSelectPageSize { get; set; } + public bool? AllowCustomersToSelectPageSize { get; set; } /// /// Gets or sets the available customer selectable page size options /// [DataMember] - public string PageSizeOptions { get; set; } + public string PageSizeOptions { get; set; } /// /// Gets or sets the available price ranges /// [Obsolete("Price ranges are calculated automatically since version 3")] - [StringLength(400)] - public string PriceRanges { get; set; } + [StringLength(400)] + public string PriceRanges { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } /// /// Gets or sets a value indicating whether the entity is subject to ACL. @@ -116,7 +116,7 @@ public partial class Manufacturer : BaseEntity, IAuditable, ISoftDeletable, ILoc /// Gets or sets a value indicating whether the entity is published /// [DataMember] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets a value indicating whether the entity has been deleted @@ -128,38 +128,38 @@ public partial class Manufacturer : BaseEntity, IAuditable, ISoftDeletable, ILoc /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the date and time of instance creation /// [DataMember] - public DateTime CreatedOnUtc { get; set; } + public DateTime CreatedOnUtc { get; set; } /// /// Gets or sets the date and time of instance update /// [DataMember] - public DateTime UpdatedOnUtc { get; set; } - - /// - /// Gets or sets a value indicating whether this manufacturer has discounts applied - /// The same as if we run Manufacturer.AppliedDiscounts.Count > 0 - /// We use this property for performance optimization: - /// if this property is set to false, then we do not need to load Applied Discounts navigation property - /// - /// - [DataMember] - public bool HasDiscountsApplied { get; set; } + public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets or sets the collection of applied discounts - /// - [DataMember] - public virtual ICollection AppliedDiscounts - { - get { return _appliedDiscounts ?? (_appliedDiscounts = new HashSet()); } - protected set { _appliedDiscounts = value; } - } - } + /// + /// Gets or sets a value indicating whether this manufacturer has discounts applied + /// The same as if we run Manufacturer.AppliedDiscounts.Count > 0 + /// We use this property for performance optimization: + /// if this property is set to false, then we do not need to load Applied Discounts navigation property + /// + /// + [DataMember] + public bool HasDiscountsApplied { get; set; } + + /// + /// Gets or sets the collection of applied discounts + /// + [DataMember] + public virtual ICollection AppliedDiscounts + { + get => _appliedDiscounts ?? (_appliedDiscounts = new HashSet()); + protected set => _appliedDiscounts = value; + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayStyle.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayStyle.cs index adee8aa61f..1275690ff9 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayStyle.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayStyle.cs @@ -7,7 +7,7 @@ namespace SmartStore.Core.Domain.Catalog /// [Flags] public enum PriceDisplayStyle - { + { /// /// Display prices without badges /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs index aeb4c24250..778aba0b13 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs @@ -1,29 +1,29 @@  namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents types of product prices to display - /// - public enum PriceDisplayType - { - /// - /// The lowest possible price of a product (default) - /// - LowestPrice = 0, + /// + /// Represents types of product prices to display + /// + public enum PriceDisplayType + { + /// + /// The lowest possible price of a product (default) + /// + LowestPrice = 0, - /// - /// The product price initially displayed on the product detail page - /// - PreSelectedPrice = 10, + /// + /// The product price initially displayed on the product detail page + /// + PreSelectedPrice = 10, - /// - /// The product price without associated data like discounts, tier prices, attributes or attribute combinations - /// - PriceWithoutDiscountsAndAttributes = 20, + /// + /// The product price without associated data like discounts, tier prices, attributes or attribute combinations + /// + PriceWithoutDiscountsAndAttributes = 20, - /// - /// Do not display a product price - /// - Hide = 30 - } + /// + /// Do not display a product price + /// + Hide = 30 + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index 17d9ea1054..06abbb9bbf 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics; -using System.Linq.Expressions; using System.Runtime.Serialization; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; @@ -18,71 +17,71 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a product /// [DataContract] - public partial class Product : BaseEntity, IAuditable, ISoftDeletable, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IMergedData - { - #region static - - private static readonly HashSet _visibilityAffectingProductProps = new HashSet - { - nameof(Product.AvailableEndDateTimeUtc), - nameof(Product.AvailableStartDateTimeUtc), - nameof(Product.Deleted), - nameof(Product.LowStockActivityId), - nameof(Product.LimitedToStores), - nameof(Product.ManageInventoryMethodId), - nameof(Product.MinStockQuantity), - nameof(Product.Published), - nameof(Product.SubjectToAcl), - nameof(Product.Visibility) - }; - - public static IReadOnlyCollection GetVisibilityAffectingPropertyNames() - { - return _visibilityAffectingProductProps; - } - - #endregion - - private ICollection _productCategories; + public partial class Product : BaseEntity, IAuditable, ISoftDeletable, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IMergedData + { + #region static + + private static readonly HashSet _visibilityAffectingProductProps = new HashSet + { + nameof(Product.AvailableEndDateTimeUtc), + nameof(Product.AvailableStartDateTimeUtc), + nameof(Product.Deleted), + nameof(Product.LowStockActivityId), + nameof(Product.LimitedToStores), + nameof(Product.ManageInventoryMethodId), + nameof(Product.MinStockQuantity), + nameof(Product.Published), + nameof(Product.SubjectToAcl), + nameof(Product.Visibility) + }; + + public static IReadOnlyCollection GetVisibilityAffectingPropertyNames() + { + return _visibilityAffectingProductProps; + } + + #endregion + + private ICollection _productCategories; private ICollection _productManufacturers; private ICollection _productPictures; private ICollection _productReviews; private ICollection _productSpecificationAttributes; private ICollection _productTags; - private ICollection _productVariantAttributes; - private ICollection _productVariantAttributeCombinations; - private ICollection _tierPrices; - private ICollection _appliedDiscounts; - private ICollection _productBundleItems; - + private ICollection _productVariantAttributes; + private ICollection _productVariantAttributeCombinations; + private ICollection _tierPrices; + private ICollection _appliedDiscounts; + private ICollection _productBundleItems; + private int _stockQuantity; private int _backorderModeId; - private string _sku; - private string _gtin; - private string _manufacturerPartNumber; - private decimal _price; - private int? _deliveryTimeId; + private string _sku; + private string _gtin; + private string _manufacturerPartNumber; + private decimal _price; + private int? _deliveryTimeId; private int? _quantityUnitId; - private decimal _length; - private decimal _width; - private decimal _height; - private decimal? _basePriceAmount; - private int? _basePriceBaseAmount; + private decimal _length; + private decimal _width; + private decimal _height; + private decimal? _basePriceAmount; + private int? _basePriceBaseAmount; - public bool MergedDataIgnore { get; set; } - public Dictionary MergedDataValues { get; set; } + public bool MergedDataIgnore { get; set; } + public Dictionary MergedDataValues { get; set; } - /// - /// Gets or sets the product type identifier - /// - [DataMember] - public int ProductTypeId { get; set; } + /// + /// Gets or sets the product type identifier + /// + [DataMember] + public int ProductTypeId { get; set; } - /// - /// Gets or sets the parent product identifier. It's used to identify associated products (only with "grouped" products) - /// - [DataMember] - public int ParentGroupedProductId { get; set; } + /// + /// Gets or sets the parent product identifier. It's used to identify associated products (only with "grouped" products) + /// + [DataMember] + public int ParentGroupedProductId { get; set; } [Obsolete("Use property Visibility instead.")] public bool VisibleIndividually { get; set; } @@ -122,383 +121,353 @@ public static IReadOnlyCollection GetVisibilityAffectingPropertyNames() /// Gets or sets the admin comment ///
[DataMember] - public string AdminComment { get; set; } + public string AdminComment { get; set; } /// /// Gets or sets a value of used product template identifier /// [DataMember] - public int ProductTemplateId { get; set; } + public int ProductTemplateId { get; set; } /// /// Gets or sets a value indicating whether to show the product on home page /// [DataMember] - public bool ShowOnHomePage { get; set; } + public bool ShowOnHomePage { get; set; } - /// - /// Gets or sets the display order for homepage products - /// - [DataMember] - public int HomePageDisplayOrder { get; set; } + /// + /// Gets or sets the display order for homepage products + /// + [DataMember] + public int HomePageDisplayOrder { get; set; } /// /// Gets or sets the meta keywords /// [DataMember] - public string MetaKeywords { get; set; } + public string MetaKeywords { get; set; } /// /// Gets or sets the meta description /// [DataMember] - public string MetaDescription { get; set; } + public string MetaDescription { get; set; } /// /// Gets or sets the meta title /// [DataMember] - public string MetaTitle { get; set; } + public string MetaTitle { get; set; } /// /// Gets or sets a value indicating whether the product allows customer reviews /// [DataMember] - public bool AllowCustomerReviews { get; set; } + public bool AllowCustomerReviews { get; set; } /// /// Gets or sets the rating sum (approved reviews) /// [DataMember] - public int ApprovedRatingSum { get; set; } + public int ApprovedRatingSum { get; set; } /// /// Gets or sets the rating sum (not approved reviews) /// [DataMember] - public int NotApprovedRatingSum { get; set; } + public int NotApprovedRatingSum { get; set; } /// /// Gets or sets the total rating votes (approved reviews) /// [DataMember] - public int ApprovedTotalReviews { get; set; } + public int ApprovedTotalReviews { get; set; } /// /// Gets or sets the total rating votes (not approved reviews) /// [DataMember] - public int NotApprovedTotalReviews { get; set; } + public int NotApprovedTotalReviews { get; set; } /// /// Gets or sets a value indicating whether the entity is subject to ACL /// [DataMember] - public bool SubjectToAcl { get; set; } + public bool SubjectToAcl { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } - /// - /// Gets or sets the SKU - /// - [DataMember] - public string Sku - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(Sku), _sku); - } - set - { - _sku = value; - } - } - - /// - /// Gets or sets the manufacturer part number - /// - [DataMember] - [Index] - public string ManufacturerPartNumber - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(ManufacturerPartNumber), _manufacturerPartNumber); - } - set - { - _manufacturerPartNumber = value; - } - } - - /// - /// Gets or sets the Global Trade Item Number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). - /// - [DataMember] - [Index] - public string Gtin - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(Gtin), _gtin); - } - set - { - _gtin = value; - } - } - - /// - /// Gets or sets a value indicating whether the product is gift card - /// - [DataMember] - public bool IsGiftCard { get; set; } + /// + /// Gets or sets the SKU + /// + [DataMember] + public string Sku + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(Sku), _sku); + set => _sku = value; + } - /// - /// Gets or sets the gift card type identifier - /// - [DataMember] - public int GiftCardTypeId { get; set; } + /// + /// Gets or sets the manufacturer part number + /// + [DataMember] + [Index] + public string ManufacturerPartNumber + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(ManufacturerPartNumber), _manufacturerPartNumber); + set => _manufacturerPartNumber = value; + } - /// - /// Gets or sets a value indicating whether the product requires that other products are added to the cart (Product X requires Product Y) - /// - [DataMember] - public bool RequireOtherProducts { get; set; } + /// + /// Gets or sets the Global Trade Item Number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). + /// + [DataMember] + [Index] + public string Gtin + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(Gtin), _gtin); + set => _gtin = value; + } - /// - /// Gets or sets a required product identifiers (comma separated) - /// - [DataMember] - public string RequiredProductIds { get; set; } + /// + /// Gets or sets a value indicating whether the product is gift card + /// + [DataMember] + public bool IsGiftCard { get; set; } - /// - /// Gets or sets a value indicating whether required products are automatically added to the cart - /// - [DataMember] - public bool AutomaticallyAddRequiredProducts { get; set; } + /// + /// Gets or sets the gift card type identifier + /// + [DataMember] + public int GiftCardTypeId { get; set; } - /// - /// Gets or sets a value indicating whether the product is a download - /// - [DataMember] - public bool IsDownload { get; set; } + /// + /// Gets or sets a value indicating whether the product requires that other products are added to the cart (Product X requires Product Y) + /// + [DataMember] + public bool RequireOtherProducts { get; set; } - /// - /// Gets or sets the download identifier - /// - [Obsolete("Since version 3.2 more than one download can be assigned to a product. See property Download.EntityId and Download.EntityName.")] + /// + /// Gets or sets a required product identifiers (comma separated) + /// + [DataMember] + public string RequiredProductIds { get; set; } + + /// + /// Gets or sets a value indicating whether required products are automatically added to the cart + /// + [DataMember] + public bool AutomaticallyAddRequiredProducts { get; set; } + + /// + /// Gets or sets a value indicating whether the product is a download + /// + [DataMember] + public bool IsDownload { get; set; } + + /// + /// Gets or sets the download identifier + /// + [Obsolete("Since version 3.2 more than one download can be assigned to a product. See property Download.EntityId and Download.EntityName.")] public int DownloadId { get; set; } - /// - /// Gets or sets a value indicating whether this downloadable product can be downloaded unlimited number of times - /// - [DataMember] - public bool UnlimitedDownloads { get; set; } + /// + /// Gets or sets a value indicating whether this downloadable product can be downloaded unlimited number of times + /// + [DataMember] + public bool UnlimitedDownloads { get; set; } - /// - /// Gets or sets the maximum number of downloads - /// - [DataMember] - public int MaxNumberOfDownloads { get; set; } + /// + /// Gets or sets the maximum number of downloads + /// + [DataMember] + public int MaxNumberOfDownloads { get; set; } - /// - /// Gets or sets the number of days during customers keeps access to the file. - /// - [DataMember] - public int? DownloadExpirationDays { get; set; } + /// + /// Gets or sets the number of days during customers keeps access to the file. + /// + [DataMember] + public int? DownloadExpirationDays { get; set; } - /// - /// Gets or sets the download activation type - /// - [DataMember] - public int DownloadActivationTypeId { get; set; } + /// + /// Gets or sets the download activation type + /// + [DataMember] + public int DownloadActivationTypeId { get; set; } - /// - /// Gets or sets a value indicating whether the product has a sample download file - /// - [DataMember] - public bool HasSampleDownload { get; set; } + /// + /// Gets or sets a value indicating whether the product has a sample download file + /// + [DataMember] + public bool HasSampleDownload { get; set; } - /// - /// Gets or sets the sample download identifier - /// - [DataMember] - public int? SampleDownloadId { get; set; } + /// + /// Gets or sets the sample download identifier + /// + [DataMember] + public int? SampleDownloadId { get; set; } - /// - /// Gets or sets the sample download - /// - [DataMember] - public Download SampleDownload { get; set; } + /// + /// Gets or sets the sample download + /// + [DataMember] + public Download SampleDownload { get; set; } - /// - /// Gets or sets a value indicating whether the product has user agreement - /// - [DataMember] - public bool HasUserAgreement { get; set; } + /// + /// Gets or sets a value indicating whether the product has user agreement + /// + [DataMember] + public bool HasUserAgreement { get; set; } - /// - /// Gets or sets the text of license agreement - /// - [DataMember] - public string UserAgreementText { get; set; } + /// + /// Gets or sets the text of license agreement + /// + [DataMember] + public string UserAgreementText { get; set; } - /// - /// Gets or sets a value indicating whether the product is recurring - /// - [DataMember] - public bool IsRecurring { get; set; } + /// + /// Gets or sets a value indicating whether the product is recurring + /// + [DataMember] + public bool IsRecurring { get; set; } - /// - /// Gets or sets the cycle length - /// - [DataMember] - public int RecurringCycleLength { get; set; } + /// + /// Gets or sets the cycle length + /// + [DataMember] + public int RecurringCycleLength { get; set; } - /// - /// Gets or sets the cycle period - /// - [DataMember] - public int RecurringCyclePeriodId { get; set; } + /// + /// Gets or sets the cycle period + /// + [DataMember] + public int RecurringCyclePeriodId { get; set; } - /// - /// Gets or sets the total cycles - /// - [DataMember] - public int RecurringTotalCycles { get; set; } + /// + /// Gets or sets the total cycles + /// + [DataMember] + public int RecurringTotalCycles { get; set; } - /// - /// Gets or sets a value indicating whether the entity is ship enabled - /// - [DataMember] - public bool IsShipEnabled { get; set; } + /// + /// Gets or sets a value indicating whether the entity is ship enabled + /// + [DataMember] + public bool IsShipEnabled { get; set; } - /// - /// Gets or sets a value indicating whether the entity is free shipping - /// - [DataMember] - public bool IsFreeShipping { get; set; } + /// + /// Gets or sets a value indicating whether the entity is free shipping + /// + [DataMember] + public bool IsFreeShipping { get; set; } - /// - /// Gets or sets the additional shipping charge - /// - [DataMember] - public decimal AdditionalShippingCharge { get; set; } + /// + /// Gets or sets the additional shipping charge + /// + [DataMember] + public decimal AdditionalShippingCharge { get; set; } - /// - /// Gets or sets a value indicating whether the product is marked as tax exempt - /// - [DataMember] - public bool IsTaxExempt { get; set; } + /// + /// Gets or sets a value indicating whether the product is marked as tax exempt + /// + [DataMember] + public bool IsTaxExempt { get; set; } - /// - /// Gets or sets a value indicating whether the product is an electronic service - /// bound to EU VAT regulations for digital goods. - /// - [DataMember] - public bool IsEsd { get; set; } + /// + /// Gets or sets a value indicating whether the product is an electronic service + /// bound to EU VAT regulations for digital goods. + /// + [DataMember] + public bool IsEsd { get; set; } - /// - /// Gets or sets the tax category identifier - /// - [DataMember] - public int TaxCategoryId { get; set; } + /// + /// Gets or sets the tax category identifier + /// + [DataMember] + public int TaxCategoryId { get; set; } - /// - /// Gets or sets a value indicating how to manage inventory - /// - [DataMember] - public int ManageInventoryMethodId { get; set; } + /// + /// Gets or sets a value indicating how to manage inventory + /// + [DataMember] + public int ManageInventoryMethodId { get; set; } - /// - /// Gets or sets the stock quantity - /// - [DataMember] - public int StockQuantity - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(StockQuantity), _stockQuantity); - } - set - { - _stockQuantity = value; - } - } - - /// - /// Gets or sets a value indicating whether to display stock availability - /// - [DataMember] - public bool DisplayStockAvailability { get; set; } + /// + /// Gets or sets the stock quantity + /// + [DataMember] + public int StockQuantity + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(StockQuantity), _stockQuantity); + set => _stockQuantity = value; + } - /// - /// Gets or sets a value indicating whether to display stock quantity - /// - [DataMember] - public bool DisplayStockQuantity { get; set; } + /// + /// Gets or sets a value indicating whether to display stock availability + /// + [DataMember] + public bool DisplayStockAvailability { get; set; } - /// - /// Gets or sets the minimum stock quantity - /// - [DataMember] - public int MinStockQuantity { get; set; } + /// + /// Gets or sets a value indicating whether to display stock quantity + /// + [DataMember] + public bool DisplayStockQuantity { get; set; } - /// - /// Gets or sets the low stock activity identifier - /// - [DataMember] - public int LowStockActivityId { get; set; } + /// + /// Gets or sets the minimum stock quantity + /// + [DataMember] + public int MinStockQuantity { get; set; } - /// - /// Gets or sets the quantity when admin should be notified - /// - [DataMember] - public int NotifyAdminForQuantityBelow { get; set; } + /// + /// Gets or sets the low stock activity identifier + /// + [DataMember] + public int LowStockActivityId { get; set; } - /// - /// Gets or sets a value backorder mode identifier - /// - [DataMember] - public int BackorderModeId + /// + /// Gets or sets the quantity when admin should be notified + /// + [DataMember] + public int NotifyAdminForQuantityBelow { get; set; } + + /// + /// Gets or sets a value backorder mode identifier + /// + [DataMember] + public int BackorderModeId { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(BackorderModeId), _backorderModeId); - } - set - { - _backorderModeId = value; - } + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(BackorderModeId), _backorderModeId); + set => _backorderModeId = value; } - /// - /// Gets or sets a value indicating whether to back in stock subscriptions are allowed - /// - [DataMember] - public bool AllowBackInStockSubscriptions { get; set; } + /// + /// Gets or sets a value indicating whether to back in stock subscriptions are allowed + /// + [DataMember] + public bool AllowBackInStockSubscriptions { get; set; } - /// - /// Gets or sets the order minimum quantity - /// - [DataMember] - public int OrderMinimumQuantity { get; set; } + /// + /// Gets or sets the order minimum quantity + /// + [DataMember] + public int OrderMinimumQuantity { get; set; } - /// - /// Gets or sets the order maximum quantity - /// - [DataMember] - public int OrderMaximumQuantity { get; set; } + /// + /// Gets or sets the order maximum quantity + /// + [DataMember] + public int OrderMaximumQuantity { get; set; } /// /// Gets or sets the quantity step @@ -522,259 +491,235 @@ public int BackorderModeId /// Gets or sets the comma seperated list of allowed quantities. null or empty if any quantity is allowed /// [DataMember] - public string AllowedQuantities { get; set; } + public string AllowedQuantities { get; set; } - /// - /// Gets or sets a value indicating whether to disable buy (Add to cart) button - /// - [DataMember] - public bool DisableBuyButton { get; set; } + /// + /// Gets or sets a value indicating whether to disable buy (Add to cart) button + /// + [DataMember] + public bool DisableBuyButton { get; set; } - /// - /// Gets or sets a value indicating whether to disable "Add to wishlist" button - /// - [DataMember] - public bool DisableWishlistButton { get; set; } + /// + /// Gets or sets a value indicating whether to disable "Add to wishlist" button + /// + [DataMember] + public bool DisableWishlistButton { get; set; } - /// - /// Gets or sets a value indicating whether this item is available for Pre-Order - /// - [DataMember] - public bool AvailableForPreOrder { get; set; } + /// + /// Gets or sets a value indicating whether this item is available for Pre-Order + /// + [DataMember] + public bool AvailableForPreOrder { get; set; } - /// - /// Gets or sets a value indicating whether to show "Call for Pricing" or "Call for quote" instead of price - /// - [DataMember] - public bool CallForPrice { get; set; } + /// + /// Gets or sets a value indicating whether to show "Call for Pricing" or "Call for quote" instead of price + /// + [DataMember] + public bool CallForPrice { get; set; } - /// - /// Gets or sets the price - /// - [DataMember] - public decimal Price - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(Price), _price); - } - set - { - _price = value; - } - } - - /// - /// Gets or sets the old price - /// - [DataMember] - public decimal OldPrice { get; set; } + /// + /// Gets or sets the price + /// + [DataMember] + public decimal Price + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(Price), _price); + set => _price = value; + } - /// - /// Gets or sets the product cost - /// - [DataMember] - public decimal ProductCost { get; set; } + /// + /// Gets or sets the old price + /// + [DataMember] + public decimal OldPrice { get; set; } - /// - /// Gets or sets the product special price - /// - [DataMember] - public decimal? SpecialPrice { get; set; } + /// + /// Gets or sets the product cost + /// + [DataMember] + public decimal ProductCost { get; set; } - /// - /// Gets or sets the start date and time of the special price - /// - [DataMember] - public DateTime? SpecialPriceStartDateTimeUtc { get; set; } + /// + /// Gets or sets the product special price + /// + [DataMember] + public decimal? SpecialPrice { get; set; } - /// - /// Gets or sets the end date and time of the special price - /// - [DataMember] - public DateTime? SpecialPriceEndDateTimeUtc { get; set; } + /// + /// Gets or sets the start date and time of the special price + /// + [DataMember] + public DateTime? SpecialPriceStartDateTimeUtc { get; set; } - /// - /// Gets or sets a value indicating whether a customer enters price - /// - [DataMember] - public bool CustomerEntersPrice { get; set; } + /// + /// Gets or sets the end date and time of the special price + /// + [DataMember] + public DateTime? SpecialPriceEndDateTimeUtc { get; set; } - /// - /// Gets or sets the minimum price entered by a customer - /// - [DataMember] - public decimal MinimumCustomerEnteredPrice { get; set; } + /// + /// Gets or sets a value indicating whether a customer enters price + /// + [DataMember] + public bool CustomerEntersPrice { get; set; } - /// - /// Gets or sets the maximum price entered by a customer - /// - [DataMember] - public decimal MaximumCustomerEnteredPrice { get; set; } - - /// - /// Gets or sets a value indicating whether this product has tier prices configured - /// The same as if we run this.TierPrices.Count > 0 - /// We use this property for performance optimization: - /// if this property is set to false, then we do not need to load tier prices navigation property - /// - /// - [DataMember] - public bool HasTierPrices { get; set; } + /// + /// Gets or sets the minimum price entered by a customer + /// + [DataMember] + public decimal MinimumCustomerEnteredPrice { get; set; } - /// - /// Gets or sets a value for the lowest attribute combination price override - /// - [DataMember] - public decimal? LowestAttributeCombinationPrice { get; set; } - - /// - /// Gets or sets a value indicating whether this product has discounts applied - /// The same as if we run this.AppliedDiscounts.Count > 0 - /// We use this property for performance optimization: - /// if this property is set to false, then we do not need to load Applied Discounts navifation property - /// - /// - [DataMember] - public bool HasDiscountsApplied { get; set; } + /// + /// Gets or sets the maximum price entered by a customer + /// + [DataMember] + public decimal MaximumCustomerEnteredPrice { get; set; } - /// - /// Gets or sets the weight - /// - [DataMember] - public decimal Weight { get; set; } + /// + /// Gets or sets a value indicating whether this product has tier prices configured + /// The same as if we run this.TierPrices.Count > 0 + /// We use this property for performance optimization: + /// if this property is set to false, then we do not need to load tier prices navigation property + /// + /// + [DataMember] + public bool HasTierPrices { get; set; } - /// - /// Gets or sets the length - /// - [DataMember] - public decimal Length - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(Length), _length); - } - set - { - _length = value; - } - } - - /// - /// Gets or sets the width - /// - [DataMember] - public decimal Width - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(Width), _width); - } - set - { - _width = value; - } - } - - /// - /// Gets or sets the height - /// - [DataMember] - public decimal Height - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(Height), _height); - } - set - { - _height = value; - } - } - - /// - /// Gets or sets the available start date and time - /// - [DataMember] - public DateTime? AvailableStartDateTimeUtc { get; set; } + /// + /// Gets or sets a value for the lowest attribute combination price override + /// + [DataMember] + public decimal? LowestAttributeCombinationPrice { get; set; } - /// - /// Gets or sets the available end date and time - /// - [DataMember] - public DateTime? AvailableEndDateTimeUtc { get; set; } + /// + /// Gets or sets the behaviour when selecting product attributes. + /// + [DataMember] + public AttributeChoiceBehaviour AttributeChoiceBehaviour { get; set; } - /// - /// Gets or sets a display order. This value is used when sorting associated products (used with "grouped" products) - /// - [DataMember] - public int DisplayOrder { get; set; } + /// + /// Gets or sets a value indicating whether this product has discounts applied + /// The same as if we run this.AppliedDiscounts.Count > 0 + /// We use this property for performance optimization: + /// if this property is set to false, then we do not need to load Applied Discounts navifation property + /// + /// + [DataMember] + public bool HasDiscountsApplied { get; set; } + + /// + /// Gets or sets the weight + /// + [DataMember] + public decimal Weight { get; set; } + + /// + /// Gets or sets the length + /// + [DataMember] + public decimal Length + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(Length), _length); + set => _length = value; + } + + /// + /// Gets or sets the width + /// + [DataMember] + public decimal Width + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(Width), _width); + set => _width = value; + } + + /// + /// Gets or sets the height + /// + [DataMember] + public decimal Height + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(Height), _height); + set => _height = value; + } + + /// + /// Gets or sets the available start date and time + /// + [DataMember] + public DateTime? AvailableStartDateTimeUtc { get; set; } + + /// + /// Gets or sets the available end date and time + /// + [DataMember] + public DateTime? AvailableEndDateTimeUtc { get; set; } + + /// + /// Gets or sets a display order. This value is used when sorting associated products (used with "grouped" products) + /// + [DataMember] + public int DisplayOrder { get; set; } /// /// Gets or sets a value indicating whether the entity is published /// [DataMember] [Index("IX_Product_Published_Deleted_IsSystemProduct", 1)] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets a value indicating whether the entity has been deleted /// [Index] [Index("IX_Product_Published_Deleted_IsSystemProduct", 2)] - public bool Deleted { get; set; } + public bool Deleted { get; set; } - /// - /// Gets or sets a value indicating whether the entity is a system product. - /// - [DataMember] + /// + /// Gets or sets a value indicating whether the entity is a system product. + /// + [DataMember] [Index] [Index("IX_Product_SystemName_IsSystemProduct", 2)] [Index("IX_Product_Published_Deleted_IsSystemProduct", 3)] - public bool IsSystemProduct { get; set; } + public bool IsSystemProduct { get; set; } - /// - /// Gets or sets the product system name. - /// - [DataMember] - [Index("IX_Product_SystemName_IsSystemProduct", 1)] - public string SystemName { get; set; } + /// + /// Gets or sets the product system name. + /// + [DataMember] + [Index("IX_Product_SystemName_IsSystemProduct", 1)] + public string SystemName { get; set; } - /// - /// Gets or sets the date and time of product creation - /// - [DataMember] - public DateTime CreatedOnUtc { get; set; } + /// + /// Gets or sets the date and time of product creation + /// + [DataMember] + public DateTime CreatedOnUtc { get; set; } /// /// Gets or sets the date and time of product update /// [DataMember] - public DateTime UpdatedOnUtc { get; set; } + public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets or sets the delivery time identifier - /// - [DataMember] - public int? DeliveryTimeId - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(DeliveryTimeId), _deliveryTimeId); - } - set - { - _deliveryTimeId = value; - } - } + /// + /// Gets or sets the delivery time identifier + /// + [DataMember] + public int? DeliveryTimeId + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(DeliveryTimeId), _deliveryTimeId); + set => _deliveryTimeId = value; + } - [DataMember] - public virtual DeliveryTime DeliveryTime { get; set; } + [DataMember] + public virtual DeliveryTime DeliveryTime { get; set; } /// /// Gets or sets the quantity unit identifier @@ -783,123 +728,99 @@ public int? DeliveryTimeId public int? QuantityUnitId { [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(QuantityUnitId), _quantityUnitId); - } - set - { - _quantityUnitId = value; - } + get => this.GetMergedDataValue(nameof(QuantityUnitId), _quantityUnitId); + set => _quantityUnitId = value; } [DataMember] public virtual QuantityUnit QuantityUnit { get; set; } - /// - /// Gets or sets the customs tariff number - /// - [DataMember] - public string CustomsTariffNumber { get; set; } + /// + /// Gets or sets the customs tariff number + /// + [DataMember] + public string CustomsTariffNumber { get; set; } - /// - /// Gets or sets the country of origin identifier - /// - [DataMember] - public int? CountryOfOriginId { get; set; } + /// + /// Gets or sets the country of origin identifier + /// + [DataMember] + public int? CountryOfOriginId { get; set; } - /// - /// Gets or sets the country of origin - /// - [DataMember] - public virtual Country CountryOfOrigin { get; set; } + /// + /// Gets or sets the country of origin + /// + [DataMember] + public virtual Country CountryOfOrigin { get; set; } - /// - /// Gets or sets if base price quotation (PAnGV) is enabled - /// - [DataMember] - public bool BasePriceEnabled { get; set; } + /// + /// Gets or sets if base price quotation (PAnGV) is enabled + /// + [DataMember] + public bool BasePriceEnabled { get; set; } - /// - /// Measure unit for the base price (e.g. "kg", "g", "qm" etc.) - /// - [DataMember] - public string BasePriceMeasureUnit { get; set; } + /// + /// Measure unit for the base price (e.g. "kg", "g", "qm" etc.) + /// + [DataMember] + public string BasePriceMeasureUnit { get; set; } - /// - /// Amount of product per packing unit in the given measure unit + /// + /// Amount of product per packing unit in the given measure unit /// (e.g. 250 ml shower gel: "0.25" if MeasureUnit = "liter" and BaseAmount = 1) - /// - [DataMember] - public decimal? BasePriceAmount - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(BasePriceAmount), _basePriceAmount); - } - set - { - _basePriceAmount = value; - } - } - - /// - /// Reference value for the given measure unit - /// (e.g. "1" liter. Formula: [BaseAmount] [MeasureUnit] = [SellingPrice] / [Amount]) - /// - [DataMember] - public int? BasePriceBaseAmount - { - [DebuggerStepThrough] - get - { - return this.GetMergedDataValue(nameof(BasePriceBaseAmount), _basePriceBaseAmount); - } - set - { - _basePriceBaseAmount = value; - } - } + /// + [DataMember] + public decimal? BasePriceAmount + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(BasePriceAmount), _basePriceAmount); + set => _basePriceAmount = value; + } - [DataMember] - public bool BasePriceHasValue - { - get - { - return BasePriceEnabled && BasePriceAmount.GetValueOrDefault() > 0 && BasePriceBaseAmount.GetValueOrDefault() > 0 && BasePriceMeasureUnit.HasValue(); - } - } - - /// - /// Optional title text of a product bundle - /// - [DataMember] - public string BundleTitleText { get; set; } + /// + /// Reference value for the given measure unit + /// (e.g. "1" liter. Formula: [BaseAmount] [MeasureUnit] = [SellingPrice] / [Amount]) + /// + [DataMember] + public int? BasePriceBaseAmount + { + [DebuggerStepThrough] + get => this.GetMergedDataValue(nameof(BasePriceBaseAmount), _basePriceBaseAmount); + set => _basePriceBaseAmount = value; + } - /// - /// Per item shipping of bundle items - /// - [DataMember] - public bool BundlePerItemShipping { get; set; } + [DataMember] + public bool BasePriceHasValue => BasePriceEnabled && BasePriceAmount.GetValueOrDefault() > 0 && BasePriceBaseAmount.GetValueOrDefault() > 0 && BasePriceMeasureUnit.HasValue(); - /// - /// Per item pricing of bundle items - /// - [DataMember] - public bool BundlePerItemPricing { get; set; } + /// + /// Optional title text of a product bundle + /// + [DataMember] + public string BundleTitleText { get; set; } - /// - /// Per item shopping cart handling of bundle items - /// - [DataMember] - public bool BundlePerItemShoppingCart { get; set; } + /// + /// Per item shipping of bundle items + /// + [DataMember] + public bool BundlePerItemShipping { get; set; } - /// - /// Gets or sets the main picture id - /// - [DataMember] - public int? MainPictureId { get; set; } + /// + /// Per item pricing of bundle items + /// + [DataMember] + public bool BundlePerItemPricing { get; set; } + + /// + /// Per item shopping cart handling of bundle items + /// + [DataMember] + public bool BundlePerItemShoppingCart { get; set; } + + /// + /// Gets or sets the main picture id + /// + [DataMember] + public int? MainPictureId { get; set; } /// /// Gets or sets a value that indictaes whether the product has a preview picture @@ -911,160 +832,118 @@ public bool BasePriceHasValue /// Gets or sets the product type /// [DataMember] - public ProductType ProductType - { - get - { - return (ProductType)this.ProductTypeId; - } - set - { - this.ProductTypeId = (int)value; - } - } - - public string ProductTypeLabelHint - { - get - { - switch (ProductType) - { - case ProductType.SimpleProduct: - return "secondary d-none"; - case ProductType.GroupedProduct: - return "success"; - case ProductType.BundledProduct: - return "info"; - default: - return ""; - } - } - } - - /// - /// Gets or sets the backorder mode - /// - [DataMember] - public BackorderMode BackorderMode - { - get - { - return (BackorderMode)this.BackorderModeId; - } - set - { - this.BackorderModeId = (int)value; - } - } - - /// - /// Gets or sets the download activation type - /// - [DataMember] - public DownloadActivationType DownloadActivationType - { - get - { - return (DownloadActivationType)this.DownloadActivationTypeId; - } - set - { - this.DownloadActivationTypeId = (int)value; - } - } - - /// - /// Gets or sets the gift card type - /// - [DataMember] - public GiftCardType GiftCardType - { - get - { - return (GiftCardType)this.GiftCardTypeId; - } - set - { - this.GiftCardTypeId = (int)value; - } - } - - /// - /// Gets or sets the low stock activity - /// - [DataMember] - public LowStockActivity LowStockActivity - { - get - { - return (LowStockActivity)this.LowStockActivityId; - } - set - { - this.LowStockActivityId = (int)value; - } - } - - /// - /// Gets or sets the value indicating how to manage inventory - /// - [DataMember] - public ManageInventoryMethod ManageInventoryMethod - { - get - { - return (ManageInventoryMethod)this.ManageInventoryMethodId; - } - set - { - this.ManageInventoryMethodId = (int)value; - } - } - - /// - /// Gets or sets the cycle period for recurring products - /// - [DataMember] - public RecurringProductCyclePeriod RecurringCyclePeriod - { - get - { - return (RecurringProductCyclePeriod)this.RecurringCyclePeriodId; - } - set - { - this.RecurringCyclePeriodId = (int)value; - } - } - - /// + public ProductType ProductType + { + get => (ProductType)this.ProductTypeId; + set => this.ProductTypeId = (int)value; + } + + public string ProductTypeLabelHint + { + get + { + switch (ProductType) + { + case ProductType.SimpleProduct: + return "secondary d-none"; + case ProductType.GroupedProduct: + return "success"; + case ProductType.BundledProduct: + return "info"; + default: + return ""; + } + } + } + + /// + /// Gets or sets the backorder mode + /// + [DataMember] + public BackorderMode BackorderMode + { + get => (BackorderMode)this.BackorderModeId; + set => this.BackorderModeId = (int)value; + } + + /// + /// Gets or sets the download activation type + /// + [DataMember] + public DownloadActivationType DownloadActivationType + { + get => (DownloadActivationType)this.DownloadActivationTypeId; + set => this.DownloadActivationTypeId = (int)value; + } + + /// + /// Gets or sets the gift card type + /// + [DataMember] + public GiftCardType GiftCardType + { + get => (GiftCardType)this.GiftCardTypeId; + set => this.GiftCardTypeId = (int)value; + } + + /// + /// Gets or sets the low stock activity + /// + [DataMember] + public LowStockActivity LowStockActivity + { + get => (LowStockActivity)this.LowStockActivityId; + set => this.LowStockActivityId = (int)value; + } + + /// + /// Gets or sets the value indicating how to manage inventory + /// + [DataMember] + public ManageInventoryMethod ManageInventoryMethod + { + get => (ManageInventoryMethod)this.ManageInventoryMethodId; + set => this.ManageInventoryMethodId = (int)value; + } + + /// + /// Gets or sets the cycle period for recurring products + /// + [DataMember] + public RecurringProductCyclePeriod RecurringCyclePeriod + { + get => (RecurringProductCyclePeriod)this.RecurringCyclePeriodId; + set => this.RecurringCyclePeriodId = (int)value; + } + + /// /// Gets or sets the collection of ProductCategory /// [DataMember] public virtual ICollection ProductCategories { - get { return _productCategories ?? (_productCategories = new HashSet()); } - protected set { _productCategories = value; } + get => _productCategories ?? (_productCategories = new HashSet()); + protected set => _productCategories = value; } /// /// Gets or sets the collection of ProductManufacturer /// [DataMember] - public virtual ICollection ProductManufacturers + public virtual ICollection ProductManufacturers { - get { return _productManufacturers ?? (_productManufacturers = new HashSet()); } - protected set { _productManufacturers = value; } + get => _productManufacturers ?? (_productManufacturers = new HashSet()); + protected set => _productManufacturers = value; } /// /// Gets or sets the collection of ProductPicture /// [DataMember] - public virtual ICollection ProductPictures + public virtual ICollection ProductPictures { - get { return _productPictures ?? (_productPictures = new List()); } - protected set { _productPictures = value; } + get => _productPictures ?? (_productPictures = new List()); + protected set => _productPictures = value; } /// @@ -1072,78 +951,78 @@ public virtual ICollection ProductPictures /// public virtual ICollection ProductReviews { - get { return _productReviews ?? (_productReviews = new HashSet()); } - protected set { _productReviews = value; } + get => _productReviews ?? (_productReviews = new HashSet()); + protected set => _productReviews = value; } /// /// Gets or sets the product specification attribute /// [DataMember] - public virtual ICollection ProductSpecificationAttributes + public virtual ICollection ProductSpecificationAttributes { - get { return _productSpecificationAttributes ?? (_productSpecificationAttributes = new HashSet()); } - protected set { _productSpecificationAttributes = value; } + get => _productSpecificationAttributes ?? (_productSpecificationAttributes = new HashSet()); + protected set => _productSpecificationAttributes = value; } /// /// Gets or sets the product tags /// [DataMember] - public virtual ICollection ProductTags + public virtual ICollection ProductTags { - get { return _productTags ?? (_productTags = new HashSet()); } - protected set { _productTags = value; } + get => _productTags ?? (_productTags = new HashSet()); + protected set => _productTags = value; } - /// - /// Gets or sets the product attributes - /// - [DataMember] - public virtual ICollection ProductVariantAttributes - { - get { return _productVariantAttributes ?? (_productVariantAttributes = new HashSet()); } - protected set { _productVariantAttributes = value; } - } - - /// - /// Gets or sets the product attribute combinations - /// - [DataMember] - public virtual ICollection ProductVariantAttributeCombinations - { - get { return _productVariantAttributeCombinations ?? (_productVariantAttributeCombinations = new List()); } - protected set { _productVariantAttributeCombinations = value; } - } - - /// - /// Gets or sets the tier prices - /// - [DataMember] - public virtual ICollection TierPrices - { - get { return _tierPrices ?? (_tierPrices = new HashSet()); } - protected set { _tierPrices = value; } - } - - /// - /// Gets or sets the collection of applied discounts - /// - [DataMember] - public virtual ICollection AppliedDiscounts - { - get { return _appliedDiscounts ?? (_appliedDiscounts = new HashSet()); } - protected set { _appliedDiscounts = value; } - } - - /// - /// Gets or sets the collection of product bundle items - /// - [DataMember] - public virtual ICollection ProductBundleItems - { - get { return _productBundleItems ?? (_productBundleItems = new HashSet()); } - protected set { _productBundleItems = value; } - } + /// + /// Gets or sets the product attributes + /// + [DataMember] + public virtual ICollection ProductVariantAttributes + { + get => _productVariantAttributes ?? (_productVariantAttributes = new HashSet()); + protected set => _productVariantAttributes = value; + } + + /// + /// Gets or sets the product attribute combinations + /// + [DataMember] + public virtual ICollection ProductVariantAttributeCombinations + { + get => _productVariantAttributeCombinations ?? (_productVariantAttributeCombinations = new List()); + protected set => _productVariantAttributeCombinations = value; + } + + /// + /// Gets or sets the tier prices + /// + [DataMember] + public virtual ICollection TierPrices + { + get => _tierPrices ?? (_tierPrices = new HashSet()); + protected set => _tierPrices = value; + } + + /// + /// Gets or sets the collection of applied discounts + /// + [DataMember] + public virtual ICollection AppliedDiscounts + { + get => _appliedDiscounts ?? (_appliedDiscounts = new HashSet()); + protected set => _appliedDiscounts = value; + } + + /// + /// Gets or sets the collection of product bundle items + /// + [DataMember] + public virtual ICollection ProductBundleItems + { + get => _productBundleItems ?? (_productBundleItems = new HashSet()); + protected set => _productBundleItems = value; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs index 0b34ac64b3..9043816b92 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs @@ -7,51 +7,51 @@ namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a product attribute - /// - [DataContract] - public partial class ProductAttribute : BaseEntity, ILocalizedEntity, ISearchAlias - { - private ICollection _productAttributeOptionsSets; + /// + /// Represents a product attribute + /// + [DataContract] + public partial class ProductAttribute : BaseEntity, ILocalizedEntity, ISearchAlias + { + private ICollection _productAttributeOptionsSets; /// /// Gets or sets the product attribute alias /// [DataMember] public string Alias { get; set; } - + /// /// Gets or sets the name /// - [DataMember] - public string Name { get; set; } + [DataMember] + public string Name { get; set; } /// /// Gets or sets the description /// [DataMember] - public string Description { get; set; } + public string Description { get; set; } - /// - /// Gets or sets whether the attribute can be filtered - /// - [DataMember] - [Index] - public bool AllowFiltering { get; set; } + /// + /// Gets or sets whether the attribute can be filtered + /// + [DataMember] + [Index] + public bool AllowFiltering { get; set; } - /// - /// Gets or sets the display order - /// - [DataMember] + /// + /// Gets or sets the display order + /// + [DataMember] [Index] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the facet template hint. Only effective in accordance with MegaSearchPlus plugin. /// [DataMember] - public FacetTemplateHint FacetTemplateHint { get; set; } + public FacetTemplateHint FacetTemplateHint { get; set; } /// /// Specifies whether option names should be included in the search index. Only effective in accordance with MegaSearchPlus plugin. @@ -59,20 +59,20 @@ public partial class ProductAttribute : BaseEntity, ILocalizedEntity, ISearchAli [DataMember] public bool IndexOptionNames { get; set; } - /// - /// Gets or sets export mappings. - /// - [DataMember] - public string ExportMappings { get; set; } + /// + /// Gets or sets export mappings. + /// + [DataMember] + public string ExportMappings { get; set; } - /// - /// Gets or sets the prooduct attribute option sets - /// - [DataMember] - public virtual ICollection ProductAttributeOptionsSets - { - get { return _productAttributeOptionsSets ?? (_productAttributeOptionsSets = new HashSet()); } - protected set { _productAttributeOptionsSets = value; } - } - } + /// + /// Gets or sets the prooduct attribute option sets + /// + [DataMember] + public virtual ICollection ProductAttributeOptionsSets + { + get => _productAttributeOptionsSets ?? (_productAttributeOptionsSets = new HashSet()); + protected set => _productAttributeOptionsSets = value; + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOption.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOption.cs index 08e7481248..0bbe59e24d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOption.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOption.cs @@ -1,132 +1,125 @@ using System; using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; using SmartStore.Core.Search; namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a product attribute option - /// - [DataContract] - public partial class ProductAttributeOption : BaseEntity, ILocalizedEntity, ISearchAlias, ICloneable - { - /// - /// Gets or sets the product attribute options set identifier - /// - [DataMember] - public int ProductAttributeOptionsSetId { get; set; } - - /// - /// Gets or sets the option alias - /// - [DataMember] - public string Alias { get; set; } - - /// - /// Gets or sets the option name - /// - [DataMember] - public string Name { get; set; } - - /// - /// Gets or sets the media file id - /// - [DataMember] - public int MediaFileId { get; set; } - - /// - /// Gets or sets the color RGB value (used with "Boxes" attribute type) - /// - [DataMember] - public string Color { get; set; } - - /// - /// Gets or sets the price adjustment - /// - [DataMember] - public decimal PriceAdjustment { get; set; } - - /// - /// Gets or sets the weight adjustment - /// - [DataMember] - public decimal WeightAdjustment { get; set; } - - /// - /// Gets or sets a value indicating whether the option is pre-selected - /// - [DataMember] - public bool IsPreSelected { get; set; } - - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } - - /// - /// Gets or sets the type Id - /// - [DataMember] - public int ValueTypeId { get; set; } - - /// - /// Gets or sets the linked product Id - /// - [DataMember] - public int LinkedProductId { get; set; } - - /// - /// Gets or sets the quantity for the linked product - /// - [DataMember] - public int Quantity { get; set; } - - /// - /// Gets the product attribute options set - /// - [DataMember] - public virtual ProductAttributeOptionsSet ProductAttributeOptionsSet { get; set; } - - /// - /// Gets or sets the product attribute value type - /// - [DataMember] - public ProductVariantAttributeValueType ValueType - { - get - { - return (ProductVariantAttributeValueType)this.ValueTypeId; - } - set - { - this.ValueTypeId = (int)value; - } - } - - public ProductVariantAttributeValue Clone() - { - var value = new ProductVariantAttributeValue(); - value.Alias = Alias; - value.Name = Name; - value.MediaFileId = MediaFileId; - value.Color = Color; - value.PriceAdjustment = PriceAdjustment; - value.WeightAdjustment = WeightAdjustment; - value.IsPreSelected = IsPreSelected; - value.DisplayOrder = DisplayOrder; - value.ValueTypeId = ValueTypeId; - value.LinkedProductId = LinkedProductId; - value.Quantity = Quantity; - - return value; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - } + /// + /// Represents a product attribute option + /// + [DataContract] + public partial class ProductAttributeOption : BaseEntity, ILocalizedEntity, ISearchAlias, ICloneable + { + /// + /// Gets or sets the product attribute options set identifier + /// + [DataMember] + public int ProductAttributeOptionsSetId { get; set; } + + /// + /// Gets or sets the option alias + /// + [DataMember] + public string Alias { get; set; } + + /// + /// Gets or sets the option name + /// + [DataMember] + public string Name { get; set; } + + /// + /// Gets or sets the media file id + /// + [DataMember] + public int MediaFileId { get; set; } + + /// + /// Gets or sets the color RGB value (used with "Boxes" attribute type) + /// + [DataMember] + public string Color { get; set; } + + /// + /// Gets or sets the price adjustment + /// + [DataMember] + public decimal PriceAdjustment { get; set; } + + /// + /// Gets or sets the weight adjustment + /// + [DataMember] + public decimal WeightAdjustment { get; set; } + + /// + /// Gets or sets a value indicating whether the option is pre-selected + /// + [DataMember] + public bool IsPreSelected { get; set; } + + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } + + /// + /// Gets or sets the type Id + /// + [DataMember] + public int ValueTypeId { get; set; } + + /// + /// Gets or sets the linked product Id + /// + [DataMember] + public int LinkedProductId { get; set; } + + /// + /// Gets or sets the quantity for the linked product + /// + [DataMember] + public int Quantity { get; set; } + + /// + /// Gets the product attribute options set + /// + [DataMember] + public virtual ProductAttributeOptionsSet ProductAttributeOptionsSet { get; set; } + + /// + /// Gets or sets the product attribute value type + /// + [DataMember] + public ProductVariantAttributeValueType ValueType + { + get => (ProductVariantAttributeValueType)this.ValueTypeId; + set => this.ValueTypeId = (int)value; + } + + public ProductVariantAttributeValue Clone() + { + var value = new ProductVariantAttributeValue(); + value.Alias = Alias; + value.Name = Name; + value.MediaFileId = MediaFileId; + value.Color = Color; + value.PriceAdjustment = PriceAdjustment; + value.WeightAdjustment = WeightAdjustment; + value.IsPreSelected = IsPreSelected; + value.DisplayOrder = DisplayOrder; + value.ValueTypeId = ValueTypeId; + value.LinkedProductId = LinkedProductId; + value.Quantity = Quantity; + + return value; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOptionsSet.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOptionsSet.cs index c6819334d2..4dcbac9d0d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOptionsSet.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttributeOptionsSet.cs @@ -3,37 +3,37 @@ namespace SmartStore.Core.Domain.Catalog { - [DataContract] - public partial class ProductAttributeOptionsSet : BaseEntity - { - private ICollection _productAttributeOptions; + [DataContract] + public partial class ProductAttributeOptionsSet : BaseEntity + { + private ICollection _productAttributeOptions; - /// - /// Gets or sets the name - /// - [DataMember] - public string Name { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } - /// - /// Gets or sets the product attribute identifier - /// - [DataMember] - public int ProductAttributeId { get; set; } + /// + /// Gets or sets the product attribute identifier + /// + [DataMember] + public int ProductAttributeId { get; set; } - /// - /// Gets the product attribute - /// - [DataMember] - public virtual ProductAttribute ProductAttribute { get; set; } + /// + /// Gets the product attribute + /// + [DataMember] + public virtual ProductAttribute ProductAttribute { get; set; } - /// - /// Gets or sets the prooduct attribute options - /// - [DataMember] - public virtual ICollection ProductAttributeOptions - { - get { return _productAttributeOptions ?? (_productAttributeOptions = new HashSet()); } - protected set { _productAttributeOptions = value; } - } - } + /// + /// Gets or sets the prooduct attribute options + /// + [DataMember] + public virtual ICollection ProductAttributeOptions + { + get => _productAttributeOptions ?? (_productAttributeOptions = new HashSet()); + protected set => _productAttributeOptions = value; + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs index 60373a2d15..ea7b41f44c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs @@ -1,29 +1,29 @@ namespace SmartStore.Core.Domain.Catalog { public partial class ProductBundleItemData - { - public ProductBundleItemData(ProductBundleItem item) - { - Item = item; // Can be null. - } + { + public ProductBundleItemData(ProductBundleItem item) + { + Item = item; // Can be null. + } - public ProductBundleItem Item { get; private set; } - public decimal AdditionalCharge { get; set; } - } + public ProductBundleItem Item { get; private set; } + public decimal AdditionalCharge { get; set; } + } - public partial class ProductBundleItemOrderData - { - public int BundleItemId { get; set; } - public int ProductId { get; set; } - public string Sku { get; set; } - public string ProductName { get; set; } - public string ProductSeName { get; set; } - public bool VisibleIndividually { get; set; } - public int Quantity { get; set; } - public decimal PriceWithDiscount { get; set; } - public int DisplayOrder { get; set; } - public string AttributesXml { get; set; } - public string AttributesInfo { get; set; } - public bool PerItemShoppingCart { get; set; } - } + public partial class ProductBundleItemOrderData + { + public int BundleItemId { get; set; } + public int ProductId { get; set; } + public string Sku { get; set; } + public string ProductName { get; set; } + public string ProductSeName { get; set; } + public bool VisibleIndividually { get; set; } + public int Quantity { get; set; } + public decimal PriceWithDiscount { get; set; } + public int DisplayOrder { get; set; } + public string AttributesXml { get; set; } + public string AttributesInfo { get; set; } + public bool PerItemShoppingCart { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItem.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItem.cs index 38865d9b70..923e1681e7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItem.cs @@ -5,141 +5,141 @@ namespace SmartStore.Core.Domain.Catalog { - [DataContract] - public partial class ProductBundleItem : BaseEntity, IAuditable, ILocalizedEntity, ICloneable - { - private ICollection _attributeFilters; - - /// - /// Gets or sets the product identifier - /// - [DataMember] - public int ProductId { get; set; } - - /// - /// Gets or sets the product identifier of the bundle product - /// - [DataMember] - public int BundleProductId { get; set; } - - /// - /// Gets or sets the quantity - /// - [DataMember] - public int Quantity { get; set; } - - /// - /// Gets or sets the discount value - /// - [DataMember] - public decimal? Discount { get; set; } - - /// - /// Gets or sets whether the discount is in percent - /// - [DataMember] - public bool DiscountPercentage { get; set; } - - /// - /// Gets or sets the name value - /// - [DataMember] - public string Name { get; set; } - - /// - /// Gets or sets the name value - /// - [DataMember] - public string ShortDescription { get; set; } - - /// - /// Gets or sets a value indicating whether to filter attributes - /// - [DataMember] - public bool FilterAttributes { get; set; } - - /// - /// Gets or sets a value indicating whether to hide the thumbnail - /// - [DataMember] - public bool HideThumbnail { get; set; } - - /// - /// Gets or sets a value indicating whether the item is visible - /// - [DataMember] - public bool Visible { get; set; } - - /// - /// Gets or sets a value indicating whether the entity is published - /// - [DataMember] - public bool Published { get; set; } - - /// - /// Gets or sets a display order - /// - [DataMember] - public int DisplayOrder { get; set; } - - /// - /// Gets or sets the date and time of product bundle item creation - /// - [DataMember] - public DateTime CreatedOnUtc { get; set; } - - /// - /// Gets or sets the date and time of product bundle item update - /// - [DataMember] - public DateTime UpdatedOnUtc { get; set; } - - /// - /// Gets the product - /// - [DataMember] - public virtual Product Product { get; set; } - - /// - /// Gets the bundle product - /// - [DataMember] - public virtual Product BundleProduct { get; set; } - - /// - /// Gets or sets the collection of attribute filters - /// - public virtual ICollection AttributeFilters - { - get { return _attributeFilters ?? (_attributeFilters = new HashSet()); } - protected set { _attributeFilters = value; } - } - - public ProductBundleItem Clone() - { - var bundleItem = new ProductBundleItem - { - ProductId = this.ProductId, - BundleProductId = this.BundleProductId, - Quantity = this.Quantity, - Discount = this.Discount, - DiscountPercentage = this.DiscountPercentage, - Name = this.Name, - ShortDescription = this.ShortDescription, - FilterAttributes = this.FilterAttributes, - HideThumbnail = this.HideThumbnail, - Visible = this.Visible, - Published = this.Published, - DisplayOrder = this.DisplayOrder, - CreatedOnUtc = this.CreatedOnUtc, - UpdatedOnUtc = this.UpdatedOnUtc - }; - return bundleItem; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - } + [DataContract] + public partial class ProductBundleItem : BaseEntity, IAuditable, ILocalizedEntity, ICloneable + { + private ICollection _attributeFilters; + + /// + /// Gets or sets the product identifier + /// + [DataMember] + public int ProductId { get; set; } + + /// + /// Gets or sets the product identifier of the bundle product + /// + [DataMember] + public int BundleProductId { get; set; } + + /// + /// Gets or sets the quantity + /// + [DataMember] + public int Quantity { get; set; } + + /// + /// Gets or sets the discount value + /// + [DataMember] + public decimal? Discount { get; set; } + + /// + /// Gets or sets whether the discount is in percent + /// + [DataMember] + public bool DiscountPercentage { get; set; } + + /// + /// Gets or sets the name value + /// + [DataMember] + public string Name { get; set; } + + /// + /// Gets or sets the name value + /// + [DataMember] + public string ShortDescription { get; set; } + + /// + /// Gets or sets a value indicating whether to filter attributes + /// + [DataMember] + public bool FilterAttributes { get; set; } + + /// + /// Gets or sets a value indicating whether to hide the thumbnail + /// + [DataMember] + public bool HideThumbnail { get; set; } + + /// + /// Gets or sets a value indicating whether the item is visible + /// + [DataMember] + public bool Visible { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is published + /// + [DataMember] + public bool Published { get; set; } + + /// + /// Gets or sets a display order + /// + [DataMember] + public int DisplayOrder { get; set; } + + /// + /// Gets or sets the date and time of product bundle item creation + /// + [DataMember] + public DateTime CreatedOnUtc { get; set; } + + /// + /// Gets or sets the date and time of product bundle item update + /// + [DataMember] + public DateTime UpdatedOnUtc { get; set; } + + /// + /// Gets the product + /// + [DataMember] + public virtual Product Product { get; set; } + + /// + /// Gets the bundle product + /// + [DataMember] + public virtual Product BundleProduct { get; set; } + + /// + /// Gets or sets the collection of attribute filters + /// + public virtual ICollection AttributeFilters + { + get => _attributeFilters ?? (_attributeFilters = new HashSet()); + protected set => _attributeFilters = value; + } + + public ProductBundleItem Clone() + { + var bundleItem = new ProductBundleItem + { + ProductId = this.ProductId, + BundleProductId = this.BundleProductId, + Quantity = this.Quantity, + Discount = this.Discount, + DiscountPercentage = this.DiscountPercentage, + Name = this.Name, + ShortDescription = this.ShortDescription, + FilterAttributes = this.FilterAttributes, + HideThumbnail = this.HideThumbnail, + Visible = this.Visible, + Published = this.Published, + DisplayOrder = this.DisplayOrder, + CreatedOnUtc = this.CreatedOnUtc, + UpdatedOnUtc = this.UpdatedOnUtc + }; + return bundleItem; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItemAttributeFilter.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItemAttributeFilter.cs index e7ecba5eb3..0a6db35734 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItemAttributeFilter.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleItemAttributeFilter.cs @@ -2,48 +2,48 @@ namespace SmartStore.Core.Domain.Catalog { - public partial class ProductBundleItemAttributeFilter : BaseEntity, ICloneable - { - /// - /// Gets or sets the product bundle item identifier - /// - public int BundleItemId { get; set; } + public partial class ProductBundleItemAttributeFilter : BaseEntity, ICloneable + { + /// + /// Gets or sets the product bundle item identifier + /// + public int BundleItemId { get; set; } - /// - /// Gets or sets the product attribute identifier - /// - public int AttributeId { get; set; } + /// + /// Gets or sets the product attribute identifier + /// + public int AttributeId { get; set; } - /// - /// Gets or sets the product attribute value identifier - /// - public int AttributeValueId { get; set; } + /// + /// Gets or sets the product attribute value identifier + /// + public int AttributeValueId { get; set; } - /// - /// Gets or sets whether the filtered value is preselected - /// - public bool IsPreSelected { get; set; } + /// + /// Gets or sets whether the filtered value is preselected + /// + public bool IsPreSelected { get; set; } - /// - /// Gets the product bundle item - /// - public virtual ProductBundleItem BundleItem { get; set; } + /// + /// Gets the product bundle item + /// + public virtual ProductBundleItem BundleItem { get; set; } - public ProductBundleItemAttributeFilter Clone() - { - var filter = new ProductBundleItemAttributeFilter() - { - BundleItemId = this.BundleItemId, - AttributeId = this.AttributeId, - AttributeValueId = this.AttributeValueId, - IsPreSelected = this.IsPreSelected - }; - return filter; - } + public ProductBundleItemAttributeFilter Clone() + { + var filter = new ProductBundleItemAttributeFilter() + { + BundleItemId = this.BundleItemId, + AttributeId = this.AttributeId, + AttributeValueId = this.AttributeValueId, + IsPreSelected = this.IsPreSelected + }; + return filter; + } - object ICloneable.Clone() - { - return this.Clone(); - } - } + object ICloneable.Clone() + { + return this.Clone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs index 73d59fe51e..1d027e15b4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs @@ -13,19 +13,19 @@ public partial class ProductCategory : BaseEntity /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } /// /// Gets or sets the category identifier /// [DataMember] - public int CategoryId { get; set; } + public int CategoryId { get; set; } /// /// Gets or sets a value indicating whether the product is featured /// [DataMember] - [Index] + [Index] public bool IsFeaturedProduct { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs index 73edaec790..88babcda90 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs @@ -3,36 +3,36 @@ namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a product manufacturer mapping - /// - [DataContract] - public partial class ProductManufacturer : BaseEntity + /// + /// Represents a product manufacturer mapping + /// + [DataContract] + public partial class ProductManufacturer : BaseEntity { /// /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } /// /// Gets or sets the manufacturer identifier /// [DataMember] - public int ManufacturerId { get; set; } + public int ManufacturerId { get; set; } /// /// Gets or sets a value indicating whether the product is featured /// [DataMember] - [Index] - public bool IsFeaturedProduct { get; set; } + [Index] + public bool IsFeaturedProduct { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the manufacturer diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductMediaFile.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductMediaFile.cs index 630f5cf754..ad7175b88e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductMediaFile.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductMediaFile.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using Newtonsoft.Json; using SmartStore.Core.Domain.Media; namespace SmartStore.Core.Domain.Catalog @@ -17,33 +18,34 @@ public interface IMediaFile /// /// Gets the media file /// + [JsonIgnore] MediaFile MediaFile { get; set; } } - + /// /// Represents a product media file mapping /// - [DataContract] - public partial class ProductMediaFile : BaseEntity, IMediaFile + [DataContract] + public partial class ProductMediaFile : BaseEntity, IMediaFile { /// /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } /// /// Gets or sets the picture identifier /// [DataMember] - public int MediaFileId { get; set; } + public int MediaFileId { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } - + public int DisplayOrder { get; set; } + /// /// Gets the media file diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductReview.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductReview.cs index eb52702201..7d45b1e756 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductReview.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductReview.cs @@ -50,8 +50,8 @@ public partial class ProductReview : CustomerContent /// public virtual ICollection ProductReviewHelpfulnessEntries { - get { return _productReviewHelpfulnessEntries ?? (_productReviewHelpfulnessEntries = new HashSet()); } - protected set { _productReviewHelpfulnessEntries = value; } + get => _productReviewHelpfulnessEntries ?? (_productReviewHelpfulnessEntries = new HashSet()); + protected set => _productReviewHelpfulnessEntries = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs index 1440fd556f..4d4de9397e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs @@ -10,10 +10,10 @@ public enum ProductSortingEnum /// Initial = 0, - /// - /// Relevance - /// - Relevance = 1, + /// + /// Relevance + /// + Relevance = 1, /// /// Name: A to Z diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs index eb8595b3af..b168833a78 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs @@ -6,38 +6,38 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a product specification attribute /// [DataContract] - public partial class ProductSpecificationAttribute : BaseEntity + public partial class ProductSpecificationAttribute : BaseEntity { /// /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } /// /// Gets or sets the specification attribute identifier /// [DataMember] - public int SpecificationAttributeOptionId { get; set; } + public int SpecificationAttributeOptionId { get; set; } - /// - /// Gets or sets whether the attribute can be filtered. Only effective in accordance with MegaSearchPlus plugin. - /// - [DataMember] - public bool? AllowFiltering { get; set; } + /// + /// Gets or sets whether the attribute can be filtered. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + public bool? AllowFiltering { get; set; } /// /// Gets or sets whether the attribute will be shown on the product page. /// [DataMember] - public bool? ShowOnProductPage { get; set; } + public bool? ShowOnProductPage { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } - + public int DisplayOrder { get; set; } + /// /// Gets or sets the product /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductTag.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductTag.cs index c2563fcfd2..ace86b58ef 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductTag.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductTag.cs @@ -9,15 +9,15 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a product tag. /// [DataContract] - public partial class ProductTag : BaseEntity, ILocalizedEntity + public partial class ProductTag : BaseEntity, ILocalizedEntity { - private ICollection _products; + private ICollection _products; /// /// Gets or sets the name. /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets a value indicating whether the entity is published. @@ -30,9 +30,9 @@ public partial class ProductTag : BaseEntity, ILocalizedEntity /// Gets or sets the products. /// public virtual ICollection Products - { - get { return _products ?? (_products = new HashSet()); } - protected set { _products = value; } - } + { + get => _products ?? (_products = new HashSet()); + protected set => _products = value; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductType.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductType.cs index bf71926304..267c7a8b54 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductType.cs @@ -1,24 +1,24 @@  namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a product type - /// - public enum ProductType - { - /// - /// Simple product - /// - SimpleProduct = 5, + /// + /// Represents a product type + /// + public enum ProductType + { + /// + /// Simple product + /// + SimpleProduct = 5, - /// - /// Grouped product - /// - GroupedProduct = 10, + /// + /// Grouped product + /// + GroupedProduct = 10, - /// - /// Bundled product - /// - BundledProduct = 15 - } + /// + /// Bundled product + /// + BundledProduct = 15 + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttribute.cs index 96a9e5367b..44f6a4885e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttribute.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; @@ -9,98 +10,112 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a product variant attribute mapping /// [DataContract] - public partial class ProductVariantAttribute : BaseEntity, ILocalizedEntity - { + public partial class ProductVariantAttribute : BaseEntity, ILocalizedEntity + { private ICollection _productVariantAttributeValues; /// /// Gets or sets the product identifier /// [DataMember] - [Index("IX_Product_ProductAttribute_Mapping_ProductId_DisplayOrder", 1)] - public int ProductId { get; set; } + [Index("IX_Product_ProductAttribute_Mapping_ProductId_DisplayOrder", 1)] + public int ProductId { get; set; } /// /// Gets or sets the product attribute identifier /// [DataMember] - public int ProductAttributeId { get; set; } + public int ProductAttributeId { get; set; } /// /// Gets or sets a value a text prompt /// [DataMember] - public string TextPrompt { get; set; } + public string TextPrompt { get; set; } + + /// + /// Gets or sets any custom data. + /// It's not used by Smartstore but is being passed to the choice partial view. + /// + [DataMember] + [MaxLength] + public string CustomData { get; set; } /// /// Gets or sets a value indicating whether the entity is required /// [DataMember] - public bool IsRequired { get; set; } + public bool IsRequired { get; set; } /// /// Gets or sets the attribute control type identifier /// [DataMember] - [Index] - public int AttributeControlTypeId { get; set; } + [Index] + public int AttributeControlTypeId { get; set; } /// /// Gets or sets the display order /// [DataMember] - [Index("IX_Product_ProductAttribute_Mapping_ProductId_DisplayOrder", 2)] - public int DisplayOrder { get; set; } + [Index("IX_Product_ProductAttribute_Mapping_ProductId_DisplayOrder", 2)] + public int DisplayOrder { get; set; } /// /// Gets the attribute control type /// [DataMember] - public AttributeControlType AttributeControlType + public AttributeControlType AttributeControlType { - get - { - return (AttributeControlType)this.AttributeControlTypeId; - } - set + get => (AttributeControlType)this.AttributeControlTypeId; + set => this.AttributeControlTypeId = (int)value; + } + + /// + /// Returns a value whether the attribute has a list of values. + /// + public bool IsListTypeAttribute() + { + switch (AttributeControlType) { - this.AttributeControlTypeId = (int)value; + case AttributeControlType.Checkboxes: + case AttributeControlType.Boxes: + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + return true; + default: + return false; } } - public bool IsListTypeAttribute() - { - switch (AttributeControlType) - { - case AttributeControlType.Checkboxes: - case AttributeControlType.Boxes: - case AttributeControlType.DropdownList: - case AttributeControlType.RadioList: - return true; - default: - return false; - } - } + /// + /// Returns a value whether the selection of multiple values is supported. + /// + [NotMapped] + public bool IsMultipleChoice + { + get => AttributeControlType == AttributeControlType.Checkboxes; + } /// /// Gets the product attribute /// [DataMember] - public virtual ProductAttribute ProductAttribute { get; set; } + public virtual ProductAttribute ProductAttribute { get; set; } /// /// Gets the product /// public virtual Product Product { get; set; } - + /// /// Gets the product variant attribute values /// - [DataMember] - public virtual ICollection ProductVariantAttributeValues + [DataMember] + public virtual ICollection ProductVariantAttributeValues { - get { return _productVariantAttributeValues ?? (_productVariantAttributeValues = new HashSet()); } - protected set { _productVariantAttributeValues = value; } + get => _productVariantAttributeValues ?? (_productVariantAttributeValues = new HashSet()); + protected set => _productVariantAttributeValues = value; } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeCombination.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeCombination.cs index e34724a820..d3199afd8c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeCombination.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeCombination.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; namespace SmartStore.Core.Domain.Catalog { @@ -5,35 +6,37 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a product variant attribute combination /// [DataContract] - public partial class ProductVariantAttributeCombination : BaseEntity + public partial class ProductVariantAttributeCombination : BaseEntity { /// /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } /// /// Gets or sets the attributes /// [DataMember] - public string AttributesXml { get; set; } + public string AttributesXml { get; set; } /// /// Gets or sets the stock quantity /// [DataMember] - public int StockQuantity { get; set; } + [Index("IX_StockQuantity_AllowOutOfStockOrders", 1)] + public int StockQuantity { get; set; } /// /// Gets or sets a value indicating whether to allow orders when out of stock /// [DataMember] - public bool AllowOutOfStockOrders { get; set; } - + [Index("IX_StockQuantity_AllowOutOfStockOrders", 2)] + public bool AllowOutOfStockOrders { get; set; } + /// /// Gets the product /// - public virtual Product Product { get; set; } + public virtual Product Product { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValue.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValue.cs index dc9a6b0a77..7804a7f972 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValue.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValue.cs @@ -1,7 +1,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; using SmartStore.Core.Search; namespace SmartStore.Core.Domain.Catalog @@ -10,27 +9,27 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a product variant attribute value ///
[DataContract] - public partial class ProductVariantAttributeValue : BaseEntity, ILocalizedEntity, ISearchAlias - { + public partial class ProductVariantAttributeValue : BaseEntity, ILocalizedEntity, ISearchAlias + { /// /// Gets or sets the product variant attribute mapping identifier /// [DataMember] - [Index("IX_ProductVariantAttributeValue_ProductVariantAttributeId_DisplayOrder", 1)] - public int ProductVariantAttributeId { get; set; } + [Index("IX_ProductVariantAttributeValue_ProductVariantAttributeId_DisplayOrder", 1)] + public int ProductVariantAttributeId { get; set; } /// /// Gets or sets the product variant attribute alias /// [DataMember] - public string Alias { get; set; } + public string Alias { get; set; } /// /// Gets or sets the product variant attribute name /// [DataMember] - [Index] - public string Name { get; set; } + [Index] + public string Name { get; set; } /// /// Gets or sets the media file id @@ -38,75 +37,69 @@ public partial class ProductVariantAttributeValue : BaseEntity, ILocalizedEntity [DataMember] public int MediaFileId { get; set; } - /// - /// Gets or sets the color RGB value (used with "Boxes" attribute type) - /// - [DataMember] - public string Color { get; set; } + /// + /// Gets or sets the color RGB value (used with "Boxes" attribute type) + /// + [DataMember] + public string Color { get; set; } /// /// Gets or sets the price adjustment /// [DataMember] - public decimal PriceAdjustment { get; set; } + public decimal PriceAdjustment { get; set; } /// /// Gets or sets the weight adjustment /// [DataMember] - public decimal WeightAdjustment { get; set; } + public decimal WeightAdjustment { get; set; } /// /// Gets or sets a value indicating whether the value is pre-selected /// [DataMember] - public bool IsPreSelected { get; set; } + public bool IsPreSelected { get; set; } /// /// Gets or sets the display order /// [DataMember] - [Index("IX_ProductVariantAttributeValue_ProductVariantAttributeId_DisplayOrder", 2)] - public int DisplayOrder { get; set; } + [Index("IX_ProductVariantAttributeValue_ProductVariantAttributeId_DisplayOrder", 2)] + public int DisplayOrder { get; set; } - /// - /// Gets or sets the type Id - /// - [DataMember] - [Index] - public int ValueTypeId { get; set; } + /// + /// Gets or sets the type Id + /// + [DataMember] + [Index] + public int ValueTypeId { get; set; } - /// - /// Gets or sets the linked product Id - /// - [DataMember] - public int LinkedProductId { get; set; } + /// + /// Gets or sets the linked product Id + /// + [DataMember] + public int LinkedProductId { get; set; } - /// - /// Gets or sets the quantity for the linked product - /// - [DataMember] - public int Quantity { get; set; } + /// + /// Gets or sets the quantity for the linked product + /// + [DataMember] + public int Quantity { get; set; } /// /// Gets the product variant attribute /// [DataMember] - public virtual ProductVariantAttribute ProductVariantAttribute { get; set; } + public virtual ProductVariantAttribute ProductVariantAttribute { get; set; } - /// - /// Gets or sets the product attribute value type - /// - public ProductVariantAttributeValueType ValueType - { - get - { - return (ProductVariantAttributeValueType)this.ValueTypeId; - } - set - { - this.ValueTypeId = (int)value; - } - } + /// + /// Gets or sets the product attribute value type + /// + public ProductVariantAttributeValueType ValueType + { + get => (ProductVariantAttributeValueType)this.ValueTypeId; + set => this.ValueTypeId = (int)value; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValueType.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValueType.cs index 9fbde986e9..e6e7b67256 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValueType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductVariantAttributeValueType.cs @@ -1,9 +1,9 @@  namespace SmartStore.Core.Domain.Catalog { - public enum ProductVariantAttributeValueType - { - Simple = 0, - ProductLinkage = 10 - } + public enum ProductVariantAttributeValueType + { + Simple = 0, + ProductLinkage = 10 + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/RelatedProduct.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/RelatedProduct.cs index a22ae92c6a..f523da1c4b 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/RelatedProduct.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/RelatedProduct.cs @@ -5,25 +5,25 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a related product /// [DataContract] - public partial class RelatedProduct : BaseEntity + public partial class RelatedProduct : BaseEntity { /// /// Gets or sets the first product identifier /// [DataMember] - public int ProductId1 { get; set; } + public int ProductId1 { get; set; } /// /// Gets or sets the second product identifier /// [DataMember] - public int ProductId2 { get; set; } + public int ProductId2 { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/SmartStoreProductVariantAttributeCombination.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/SmartStoreProductVariantAttributeCombination.cs index a2c1d3d088..9b3a3c6463 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/SmartStoreProductVariantAttributeCombination.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/SmartStoreProductVariantAttributeCombination.cs @@ -1,7 +1,8 @@ using System; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using SmartStore.Core.Domain.Directory; using System.Runtime.Serialization; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Core.Domain.Catalog { @@ -15,40 +16,42 @@ public ProductVariantAttributeCombination() this.IsActive = true; } - [DataMember] + [DataMember] public string Sku { get; set; } - [DataMember] - public string Gtin { get; set; } + [DataMember] + [Index] + public string Gtin { get; set; } - [DataMember] - public string ManufacturerPartNumber { get; set; } + [DataMember] + [Index] + public string ManufacturerPartNumber { get; set; } - [DataMember] - public decimal? Price { get; set; } + [DataMember] + public decimal? Price { get; set; } - [DataMember] - public decimal? Length { get; set; } + [DataMember] + public decimal? Length { get; set; } - [DataMember] - public decimal? Width { get; set; } + [DataMember] + public decimal? Width { get; set; } - [DataMember] - public decimal? Height { get; set; } + [DataMember] + public decimal? Height { get; set; } - [DataMember] - public decimal? BasePriceAmount { get; set; } + [DataMember] + public decimal? BasePriceAmount { get; set; } - [DataMember] - public int? BasePriceBaseAmount { get; set; } + [DataMember] + public int? BasePriceBaseAmount { get; set; } - [DataMember] + [DataMember] public string AssignedMediaFileIds { get; set; } - [DataMember] + [DataMember] public int? DeliveryTimeId { get; set; } - [DataMember] + [DataMember] public virtual DeliveryTime DeliveryTime { get; set; } [DataMember] @@ -57,9 +60,10 @@ public ProductVariantAttributeCombination() [DataMember] public virtual QuantityUnit QuantityUnit { get; set; } - [DataMember] - public bool IsActive { get; set; } - //public bool IsDefaultCombination { get; set; } + [DataMember] + [Index] + public bool IsActive { get; set; } + //public bool IsDefaultCombination { get; set; } public int[] GetAssignedMediaIds() { diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs index c5dd44386d..f867f81ff7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs @@ -7,56 +7,56 @@ namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a specification attribute - /// - [DataContract] - public partial class SpecificationAttribute : BaseEntity, ILocalizedEntity, ISearchAlias - { + /// + /// Represents a specification attribute + /// + [DataContract] + public partial class SpecificationAttribute : BaseEntity, ILocalizedEntity, ISearchAlias + { private ICollection _specificationAttributeOptions; /// /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } - /// - /// Gets or sets the specification attribute alias - /// - [DataMember] - public string Alias { get; set; } + /// + /// Gets or sets the specification attribute alias + /// + [DataMember] + public string Alias { get; set; } - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } - /// - /// Gets or sets whether the specification attribute will be shown on the product page. - /// - [DataMember] - public bool ShowOnProductPage { get; set; } + /// + /// Gets or sets whether the specification attribute will be shown on the product page. + /// + [DataMember] + public bool ShowOnProductPage { get; set; } - /// - /// Gets or sets whether the specification attribute can be filtered. Only effective in accordance with MegaSearchPlus plugin. - /// - [DataMember] - [Index] - public bool AllowFiltering { get; set; } + /// + /// Gets or sets whether the specification attribute can be filtered. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + [Index] + public bool AllowFiltering { get; set; } - /// - /// Gets or sets the sorting of facets. Only effective in accordance with MegaSearchPlus plugin. - /// - [DataMember] - public FacetSorting FacetSorting { get; set; } + /// + /// Gets or sets the sorting of facets. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + public FacetSorting FacetSorting { get; set; } - /// - /// Gets or sets the facet template hint. Only effective in accordance with MegaSearchPlus plugin. - /// - [DataMember] - public FacetTemplateHint FacetTemplateHint { get; set; } + /// + /// Gets or sets the facet template hint. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + public FacetTemplateHint FacetTemplateHint { get; set; } /// /// Specifies whether option names should be included in the search index. Only effective in accordance with MegaSearchPlus plugin. @@ -68,10 +68,10 @@ public partial class SpecificationAttribute : BaseEntity, ILocalizedEntity, ISea /// Gets or sets the specification attribute options /// [DataMember] - public virtual ICollection SpecificationAttributeOptions + public virtual ICollection SpecificationAttributeOptions { - get { return _specificationAttributeOptions ?? (_specificationAttributeOptions = new HashSet()); } - protected set { _specificationAttributeOptions = value; } + get => _specificationAttributeOptions ?? (_specificationAttributeOptions = new HashSet()); + protected set => _specificationAttributeOptions = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttributeOption.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttributeOption.cs index 1e7fbbdf12..01089452b4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttributeOption.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttributeOption.cs @@ -1,48 +1,47 @@ using System.Collections.Generic; using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; using SmartStore.Core.Search; namespace SmartStore.Core.Domain.Catalog { - /// - /// Represents a specification attribute option - /// - [DataContract] - public partial class SpecificationAttributeOption : BaseEntity, ILocalizedEntity, ISearchAlias - { + /// + /// Represents a specification attribute option + /// + [DataContract] + public partial class SpecificationAttributeOption : BaseEntity, ILocalizedEntity, ISearchAlias + { private ICollection _productSpecificationAttributes; /// /// Gets or sets the specification attribute identifier /// [DataMember] - public int SpecificationAttributeId { get; set; } + public int SpecificationAttributeId { get; set; } /// /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } - /// - /// Gets or sets the specification attribute option alias - /// - [DataMember] - public string Alias { get; set; } + /// + /// Gets or sets the specification attribute option alias + /// + [DataMember] + public string Alias { get; set; } - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } - /// - /// Gets or sets the number value for range filtering - /// - [DataMember] - public decimal NumberValue { get; set; } + /// + /// Gets or sets the number value for range filtering + /// + [DataMember] + public decimal NumberValue { get; set; } /// /// Gets or sets the media file id. @@ -50,26 +49,26 @@ public partial class SpecificationAttributeOption : BaseEntity, ILocalizedEntity [DataMember] public int MediaFileId { get; set; } - /// - /// Gets or sets the color RGB value. - /// - [DataMember] + /// + /// Gets or sets the color RGB value. + /// + [DataMember] public string Color { get; set; } /// /// Gets or sets the specification attribute /// [DataMember] - public virtual SpecificationAttribute SpecificationAttribute { get; set; } + public virtual SpecificationAttribute SpecificationAttribute { get; set; } /// /// Gets or sets the product specification attributes /// [DataMember] - public virtual ICollection ProductSpecificationAttributes + public virtual ICollection ProductSpecificationAttributes { - get { return _productSpecificationAttributes ?? (_productSpecificationAttributes = new HashSet()); } - protected set { _productSpecificationAttributes = value; } + get => _productSpecificationAttributes ?? (_productSpecificationAttributes = new HashSet()); + protected set => _productSpecificationAttributes = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/TierPrice.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/TierPrice.cs index 7186b46490..8458fb7f3f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/TierPrice.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/TierPrice.cs @@ -7,37 +7,37 @@ namespace SmartStore.Core.Domain.Catalog /// Represents a tier price /// [DataContract] - public partial class TierPrice : BaseEntity + public partial class TierPrice : BaseEntity { /// /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } - /// - /// Gets or sets the store identifier (0 - all stores) - /// - [DataMember] - public int StoreId { get; set; } + /// + /// Gets or sets the store identifier (0 - all stores) + /// + [DataMember] + public int StoreId { get; set; } /// /// Gets or sets the customer role identifier /// [DataMember] - public int? CustomerRoleId { get; set; } + public int? CustomerRoleId { get; set; } /// /// Gets or sets the quantity /// [DataMember] - public int Quantity { get; set; } + public int Quantity { get; set; } /// /// Gets or sets the price /// [DataMember] - public decimal Price { get; set; } + public decimal Price { get; set; } /// /// Gets or sets the product diff --git a/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs b/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs index c8ebedf1c1..19dfe6e581 100644 --- a/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs +++ b/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs @@ -22,8 +22,8 @@ public class MenuRecord : BaseEntity, ILocalizedEntity, IStoreMappingSupported, /// public virtual ICollection Items { - get { return _items ?? (_items = new HashSet()); } - protected set { _items = value; } + get => _items ?? (_items = new HashSet()); + protected set => _items = value; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Cms/WidgetSettings.cs b/src/Libraries/SmartStore.Core/Domain/Cms/WidgetSettings.cs index 56aa6440a8..0284038e3e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Cms/WidgetSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Cms/WidgetSettings.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Configuration; - + namespace SmartStore.Core.Domain.Cms { public class WidgetSettings : ISettings diff --git a/src/Libraries/SmartStore.Core/Domain/Common/Address.cs b/src/Libraries/SmartStore.Core/Domain/Common/Address.cs index c4723d0e6e..ab85c7d04b 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/Address.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/Address.cs @@ -1,11 +1,11 @@ using System; -using SmartStore.Core.Domain.Directory; using System.Runtime.Serialization; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Core.Domain.Common { - [DataContract] - public class Address : BaseEntity, ICloneable + [DataContract] + public class Address : BaseEntity, ICloneable { /// /// Gets or sets the first name @@ -23,91 +23,91 @@ public class Address : BaseEntity, ICloneable /// Gets or sets the first name /// [DataMember] - public string FirstName { get; set; } + public string FirstName { get; set; } /// /// Gets or sets the last name /// [DataMember] - public string LastName { get; set; } + public string LastName { get; set; } /// /// Gets or sets the email /// [DataMember] - public string Email { get; set; } + public string Email { get; set; } /// /// Gets or sets the company /// [DataMember] - public string Company { get; set; } + public string Company { get; set; } /// /// Gets or sets the country identifier /// [DataMember] - public int? CountryId { get; set; } + public int? CountryId { get; set; } /// /// Gets or sets the state/province identifier /// [DataMember] - public int? StateProvinceId { get; set; } - + public int? StateProvinceId { get; set; } + /// /// Gets or sets the city /// - [DataMember] - public string City { get; set; } + [DataMember] + public string City { get; set; } /// /// Gets or sets the address 1 /// [DataMember] - public string Address1 { get; set; } + public string Address1 { get; set; } /// /// Gets or sets the address 2 /// [DataMember] - public string Address2 { get; set; } + public string Address2 { get; set; } /// /// Gets or sets the zip/postal code /// [DataMember] - public string ZipPostalCode { get; set; } + public string ZipPostalCode { get; set; } /// /// Gets or sets the phone number /// [DataMember] - public string PhoneNumber { get; set; } + public string PhoneNumber { get; set; } /// /// Gets or sets the fax number /// [DataMember] - public string FaxNumber { get; set; } + public string FaxNumber { get; set; } /// /// Gets or sets the date and time of instance creation /// [DataMember] - public DateTime CreatedOnUtc { get; set; } - + public DateTime CreatedOnUtc { get; set; } + /// /// Gets or sets the country /// - [DataMember] - public virtual Country Country { get; set; } + [DataMember] + public virtual Country Country { get; set; } /// /// Gets or sets the state/province /// [DataMember] - public virtual StateProvince StateProvince { get; set; } + public virtual StateProvince StateProvince { get; set; } public object Clone() { @@ -134,17 +134,11 @@ public object Clone() return addr; } - public static string DefaultAddressFormat - { - get - { - return @"{{ Salutation }} {{ Title }} {{ FirstName }} {{ LastName }} + public static string DefaultAddressFormat => @"{{ Salutation }} {{ Title }} {{ FirstName }} {{ LastName }} {{ Company }} {{ Street1 }} {{ Street2 }} {{ ZipCode }} {{ City }} {{ Country | Upcase }}"; - } - } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs index 6d39656c46..32eec46007 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs @@ -1,31 +1,26 @@ using SmartStore.Core.Configuration; -using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Common { - public class AddressSettings : BaseEntity, ISettings, ILocalizedEntity + public class AddressSettings : ISettings { - public AddressSettings() - { - ValidateEmailAddress = false; - SalutationEnabled = false; - TitleEnabled = false; - CompanyEnabled = true; - StreetAddressEnabled = true; - StreetAddressRequired = true; - StreetAddress2Enabled = true; - ZipPostalCodeEnabled = true; - ZipPostalCodeRequired = true; - CityEnabled = true; - CityRequired = true; - CountryEnabled = true; - CountryRequired = true; - StateProvinceEnabled = true; - StateProvinceRequired = false; - PhoneEnabled = true; - PhoneRequired = true; - FaxEnabled = true; - } + public AddressSettings() + { + CompanyEnabled = true; + StreetAddressEnabled = true; + StreetAddressRequired = true; + StreetAddress2Enabled = true; + ZipPostalCodeEnabled = true; + ZipPostalCodeRequired = true; + CityEnabled = true; + CityRequired = true; + CountryEnabled = true; + CountryRequired = true; + StateProvinceEnabled = true; + PhoneEnabled = true; + PhoneRequired = true; + FaxEnabled = true; + } /// /// Gets or sets a value indicating whether email address should be validated @@ -47,7 +42,7 @@ public AddressSettings() /// public bool TitleEnabled { get; set; } - /// + /// /// Gets or sets a value indicating whether 'Company' is enabled /// public bool CompanyEnabled { get; set; } @@ -97,25 +92,25 @@ public AddressSettings() /// public bool CountryEnabled { get; set; } - /// - /// Gets or sets a value indicating whether 'Country' is required - /// - public bool CountryRequired { get; set; } - - /// - /// Gets or sets a value indicating whether 'State / province' is enabled - /// - public bool StateProvinceEnabled { get; set; } - - /// - /// Gets or sets a value indicating whether 'State / province' is required - /// - public bool StateProvinceRequired { get; set; } - - /// - /// Gets or sets a value indicating whether 'Phone number' is enabled - /// - public bool PhoneEnabled { get; set; } + /// + /// Gets or sets a value indicating whether 'Country' is required + /// + public bool CountryRequired { get; set; } + + /// + /// Gets or sets a value indicating whether 'State / province' is enabled + /// + public bool StateProvinceEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether 'State / province' is required + /// + public bool StateProvinceRequired { get; set; } + + /// + /// Gets or sets a value indicating whether 'Phone number' is enabled + /// + public bool PhoneEnabled { get; set; } /// /// Gets or sets a value indicating whether 'Phone number' is required /// diff --git a/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs index 466c1bbe79..67c4aad894 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs @@ -5,13 +5,13 @@ namespace SmartStore.Core.Domain.Common { public class AdminAreaSettings : ISettings { - public AdminAreaSettings() - { - GridPageSize = 25; - DisplayProductPictures = true; - } - - public int GridPageSize { get; set; } + public AdminAreaSettings() + { + GridPageSize = 25; + DisplayProductPictures = true; + } + + public int GridPageSize { get; set; } public bool DisplayProductPictures { get; set; } } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs index 8496d98b92..eb4f050317 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs @@ -4,20 +4,9 @@ namespace SmartStore.Core.Domain.Common { public class CommonSettings : ISettings { - public CommonSettings() - { - UseStoredProceduresIfSupported = true; - AutoUpdateEnabled = true; - EntityPickerPageSize = 48; - MaxScheduleHistoryAgeInDays = 30; - MaxLogAgeInDays = 7; - MaxNumberOfScheduleHistoryEntries = 100; - MaxQueuedMessagesAgeInDays = 14; - } - - public bool UseSystemEmailForContactUsForm { get; set; } - - public bool UseStoredProceduresIfSupported { get; set; } + public bool UseSystemEmailForContactUsForm { get; set; } + + public bool UseStoredProceduresIfSupported { get; set; } = true; public bool HideAdvertisementsOnAdminArea { get; set; } @@ -26,31 +15,36 @@ public CommonSettings() /// public bool DisplayJavaScriptDisabledWarning { get; set; } - public bool AutoUpdateEnabled { get; set; } + public bool AutoUpdateEnabled { get; set; } = true; - /// - /// Gets or sets the page size for the entity picker - /// - public int EntityPickerPageSize { get; set; } + /// + /// Gets or sets the page size for the entity picker + /// + public int EntityPickerPageSize { get; set; } = 48; /// /// Gets or sets the maximum age of schedule history entries (in days). /// - public int MaxScheduleHistoryAgeInDays { get; set; } + public int MaxScheduleHistoryAgeInDays { get; set; } = 30; /// /// Gets or sets the maximum age of log entries (in days). /// - public int MaxLogAgeInDays { get; set; } + public int MaxLogAgeInDays { get; set; } = 7; /// /// Gets or sets the maximum number of schedule history entries per task. /// - public int MaxNumberOfScheduleHistoryEntries { get; set; } + public int MaxNumberOfScheduleHistoryEntries { get; set; } = 100; /// /// Gets or sets the maximum age of sent queued messages (in days). /// - public int MaxQueuedMessagesAgeInDays { get; set; } + public int MaxQueuedMessagesAgeInDays { get; set; } = 14; + + /// + /// Gets or sets the maximum registration age (in minutes) for automatic deletion of guests customers. + /// + public int MaxGuestsRegistrationAgeInMinutes { get; set; } = 1440; // 1 day (60 * 24). } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs index 9c6430e25a..1c46f410b2 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs @@ -70,11 +70,6 @@ public class CompanyInformationSettings : ISettings ///
public string Region { get; set; } - /// - /// Gets or sets the country that will be used - /// - public string CountryName { get; set; } - /// /// Gets or sets the state that will be used /// diff --git a/src/Libraries/SmartStore.Core/Domain/Common/GenericAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Common/GenericAttribute.cs index 931d8fbb51..4a8e6d5723 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/GenericAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/GenericAttribute.cs @@ -6,36 +6,36 @@ namespace SmartStore.Core.Domain.Common /// Represents a generic attribute ///
[DataContract] - public partial class GenericAttribute : BaseEntity + public partial class GenericAttribute : BaseEntity { /// /// Gets or sets the entity identifier /// [DataMember] - public int EntityId { get; set; } - + public int EntityId { get; set; } + /// /// Gets or sets the key group /// - [DataMember] - public string KeyGroup { get; set; } + [DataMember] + public string KeyGroup { get; set; } /// /// Gets or sets the key /// [DataMember, Index("IX_GenericAttribute_Key")] - public string Key { get; set; } + public string Key { get; set; } /// /// Gets or sets the value /// [DataMember] - public string Value { get; set; } + public string Value { get; set; } - /// - /// Gets or sets the store identifier - /// - [DataMember] - public int StoreId { get; set; } + /// + /// Gets or sets the store identifier + /// + [DataMember] + public int StoreId { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs index bddfb93963..c2abe7564a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs @@ -5,13 +5,13 @@ namespace SmartStore.Core.Domain.Common { public class PdfSettings : ISettings { - public PdfSettings() - { - Enabled = true; - RenderOrderNotes = true; - } - - /// + public PdfSettings() + { + Enabled = true; + RenderOrderNotes = true; + } + + /// /// Gets or sets a value indicating PDF is supported /// public bool Enabled { get; set; } @@ -31,14 +31,14 @@ public PdfSettings() /// public bool RenderOrderNotes { get; set; } - /// - /// Gets or sets a value indicating whether to attach the order PDF to 'Order Placed (customer)' email - /// - public bool AttachOrderPdfToOrderPlacedEmail { get; set; } + /// + /// Gets or sets a value indicating whether to attach the order PDF to 'Order Placed (customer)' email + /// + public bool AttachOrderPdfToOrderPlacedEmail { get; set; } - /// - /// Gets or sets a value indicating whether to attach the order PDF to 'Order Completed (customer)' email - /// - public bool AttachOrderPdfToOrderCompletedEmail { get; set; } + /// + /// Gets or sets a value indicating whether to attach the order PDF to 'Order Completed (customer)' email + /// + public bool AttachOrderPdfToOrderCompletedEmail { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs index 8acd3065db..13822d6f78 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/PerformanceSettings.cs @@ -1,34 +1,39 @@ -using System; -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Common { - public class PerformanceSettings : ISettings - { - /// - /// The number of entries in a single cache segment - /// when greedy loading is disabled. The larger the catalog, - /// the smaller this value should be. We recommend segment - /// size 500 for catalogs smaller than 100.000 items. - /// - public int CacheSegmentSize { get; set; } = 500; + public class PerformanceSettings : ISettings + { + /// + /// The number of entries in a single cache segment + /// when greedy loading is disabled. The larger the catalog, + /// the smaller this value should be. We recommend segment + /// size 500 for catalogs smaller than 100.000 items. + /// + public int CacheSegmentSize { get; set; } = 500; - /// - /// By default only instant search prefetches translations. - /// All other product listings work against the segmented cache. - /// In very large multilingual catalogs (> 500.000) setting this to true - /// can result in higher request performance as well as less - /// resource usage. - /// - public bool AlwaysPrefetchTranslations { get; set; } + /// + /// By default only instant search prefetches translations. + /// All other product listings work against the segmented cache. + /// In very large multilingual catalogs (> 500.000) setting this to true + /// can result in higher request performance as well as less + /// resource usage. + /// + public bool AlwaysPrefetchTranslations { get; set; } - /// - /// By default only instant search prefetches url slugs. - /// All other product listings work against the segmented cache. - /// In very large catalogs (> 500.000) setting this to true - /// can result in higher request performance as well as less - /// resource usage. - /// - public bool AlwaysPrefetchUrlSlugs { get; set; } - } + /// + /// By default only instant search prefetches url slugs. + /// All other product listings work against the segmented cache. + /// In very large catalogs (> 500.000) setting this to true + /// can result in higher request performance as well as less + /// resource usage. + /// + public bool AlwaysPrefetchUrlSlugs { get; set; } + + /// + /// Maximum number of attribute combinations to be loaded and parsed + /// to make them unavailable for selection on the product detail page. + /// + public int MaxUnavailableAttributeCombinations { get; set; } = 10000; + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs index 1a45382602..773b2c914e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs @@ -4,6 +4,16 @@ namespace SmartStore.Core.Domain.Common { public class SocialSettings : ISettings { + /// + /// Gets or sets facebook app id + /// + public string FacebookAppId { get; set; } = null; + + /// + /// Gets or sets twitter account site name + /// + public string TwitterSite { get; set; } = null; + /// /// Gets or sets the value that determines whether social links should be show in the footer /// diff --git a/src/Libraries/SmartStore.Core/Domain/Configuration/Setting.cs b/src/Libraries/SmartStore.Core/Domain/Configuration/Setting.cs index 0c981c906a..1ae9d1e412 100644 --- a/src/Libraries/SmartStore.Core/Domain/Configuration/Setting.cs +++ b/src/Libraries/SmartStore.Core/Domain/Configuration/Setting.cs @@ -6,34 +6,34 @@ namespace SmartStore.Core.Domain.Configuration /// Represents a setting ///
[DataContract] - [DebuggerDisplay("{Name}: {Value}")] - public partial class Setting : BaseEntity + [DebuggerDisplay("{Name}: {Value}")] + public partial class Setting : BaseEntity { public Setting() { } - public Setting(string name, string value, int storeId = 0) - { + public Setting(string name, string value, int storeId = 0) + { this.Name = name; this.Value = value; - this.StoreId = storeId; + this.StoreId = storeId; } - + /// /// Gets or sets the name /// - [DataMember] - public string Name { get; set; } + [DataMember] + public string Name { get; set; } /// /// Gets or sets the value /// [DataMember] - public string Value { get; set; } + public string Value { get; set; } - /// - /// Gets or sets the store for which this setting is valid. 0 is set when the setting is for all stores - /// - [DataMember] - public int StoreId { get; set; } + /// + /// Gets or sets the store for which this setting is valid. 0 is set when the setting is for all stores + /// + [DataMember] + public int StoreId { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs b/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs index a9eeb3e85a..c0582bce24 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs @@ -13,7 +13,7 @@ namespace SmartStore.Core.Domain.Customers /// Represents a customer /// [DataContract] - public partial class Customer : BaseEntity, ISoftDeletable + public partial class Customer : BaseEntity, ISoftDeletable { private ICollection _externalAuthenticationRecords; private ICollection _customerContent; @@ -21,15 +21,15 @@ public partial class Customer : BaseEntity, ISoftDeletable private ICollection _shoppingCartItems; private ICollection _orders; private ICollection _rewardPointsHistory; - private ICollection _walletHistory; - private ICollection _returnRequests; + private ICollection _walletHistory; + private ICollection _returnRequests; private ICollection
_addresses; private ICollection _forumTopics; private ICollection _forumPosts; - /// - /// Ctor - /// + /// + /// Ctor + /// public Customer() { this.CustomerGuid = Guid.NewGuid(); @@ -42,40 +42,40 @@ public Customer() [DataMember] public Guid CustomerGuid { get; set; } - /// - /// Gets or sets the username - /// + /// + /// Gets or sets the username + /// [DataMember] public string Username { get; set; } - /// - /// Gets or sets the email - /// + /// + /// Gets or sets the email + /// [DataMember] public string Email { get; set; } - /// - /// Gets or sets the password - /// + /// + /// Gets or sets the password + /// public string Password { get; set; } - /// - /// Gets or sets the password format - /// + /// + /// Gets or sets the password format + /// public int PasswordFormatId { get; set; } - /// - /// Gets or sets the password format - /// + /// + /// Gets or sets the password format + /// public PasswordFormat PasswordFormat { - get { return (PasswordFormat)PasswordFormatId; } - set { this.PasswordFormatId = (int)value; } + get => (PasswordFormat)PasswordFormatId; + set => this.PasswordFormatId = (int)value; } - /// - /// Gets or sets the password salt - /// + /// + /// Gets or sets the password salt + /// public string PasswordSalt { get; set; } /// @@ -94,82 +94,85 @@ public PasswordFormat PasswordFormat /// Gets or sets the affiliate identifier /// [DataMember] - public int AffiliateId { get; set; } - + public int AffiliateId { get; set; } + /// /// Gets or sets a value indicating whether the customer is active /// - [DataMember] - public bool Active { get; set; } + [DataMember] + public bool Active { get; set; } /// /// Gets or sets a value indicating whether the customer has been deleted /// [Index] + [Index("IX_Customer_Deleted_IsSystemAccount", 1)] public bool Deleted { get; set; } /// /// Gets or sets a value indicating whether the customer account is system /// [DataMember] - public bool IsSystemAccount { get; set; } + [Index] + [Index("IX_Customer_Deleted_IsSystemAccount", 2)] + public bool IsSystemAccount { get; set; } /// /// Gets or sets the customer system name /// [DataMember] - [Index] - public string SystemName { get; set; } + [Index] + public string SystemName { get; set; } /// /// Gets or sets the last IP address /// [DataMember, Index("IX_Customer_LastIpAddress")] - public string LastIpAddress { get; set; } + public string LastIpAddress { get; set; } /// /// Gets or sets the date and time of entity creation /// [DataMember, Index("IX_Customer_CreatedOn")] - public DateTime CreatedOnUtc { get; set; } + public DateTime CreatedOnUtc { get; set; } /// /// Gets or sets the date and time of last login /// [DataMember] - public DateTime? LastLoginDateUtc { get; set; } + public DateTime? LastLoginDateUtc { get; set; } /// /// Gets or sets the date and time of last activity /// [DataMember, Index("IX_Customer_LastActivity")] - public DateTime LastActivityDateUtc { get; set; } + public DateTime LastActivityDateUtc { get; set; } - /// - /// For future use - /// - public string Salutation { get; set; } + /// + /// For future use + /// + public string Salutation { get; set; } - [DataMember] - public string Title { get; set; } + [DataMember] + public string Title { get; set; } - [DataMember] - public string FirstName { get; set; } + [DataMember] + public string FirstName { get; set; } - [DataMember] - public string LastName { get; set; } + [DataMember] + public string LastName { get; set; } - [DataMember, Index("IX_Customer_FullName")] - public string FullName { get; set; } + [DataMember, Index("IX_Customer_FullName")] + public string FullName { get; set; } - [DataMember, Index("IX_Customer_Company")] - public string Company { get; set; } + [DataMember, Index("IX_Customer_Company")] + public string Company { get; set; } - [DataMember, Index("IX_Customer_CustomerNumber")] - public string CustomerNumber { get; set; } + [DataMember, Index("IX_Customer_CustomerNumber")] + public string CustomerNumber { get; set; } - [DataMember, Index("IX_Customer_BirthDate")] - public DateTime? BirthDate { get; set; } + [DataMember, Index("IX_Customer_BirthDate")] + public DateTime? BirthDate { get; set; } [DataMember] public string Gender { get; set; } @@ -199,8 +202,8 @@ public PasswordFormat PasswordFormat /// public virtual ICollection ExternalAuthenticationRecords { - get { return _externalAuthenticationRecords ?? (_externalAuthenticationRecords = new HashSet()); } - protected set { _externalAuthenticationRecords = value; } + get => _externalAuthenticationRecords ?? (_externalAuthenticationRecords = new HashSet()); + protected set => _externalAuthenticationRecords = value; } /// @@ -208,8 +211,8 @@ public virtual ICollection ExternalAuthenticationR /// public virtual ICollection CustomerContent { - get { return _customerContent ?? (_customerContent = new HashSet()); } - protected set { _customerContent = value; } + get => _customerContent ?? (_customerContent = new HashSet()); + protected set => _customerContent = value; } /// @@ -218,8 +221,8 @@ public virtual ICollection CustomerContent [DataMember] public virtual ICollection CustomerRoleMappings { - get { return _customerRoleMappings ?? (_customerRoleMappings = new HashSet()); } - protected set { _customerRoleMappings = value; } + get => _customerRoleMappings ?? (_customerRoleMappings = new HashSet()); + protected set => _customerRoleMappings = value; } /// @@ -227,74 +230,69 @@ public virtual ICollection CustomerRoleMappings /// public virtual ICollection ShoppingCartItems { - get { return _shoppingCartItems ?? (_shoppingCartItems = new HashSet()); } - protected set { _shoppingCartItems = value; } + get => _shoppingCartItems ?? (_shoppingCartItems = new HashSet()); + protected set => _shoppingCartItems = value; } /// /// Gets or sets orders /// [DataMember] - public virtual ICollection Orders + public virtual ICollection Orders { - get { return _orders ?? (_orders = new HashSet()); } - protected internal set { _orders = value; } + get => _orders ?? (_orders = new HashSet()); + protected internal set => _orders = value; } /// /// Gets or sets reward points history /// + [DataMember] public virtual ICollection RewardPointsHistory { - get { return _rewardPointsHistory ?? (_rewardPointsHistory = new HashSet()); } - protected set { _rewardPointsHistory = value; } + get => _rewardPointsHistory ?? (_rewardPointsHistory = new HashSet()); + protected set => _rewardPointsHistory = value; } - /// - /// Gets or sets the wallet history. - /// - public virtual ICollection WalletHistory - { - get - { - return _walletHistory ?? (_walletHistory = new HashSet()); - } - protected set - { - _walletHistory = value; - } - } + /// + /// Gets or sets the wallet history. + /// + public virtual ICollection WalletHistory + { + get => _walletHistory ?? (_walletHistory = new HashSet()); + protected set => _walletHistory = value; + } /// /// Gets or sets return request of this customer /// [DataMember] - public virtual ICollection ReturnRequests + public virtual ICollection ReturnRequests { - get { return _returnRequests ?? (_returnRequests = new HashSet()); } - protected set { _returnRequests = value; } + get => _returnRequests ?? (_returnRequests = new HashSet()); + protected set => _returnRequests = value; } - + /// /// Default billing address /// - [DataMember] - public virtual Address BillingAddress { get; set; } + [DataMember] + public virtual Address BillingAddress { get; set; } /// /// Default shipping address /// [DataMember] - public virtual Address ShippingAddress { get; set; } + public virtual Address ShippingAddress { get; set; } /// /// Gets or sets customer addresses /// [DataMember] - public virtual ICollection
Addresses + public virtual ICollection
Addresses { - get { return _addresses ?? (_addresses = new HashSet
()); } - protected set { _addresses = value; } + get => _addresses ?? (_addresses = new HashSet
()); + protected set => _addresses = value; } /// @@ -302,8 +300,8 @@ public virtual ICollection
Addresses ///
public virtual ICollection ForumTopics { - get { return _forumTopics ?? (_forumTopics = new HashSet()); } - protected set { _forumTopics = value; } + get => _forumTopics ?? (_forumTopics = new HashSet()); + protected set => _forumTopics = value; } /// @@ -311,24 +309,24 @@ public virtual ICollection ForumTopics /// public virtual ICollection ForumPosts { - get { return _forumPosts ?? (_forumPosts = new HashSet()); } - protected set { _forumPosts = value; } + get => _forumPosts ?? (_forumPosts = new HashSet()); + protected set => _forumPosts = value; } - #endregion + #endregion - #region Utils + #region Utils - /// - /// Gets a string identifier for the customer's roles by joining all role ids - /// - /// true ignores all inactive roles - /// The identifier - public string GetRolesIdent(bool onlyActiveCustomerRoles = true) - { + /// + /// Gets a string identifier for the customer's roles by joining all role ids + /// + /// true ignores all inactive roles + /// The identifier + public string GetRolesIdent(bool onlyActiveCustomerRoles = true) + { return string.Join(",", GetRoleIds(onlyActiveCustomerRoles)); - } + } /// /// Get identifiers of assigned customer roles. @@ -344,50 +342,50 @@ public int[] GetRoleIds(bool onlyActiveCustomerRoles = true) .ToArray(); } - public virtual void RemoveAddress(Address address) - { - if (this.Addresses.Contains(address)) - { - if (this.BillingAddress == address) this.BillingAddress = null; - if (this.ShippingAddress == address) this.ShippingAddress = null; - - this.Addresses.Remove(address); - } - } - - public void AddRewardPointsHistoryEntry( - int points, - string message = "", - Order usedWithOrder = null, - decimal usedAmount = 0M) - { - int newPointsBalance = this.GetRewardPointsBalance() + points; - - var rewardPointsHistory = new RewardPointsHistory() - { - Customer = this, - UsedWithOrder = usedWithOrder, - Points = points, - PointsBalance = newPointsBalance, - UsedAmount = usedAmount, - Message = message, - CreatedOnUtc = DateTime.UtcNow - }; - - this.RewardPointsHistory.Add(rewardPointsHistory); - } - - /// - /// Gets reward points balance - /// - public int GetRewardPointsBalance() - { - int result = 0; - if (this.RewardPointsHistory.Count > 0) - result = this.RewardPointsHistory.OrderByDescending(rph => rph.CreatedOnUtc).ThenByDescending(rph => rph.Id).FirstOrDefault().PointsBalance; - return result; - } - - #endregion - } + public virtual void RemoveAddress(Address address) + { + if (this.Addresses.Contains(address)) + { + if (this.BillingAddress == address) this.BillingAddress = null; + if (this.ShippingAddress == address) this.ShippingAddress = null; + + this.Addresses.Remove(address); + } + } + + public void AddRewardPointsHistoryEntry( + int points, + string message = "", + Order usedWithOrder = null, + decimal usedAmount = 0M) + { + int newPointsBalance = this.GetRewardPointsBalance() + points; + + var rewardPointsHistory = new RewardPointsHistory() + { + Customer = this, + UsedWithOrder = usedWithOrder, + Points = points, + PointsBalance = newPointsBalance, + UsedAmount = usedAmount, + Message = message, + CreatedOnUtc = DateTime.UtcNow + }; + + this.RewardPointsHistory.Add(rewardPointsHistory); + } + + /// + /// Gets reward points balance + /// + public int GetRewardPointsBalance() + { + int result = 0; + if (this.RewardPointsHistory.Count > 0) + result = this.RewardPointsHistory.OrderByDescending(rph => rph.CreatedOnUtc).ThenByDescending(rph => rph.Id).FirstOrDefault().PointsBalance; + return result; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerContent.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerContent.cs index 1041f57d17..c3640a7a8c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerContent.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerContent.cs @@ -3,46 +3,46 @@ namespace SmartStore.Core.Domain.Customers { - /// - /// Represents a customer generated content - /// - [DataContract] - public partial class CustomerContent : BaseEntity, IAuditable - { - /// - /// Gets or sets the customer identifier - /// - [DataMember] - public int CustomerId { get; set; } + /// + /// Represents a customer generated content + /// + [DataContract] + public partial class CustomerContent : BaseEntity, IAuditable + { + /// + /// Gets or sets the customer identifier + /// + [DataMember] + public int CustomerId { get; set; } - /// - /// Gets or sets the IP address - /// - [DataMember] - public string IpAddress { get; set; } + /// + /// Gets or sets the IP address + /// + [DataMember] + public string IpAddress { get; set; } - /// - /// Gets or sets a value indicating whether the content is approved - /// - [DataMember] - public bool IsApproved { get; set; } + /// + /// Gets or sets a value indicating whether the content is approved + /// + [DataMember] + public bool IsApproved { get; set; } - /// - /// Gets or sets the date and time of instance creation - /// - [DataMember] - public DateTime CreatedOnUtc { get; set; } + /// + /// Gets or sets the date and time of instance creation + /// + [DataMember] + public DateTime CreatedOnUtc { get; set; } - /// - /// Gets or sets the date and time of instance update - /// - [DataMember] - public DateTime UpdatedOnUtc { get; set; } + /// + /// Gets or sets the date and time of instance update + /// + [DataMember] + public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets or sets the customer - /// - [DataMember] - public virtual Customer Customer { get; set; } + /// + /// Gets or sets the customer + /// + [DataMember] + public virtual Customer Customer { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs index c10aa440f4..c505353e62 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs @@ -17,13 +17,13 @@ public enum CustomerNameFormat /// Show full names /// ShowFullNames = 3, - /// - /// Show first name - /// - ShowFirstName = 4, - /// - /// Show shorted name and city - /// - ShowNameAndCity = 5 + /// + /// Show first name + /// + ShowFirstName = 4, + /// + /// Show shorted name and city + /// + ShowNameAndCity = 5 } } diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerRole.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerRole.cs index e5e99a0ff9..f5f8e96436 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerRole.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerRole.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; using SmartStore.Core.Domain.Security; using SmartStore.Rules.Domain; @@ -42,27 +43,44 @@ public partial class CustomerRole : BaseEntity, IRulesContainer /// Gets or sets a value indicating whether the customer role is active ///
[DataMember] + [Index] public bool Active { get; set; } /// /// Gets or sets a value indicating whether the customer role is system /// [DataMember] + [Index] + [Index("IX_CustomerRole_SystemName_IsSystemRole", 2)] public bool IsSystemRole { get; set; } /// /// Gets or sets the customer role system name /// [DataMember] + [Index] + [Index("IX_CustomerRole_SystemName_IsSystemRole", 1)] public string SystemName { get; set; } + /// + /// Gets or sets a minimum order amount + /// + [DataMember] + public decimal? OrderTotalMinimum { get; set; } + + /// + /// Gets or sets a maximum order amount + /// + [DataMember] + public decimal? OrderTotalMaximum { get; set; } + /// /// Gets or sets the permission role mappings. /// public virtual ICollection PermissionRoleMappings { - get { return _permissionRoleMappings ?? (_permissionRoleMappings = new HashSet()); } - protected set { _permissionRoleMappings = value; } + get => _permissionRoleMappings ?? (_permissionRoleMappings = new HashSet()); + protected set => _permissionRoleMappings = value; } /// @@ -70,8 +88,8 @@ public virtual ICollection PermissionRoleMappings /// public virtual ICollection RuleSets { - get { return _ruleSets ?? (_ruleSets = new HashSet()); } - protected set { _ruleSets = value; } + get => _ruleSets ?? (_ruleSets = new HashSet()); + protected set => _ruleSets = value; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs index 20033f0c21..0e4c01cbd5 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs @@ -2,29 +2,29 @@ namespace SmartStore.Core.Domain.Customers { - public class CustomerSettings : ISettings + public class CustomerSettings : ISettings { - public CustomerSettings() - { - CustomerLoginType = CustomerLoginType.UsernameOrEmail; + public CustomerSettings() + { + CustomerLoginType = CustomerLoginType.UsernameOrEmail; CustomerNumberMethod = CustomerNumberMethod.Disabled; CustomerNumberVisibility = CustomerNumberVisibility.None; - DefaultPasswordFormat = PasswordFormat.Hashed; - HashedPasswordFormat = "SHA1"; - PasswordMinLength = 6; - UserRegistrationType = UserRegistrationType.Standard; - AvatarMaximumSizeBytes = 512000; - CustomerNameFormat = CustomerNameFormat.ShowFirstName; - CustomerNameFormatMaxLength = 64; - GenderEnabled = true; - DateOfBirthEnabled = true; - CompanyEnabled = true; - NewsletterEnabled = true; - OnlineCustomerMinutes = 20; - StoreLastVisitedPage = true; - FirstNameRequired = false; - LastNameRequired = false; - } + DefaultPasswordFormat = PasswordFormat.Hashed; + HashedPasswordFormat = "SHA1"; + PasswordMinLength = 6; + UserRegistrationType = UserRegistrationType.Standard; + AvatarMaximumSizeBytes = 512000; + CustomerNameFormat = CustomerNameFormat.ShowFirstName; + CustomerNameFormatMaxLength = 64; + GenderEnabled = true; + DateOfBirthEnabled = true; + CompanyEnabled = true; + NewsletterEnabled = true; + OnlineCustomerMinutes = 20; + StoreLastVisitedPage = true; + FirstNameRequired = false; + LastNameRequired = false; + } /// /// Gets or sets a value indicating the customer login type @@ -62,10 +62,25 @@ public CustomerSettings() public string HashedPasswordFormat { get; set; } /// - /// Gets or sets a minimum password length + /// Gets or sets a minimum password length. /// public int PasswordMinLength { get; set; } + /// + /// Specifies the minimum number of digits for a password. + /// + public int MinDigitsInPassword { get; set; } + + /// + /// Specifies the minimum number of special characters for a password (not numeric and not alphabetic). + /// + public int MinSpecialCharsInPassword { get; set; } + + /// + /// Specifies the minimum number of uppercase characters for a password. + /// + public int MinUppercaseCharsInPassword { get; set; } + /// /// User registration type /// @@ -100,7 +115,7 @@ public CustomerSettings() /// Gets or sets a value indicating whether 'New customer' notification message should be sent to a store owner /// public bool NotifyNewCustomerRegistration { get; set; } - + /// /// Gets or sets a value indicating whether to hide 'Downloable products' tab on 'My account' page /// @@ -121,10 +136,10 @@ public CustomerSettings() /// public CustomerNameFormat CustomerNameFormat { get; set; } - /// - /// Gets or sets a value indicating the maximum length of a formatted customer name - /// - public int CustomerNameFormatMaxLength { get; set; } + /// + /// Gets or sets a value indicating the maximum length of a formatted customer name + /// + public int CustomerNameFormatMaxLength { get; set; } /// /// Gets or sets a value indicating whether 'Newsletter' form field is enabled @@ -145,7 +160,7 @@ public CustomerSettings() /// Gets or sets a value indicating we should store last visited page URL for each customer /// public bool StoreLastVisitedPage { get; set; } - + #region Form fields /// @@ -158,27 +173,27 @@ public CustomerSettings() /// public bool TitleEnabled { get; set; } - /// - /// Gets or sets a value indicating whether 'FirstName' is required - /// - public bool FirstNameRequired { get; set; } + /// + /// Gets or sets a value indicating whether 'FirstName' is required + /// + public bool FirstNameRequired { get; set; } - /// - /// Gets or sets a value indicating whether 'LastName' is required - /// - public bool LastNameRequired { get; set; } + /// + /// Gets or sets a value indicating whether 'LastName' is required + /// + public bool LastNameRequired { get; set; } - /// - /// Gets or sets a value indicating whether 'Date of Birth' is enabled - /// - public bool DateOfBirthEnabled { get; set; } + /// + /// Gets or sets a value indicating whether 'Date of Birth' is enabled + /// + public bool DateOfBirthEnabled { get; set; } /// /// Gets or sets a value indicating whether 'Company' is enabled /// public bool CompanyEnabled { get; set; } - /// + /// /// Gets or sets a value indicating whether 'Company' is required /// public bool CompanyRequired { get; set; } @@ -187,7 +202,7 @@ public CustomerSettings() /// Gets or sets a value indicating whether 'Street address' is enabled /// public bool StreetAddressEnabled { get; set; } - + /// /// Gets or sets a value indicating whether 'Street address' is required /// @@ -233,6 +248,11 @@ public CustomerSettings() /// public bool StateProvinceEnabled { get; set; } + /// + /// Gets or sets a value indicating whether 'StateProvince' is required + /// + public bool StateProvinceRequired { get; set; } + /// /// Gets or sets a value indicating whether 'Phone number' is enabled /// @@ -255,9 +275,9 @@ public CustomerSettings() #endregion - /// - /// Identifier of a customer role that new registered customers will be assigned to - /// - public int RegisterCustomerRoleId { get; set; } - } + /// + /// Identifier of a customer role that new registered customers will be assigned to + /// + public int RegisterCustomerRoleId { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationRecord.cs b/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationRecord.cs index 2a17563b33..d64523cb89 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationRecord.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationRecord.cs @@ -39,7 +39,7 @@ public partial class ExternalAuthenticationRecord : BaseEntity /// Gets or sets the provider /// public string ProviderSystemName { get; set; } - + /// /// Gets or sets the customer /// diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationSettings.cs index dc1b56b566..58aef070ee 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/ExternalAuthenticationSettings.cs @@ -9,7 +9,7 @@ public ExternalAuthenticationSettings() { ActiveAuthenticationMethodSystemNames = new List(); } - + public bool AutoRegisterEnabled { get; set; } /// /// Gets or sets an system names of active payment methods diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs b/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs index 4b35797ec7..32a36b8ade 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs @@ -1,6 +1,6 @@  -namespace SmartStore.Core.Domain.Customers -{ +namespace SmartStore.Core.Domain.Customers +{ public enum PasswordFormat { Clear = 0, diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/PrivacySettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/PrivacySettings.cs index f88ec44f6a..111f52de58 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/PrivacySettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/PrivacySettings.cs @@ -1,42 +1,47 @@ using SmartStore.Core.Configuration; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Orders; namespace SmartStore.Core.Domain.Customers { - public class PrivacySettings : BaseEntity, ISettings, ILocalizedEntity - { - public PrivacySettings() - { - EnableCookieConsent = true; - StoreLastIpAddress = false; - DisplayGdprConsentOnForms = true; - FullNameOnContactUsRequired = false; - } - - /// - /// Specifies whether cookie hint and consent will be displayed to customers in the frontent - /// - public bool EnableCookieConsent { get; set; } - - /// - /// Gets or sets a value indicating whether to store last IP address for each customer - /// - public bool StoreLastIpAddress { get; set; } - - /// - /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms - /// - public bool DisplayGdprConsentOnForms { get; set; } - - /// - /// Gets or sets a value indicating whether the full name field is required on contact us requests - /// - public bool FullNameOnContactUsRequired { get; set; } - - /// - /// Gets or sets a value indicating whether the full name field is required on product requests - /// - public bool FullNameOnProductRequestRequired { get; set; } - } + public class PrivacySettings : ISettings + { + /// + /// Specifies whether cookie hint and consent will be displayed to customers in the frontent + /// + public bool EnableCookieConsent { get; set; } = true; + + /// + /// Specifies whether the cookie dialog will be display in a modal dialog + /// + public bool ModalCookieConsent { get; set; } = true; + + /// + /// Defines the global SameSiteMode for cookies. + /// + public SameSiteType SameSiteMode { get; set; } = SameSiteType.Lax; + + /// + /// Gets or sets a value indicating whether to store last IP address for each customer + /// + public bool StoreLastIpAddress { get; set; } + + /// + /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms + /// + public bool DisplayGdprConsentOnForms { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the full name field is required on contact us requests + /// + public bool FullNameOnContactUsRequired { get; set; } + + /// + /// Gets or sets a value indicating whether the full name field is required on product requests + /// + public bool FullNameOnProductRequestRequired { get; set; } + + /// + /// Gets or sets cookie infos (JSON serialized) + /// + public string CookieInfos { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsHistory.cs b/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsHistory.cs index a0fdc20f1d..f4ce336430 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsHistory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsHistory.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; using SmartStore.Core.Domain.Orders; namespace SmartStore.Core.Domain.Customers @@ -6,46 +7,55 @@ namespace SmartStore.Core.Domain.Customers /// /// Represents a reward point history entry /// + [DataContract] public partial class RewardPointsHistory : BaseEntity { /// /// Gets or sets the customer identifier /// + [DataMember] public int CustomerId { get; set; } /// /// Gets or sets the points redeemed/added /// + [DataMember] public int Points { get; set; } /// /// Gets or sets the points balance /// + [DataMember] public int PointsBalance { get; set; } /// /// Gets or sets the used amount /// + [DataMember] public decimal UsedAmount { get; set; } /// /// Gets or sets the message /// + [DataMember] public string Message { get; set; } /// /// Gets or sets the date and time of instance creation /// + [DataMember] public DateTime CreatedOnUtc { get; set; } /// /// Gets or sets the order for which points were redeemed as a payment /// + [DataMember] public virtual Order UsedWithOrder { get; set; } /// /// Gets or sets the customer /// + [DataMember] public virtual Customer Customer { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs index f0e99e9502..543a1ffd86 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs @@ -5,16 +5,16 @@ namespace SmartStore.Core.Domain.Customers { public class RewardPointsSettings : ISettings { - public RewardPointsSettings() - { - ExchangeRate = 1; - PointsForPurchases_Amount = 10; - PointsForPurchases_Points = 1; - PointsForPurchases_Awarded = OrderStatus.Complete; - PointsForPurchases_Canceled = OrderStatus.Cancelled; - } - - /// + public RewardPointsSettings() + { + ExchangeRate = 1; + PointsForPurchases_Amount = 10; + PointsForPurchases_Points = 1; + PointsForPurchases_Awarded = OrderStatus.Complete; + PointsForPurchases_Canceled = OrderStatus.Cancelled; + } + + /// /// Gets or sets a value indicating whether Reward Points Program is enabled /// public bool Enabled { get; set; } @@ -24,20 +24,20 @@ public RewardPointsSettings() /// public decimal ExchangeRate { get; set; } - /// - /// Gets or sets a value whether to round down reward points - /// - public bool RoundDownRewardPoints { get; set; } + /// + /// Gets or sets a value whether to round down reward points + /// + public bool RoundDownRewardPoints { get; set; } - /// - /// Gets or sets a number of points awarded for registration - /// - public int PointsForRegistration { get; set; } + /// + /// Gets or sets a number of points awarded for registration + /// + public int PointsForRegistration { get; set; } - /// - /// Gets or sets a number of points awarded for a product review - /// - public int PointsForProductReview { get; set; } + /// + /// Gets or sets a number of points awarded for a product review + /// + public int PointsForProductReview { get; set; } /// /// Gets or sets a number of points awarded for purchases (amount in primary store currency) @@ -48,7 +48,7 @@ public RewardPointsSettings() /// Gets or sets a number of points awarded for purchases /// public int PointsForPurchases_Points { get; set; } - + /// /// Points are awarded when the order status is /// diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/SameSiteType.cs b/src/Libraries/SmartStore.Core/Domain/Customers/SameSiteType.cs new file mode 100644 index 0000000000..de98665704 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Customers/SameSiteType.cs @@ -0,0 +1,28 @@ +namespace SmartStore.Core.Domain.Customers +{ + /// + /// Represents the SameSiteMode for Cookies + /// + public enum SameSiteType + { + /// + /// No SameSite header should be included on requests. + /// + Unspecified = -1, + + /// + /// No mode specified. This setting must be used if the shop runs also in an iframe (SSL must be configured correctly for this). + /// + None = 0, + + /// + /// Cookies are sent with requests on the same website and with cross-site navigation at the highest level. + /// + Lax = 1, + + /// + /// If the value is "Strict" or invalid, cookies will only be sent to requests on the same website. + /// + Strict = 2 + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs index 2e19a06b50..4a6b1e4c46 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs @@ -4,44 +4,44 @@ namespace SmartStore.Core.Domain.Customers public static partial class SystemCustomerAttributeNames { // Form fields - public static string StreetAddress { get { return "StreetAddress"; } } - public static string StreetAddress2 { get { return "StreetAddress2"; } } - public static string ZipPostalCode { get { return "ZipPostalCode"; } } - public static string City { get { return "City"; } } - public static string StateProvinceId { get { return "StateProvinceId"; } } - public static string CountryId { get { return "CountryId"; } } - public static string Phone { get { return "Phone"; } } - public static string Fax { get { return "Fax"; } } - public static string VatNumber { get { return "VatNumber"; } } + public static string StreetAddress => "StreetAddress"; + public static string StreetAddress2 => "StreetAddress2"; + public static string ZipPostalCode => "ZipPostalCode"; + public static string City => "City"; + public static string StateProvinceId => "StateProvinceId"; + public static string CountryId => "CountryId"; + public static string Phone => "Phone"; + public static string Fax => "Fax"; + public static string VatNumber => "VatNumber"; // Other attributes - public static string DiscountCouponCode { get { return "DiscountCouponCode"; } } - public static string GiftCardCouponCodes { get { return "GiftCardCouponCodes"; } } - public static string CheckoutAttributes { get { return "CheckoutAttributes"; } } - public static string AvatarPictureId { get { return "AvatarPictureId"; } } - public static string AvatarColor { get { return "AvatarColor"; } } - public static string ForumPostCount { get { return "ForumPostCount"; } } - public static string Signature { get { return "Signature"; } } - public static string PasswordRecoveryToken { get { return "PasswordRecoveryToken"; } } - public static string AccountActivationToken { get { return "AccountActivationToken"; } } - public static string LastVisitedPage { get { return "LastVisitedPage"; } } - public static string ImpersonatedCustomerId { get { return "ImpersonatedCustomerId"; } } - public static string AdminAreaStoreScopeConfiguration { get { return "AdminAreaStoreScopeConfiguration"; } } - public static string MostRecentlyUsedCategories { get { return "MostRecentlyUsedCategories"; } } - public static string MostRecentlyUsedManufacturers { get { return "MostRecentlyUsedManufacturers"; } } - public static string WalletEnabled { get { return "WalletEnabled"; } } - public static string HasConsentedToGdpr { get { return "HasConsentedToGdpr"; } } + public static string DiscountCouponCode => "DiscountCouponCode"; + public static string GiftCardCouponCodes => "GiftCardCouponCodes"; + public static string CheckoutAttributes => "CheckoutAttributes"; + public static string AvatarPictureId => "AvatarPictureId"; + public static string AvatarColor => "AvatarColor"; + public static string ForumPostCount => "ForumPostCount"; + public static string Signature => "Signature"; + public static string PasswordRecoveryToken => "PasswordRecoveryToken"; + public static string AccountActivationToken => "AccountActivationToken"; + public static string LastVisitedPage => "LastVisitedPage"; + public static string ImpersonatedCustomerId => "ImpersonatedCustomerId"; + public static string AdminAreaStoreScopeConfiguration => "AdminAreaStoreScopeConfiguration"; + public static string MostRecentlyUsedCategories => "MostRecentlyUsedCategories"; + public static string MostRecentlyUsedManufacturers => "MostRecentlyUsedManufacturers"; + public static string WalletEnabled => "WalletEnabled"; + public static string HasConsentedToGdpr => "HasConsentedToGdpr"; // Depends on store - public static string CurrencyId { get { return "CurrencyId"; } } - public static string LanguageId { get { return "LanguageId"; } } - public static string SelectedPaymentMethod { get { return "SelectedPaymentMethod"; } } - public static string SelectedShippingOption { get { return "SelectedShippingOption"; } } - public static string OfferedShippingOptions { get { return "OfferedShippingOptions"; } } - public static string LastContinueShoppingPage { get { return "LastContinueShoppingPage"; } } - public static string NotifiedAboutNewPrivateMessages { get { return "NotifiedAboutNewPrivateMessages"; } } - public static string WorkingThemeName { get { return "WorkingThemeName"; } } - public static string UseRewardPointsDuringCheckout { get { return "UseRewardPointsDuringCheckout"; } } - public static string UseCreditBalanceDuringCheckout { get { return "UseCreditBalanceDuringCheckout"; } } - } + public static string CurrencyId => "CurrencyId"; + public static string LanguageId => "LanguageId"; + public static string SelectedPaymentMethod => "SelectedPaymentMethod"; + public static string SelectedShippingOption => "SelectedShippingOption"; + public static string OfferedShippingOptions => "OfferedShippingOptions"; + public static string LastContinueShoppingPage => "LastContinueShoppingPage"; + public static string NotifiedAboutNewPrivateMessages => "NotifiedAboutNewPrivateMessages"; + public static string WorkingThemeName => "WorkingThemeName"; + public static string UseRewardPointsDuringCheckout => "UseRewardPointsDuringCheckout"; + public static string UseCreditBalanceDuringCheckout => "UseCreditBalanceDuringCheckout"; + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerNames.cs b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerNames.cs index 48ed1b0dfd..5e52629940 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerNames.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerNames.cs @@ -3,8 +3,8 @@ namespace SmartStore.Core.Domain.Customers { public static partial class SystemCustomerNames { - public static string SearchEngine { get { return "SearchEngine"; } } - public static string BackgroundTask { get { return "BackgroundTask"; } } - public static string PdfConverter { get { return "PdfConverter"; } } + public static string SearchEngine => "SearchEngine"; + public static string BackgroundTask => "BackgroundTask"; + public static string PdfConverter => "PdfConverter"; } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerRoleNames.cs b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerRoleNames.cs index 214fd18191..c70a4b84ab 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerRoleNames.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerRoleNames.cs @@ -3,14 +3,14 @@ namespace SmartStore.Core.Domain.Customers { public static partial class SystemCustomerRoleNames { - public static string SuperAdministrators { get { return "SuperAdmins"; } } + public static string SuperAdministrators => "SuperAdmins"; - public static string Administrators { get { return "Administrators"; } } - - public static string ForumModerators { get { return "ForumModerators"; } } + public static string Administrators => "Administrators"; - public static string Registered { get { return "Registered"; } } + public static string ForumModerators => "ForumModerators"; - public static string Guests { get { return "Guests"; } } + public static string Registered => "Registered"; + + public static string Guests => "Guests"; } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/WalletHistory.cs b/src/Libraries/SmartStore.Core/Domain/Customers/WalletHistory.cs index d85437c4f7..4befa9f0d9 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/WalletHistory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/WalletHistory.cs @@ -4,71 +4,71 @@ namespace SmartStore.Core.Domain.Customers { - /// - /// Represents a digital wallet history entry. - /// - public class WalletHistory : BaseEntity - { - /// - /// Gets or sets the store identifier. Should not be zero. - /// - [Index("IX_StoreId_CreatedOn", 0)] - public int StoreId { get; set; } + /// + /// Represents a digital wallet history entry. + /// + public class WalletHistory : BaseEntity + { + /// + /// Gets or sets the store identifier. Should not be zero. + /// + [Index("IX_StoreId_CreatedOn", 0)] + public int StoreId { get; set; } - /// - /// Gets or sets the customer identifier. - /// - public int CustomerId { get; set; } + /// + /// Gets or sets the customer identifier. + /// + public int CustomerId { get; set; } - /// - /// Gets or sets the order identifier. - /// - public int? OrderId { get; set; } + /// + /// Gets or sets the order identifier. + /// + public int? OrderId { get; set; } - /// - /// Gets or sets the amount of the entry. - /// - public decimal Amount { get; set; } + /// + /// Gets or sets the amount of the entry. + /// + public decimal Amount { get; set; } - /// - /// Gets or sets the amount balance when the entry was created. - /// - public decimal AmountBalance { get; set; } + /// + /// Gets or sets the amount balance when the entry was created. + /// + public decimal AmountBalance { get; set; } - /// - /// Gets or sets the amount balance per store when the entry was created. - /// - public decimal AmountBalancePerStore { get; set; } + /// + /// Gets or sets the amount balance per store when the entry was created. + /// + public decimal AmountBalancePerStore { get; set; } - /// - /// Gets or sets the date ehen the entry was created (in UTC). - /// - [Index("IX_StoreId_CreatedOn", 1)] - public DateTime CreatedOnUtc { get; set; } + /// + /// Gets or sets the date ehen the entry was created (in UTC). + /// + [Index("IX_StoreId_CreatedOn", 1)] + public DateTime CreatedOnUtc { get; set; } - /// - /// Gets or sets the reason for posting this entry. - /// - public WalletPostingReason? Reason { get; set; } + /// + /// Gets or sets the reason for posting this entry. + /// + public WalletPostingReason? Reason { get; set; } - /// - /// Gets or sets the message. - /// - public string Message { get; set; } + /// + /// Gets or sets the message. + /// + public string Message { get; set; } - /// - /// Gets or sets the admin comment. - /// - public string AdminComment { get; set; } + /// + /// Gets or sets the admin comment. + /// + public string AdminComment { get; set; } - /// - /// Gets or sets the customer. - /// - public virtual Customer Customer { get; set; } + /// + /// Gets or sets the customer. + /// + public virtual Customer Customer { get; set; } - /// - /// Gets or sets the order for which the wallet entry was used. - /// - public virtual Order Order { get; set; } - } + /// + /// Gets or sets the order for which the wallet entry was used. + /// + public virtual Order Order { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/WalletPostingReason.cs b/src/Libraries/SmartStore.Core/Domain/Customers/WalletPostingReason.cs index 06a829dba8..489bdac561 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/WalletPostingReason.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/WalletPostingReason.cs @@ -1,38 +1,38 @@ namespace SmartStore.Core.Domain.Customers { - /// - /// Represents the reason for creating a wallet history entry. - /// - public enum WalletPostingReason - { - /// - /// Any administration reason. - /// - Admin = 0, + /// + /// Represents the reason for creating a wallet history entry. + /// + public enum WalletPostingReason + { + /// + /// Any administration reason. + /// + Admin = 0, - /// - /// The Customer has purchased goods which have been paid in part or in full by wallet. - /// - Purchase, + /// + /// The Customer has purchased goods which have been paid in part or in full by wallet. + /// + Purchase, - /// - /// The customer has bought wallet credits. - /// - Refill, + /// + /// The customer has bought wallet credits. + /// + Refill, - /// - /// The admin has refunded the used credit balance. - /// - Refund, + /// + /// The admin has refunded the used credit balance. + /// + Refund, - /// - /// The admin has refunded a part of the used credit balance. - /// - PartialRefund, + /// + /// The admin has refunded a part of the used credit balance. + /// + PartialRefund, - /// - /// The admin has debited the wallet, e.g. because the purchase of credit was cancelled. - /// - Debit - } + /// + /// The admin has debited the wallet, e.g. because the purchase of credit was cancelled. + /// + Debit + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs index 7a3ab1f8ae..7872dc0072 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs @@ -1,26 +1,26 @@ namespace SmartStore.Core.Domain.DataExchange { - public delegate void ProgressValueSetter(int value, int maximum, string message); + public delegate void ProgressValueSetter(int value, int maximum, string message); - /// - /// Data exchange abortion types - /// - public enum DataExchangeAbortion - { - /// - /// No abortion. Go on with processing. - /// - None = 0, + /// + /// Data exchange abortion types + /// + public enum DataExchangeAbortion + { + /// + /// No abortion. Go on with processing. + /// + None = 0, - /// - /// Break item processing but not the rest of the execution. Typically used for demo limitations. - /// - Soft, + /// + /// Break item processing but not the rest of the execution. Typically used for demo limitations. + /// + Soft, - /// - /// Break processing immediately - /// - Hard - } + /// + /// Break processing immediately + /// + Hard + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs index 4b0bc6d497..6546707890 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs @@ -2,27 +2,27 @@ namespace SmartStore.Core.Domain.DataExchange { - public class DataExchangeSettings : ISettings - { - public DataExchangeSettings() - { - MaxFileNameLength = 50; - ImageDownloadTimeout = 10; - } + public class DataExchangeSettings : ISettings + { + public DataExchangeSettings() + { + MaxFileNameLength = 50; + ImageDownloadTimeout = 10; + } - /// - /// The maximum length of file names (in characters) of files created by the export framework - /// - public int MaxFileNameLength { get; set; } + /// + /// The maximum length of file names (in characters) of files created by the export framework + /// + public int MaxFileNameLength { get; set; } - /// - /// Relative path to a folder with images to be imported - /// - public string ImageImportFolder { get; set; } + /// + /// Relative path to a folder with images to be imported + /// + public string ImageImportFolder { get; set; } - /// - /// The timeout for image download per entity in minutes - /// - public int ImageDownloadTimeout { get; set; } - } + /// + /// The timeout for image download per entity in minutes + /// + public int ImageDownloadTimeout { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs index b9e323c670..37df2f481a 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs @@ -3,139 +3,127 @@ namespace SmartStore.Core.Domain { - public class ExportDeployment : BaseEntity, ICloneable - { - /// - /// The profile identifier - /// - public int ProfileId { get; set; } - - /// - /// Name of the deployment - /// - public string Name { get; set; } - - /// - /// Whether the deployment is enabled - /// - public bool Enabled { get; set; } - - /// - /// XML with information about the last deployment result - /// - public string ResultInfo { get; set; } - - /// - /// The deployment type identifier - /// - public int DeploymentTypeId { get; set; } - - /// - /// The deployment type - /// - public ExportDeploymentType DeploymentType - { - get - { - return (ExportDeploymentType)DeploymentTypeId; - } - set - { - DeploymentTypeId = (int)value; - } - } - - public string Username { get; set; } - - public string Password { get; set; } - - /// - /// Deployment URL - /// - public string Url { get; set; } - - /// - /// The type identifier of how to transmit via HTTP - /// - public int HttpTransmissionTypeId { get; set; } - - /// - /// The type of how to transmit via HTTP - /// - public ExportHttpTransmissionType HttpTransmissionType - { - get - { - return (ExportHttpTransmissionType)HttpTransmissionTypeId; - } - set - { - HttpTransmissionTypeId = (int)value; - } - } - - /// - /// The file system path - /// - public string FileSystemPath { get; set; } - - /// - /// Path of a subfolder - /// - public string SubFolder { get; set; } - - /// - /// Multiple email addresses can be separated by commas - /// - public string EmailAddresses { get; set; } - - /// - /// Subject of the email - /// - public string EmailSubject { get; set; } - - /// - /// Identifier of the email account - /// - public int EmailAccountId { get; set; } - - /// - /// Whether to use FTP active or passive mode - /// - public bool PassiveMode { get; set; } - - /// - /// Whether to use SSL - /// - public bool UseSsl { get; set; } - - public virtual ExportProfile Profile { get; set; } - - public ExportDeployment Clone() - { - var deployment = new ExportDeployment - { - Name = this.Name, - Enabled = this.Enabled, - DeploymentTypeId = this.DeploymentTypeId, - Username = this.Username, - Password = this.Password, - Url = this.Url, - HttpTransmissionTypeId = this.HttpTransmissionTypeId, - FileSystemPath = this.FileSystemPath, - SubFolder = this.SubFolder, - EmailAddresses = this.EmailAddresses, - EmailSubject = this.EmailSubject, - EmailAccountId = this.EmailAccountId, - PassiveMode = this.PassiveMode, - UseSsl = this.UseSsl - }; - return deployment; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - } + public class ExportDeployment : BaseEntity, ICloneable + { + /// + /// The profile identifier + /// + public int ProfileId { get; set; } + + /// + /// Name of the deployment + /// + public string Name { get; set; } + + /// + /// Whether the deployment is enabled + /// + public bool Enabled { get; set; } + + /// + /// XML with information about the last deployment result + /// + public string ResultInfo { get; set; } + + /// + /// The deployment type identifier + /// + public int DeploymentTypeId { get; set; } + + /// + /// The deployment type + /// + public ExportDeploymentType DeploymentType + { + get => (ExportDeploymentType)DeploymentTypeId; + set => DeploymentTypeId = (int)value; + } + + public string Username { get; set; } + + public string Password { get; set; } + + /// + /// Deployment URL + /// + public string Url { get; set; } + + /// + /// The type identifier of how to transmit via HTTP + /// + public int HttpTransmissionTypeId { get; set; } + + /// + /// The type of how to transmit via HTTP + /// + public ExportHttpTransmissionType HttpTransmissionType + { + get => (ExportHttpTransmissionType)HttpTransmissionTypeId; + set => HttpTransmissionTypeId = (int)value; + } + + /// + /// The file system path + /// + public string FileSystemPath { get; set; } + + /// + /// Path of a subfolder + /// + public string SubFolder { get; set; } + + /// + /// Multiple email addresses can be separated by commas + /// + public string EmailAddresses { get; set; } + + /// + /// Subject of the email + /// + public string EmailSubject { get; set; } + + /// + /// Identifier of the email account + /// + public int EmailAccountId { get; set; } + + /// + /// Whether to use FTP active or passive mode + /// + public bool PassiveMode { get; set; } + + /// + /// Whether to use SSL + /// + public bool UseSsl { get; set; } + + public virtual ExportProfile Profile { get; set; } + + public ExportDeployment Clone() + { + var deployment = new ExportDeployment + { + Name = this.Name, + Enabled = this.Enabled, + DeploymentTypeId = this.DeploymentTypeId, + Username = this.Username, + Password = this.Password, + Url = this.Url, + HttpTransmissionTypeId = this.HttpTransmissionTypeId, + FileSystemPath = this.FileSystemPath, + SubFolder = this.SubFolder, + EmailAddresses = this.EmailAddresses, + EmailSubject = this.EmailSubject, + EmailAccountId = this.EmailAccountId, + PassiveMode = this.PassiveMode, + UseSsl = this.UseSsl + }; + return deployment; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs index 4bd1648beb..f11b52fff5 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -2,18 +2,18 @@ namespace SmartStore.Core.Domain.DataExchange { - /// - /// Supported entity types - /// - public enum ExportEntityType - { - Product = 0, - Category, - Manufacturer, - Customer, - Order, - NewsLetterSubscription, - ShoppingCartItem + /// + /// Supported entity types + /// + public enum ExportEntityType + { + Product = 0, + Category, + Manufacturer, + Customer, + Order, + NewsLetterSubscription, + ShoppingCartItem } /// @@ -30,138 +30,138 @@ public enum RelatedEntityType /// Supported deployment types /// public enum ExportDeploymentType - { - FileSystem = 0, - Email, - Http, - Ftp, - PublicFolder - } - - /// - /// Supported HTTP transmission types - /// - public enum ExportHttpTransmissionType - { - SimplePost = 0, - MultipartFormDataPost - } - - /// - /// Controls the merging of various data as product description - /// - public enum ExportDescriptionMerging - { - None = 0, - ShortDescriptionOrNameIfEmpty, - ShortDescription, - Description, - NameAndShortDescription, - NameAndDescription, - ManufacturerAndNameAndShortDescription, - ManufacturerAndNameAndDescription - } - - /// - /// Controls the merging of various data while exporting attribute combinations as products - /// - public enum ExportAttributeValueMerging - { - None = 0, - AppendAllValuesToName - } - - /// - /// Controls data processing and projection items supported by an export provider - /// - [Flags] - public enum ExportFeatures - { - None = 0, - - /// - /// Whether to automatically create a file based public deployment when an export profile is created - /// - CreatesInitialPublicDeployment = 1, - - /// - /// Whether to offer option to include\exclude grouped products - /// - CanOmitGroupedProducts = 1 << 2, - - /// - /// Whether to offer option to export attribute combinations as products - /// - CanProjectAttributeCombinations = 1 << 3, - - /// - /// Whether to offer further options to manipulate the product description - /// - CanProjectDescription = 1 << 4, - - /// - /// Whether to offer option to enter a brand fallback - /// - OffersBrandFallback = 1 << 5, - - /// - /// Whether to offer option to set a picture size and to get the URL of the main image - /// - CanIncludeMainPicture = 1 << 6, - - /// - /// Whether to use SKU as manufacturer part number if MPN is empty - /// - UsesSkuAsMpnFallback = 1 << 7, - - /// - /// Whether to offer option to enter a shipping time fallback - /// - OffersShippingTimeFallback = 1 << 8, - - /// - /// Whether to offer option to enter a shipping costs fallback and a free shipping threshold - /// - OffersShippingCostsFallback = 1 << 9, - - /// - /// Whether to get the calculated old product price - /// - UsesOldPrice = 1 << 10, - - /// - /// Whether to get the calculated special and regular price (ignoring special offers) - /// - UsesSpecialPrice = 1 << 11, - - /// - /// Whether to not automatically send a completion email - /// - CanOmitCompletionMail = 1 << 12, - - /// - /// Whether to provide additional data of attribute combinations - /// - UsesAttributeCombination = 1 << 13, - - /// - /// Whether to export attribute combinations as products including parent product. Only effective with CanProjectAttributeCombinations. - /// - UsesAttributeCombinationParent = 1 << 14, + { + FileSystem = 0, + Email, + Http, + Ftp, + PublicFolder + } + + /// + /// Supported HTTP transmission types + /// + public enum ExportHttpTransmissionType + { + SimplePost = 0, + MultipartFormDataPost + } + + /// + /// Controls the merging of various data as product description + /// + public enum ExportDescriptionMerging + { + None = 0, + ShortDescriptionOrNameIfEmpty, + ShortDescription, + Description, + NameAndShortDescription, + NameAndDescription, + ManufacturerAndNameAndShortDescription, + ManufacturerAndNameAndDescription + } + + /// + /// Controls the merging of various data while exporting attribute combinations as products + /// + public enum ExportAttributeValueMerging + { + None = 0, + AppendAllValuesToName + } + + /// + /// Controls data processing and projection items supported by an export provider + /// + [Flags] + public enum ExportFeatures + { + None = 0, + + /// + /// Whether to automatically create a file based public deployment when an export profile is created + /// + CreatesInitialPublicDeployment = 1, + + /// + /// Whether to offer option to include\exclude grouped products + /// + CanOmitGroupedProducts = 1 << 2, + + /// + /// Whether to offer option to export attribute combinations as products + /// + CanProjectAttributeCombinations = 1 << 3, + + /// + /// Whether to offer further options to manipulate the product description + /// + CanProjectDescription = 1 << 4, + + /// + /// Whether to offer option to enter a brand fallback + /// + OffersBrandFallback = 1 << 5, + + /// + /// Whether to offer option to set a picture size and to get the URL of the main image + /// + CanIncludeMainPicture = 1 << 6, + + /// + /// Whether to use SKU as manufacturer part number if MPN is empty + /// + UsesSkuAsMpnFallback = 1 << 7, + + /// + /// Whether to offer option to enter a shipping time fallback + /// + OffersShippingTimeFallback = 1 << 8, + + /// + /// Whether to offer option to enter a shipping costs fallback and a free shipping threshold + /// + OffersShippingCostsFallback = 1 << 9, + + /// + /// Whether to get the calculated old product price + /// + UsesOldPrice = 1 << 10, + + /// + /// Whether to get the calculated special and regular price (ignoring special offers) + /// + UsesSpecialPrice = 1 << 11, + + /// + /// Whether to not automatically send a completion email + /// + CanOmitCompletionMail = 1 << 12, + + /// + /// Whether to provide additional data of attribute combinations + /// + UsesAttributeCombination = 1 << 13, + + /// + /// Whether to export attribute combinations as products including parent product. Only effective with CanProjectAttributeCombinations. + /// + UsesAttributeCombinationParent = 1 << 14, /// /// Whether to provide extra data units for related data /// UsesRelatedDataUnits = 1 << 15 - } - - /// - /// Possible order status change after order exporting - /// - public enum ExportOrderStatusChange - { - None = 0, - Processing, - Complete - } + } + + /// + /// Possible order status change after order exporting + /// + public enum ExportOrderStatusChange + { + None = 0, + Processing, + Complete + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs index 970fcae7cb..2c80250e93 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs @@ -3,9 +3,9 @@ namespace SmartStore.Core.Domain.DataExchange { - [Serializable] - public class ExportFilter - { + [Serializable] + public class ExportFilter + { public ExportFilter() { Visibility = ProductVisibility.Full; @@ -16,87 +16,87 @@ public ExportFilter() /// public int StoreId { get; set; } - /// - /// Entity created from - /// - public DateTime? CreatedFrom { get; set; } + /// + /// Entity created from + /// + public DateTime? CreatedFrom { get; set; } - /// - /// Entity created to - /// - public DateTime? CreatedTo { get; set; } + /// + /// Entity created to + /// + public DateTime? CreatedTo { get; set; } - #region Product + #region Product - /// - /// Minimum product identifier - /// - public int? IdMinimum { get; set; } + /// + /// Minimum product identifier + /// + public int? IdMinimum { get; set; } - /// - /// Maximum product identifier - /// - public int? IdMaximum { get; set; } + /// + /// Maximum product identifier + /// + public int? IdMaximum { get; set; } - /// - /// Minimum price - /// - public decimal? PriceMinimum { get; set; } + /// + /// Minimum price + /// + public decimal? PriceMinimum { get; set; } - /// - /// Maximum price - /// - public decimal? PriceMaximum { get; set; } + /// + /// Maximum price + /// + public decimal? PriceMaximum { get; set; } - /// - /// Minimum product availability - /// - public int? AvailabilityMinimum { get; set; } + /// + /// Minimum product availability + /// + public int? AvailabilityMinimum { get; set; } - /// - /// Maximum product availability - /// - public int? AvailabilityMaximum { get; set; } + /// + /// Maximum product availability + /// + public int? AvailabilityMaximum { get; set; } - /// - /// A value indicating whether to load only published or non published products - /// - public bool? IsPublished { get; set; } + /// + /// A value indicating whether to load only published or non published products + /// + public bool? IsPublished { get; set; } - /// - /// Category identifiers - /// - public int[] CategoryIds { get; set; } + /// + /// Category identifiers + /// + public int[] CategoryIds { get; set; } - /// - /// A value indicating whether to load products without any catgory mapping - /// - public bool? WithoutCategories { get; set; } + /// + /// A value indicating whether to load products without any catgory mapping + /// + public bool? WithoutCategories { get; set; } - /// - /// Manufacturer identifier - /// - public int? ManufacturerId { get; set; } + /// + /// Manufacturer identifier + /// + public int? ManufacturerId { get; set; } - /// - /// A value indicating whether to load products without any manufacturer mapping - /// - public bool? WithoutManufacturers { get; set; } + /// + /// A value indicating whether to load products without any manufacturer mapping + /// + public bool? WithoutManufacturers { get; set; } - /// - /// Identifiers of product tag - /// - public int? ProductTagId { get; set; } + /// + /// Identifiers of product tag + /// + public int? ProductTagId { get; set; } - /// - /// A value indicating whether to load products that are marked as featured (relates only to categories and manufacturers) - /// - public bool? FeaturedProducts { get; set; } + /// + /// A value indicating whether to load products that are marked as featured (relates only to categories and manufacturers) + /// + public bool? FeaturedProducts { get; set; } - /// - /// Filter by product type - /// - public ProductType? ProductType { get; set; } + /// + /// Filter by product type + /// + public ProductType? ProductType { get; set; } /// /// Filter by visibility. @@ -112,73 +112,73 @@ public ExportFilter() /// public int[] OrderStatusIds { get; set; } - /// - /// Filter by payment status - /// - public int[] PaymentStatusIds { get; set; } + /// + /// Filter by payment status + /// + public int[] PaymentStatusIds { get; set; } - /// - /// Filter by shipping status - /// - public int[] ShippingStatusIds { get; set; } + /// + /// Filter by shipping status + /// + public int[] ShippingStatusIds { get; set; } - #endregion + #endregion - #region Customer + #region Customer - /// - /// Filter by active or inactive customers - /// - public bool? IsActiveCustomer { get; set; } + /// + /// Filter by active or inactive customers + /// + public bool? IsActiveCustomer { get; set; } - /// - /// Filter by tax exempt customers - /// - public bool? IsTaxExempt { get; set; } + /// + /// Filter by tax exempt customers + /// + public bool? IsTaxExempt { get; set; } - /// - /// Identifiers of customer roles - /// - public int[] CustomerRoleIds { get; set; } + /// + /// Identifiers of customer roles + /// + public int[] CustomerRoleIds { get; set; } - /// - /// Filter by billing country identifiers - /// - public int[] BillingCountryIds { get; set; } + /// + /// Filter by billing country identifiers + /// + public int[] BillingCountryIds { get; set; } - /// - /// Filter by shipping country identifiers - /// - public int[] ShippingCountryIds { get; set; } + /// + /// Filter by shipping country identifiers + /// + public int[] ShippingCountryIds { get; set; } - /// - /// Filter by last activity date from - /// - public DateTime? LastActivityFrom { get; set; } + /// + /// Filter by last activity date from + /// + public DateTime? LastActivityFrom { get; set; } - /// - /// Filter by last activity date to - /// - public DateTime? LastActivityTo { get; set; } + /// + /// Filter by last activity date to + /// + public DateTime? LastActivityTo { get; set; } - /// - /// Filter by at least spent amount - /// - public decimal? HasSpentAtLeastAmount { get; set; } + /// + /// Filter by at least spent amount + /// + public decimal? HasSpentAtLeastAmount { get; set; } - /// - /// Filter by at least placed orders - /// - public int? HasPlacedAtLeastOrders { get; set; } + /// + /// Filter by at least placed orders + /// + public int? HasPlacedAtLeastOrders { get; set; } - #endregion + #endregion - #region Newsletter Subscription + #region Newsletter Subscription - /// - /// Filter by active or inactive subscriber - /// - public bool? IsActiveSubscriber { get; set; } + /// + /// Filter by active or inactive subscriber + /// + public bool? IsActiveSubscriber { get; set; } /// /// Filter by language @@ -194,6 +194,6 @@ public ExportFilter() /// public int? ShoppingCartTypeId { get; set; } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs index 3d75cf06ef..e14dc21145 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs @@ -5,51 +5,51 @@ namespace SmartStore.Core.Domain { public class ExportProfile : BaseEntity, ICloneable - { - private ICollection _deployments; - - public ExportProfile() - { - Enabled = true; - PerStore = true; - Cleanup = true; - EmailAccountId = 0; - } - - /// - /// The name of the profile - /// - public string Name { get; set; } - - /// - /// The root path of the export folder - /// - public string FolderName { get; set; } - - /// - /// The pattern for file names - /// - public string FileNamePattern { get; set; } - - /// - /// The system name of the profile - /// - public string SystemName { get; set; } - - /// - /// The system name of the export provider - /// - public string ProviderSystemName { get; set; } - - /// - /// Whether the profile is an unremovable system profile - /// - public bool IsSystemProfile { get; set; } - - /// - /// Whether the export profile is enabled - /// - public bool Enabled { get; set; } + { + private ICollection _deployments; + + public ExportProfile() + { + Enabled = true; + PerStore = true; + Cleanup = true; + EmailAccountId = 0; + } + + /// + /// The name of the profile + /// + public string Name { get; set; } + + /// + /// The root path of the export folder + /// + public string FolderName { get; set; } + + /// + /// The pattern for file names + /// + public string FileNamePattern { get; set; } + + /// + /// The system name of the profile + /// + public string SystemName { get; set; } + + /// + /// The system name of the export provider + /// + public string ProviderSystemName { get; set; } + + /// + /// Whether the profile is an unremovable system profile + /// + public bool IsSystemProfile { get; set; } + + /// + /// Whether the export profile is enabled + /// + public bool Enabled { get; set; } /// /// Whether the export related data @@ -61,110 +61,110 @@ public ExportProfile() /// public int SchedulingTaskId { get; set; } - /// - /// XML with filtering information - /// - public string Filtering { get; set; } - - /// - /// XML with projection information - /// - public string Projection { get; set; } - - /// - /// XML with provider specific configuration data - /// - public string ProviderConfigData { get; set; } - - /// - /// XML with information about the last export - /// - public string ResultInfo { get; set; } - - /// - /// The number of records to be skipped - /// - public int Offset { get; set; } - - /// - /// How many records to be loaded per database round-trip - /// - public int Limit { get; set; } - - /// - /// The maximum number of records of one processed batch - /// - public int BatchSize { get; set; } - - /// - /// Whether to start a separate run-through for each store - /// - public bool PerStore { get; set; } - - /// - /// Email Account identifier used to send a notification message an export completes - /// - public int EmailAccountId { get; set; } - - /// - /// Email addresses where to send the notification message - /// - public string CompletedEmailAddresses { get; set; } - - /// - /// Whether to combine and compress the export files in a ZIP archive - /// - public bool CreateZipArchive { get; set; } - - /// - /// Whether to delete unneeded files after deployment - /// - public bool Cleanup { get; set; } - - - /// - /// The scheduling task - /// - public virtual ScheduleTask ScheduleTask { get; set; } - - /// - /// Gets or sets export deployments - /// - public virtual ICollection Deployments - { - get { return _deployments ?? (_deployments = new HashSet()); } - set { _deployments = value; } - } - - public ExportProfile Clone() - { - var profile = new ExportProfile - { - Name = this.Name, - FolderName = null, - FileNamePattern = this.FileNamePattern, - ProviderSystemName = this.ProviderSystemName, - Enabled = this.Enabled, - SchedulingTaskId = 0, - Filtering = this.Filtering, - Projection = this.Projection, - ProviderConfigData = this.ProviderConfigData, - ResultInfo = null, - Offset = this.Offset, - Limit = this.Limit, - BatchSize = this.BatchSize, - PerStore = this.PerStore, - EmailAccountId = this.EmailAccountId, - CompletedEmailAddresses = this.CompletedEmailAddresses, - CreateZipArchive = this.CreateZipArchive, - Cleanup = this.Cleanup - }; - return profile; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - } + /// + /// XML with filtering information + /// + public string Filtering { get; set; } + + /// + /// XML with projection information + /// + public string Projection { get; set; } + + /// + /// XML with provider specific configuration data + /// + public string ProviderConfigData { get; set; } + + /// + /// XML with information about the last export + /// + public string ResultInfo { get; set; } + + /// + /// The number of records to be skipped + /// + public int Offset { get; set; } + + /// + /// How many records to be loaded per database round-trip + /// + public int Limit { get; set; } + + /// + /// The maximum number of records of one processed batch + /// + public int BatchSize { get; set; } + + /// + /// Whether to start a separate run-through for each store + /// + public bool PerStore { get; set; } + + /// + /// Email Account identifier used to send a notification message an export completes + /// + public int EmailAccountId { get; set; } + + /// + /// Email addresses where to send the notification message + /// + public string CompletedEmailAddresses { get; set; } + + /// + /// Whether to combine and compress the export files in a ZIP archive + /// + public bool CreateZipArchive { get; set; } + + /// + /// Whether to delete unneeded files after deployment + /// + public bool Cleanup { get; set; } + + + /// + /// The scheduling task + /// + public virtual ScheduleTask ScheduleTask { get; set; } + + /// + /// Gets or sets export deployments + /// + public virtual ICollection Deployments + { + get => _deployments ?? (_deployments = new HashSet()); + set => _deployments = value; + } + + public ExportProfile Clone() + { + var profile = new ExportProfile + { + Name = this.Name, + FolderName = null, + FileNamePattern = this.FileNamePattern, + ProviderSystemName = this.ProviderSystemName, + Enabled = this.Enabled, + SchedulingTaskId = 0, + Filtering = this.Filtering, + Projection = this.Projection, + ProviderConfigData = this.ProviderConfigData, + ResultInfo = null, + Offset = this.Offset, + Limit = this.Limit, + BatchSize = this.BatchSize, + PerStore = this.PerStore, + EmailAccountId = this.EmailAccountId, + CompletedEmailAddresses = this.CompletedEmailAddresses, + CreateZipArchive = this.CreateZipArchive, + Cleanup = this.Cleanup + }; + return profile; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs index f4ca7317fc..ff48168f71 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs @@ -4,197 +4,179 @@ namespace SmartStore.Core.Domain.DataExchange { - /// - /// Settings projected onto an export - /// - /// - /// Note possible projection controlling: a) developer controls, b) merchant controls, c) developer controls what the merchant can control - /// - [Serializable] - public class ExportProjection - { - public ExportProjection() - { - OnlyIndividuallyVisibleAssociated = true; - } - - #region All entity types - - /// - /// Store identifier - /// - public int? StoreId { get; set; } - - /// - /// The language to be applied to the export - /// - public int? LanguageId { get; set; } - - /// - /// The currency to be applied to the export - /// - public int? CurrencyId { get; set; } - - /// - /// Customer identifier - /// - public int? CustomerId { get; set; } - - #endregion - - #region Product - - /// - /// Description merging identifier - /// - public int DescriptionMergingId { get; set; } - - /// - /// Decription merging - /// - [XmlIgnore] - public ExportDescriptionMerging DescriptionMerging - { - get - { - return (ExportDescriptionMerging)DescriptionMergingId; - } - set - { - DescriptionMergingId = (int)value; - } - } - - /// - /// Convert HTML decription to plain text - /// - public bool DescriptionToPlainText { get; set; } - - /// - /// Comma separated text to append to the decription - /// - public string AppendDescriptionText { get; set; } - - /// - /// Remove critical characters from the description - /// - public bool RemoveCriticalCharacters { get; set; } - - /// - /// Comma separated list of critical characters - /// - public string CriticalCharacters { get; set; } - - /// - /// The price type for calculating the product price - /// - public PriceDisplayType? PriceType { get; set; } - - /// - /// Convert net to gross prices - /// - public bool ConvertNetToGrossPrices { get; set; } - - /// - /// Fallback for product brand - /// - public string Brand { get; set; } - - /// - /// Number of images per object to be exported - /// - public int? NumberOfMediaFiles { get; set; } - - /// - /// Picture size - /// - public int PictureSize { get; set; } - - /// - /// Fallback for shipping time - /// - public string ShippingTime { get; set; } - - /// - /// Fallback for shipping costs - /// - public decimal? ShippingCosts { get; set; } - - /// - /// Free shipping threshold - /// - public decimal? FreeShippingThreshold { get; set; } - - /// - /// Whether to export attribute combinations as products - /// - public bool AttributeCombinationAsProduct { get; set; } - - /// - /// Identifier for merging attribute values of attribute combinations - /// - public int AttributeCombinationValueMergingId { get; set; } - - /// - /// Merging attribute values of attribute combinations - /// - [XmlIgnore] - public ExportAttributeValueMerging AttributeCombinationValueMerging - { - get - { - return (ExportAttributeValueMerging)AttributeCombinationValueMergingId; - } - set - { - AttributeCombinationValueMergingId = (int)value; - } - } - - /// - /// Whether to export grouped products - /// - public bool NoGroupedProducts { get; set; } - - /// - /// Whether to export associated products that marked as "visible individually". false to load all records, true to load "visible individually" only - /// - public bool OnlyIndividuallyVisibleAssociated { get; set; } - - #endregion - - #region Order - - /// - /// Identifier of the new state for orders - /// - public int OrderStatusChangeId { get; set; } - - /// - /// New state for orders - /// - [XmlIgnore] - public ExportOrderStatusChange OrderStatusChange - { - get - { - return (ExportOrderStatusChange)OrderStatusChangeId; - } - set - { - OrderStatusChangeId = (int)value; - } - } - - #endregion - - #region Shopping Cart Item - - /// - /// Whether to export bundle products - /// - public bool NoBundleProducts { get; set; } - - #endregion - } + /// + /// Settings projected onto an export + /// + /// + /// Note possible projection controlling: a) developer controls, b) merchant controls, c) developer controls what the merchant can control + /// + [Serializable] + public class ExportProjection + { + public ExportProjection() + { + OnlyIndividuallyVisibleAssociated = true; + } + + #region All entity types + + /// + /// Store identifier + /// + public int? StoreId { get; set; } + + /// + /// The language to be applied to the export + /// + public int? LanguageId { get; set; } + + /// + /// The currency to be applied to the export + /// + public int? CurrencyId { get; set; } + + /// + /// Customer identifier + /// + public int? CustomerId { get; set; } + + #endregion + + #region Product + + /// + /// Description merging identifier + /// + public int DescriptionMergingId { get; set; } + + /// + /// Decription merging + /// + [XmlIgnore] + public ExportDescriptionMerging DescriptionMerging + { + get => (ExportDescriptionMerging)DescriptionMergingId; + set => DescriptionMergingId = (int)value; + } + + /// + /// Convert HTML decription to plain text + /// + public bool DescriptionToPlainText { get; set; } + + /// + /// Comma separated text to append to the decription + /// + public string AppendDescriptionText { get; set; } + + /// + /// Remove critical characters from the description + /// + public bool RemoveCriticalCharacters { get; set; } + + /// + /// Comma separated list of critical characters + /// + public string CriticalCharacters { get; set; } + + /// + /// The price type for calculating the product price + /// + public PriceDisplayType? PriceType { get; set; } + + /// + /// Convert net to gross prices + /// + public bool ConvertNetToGrossPrices { get; set; } + + /// + /// Fallback for product brand + /// + public string Brand { get; set; } + + /// + /// Number of images per object to be exported + /// + public int? NumberOfMediaFiles { get; set; } + + /// + /// Picture size + /// + public int PictureSize { get; set; } + + /// + /// Fallback for shipping time + /// + public string ShippingTime { get; set; } + + /// + /// Fallback for shipping costs + /// + public decimal? ShippingCosts { get; set; } + + /// + /// Free shipping threshold + /// + public decimal? FreeShippingThreshold { get; set; } + + /// + /// Whether to export attribute combinations as products + /// + public bool AttributeCombinationAsProduct { get; set; } + + /// + /// Identifier for merging attribute values of attribute combinations + /// + public int AttributeCombinationValueMergingId { get; set; } + + /// + /// Merging attribute values of attribute combinations + /// + [XmlIgnore] + public ExportAttributeValueMerging AttributeCombinationValueMerging + { + get => (ExportAttributeValueMerging)AttributeCombinationValueMergingId; + set => AttributeCombinationValueMergingId = (int)value; + } + + /// + /// Whether to export grouped products + /// + public bool NoGroupedProducts { get; set; } + + /// + /// Whether to export associated products that marked as "visible individually". false to load all records, true to load "visible individually" only + /// + public bool OnlyIndividuallyVisibleAssociated { get; set; } + + #endregion + + #region Order + + /// + /// Identifier of the new state for orders + /// + public int OrderStatusChangeId { get; set; } + + /// + /// New state for orders + /// + [XmlIgnore] + public ExportOrderStatusChange OrderStatusChange + { + get => (ExportOrderStatusChange)OrderStatusChangeId; + set => OrderStatusChangeId = (int)value; + } + + #endregion + + #region Shopping Cart Item + + /// + /// Whether to export bundle products + /// + public bool NoBundleProducts { get; set; } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs index 94bbd69246..399914eb28 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs @@ -2,27 +2,27 @@ namespace SmartStore.Core.Domain.DataExchange { - /// - /// Supported entity types - /// - public enum ImportEntityType - { - Product = 0, - Category, - Customer, - NewsLetterSubscription - } + /// + /// Supported entity types + /// + public enum ImportEntityType + { + Product = 0, + Category, + Customer, + NewsLetterSubscription + } - public enum ImportFileType - { - CSV = 0, - XLSX - } + public enum ImportFileType + { + CSV = 0, + XLSX + } - [Flags] - public enum ImportModeFlags - { - Insert = 1, - Update = 2 - } + [Flags] + public enum ImportModeFlags + { + Insert = 1, + Update = 2 + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs index 4322a88f2f..6c00160ae9 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs @@ -3,62 +3,50 @@ namespace SmartStore.Core.Domain { - public class ImportProfile : BaseEntity - { - /// - /// The name of the profile - /// - public string Name { get; set; } - - /// - /// The name of the folder (file system) - /// - public string FolderName { get; set; } - - /// - /// The identifier of the file type - /// - public int FileTypeId { get; set; } - - /// - /// The file type - /// - public ImportFileType FileType - { - get - { - return (ImportFileType)FileTypeId; - } - set - { - FileTypeId = (int)value; - } - } - - /// - /// The identifier of the entity type - /// - public int EntityTypeId { get; set; } - - /// - /// The entity type - /// - public ImportEntityType EntityType - { - get - { - return (ImportEntityType)EntityTypeId; - } - set - { - EntityTypeId = (int)value; - } - } - - /// - /// Whether the profile is enabled - /// - public bool Enabled { get; set; } + public class ImportProfile : BaseEntity + { + /// + /// The name of the profile + /// + public string Name { get; set; } + + /// + /// The name of the folder (file system) + /// + public string FolderName { get; set; } + + /// + /// The identifier of the file type + /// + public int FileTypeId { get; set; } + + /// + /// The file type + /// + public ImportFileType FileType + { + get => (ImportFileType)FileTypeId; + set => FileTypeId = (int)value; + } + + /// + /// The identifier of the entity type + /// + public int EntityTypeId { get; set; } + + /// + /// The entity type + /// + public ImportEntityType EntityType + { + get => (ImportEntityType)EntityTypeId; + set => EntityTypeId = (int)value; + } + + /// + /// Whether the profile is enabled + /// + public bool Enabled { get; set; } /// /// Whether the import related data @@ -70,49 +58,49 @@ public ImportEntityType EntityType /// public int Skip { get; set; } - /// - /// Maximum number of records to return - /// - public int Take { get; set; } - - /// - /// Whether to only update existing data - /// - public bool UpdateOnly { get; set; } - - /// - /// Name of key fields to identify existing records during import - /// - public string KeyFieldNames { get; set; } - - /// - /// File type specific configuration - /// - public string FileTypeConfiguration { get; set; } - - /// - /// XML with extra data - /// - public string ExtraData { get; set; } - - /// - /// Mapping of import columns - /// - public string ColumnMapping { get; set; } - - /// - /// XML with information about the last import - /// - public string ResultInfo { get; set; } - - /// - /// The scheduling task identifier - /// - public int SchedulingTaskId { get; set; } - - /// - /// The scheduling task - /// - public virtual ScheduleTask ScheduleTask { get; set; } - } + /// + /// Maximum number of records to return + /// + public int Take { get; set; } + + /// + /// Whether to only update existing data + /// + public bool UpdateOnly { get; set; } + + /// + /// Name of key fields to identify existing records during import + /// + public string KeyFieldNames { get; set; } + + /// + /// File type specific configuration + /// + public string FileTypeConfiguration { get; set; } + + /// + /// XML with extra data + /// + public string ExtraData { get; set; } + + /// + /// Mapping of import columns + /// + public string ColumnMapping { get; set; } + + /// + /// XML with information about the last import + /// + public string ResultInfo { get; set; } + + /// + /// The scheduling task identifier + /// + public int SchedulingTaskId { get; set; } + + /// + /// The scheduling task + /// + public virtual ScheduleTask ScheduleTask { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs index d8067400cb..09230db0a7 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs @@ -1,85 +1,81 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; using SmartStore.Core.Data.Hooks; namespace SmartStore.Core.Domain.DataExchange { - /// - /// Holds info about a synchronization operation with an external system - /// - [DataContract] - [Hookable(false)] - public partial class SyncMapping : BaseEntity - { - public SyncMapping() - { - this.SyncedOnUtc = DateTime.UtcNow; - } + /// + /// Holds info about a synchronization operation with an external system + /// + [DataContract] + [Hookable(false)] + public partial class SyncMapping : BaseEntity + { + public SyncMapping() + { + this.SyncedOnUtc = DateTime.UtcNow; + } - /// - /// Gets or sets the entity identifier in SmartStore - /// - [Index("IX_SyncMapping_ByEntity", 0, IsUnique = true)] - [DataMember] - public int EntityId { get; set; } + /// + /// Gets or sets the entity identifier in SmartStore + /// + [Index("IX_SyncMapping_ByEntity", 0, IsUnique = true)] + [DataMember] + public int EntityId { get; set; } - /// - /// Gets or sets the entity's key in the external application - /// - [Index("IX_SyncMapping_BySource", 0, IsUnique = true)] - [DataMember] - public string SourceKey { get; set; } + /// + /// Gets or sets the entity's key in the external application + /// + [Index("IX_SyncMapping_BySource", 0, IsUnique = true)] + [DataMember] + public string SourceKey { get; set; } - /// - /// Gets or sets a name representing the entity type - /// - [Index("IX_SyncMapping_ByEntity", 1, IsUnique = true)] - [Index("IX_SyncMapping_BySource", 1, IsUnique = true)] - [DataMember] - public string EntityName { get; set; } + /// + /// Gets or sets a name representing the entity type + /// + [Index("IX_SyncMapping_ByEntity", 1, IsUnique = true)] + [Index("IX_SyncMapping_BySource", 1, IsUnique = true)] + [DataMember] + public string EntityName { get; set; } - /// - /// Gets or sets a name for the external application - /// - [Index("IX_SyncMapping_ByEntity", 2, IsUnique = true)] - [Index("IX_SyncMapping_BySource", 2, IsUnique = true)] - [DataMember] - public string ContextName { get; set; } + /// + /// Gets or sets a name for the external application + /// + [Index("IX_SyncMapping_ByEntity", 2, IsUnique = true)] + [Index("IX_SyncMapping_BySource", 2, IsUnique = true)] + [DataMember] + public string ContextName { get; set; } - /// - /// Gets or sets an optional content hash reflecting the source model at the time of last sync - /// - [DataMember] - public string SourceHash { get; set; } + /// + /// Gets or sets an optional content hash reflecting the source model at the time of last sync + /// + [DataMember] + public string SourceHash { get; set; } - /// - /// Gets or sets a custom integer value - /// - [DataMember] - public int? CustomInt { get; set; } + /// + /// Gets or sets a custom integer value + /// + [DataMember] + public int? CustomInt { get; set; } - /// - /// Gets or sets a custom string value - /// - [DataMember] - public string CustomString { get; set; } + /// + /// Gets or sets a custom string value + /// + [DataMember] + public string CustomString { get; set; } - /// - /// Gets or sets a custom bool value - /// - [DataMember] - public bool? CustomBool { get; set; } + /// + /// Gets or sets a custom bool value + /// + [DataMember] + public bool? CustomBool { get; set; } - /// - /// Gets or sets the date of the last sync operation - /// - [DataMember] - public DateTime SyncedOnUtc { get; set; } + /// + /// Gets or sets the date of the last sync operation + /// + [DataMember] + public DateTime SyncedOnUtc { get; set; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs b/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs index 763d91dccc..2d7bb62f3e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs @@ -6,11 +6,11 @@ namespace SmartStore.Core.Domain.Directory { - /// - /// Represents a country - /// - [DataContract] - public partial class Country : BaseEntity, ILocalizedEntity, IStoreMappingSupported + /// + /// Represents a country + /// + [DataContract] + public partial class Country : BaseEntity, ILocalizedEntity, IStoreMappingSupported { private ICollection _stateProvinces; @@ -18,81 +18,92 @@ public partial class Country : BaseEntity, ILocalizedEntity, IStoreMappingSuppor /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets a value indicating whether billing is allowed to this country /// [DataMember] - public bool AllowsBilling { get; set; } + public bool AllowsBilling { get; set; } /// /// Gets or sets a value indicating whether shipping is allowed to this country /// [DataMember] - public bool AllowsShipping { get; set; } + public bool AllowsShipping { get; set; } /// /// Gets or sets the two letter ISO code /// [DataMember] - public string TwoLetterIsoCode { get; set; } + public string TwoLetterIsoCode { get; set; } /// /// Gets or sets the three letter ISO code /// [DataMember] - public string ThreeLetterIsoCode { get; set; } + public string ThreeLetterIsoCode { get; set; } /// /// Gets or sets the numeric ISO code /// [DataMember] - public int NumericIsoCode { get; set; } + public int NumericIsoCode { get; set; } /// /// Gets or sets a value indicating whether customers in this country must be charged EU VAT /// [DataMember] - public bool SubjectToVat { get; set; } + public bool SubjectToVat { get; set; } /// /// Gets or sets a value indicating whether the entity is published /// [DataMember] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } - /// - /// Gets or sets a value indicating whether the cookie manager should be displayed for visitors of this country. - /// - [DataMember] - public bool DisplayCookieManager { get; set; } + /// + /// Gets or sets a value indicating whether the cookie manager should be displayed for visitors of this country. + /// + [DataMember] + public bool DisplayCookieManager { get; set; } = true; - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } - - /// - /// Gets or sets the international mailing address format - /// - [DataMember, MaxLength] - public string AddressFormat { get; set; } - - /// - /// Gets or sets the state/provinces - /// - public virtual ICollection StateProvinces + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } + + /// + /// Gets or sets the international mailing address format + /// + [DataMember, MaxLength] + public string AddressFormat { get; set; } + + /// + /// Gets or sets the identfier of the default currency. + /// + [DataMember] + public int? DefaultCurrencyId { get; set; } + + /// + /// Gets or sets the default currency. + /// + public virtual Currency DefaultCurrency { get; set; } + + /// + /// Gets or sets the state/provinces + /// + public virtual ICollection StateProvinces { - get { return _stateProvinces ?? (_stateProvinces = new HashSet()); } - protected set { _stateProvinces = value; } + get => _stateProvinces ?? (_stateProvinces = new HashSet()); + protected set => _stateProvinces = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs b/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs index 07cae0cc9c..e892b06553 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs @@ -12,78 +12,73 @@ namespace SmartStore.Core.Domain.Directory /// Represents a currency /// [DataContract] - public partial class Currency : BaseEntity, IAuditable, ILocalizedEntity, IStoreMappingSupported, ICloneable + public partial class Currency : BaseEntity, IAuditable, ILocalizedEntity, IStoreMappingSupported, ICloneable { - public Currency() - { - RoundNumDecimals = 2; - } - /// /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the ISO currency code /// [DataMember] - public string CurrencyCode { get; set; } + public string CurrencyCode { get; set; } /// /// Gets or sets the rate /// [DataMember] - public decimal Rate { get; set; } + public decimal Rate { get; set; } /// /// Gets or sets the display locale /// [DataMember] - public string DisplayLocale { get; set; } + public string DisplayLocale { get; set; } /// /// Gets or sets the custom formatting /// [DataMember] - public string CustomFormatting { get; set; } + public string CustomFormatting { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } /// /// Gets or sets a value indicating whether the entity is published /// [DataMember] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the date and time of instance creation /// [DataMember] - public DateTime CreatedOnUtc { get; set; } + public DateTime CreatedOnUtc { get; set; } /// /// Gets or sets the date and time of instance update /// [DataMember] - public DateTime UpdatedOnUtc { get; set; } + public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets or sets the (comma separated) list of domain endings (e.g. country code top-level domains) to which this currency is the default one - /// - [DataMember] - public string DomainEndings { get; set; } + /// + /// Gets or sets the (comma separated) list of domain endings (e.g. country code top-level domains) to which this currency is the default one + /// + [DataMember] + public string DomainEndings { get; set; } #region Rounding @@ -97,7 +92,7 @@ public Currency() /// Gets or sets the number of decimal places to round to /// [DataMember] - public int RoundNumDecimals { get; set; } + public int RoundNumDecimals { get; set; } = 2; /// /// Gets or sets a value indicating whether to round the order total @@ -133,29 +128,29 @@ object ICloneable.Clone() private NumberFormatInfo _numberFormat; - [NotMapped, JsonIgnore, IgnoreDataMember] - public NumberFormatInfo NumberFormat - { - get - { - if (_numberFormat == null && DisplayLocale.HasValue()) - { - try - { - _numberFormat = CultureInfo.CreateSpecificCulture(DisplayLocale).NumberFormat; - } - catch { } - } - - if (_numberFormat == null) - { - _numberFormat = NumberFormatInfo.CurrentInfo; - } - - return _numberFormat; - } - } - - #endregion - } + [NotMapped, JsonIgnore, IgnoreDataMember] + public NumberFormatInfo NumberFormat + { + get + { + if (_numberFormat == null && DisplayLocale.HasValue()) + { + try + { + _numberFormat = CultureInfo.CreateSpecificCulture(DisplayLocale).NumberFormat; + } + catch { } + } + + if (_numberFormat == null) + { + _numberFormat = NumberFormatInfo.CurrentInfo; + } + + return _numberFormat; + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/DeliveryTime.cs b/src/Libraries/SmartStore.Core/Domain/Directory/DeliveryTime.cs index 33f66da021..5d54d71d2d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/DeliveryTime.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/DeliveryTime.cs @@ -1,43 +1,107 @@ -using System; -using SmartStore.Core.Domain.Localization; using System.Runtime.Serialization; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Directory { /// - /// Represents a currency + /// Represents a delivery time /// [DataContract] - public partial class DeliveryTime : BaseEntity, ILocalizedEntity + public partial class DeliveryTime : BaseEntity, ILocalizedEntity { /// /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the hex value /// [DataMember] - public string ColorHexValue { get; set; } + public string ColorHexValue { get; set; } /// /// Gets or sets the display locale /// [DataMember] - public string DisplayLocale { get; set; } + public string DisplayLocale { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the display order /// [DataMember] public bool? IsDefault { get; set; } + + /// + /// Specifies the earliest time of delivery in days. + /// + [DataMember] + public int? MinDays { get; set; } + + /// + /// Specifies the latest time of delivery in days. + /// + [DataMember] + public int? MaxDays { get; set; } + + #region Utils + + /// + /// Intended to be used for submission of products to third parties which require the delivery days as integer e.g. price comparison portals or after sales services. + /// + /// Days as integer or null if no days are set for the current delivery time. + public int? GetMinOrMaxOrAverageDays() + { + if (MinDays > 0 && MaxDays > 0) + { + return ((int)MinDays + (int)MaxDays) / 2; + } + else if (MinDays > 0) + { + return (int)MinDays; + } + else if (MaxDays > 0) + { + return (int)MaxDays; + } + + return null; + } + + #endregion + } + + + /// + /// Represents how to present delivery times. + /// + public enum DeliveryTimesPresentation + { + /// + /// Do not display. + /// + None = 0, + + /// + /// Display label only. + /// + LabelOnly = 5, + + /// + /// Display date only (if possible). + /// + DateOnly = 10, + + /// + /// Display label and date. + /// + LabelAndDate = 15 } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/ExchangeRate.cs b/src/Libraries/SmartStore.Core/Domain/Directory/ExchangeRate.cs index 271ac9b2fc..8f8c8e606a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/ExchangeRate.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/ExchangeRate.cs @@ -3,45 +3,35 @@ namespace SmartStore.Core.Domain.Directory { - /// - /// Represents an exchange rate - /// - [DebuggerDisplay("{CurrencyCode} {Rate}")] + /// + /// Represents an exchange rate + /// + [DebuggerDisplay("{CurrencyCode} {Rate}")] public partial class ExchangeRate { /// - /// Creates a new instance of the ExchangeRate class + /// Name of the currrency /// - public ExchangeRate() - { - Name = string.Empty; - CurrencyCode = string.Empty; - Rate = 1.0m; - } + public string Name { get; set; } = string.Empty; - /// - /// Name of the currrency - /// - public string Name { get; set; } - - /// - /// The three letter ISO code for the Exchange Rate, e.g. USD - /// - public string CurrencyCode { get; set; } + /// + /// The three letter ISO code for the Exchange Rate, e.g. USD + /// + public string CurrencyCode { get; set; } = string.Empty; - /// - /// Indicates whether the currency is available in the store - /// - public bool IsStoreCurrency { get; set; } + /// + /// Indicates whether the currency is available in the store + /// + public bool IsStoreCurrency { get; set; } - /// - /// The conversion rate of this currency from the base currency - /// - public decimal Rate { get; set; } + /// + /// The conversion rate of this currency from the base currency + /// + public decimal Rate { get; set; } = 1.0m; /// /// When was this exchange rate updated from the data source (the internet data xml feed) /// public DateTime UpdatedOn { get; set; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs index 2e02b62768..cf65992ff9 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs @@ -3,34 +3,34 @@ namespace SmartStore.Core.Domain.Directory { - /// - /// Represents a measure dimension - /// - [DataContract] - public partial class MeasureDimension : BaseEntity, ILocalizedEntity + /// + /// Represents a measure dimension + /// + [DataContract] + public partial class MeasureDimension : BaseEntity, ILocalizedEntity { - /// - /// Gets or sets the name - /// - [DataMember] - public string Name { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } - /// - /// Gets or sets the system keyword - /// - [DataMember] - public string SystemKeyword { get; set; } + /// + /// Gets or sets the system keyword + /// + [DataMember] + public string SystemKeyword { get; set; } - /// - /// Gets or sets the ratio - /// - [DataMember] - public decimal Ratio { get; set; } + /// + /// Gets or sets the ratio + /// + [DataMember] + public decimal Ratio { get; set; } - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs index d798c43588..ac1bfda3be 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs @@ -3,34 +3,34 @@ namespace SmartStore.Core.Domain.Directory { - /// - /// Represents a measure weight - /// - [DataContract] - public partial class MeasureWeight : BaseEntity, ILocalizedEntity + /// + /// Represents a measure weight + /// + [DataContract] + public partial class MeasureWeight : BaseEntity, ILocalizedEntity { - /// - /// Gets or sets the name - /// - [DataMember] - public string Name { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } - /// - /// Gets or sets the system keyword - /// - [DataMember] - public string SystemKeyword { get; set; } + /// + /// Gets or sets the system keyword + /// + [DataMember] + public string SystemKeyword { get; set; } - /// - /// Gets or sets the ratio - /// - [DataMember] - public decimal Ratio { get; set; } + /// + /// Gets or sets the ratio + /// + [DataMember] + public decimal Ratio { get; set; } - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs index 4e671a5384..77320a1777 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs @@ -13,7 +13,7 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the name plural. @@ -25,24 +25,24 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity /// Gets or sets the description /// [DataMember] - public string Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the display locale /// [DataMember] - public string DisplayLocale { get; set; } + public string DisplayLocale { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the default quantity unit /// [DataMember] - public bool IsDefault { get; set; } + public bool IsDefault { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/StateProvince.cs b/src/Libraries/SmartStore.Core/Domain/Directory/StateProvince.cs index f349001fa8..0151457ba6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/StateProvince.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/StateProvince.cs @@ -1,6 +1,6 @@ -using SmartStore.Core.Domain.Localization; using System.Runtime.Serialization; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Directory { @@ -8,43 +8,43 @@ namespace SmartStore.Core.Domain.Directory /// Represents a state/province /// [DataContract] - public partial class StateProvince : BaseEntity, ILocalizedEntity + public partial class StateProvince : BaseEntity, ILocalizedEntity { /// /// Gets or sets the country identifier /// [DataMember] - public int CountryId { get; set; } + public int CountryId { get; set; } /// /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the abbreviation /// [DataMember] - public string Abbreviation { get; set; } + public string Abbreviation { get; set; } /// /// Gets or sets a value indicating whether the entity is published /// [DataMember] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets the country /// [DataMember] - public virtual Country Country { get; set; } + public virtual Country Country { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Discounts/Discount.cs b/src/Libraries/SmartStore.Core/Domain/Discounts/Discount.cs index a96a309f89..416cfdb06d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Discounts/Discount.cs +++ b/src/Libraries/SmartStore.Core/Domain/Discounts/Discount.cs @@ -11,110 +11,98 @@ namespace SmartStore.Core.Domain.Discounts /// Represents a discount /// [DataContract] - [DebuggerDisplay("{Name} - {DiscountType}")] - public partial class Discount : BaseEntity, IRulesContainer + [DebuggerDisplay("{Name} - {DiscountType}")] + public partial class Discount : BaseEntity, IRulesContainer { private ICollection _ruleSets; private ICollection _appliedToCategories; - private ICollection _appliedToManufacturers; - private ICollection _appliedToProducts; + private ICollection _appliedToManufacturers; + private ICollection _appliedToProducts; /// /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the discount type identifier /// [DataMember] - public int DiscountTypeId { get; set; } + public int DiscountTypeId { get; set; } /// /// Gets or sets a value indicating whether to use percentage /// [DataMember] - public bool UsePercentage { get; set; } + public bool UsePercentage { get; set; } /// /// Gets or sets the discount percentage /// [DataMember] - public decimal DiscountPercentage { get; set; } + public decimal DiscountPercentage { get; set; } /// /// Gets or sets the discount amount /// [DataMember] - public decimal DiscountAmount { get; set; } + public decimal DiscountAmount { get; set; } /// /// Gets or sets the discount start date and time /// [DataMember] - public DateTime? StartDateUtc { get; set; } + public DateTime? StartDateUtc { get; set; } /// /// Gets or sets the discount end date and time /// [DataMember] - public DateTime? EndDateUtc { get; set; } + public DateTime? EndDateUtc { get; set; } /// /// Gets or sets a value indicating whether discount requires coupon code /// [DataMember] - public bool RequiresCouponCode { get; set; } + public bool RequiresCouponCode { get; set; } /// /// Gets or sets the coupon code /// [DataMember] - public string CouponCode { get; set; } + public string CouponCode { get; set; } /// /// Gets or sets the discount limitation identifier /// [DataMember] - public int DiscountLimitationId { get; set; } + public int DiscountLimitationId { get; set; } /// /// Gets or sets the discount limitation times (used when Limitation is set to "N Times Only" or "N Times Per Customer") /// [DataMember] - public int LimitationTimes { get; set; } + public int LimitationTimes { get; set; } /// /// Gets or sets the discount type /// [DataMember] - public DiscountType DiscountType + public DiscountType DiscountType { - get - { - return (DiscountType)this.DiscountTypeId; - } - set - { - this.DiscountTypeId = (int)value; - } + get => (DiscountType)this.DiscountTypeId; + set => this.DiscountTypeId = (int)value; } /// /// Gets or sets the discount limitation /// [DataMember] - public DiscountLimitationType DiscountLimitation + public DiscountLimitationType DiscountLimitation { - get - { - return (DiscountLimitationType)this.DiscountLimitationId; - } - set - { - this.DiscountLimitationId = (int)value; - } + get => (DiscountLimitationType)this.DiscountLimitationId; + set => this.DiscountLimitationId = (int)value; } /// @@ -122,8 +110,8 @@ public DiscountLimitationType DiscountLimitation /// public virtual ICollection RuleSets { - get { return _ruleSets ?? (_ruleSets = new HashSet()); } - protected set { _ruleSets = value; } + get => _ruleSets ?? (_ruleSets = new HashSet()); + protected set => _ruleSets = value; } /// @@ -132,27 +120,27 @@ public virtual ICollection RuleSets [DataMember] public virtual ICollection AppliedToCategories { - get { return _appliedToCategories ?? (_appliedToCategories = new HashSet()); } - protected set { _appliedToCategories = value; } + get => _appliedToCategories ?? (_appliedToCategories = new HashSet()); + protected set => _appliedToCategories = value; } - /// - /// Gets or sets the manufacturers - /// - [DataMember] - public virtual ICollection AppliedToManufacturers - { - get { return _appliedToManufacturers ?? (_appliedToManufacturers = new HashSet()); } - protected set { _appliedToManufacturers = value; } - } - - /// - /// Gets or sets the products - /// - public virtual ICollection AppliedToProducts - { - get { return _appliedToProducts ?? (_appliedToProducts = new HashSet()); } - protected set { _appliedToProducts = value; } - } + /// + /// Gets or sets the manufacturers + /// + [DataMember] + public virtual ICollection AppliedToManufacturers + { + get => _appliedToManufacturers ?? (_appliedToManufacturers = new HashSet()); + protected set => _appliedToManufacturers = value; + } + + /// + /// Gets or sets the products + /// + public virtual ICollection AppliedToProducts + { + get => _appliedToProducts ?? (_appliedToProducts = new HashSet()); + protected set => _appliedToProducts = value; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Discounts/DiscountType.cs b/src/Libraries/SmartStore.Core/Domain/Discounts/DiscountType.cs index c70468a740..d260fcabc6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Discounts/DiscountType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Discounts/DiscountType.cs @@ -17,14 +17,14 @@ public enum DiscountType /// Assigned to categories (all products in a category) /// AssignedToCategories = 5, - /// - /// Assigned to manufacturers (all products of a manufacturer) - /// - AssignedToManufacturers = 6, - /// - /// Assigned to shipping - /// - AssignedToShipping = 10, + /// + /// Assigned to manufacturers (all products of a manufacturer) + /// + AssignedToManufacturers = 6, + /// + /// Assigned to shipping + /// + AssignedToShipping = 10, /// /// Assigned to order subtotal /// diff --git a/src/Libraries/SmartStore.Core/Domain/Forums/ForumGroup.cs b/src/Libraries/SmartStore.Core/Domain/Forums/ForumGroup.cs index ae83f4f8f9..3b21ddda01 100644 --- a/src/Libraries/SmartStore.Core/Domain/Forums/ForumGroup.cs +++ b/src/Libraries/SmartStore.Core/Domain/Forums/ForumGroup.cs @@ -58,8 +58,8 @@ public partial class ForumGroup : BaseEntity, IAuditable, IStoreMappingSupported /// public virtual ICollection Forums { - get { return _forums ?? (_forums = new HashSet()); } - protected set { _forums = value; } + get => _forums ?? (_forums = new HashSet()); + protected set => _forums = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Forums/ForumPost.cs b/src/Libraries/SmartStore.Core/Domain/Forums/ForumPost.cs index e65c80573d..3098a9cdc0 100644 --- a/src/Libraries/SmartStore.Core/Domain/Forums/ForumPost.cs +++ b/src/Libraries/SmartStore.Core/Domain/Forums/ForumPost.cs @@ -9,7 +9,7 @@ namespace SmartStore.Core.Domain.Forums /// Represents a forum post /// public partial class ForumPost : BaseEntity, IAuditable - { + { private ICollection _forumPostVotes; /// @@ -64,8 +64,8 @@ public partial class ForumPost : BaseEntity, IAuditable /// public virtual ICollection ForumPostVotes { - get { return _forumPostVotes ?? (_forumPostVotes = new HashSet()); } - protected set { _forumPostVotes = value; } + get => _forumPostVotes ?? (_forumPostVotes = new HashSet()); + protected set => _forumPostVotes = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Forums/ForumSettings.cs b/src/Libraries/SmartStore.Core/Domain/Forums/ForumSettings.cs index 86a2b5e423..4d03094351 100644 --- a/src/Libraries/SmartStore.Core/Domain/Forums/ForumSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Forums/ForumSettings.cs @@ -4,41 +4,15 @@ namespace SmartStore.Core.Domain.Forums { public class ForumSettings : ISettings { - public ForumSettings() - { - ForumsEnabled = true; - RelativeDateTimeFormattingEnabled = true; - TopicSubjectMaxLength = 450; - PostMaxLength = 4000; - StrippedTopicMaxLength = 45; - TopicsPageSize = 20; - PostsPageSize = 20; - ForumFeedCount = 20; - SearchResultsPageSize = 20; - LatestCustomerPostsPageSize = 20; - AllowSorting = true; - ShowCustomersPostCount = true; - ForumEditor = EditorType.BBCodeEditor; - SignaturesEnabled = true; - PrivateMessagesPageSize = 20; - ForumSubscriptionsPageSize = 20; - PMSubjectMaxLength = 450; - PMTextMaxLength = 4000; - HomePageActiveDiscussionsTopicCount = 5; - ActiveDiscussionsPageTopicCount = 50; - ActiveDiscussionsFeedCount = 25; - AllowCustomersToVoteOnPosts = true; - } - - /// + /// /// Gets or sets a value indicating whether forums are enabled /// - public bool ForumsEnabled { get; set; } + public bool ForumsEnabled { get; set; } = true; /// /// Gets or sets a value indicating whether relative date and time formatting is enabled (e.g. 2 hours ago, a month ago) /// - public bool RelativeDateTimeFormattingEnabled { get; set; } + public bool RelativeDateTimeFormattingEnabled { get; set; } = true; /// /// Gets or sets a value indicating whether customers are allowed to edit posts that they created @@ -68,7 +42,7 @@ public ForumSettings() /// /// Gets or sets a value indicating whether customer can vote on posts /// - public bool AllowCustomersToVoteOnPosts { get; set; } + public bool AllowCustomersToVoteOnPosts { get; set; } = true; /// /// Gets or sets a value indicating whether guests are allowed to vote on posts @@ -78,57 +52,57 @@ public ForumSettings() /// /// Gets or sets maximum length of topic subject /// - public int TopicSubjectMaxLength { get; set; } + public int TopicSubjectMaxLength { get; set; } = 450; /// /// Gets or sets the maximum length for stripped forum topic names /// - public int StrippedTopicMaxLength { get; set; } + public int StrippedTopicMaxLength { get; set; } = 45; /// /// Gets or sets maximum length of post /// - public int PostMaxLength { get; set; } + public int PostMaxLength { get; set; } = 4000; /// /// Gets or sets the page size for topics in forums /// - public int TopicsPageSize { get; set; } + public int TopicsPageSize { get; set; } = 20; /// /// Gets or sets the page size for posts in topics /// - public int PostsPageSize { get; set; } + public int PostsPageSize { get; set; } = 20; /// /// Gets or sets the page size for search result /// - public int SearchResultsPageSize { get; set; } + public int SearchResultsPageSize { get; set; } = 20; /// /// Gets or sets a value indicating whether sorting is enabled. /// - public bool AllowSorting { get; set; } + public bool AllowSorting { get; set; } = true; /// /// Gets or sets the page size for latest customer posts /// - public int LatestCustomerPostsPageSize { get; set; } + public int LatestCustomerPostsPageSize { get; set; } = 20; /// /// Gets or sets a value indicating whether to show customers forum post count /// - public bool ShowCustomersPostCount { get; set; } + public bool ShowCustomersPostCount { get; set; } = true; /// /// Gets or sets a forum editor type /// - public EditorType ForumEditor { get; set; } + public EditorType ForumEditor { get; set; } = EditorType.BBCodeEditor; /// /// Gets or sets a value indicating whether customers are allowed to specify a signature /// - public bool SignaturesEnabled { get; set; } + public bool SignaturesEnabled { get; set; } = true; /// /// Gets or sets a value indicating whether private messages are allowed @@ -143,12 +117,12 @@ public ForumSettings() /// /// Gets or sets the page size for private messages /// - public int PrivateMessagesPageSize { get; set; } + public int PrivateMessagesPageSize { get; set; } = 20; /// /// Gets or sets the page size for (My Account) Forum Subscriptions /// - public int ForumSubscriptionsPageSize { get; set; } + public int ForumSubscriptionsPageSize { get; set; } = 20; /// /// Gets or sets a value indicating whether a customer should be notified about new private messages @@ -158,27 +132,27 @@ public ForumSettings() /// /// Gets or sets maximum length of pm subject /// - public int PMSubjectMaxLength { get; set; } + public int PMSubjectMaxLength { get; set; } = 450; /// /// Gets or sets maximum length of pm message /// - public int PMTextMaxLength { get; set; } + public int PMTextMaxLength { get; set; } = 4000; /// /// Gets or sets the number of items to display for Active Discussions on forums home page /// - public int HomePageActiveDiscussionsTopicCount { get; set; } + public int HomePageActiveDiscussionsTopicCount { get; set; } = 5; /// /// Gets or sets the number of items to display for Active Discussions page /// - public int ActiveDiscussionsPageTopicCount { get; set; } + public int ActiveDiscussionsPageTopicCount { get; set; } = 50; /// /// Gets or sets the number of items to display for Active Discussions RSS Feed /// - public int ActiveDiscussionsFeedCount { get; set; } + public int ActiveDiscussionsFeedCount { get; set; } = 25; /// /// Gets or sets the whether the Active Discussions RSS Feed is enabled @@ -193,6 +167,21 @@ public ForumSettings() /// /// Gets or sets the number of items to display for Forum RSS Feed /// - public int ForumFeedCount { get; set; } + public int ForumFeedCount { get; set; } = 20; + + /// + /// Gets or sets the meta title for forum index page + /// + public string MetaTitle { get; set; } + + /// + /// Gets or sets the meta description for forum index page + /// + public string MetaDescription { get; set; } + + /// + /// Gets or sets the meta keywords for forum index page + /// + public string MetaKeywords { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Forums/ForumTopic.cs b/src/Libraries/SmartStore.Core/Domain/Forums/ForumTopic.cs index 63d6134530..d7059778c1 100644 --- a/src/Libraries/SmartStore.Core/Domain/Forums/ForumTopic.cs +++ b/src/Libraries/SmartStore.Core/Domain/Forums/ForumTopic.cs @@ -8,7 +8,7 @@ namespace SmartStore.Core.Domain.Forums /// Represents a forum topic /// public partial class ForumTopic : BaseEntity, IAuditable - { + { /// /// Gets or sets the forum identifier /// @@ -87,14 +87,8 @@ public partial class ForumTopic : BaseEntity, IAuditable /// public ForumTopicType ForumTopicType { - get - { - return (ForumTopicType)this.TopicTypeId; - } - set - { - this.TopicTypeId = (int)value; - } + get => (ForumTopicType)this.TopicTypeId; + set => this.TopicTypeId = (int)value; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Forums/PrivateMessage.cs b/src/Libraries/SmartStore.Core/Domain/Forums/PrivateMessage.cs index 0c7bfa9265..3e447d6e17 100644 --- a/src/Libraries/SmartStore.Core/Domain/Forums/PrivateMessage.cs +++ b/src/Libraries/SmartStore.Core/Domain/Forums/PrivateMessage.cs @@ -8,10 +8,10 @@ namespace SmartStore.Core.Domain.Forums /// public partial class PrivateMessage : BaseEntity { - /// - /// Gets or sets the store identifier - /// - public int StoreId { get; set; } + /// + /// Gets or sets the store identifier + /// + public int StoreId { get; set; } /// /// Gets or sets the customer identifier who sent the message diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs b/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs index 5f022ea319..49f6e41be7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs @@ -6,6 +6,6 @@ namespace SmartStore.Core.Domain.Localization /// public interface ILocalizedEntity { - int Id { get; } - } + int Id { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs b/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs index 1d4b30386e..f80cb6bb74 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using System.Runtime.Serialization; using System.Diagnostics; using System.Globalization; +using System.Runtime.Serialization; using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Localization @@ -10,8 +10,8 @@ namespace SmartStore.Core.Domain.Localization /// Represents a language /// [DataContract] - [DebuggerDisplay("{LanguageCulture}")] - public partial class Language : BaseEntity, IStoreMappingSupported + [DebuggerDisplay("{LanguageCulture}")] + public partial class Language : BaseEntity, IStoreMappingSupported { private ICollection _localeStringResources; @@ -19,74 +19,74 @@ public partial class Language : BaseEntity, IStoreMappingSupported /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the language culture (e.g. "en-US") /// [DataMember] - public string LanguageCulture { get; set; } + public string LanguageCulture { get; set; } /// /// Gets or sets the unique SEO code (e.g. "en") /// [DataMember] - public string UniqueSeoCode { get; set; } - + public string UniqueSeoCode { get; set; } + /// /// Gets or sets the flag image file name /// - [DataMember] - public string FlagImageFileName { get; set; } + [DataMember] + public string FlagImageFileName { get; set; } /// /// Gets or sets a value indicating whether the language supports "Right-to-left" /// [DataMember] - public bool Rtl { get; set; } + public bool Rtl { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } /// /// Gets or sets a value indicating whether the language is published /// [DataMember] - public bool Published { get; set; } + public bool Published { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } - + public int DisplayOrder { get; set; } + /// /// Gets or sets locale string resources /// public virtual ICollection LocaleStringResources { - get { return _localeStringResources ?? (_localeStringResources = new HashSet()); } - protected set { _localeStringResources = value; } + get => _localeStringResources ?? (_localeStringResources = new HashSet()); + protected set => _localeStringResources = value; } - public string GetTwoLetterISOLanguageName() - { - if (UniqueSeoCode.HasValue()) - { - return UniqueSeoCode; - } + public string GetTwoLetterISOLanguageName() + { + if (UniqueSeoCode.HasValue()) + { + return UniqueSeoCode; + } - try - { - var ci = new CultureInfo(LanguageCulture); - return ci.TwoLetterISOLanguageName; - } - catch { } + try + { + var ci = new CultureInfo(LanguageCulture); + return ci.TwoLetterISOLanguageName; + } + catch { } - return null; - } + return null; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs index e523cb11ea..ee8df7af19 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs @@ -4,14 +4,14 @@ namespace SmartStore.Core.Domain.Localization { public class LocalizationSettings : ISettings { - public LocalizationSettings() - { - UseImagesForLanguageSelection = true; - DefaultLanguageRedirectBehaviour = DefaultLanguageRedirectBehaviour.StripSeoCode; - InvalidLanguageRedirectBehaviour = InvalidLanguageRedirectBehaviour.ReturnHttp404; - } - - /// + public LocalizationSettings() + { + UseImagesForLanguageSelection = true; + DefaultLanguageRedirectBehaviour = DefaultLanguageRedirectBehaviour.StripSeoCode; + InvalidLanguageRedirectBehaviour = InvalidLanguageRedirectBehaviour.ReturnHttp404; + } + + /// /// Default admin area language identifier /// public int DefaultAdminLanguageId { get; set; } @@ -22,7 +22,7 @@ public LocalizationSettings() public bool UseImagesForLanguageSelection { get; set; } /// - /// A value indicating whether the browser user lannguage should be detected + /// A value indicating whether the browser user language should be detected /// public bool DetectBrowserUserLanguage { get; set; } @@ -42,6 +42,11 @@ public LocalizationSettings() /// /// This setting is ignored when SeoFriendlyUrlsForLanguagesEnabled is false public InvalidLanguageRedirectBehaviour InvalidLanguageRedirectBehaviour { get; set; } + + /// + /// Whether to display region/country name in language selector (e.g. "Deutsch (Deutschland)" instead of "Deutsch") + /// + public bool DisplayRegionInLanguageSelector { get; set; } } public enum DefaultLanguageRedirectBehaviour diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs index 6996cf8549..521252dd36 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs @@ -13,41 +13,41 @@ public partial class LocalizedProperty : BaseEntity /// Gets or sets the entity identifier /// [DataMember] - [Index("IX_LocalizedProperty_Compound", Order = 1)] - public int EntityId { get; set; } + [Index("IX_LocalizedProperty_Compound", Order = 1)] + public int EntityId { get; set; } /// /// Gets or sets the language identifier /// [DataMember] - [Index("IX_LocalizedProperty_Compound", Order = 4)] - public int LanguageId { get; set; } + [Index("IX_LocalizedProperty_Compound", Order = 4)] + public int LanguageId { get; set; } /// /// Gets or sets the locale key group /// [DataMember] - [Index("IX_LocalizedProperty_Compound", Order = 3)] - [Index("IX_LocalizedProperty_LocaleKeyGroup")] - public string LocaleKeyGroup { get; set; } + [Index("IX_LocalizedProperty_Compound", Order = 3)] + [Index("IX_LocalizedProperty_LocaleKeyGroup")] + public string LocaleKeyGroup { get; set; } /// /// Gets or sets the locale key /// [DataMember] - [Index("IX_LocalizedProperty_Compound", Order = 2)] - public string LocaleKey { get; set; } + [Index("IX_LocalizedProperty_Compound", Order = 2)] + public string LocaleKey { get; set; } /// /// Gets or sets the locale value /// [DataMember] - public string LocaleValue { get; set; } - + public string LocaleValue { get; set; } + /// /// Gets the language /// - [DataMember] - public virtual Language Language { get; set; } + [DataMember] + public virtual Language Language { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs index 9cd5549e0f..0537f9868e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedPropertyCollection.cs @@ -1,90 +1,96 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Globalization; namespace SmartStore.Core.Domain.Localization { - public class LocalizedPropertyCollection : IReadOnlyCollection - { - private readonly string _keyGroup; - private readonly IDictionary _dict; - private HashSet _requestedSet; + public class LocalizedPropertyCollection : IReadOnlyCollection + { + private readonly string _keyGroup; + private readonly IDictionary _dict; + private HashSet _requestedSet; - public LocalizedPropertyCollection(string keyGroup, int[] requestedSet, IEnumerable items) - { - Guard.NotEmpty(keyGroup, nameof(keyGroup)); - Guard.NotNull(items, nameof(items)); + public LocalizedPropertyCollection(string keyGroup, int[] requestedSet, IEnumerable items) + { + Guard.NotEmpty(keyGroup, nameof(keyGroup)); + Guard.NotNull(items, nameof(items)); - _keyGroup = keyGroup; - _dict = items.ToDictionarySafe(x => CreateKey(x.LocaleKey, x.EntityId, x.LanguageId), StringComparer.OrdinalIgnoreCase); + _keyGroup = keyGroup; + _dict = items.ToDictionarySafe(x => CreateKey(x.LocaleKey, x.EntityId, x.LanguageId), StringComparer.OrdinalIgnoreCase); - if (requestedSet != null && requestedSet.Length > 0) - { - _requestedSet = new HashSet(requestedSet); - } - } + if (requestedSet != null && requestedSet.Length > 0) + { + _requestedSet = new HashSet(requestedSet); + } + } - public void MergeWith(LocalizedPropertyCollection other) - { - Guard.NotNull(other, nameof(other)); + public void MergeWith(LocalizedPropertyCollection other) + { + Guard.NotNull(other, nameof(other)); - if (!this._keyGroup.IsCaseInsensitiveEqual(other._keyGroup)) - { - throw new InvalidOperationException("Expected keygroup '{0}', but was '{1}'".FormatInvariant(this._keyGroup, other._keyGroup)); - } + if (!this._keyGroup.IsCaseInsensitiveEqual(other._keyGroup)) + { + throw new InvalidOperationException("Expected keygroup '{0}', but was '{1}'".FormatInvariant(this._keyGroup, other._keyGroup)); + } - // Merge dictionary - other._dict.Merge(this._dict, true); + // Merge dictionary + other._dict.Merge(this._dict, true); - // Merge requested set (entity ids) - if (this._requestedSet != null) - { - if (other._requestedSet == null) - { - other._requestedSet = new HashSet(this._requestedSet); - } - else - { - other._requestedSet.AddRange(this._requestedSet); - } - } - } + // Merge requested set (entity ids) + if (this._requestedSet != null) + { + if (other._requestedSet == null) + { + other._requestedSet = new HashSet(this._requestedSet); + } + else + { + other._requestedSet.AddRange(this._requestedSet); + } + } + } - public string GetValue(int languageId, int entityId, string localeKey) - { - return Find(languageId, entityId, localeKey)?.LocaleValue; - } + public string GetValue(int languageId, int entityId, string localeKey) + { + return Find(languageId, entityId, localeKey)?.LocaleValue; + } - public LocalizedProperty Find(int languageId, int entityId, string localeKey) - { - var item = _dict.Get(CreateKey(localeKey, entityId, languageId)); + public LocalizedProperty Find(int languageId, int entityId, string localeKey) + { + var item = _dict.Get(CreateKey(localeKey, entityId, languageId)); - if (item == null && (_requestedSet == null || _requestedSet.Contains(entityId))) - { - // Although the item does not exist in the local dictionary it has been requested - // from the database, which means it does not exist in the db either. - // Avoid the upcoming roundtrip. - return new LocalizedProperty - { - LocaleKeyGroup = _keyGroup, - EntityId = entityId, - LanguageId = languageId, - LocaleKey = localeKey - }; - } + if (item == null && (_requestedSet == null || _requestedSet.Contains(entityId))) + { + // Although the item does not exist in the local dictionary it has been requested + // from the database, which means it does not exist in the db either. + // Avoid the upcoming roundtrip. + return new LocalizedProperty + { + LocaleKeyGroup = _keyGroup, + EntityId = entityId, + LanguageId = languageId, + LocaleKey = localeKey + }; + } - return item; - } + return item; + } - private string CreateKey(string localeKey, int entityId, int languageId) - { - return string.Concat(localeKey, "-", entityId.ToString(CultureInfo.InvariantCulture), "-", languageId); - } + private string CreateKey(string localeKey, int entityId, int languageId) + { + return string.Concat(localeKey, "-", entityId.ToString(CultureInfo.InvariantCulture), "-", languageId); + } - public int Count => _dict.Values.Count; - public IEnumerator GetEnumerator() => _dict.Values.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _dict.Values.GetEnumerator(); - } + public int Count => _dict.Values.Count; + public IEnumerator GetEnumerator() + { + return _dict.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _dict.Values.GetEnumerator(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Logging/ActivityLogType.cs b/src/Libraries/SmartStore.Core/Domain/Logging/ActivityLogType.cs index eb59856a02..14e36bac16 100644 --- a/src/Libraries/SmartStore.Core/Domain/Logging/ActivityLogType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Logging/ActivityLogType.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; - -namespace SmartStore.Core.Domain.Logging +namespace SmartStore.Core.Domain.Logging { /// /// Represents an activity log type record /// public partial class ActivityLogType : BaseEntity { - + /// /// Gets or sets the system keyword /// diff --git a/src/Libraries/SmartStore.Core/Domain/Logging/Log.cs b/src/Libraries/SmartStore.Core/Domain/Logging/Log.cs index f1ab2cf330..a907f17dfc 100644 --- a/src/Libraries/SmartStore.Core/Domain/Logging/Log.cs +++ b/src/Libraries/SmartStore.Core/Domain/Logging/Log.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations.Schema; +using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Logging; @@ -8,13 +9,14 @@ namespace SmartStore.Core.Domain.Logging /// /// Represents a log record /// + [Hookable(false)] public partial class Log : BaseEntity { - /// - /// Gets or sets the log level identifier - /// - [Index("IX_Log_Level", IsUnique = false)] - public int LogLevelId { get; set; } + /// + /// Gets or sets the log level identifier + /// + [Index("IX_Log_Level", IsUnique = false)] + public int LogLevelId { get; set; } /// /// Gets or sets the short message @@ -51,35 +53,29 @@ public partial class Log : BaseEntity /// public DateTime CreatedOnUtc { get; set; } - /// - /// Gets or sets the logger name - /// - [Index("IX_Log_Logger", IsUnique = false)] - public string Logger { get; set; } + /// + /// Gets or sets the logger name + /// + [Index("IX_Log_Logger", IsUnique = false)] + public string Logger { get; set; } - /// - /// Gets or sets the HTTP method - /// - public string HttpMethod { get; set; } + /// + /// Gets or sets the HTTP method + /// + public string HttpMethod { get; set; } - /// - /// Gets or sets the user name - /// - public string UserName { get; set; } + /// + /// Gets or sets the user name + /// + public string UserName { get; set; } - /// - /// Gets or sets the log level - /// - public LogLevel LogLevel + /// + /// Gets or sets the log level + /// + public LogLevel LogLevel { - get - { - return (LogLevel)this.LogLevelId; - } - set - { - this.LogLevelId = (int)value; - } + get => (LogLevel)this.LogLevelId; + set => this.LogLevelId = (int)value; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Media/Download.cs b/src/Libraries/SmartStore.Core/Domain/Media/Download.cs index da677877e8..95c3a6351e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/Download.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/Download.cs @@ -1,5 +1,3 @@ -using NuGet; -using SmartStore.Core.Domain.Catalog; using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -11,40 +9,40 @@ namespace SmartStore.Core.Domain.Media /// Represents a download /// [DataContract] - public partial class Download : BaseEntity, ITransient - { - /// + public partial class Download : BaseEntity//, ITransient + { + /// /// Gets or sets a GUID /// - [DataMember] - [Index] - public Guid DownloadGuid { get; set; } + [DataMember] + [Index] + public Guid DownloadGuid { get; set; } /// /// Gets or sets a value indicating whether DownloadUrl property should be used /// [DataMember] - public bool UseDownloadUrl { get; set; } + public bool UseDownloadUrl { get; set; } /// /// Gets or sets a download URL /// [DataMember] - public string DownloadUrl { get; set; } + public string DownloadUrl { get; set; } - /// - /// Gets or sets a value indicating whether the entity transient/preliminary - /// - [DataMember] - [Index("IX_UpdatedOn_IsTransient", 1)] - public bool IsTransient { get; set; } + /// + /// Gets or sets a value indicating whether the entity transient/preliminary + /// + [DataMember] + [Index("IX_UpdatedOn_IsTransient", 1)] + public bool IsTransient { get; set; } - /// - /// Gets or sets the date and time of instance update - /// - [DataMember] - [Index("IX_UpdatedOn_IsTransient", 0)] - public DateTime UpdatedOnUtc { get; set; } + /// + /// Gets or sets the date and time of instance update + /// + [DataMember] + [Index("IX_UpdatedOn_IsTransient", 0)] + public DateTime UpdatedOnUtc { get; set; } /// /// Gets or sets the media file identifier @@ -57,7 +55,7 @@ public partial class Download : BaseEntity, ITransient /// [DataMember] public virtual MediaFile MediaFile { get; set; } - + /// /// Gets or sets a value indicating the corresponding entity id /// diff --git a/src/Libraries/SmartStore.Core/Domain/Media/Enums.cs b/src/Libraries/SmartStore.Core/Domain/Media/Enums.cs index 94eb05184c..cb16901a9c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/Enums.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/Enums.cs @@ -5,12 +5,12 @@ namespace SmartStore.Core.Domain.Media /// public enum FallbackPictureType { - NoFallback = 0, + NoFallback = 0, - /// - /// Entities (products, categories, manufacturers) - /// - Entity = 1, + /// + /// Entities (products, categories, manufacturers) + /// + Entity = 1, } public enum ThumbnailScaleMode diff --git a/src/Libraries/SmartStore.Core/Domain/Media/IHasMedia.cs b/src/Libraries/SmartStore.Core/Domain/Media/IHasMedia.cs index 7cee5227c9..f0b70edaba 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/IHasMedia.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/IHasMedia.cs @@ -1,15 +1,15 @@ namespace SmartStore.Core.Domain.Media { - public interface IHasMedia - { - /// - /// Gets or sets the media storage identifier - /// - int? MediaStorageId { get; set; } + public interface IHasMedia + { + /// + /// Gets or sets the media storage identifier + /// + int? MediaStorageId { get; set; } - /// - /// Gets or sets the media storage - /// - MediaStorage MediaStorage { get; set; } - } + /// + /// Gets or sets the media storage + /// + MediaStorage MediaStorage { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaAlbum.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaAlbum.cs index 7eddbe81e2..7d95e78ceb 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaAlbum.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaAlbum.cs @@ -1,5 +1,4 @@ -using System; -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace SmartStore.Core.Domain.Media { diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaFile.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaFile.cs index 2e5c6f15ca..2681666518 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaFile.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaFile.cs @@ -7,18 +7,44 @@ namespace SmartStore.Core.Domain.Media { - [DataContract] - public partial class MediaFile : BaseEntity, ITransient, IHasMedia, IAuditable, ISoftDeletable, ILocalizedEntity - { - private ICollection _productMediaFiles; - private ICollection _tags; - private ICollection _tracks; + [DataContract] + public partial class MediaFile : BaseEntity, ITransient, IHasMedia, IAuditable, ISoftDeletable, ILocalizedEntity + { + #region static - /// - /// Gets or sets the associated folder identifier. - /// - [DataMember] - public int? FolderId { get; set; } + private static readonly HashSet _outputAffectingProductProps = new HashSet + { + nameof(MediaFile.FolderId), + nameof(MediaFile.Name), + nameof(MediaFile.Alt), + nameof(MediaFile.Title), + nameof(MediaFile.Hidden), + nameof(MediaFile.Deleted) + }; + + public static IReadOnlyCollection GetOutputAffectingPropertyNames() + { + return _outputAffectingProductProps; + } + + #endregion + + private ICollection _productMediaFiles; + private ICollection _tags; + private ICollection _tracks; + + /// + /// Gets or sets the associated folder identifier. + /// + [DataMember] + [Index("IX_Media_MediaType", 0)] + [Index("IX_Media_Extension", 0)] + [Index("IX_Media_PixelSize", 0)] + [Index("IX_Media_Name", 0)] + [Index("IX_Media_Size", 0)] + [Index("IX_Media_UpdatedOnUtc", 0)] + [Index("IX_Media_FolderId", 0)] + public int? FolderId { get; set; } /// /// Gets or sets the associated folder. @@ -26,147 +52,160 @@ public partial class MediaFile : BaseEntity, ITransient, IHasMedia, IAuditable, [DataMember] public virtual MediaFolder Folder { get; set; } - /// - /// Gets or sets the SEO friendly name of the media file including file extension - /// - [DataMember] - public string Name { get; set; } - - /// - /// Gets or sets the localizable image ALT text - /// - [DataMember] - public string Alt { get; set; } - - /// - /// Gets or sets the localizable media file title text - /// - [DataMember] - public string Title { get; set; } - - /// - /// Gets or sets the (dotless) file extension - /// - [DataMember] - public string Extension { get; set; } - - /// - /// Gets or sets the file MIME type - /// - [DataMember] - public string MimeType { get; set; } - - /// - /// Gets or sets the file media type (image, video, audio, document etc.) - /// - [DataMember] - public string MediaType { get; set; } - - /// - /// Gets or sets the file size in bytes - /// - [DataMember] - public int Size { get; set; } - - /// - /// Gets or sets the total pixel size of an image (width * height) - /// - [DataMember] - public int? PixelSize { get; set; } - - /// - /// Gets or sets the file metadata as raw JSON dictionary (width, height, video length, EXIF etc.) - /// - [DataMember] - public string Metadata { get; set; } - - /// - /// Gets or sets the image width (if file is an image) - /// - [DataMember] - public int? Width { get; set; } - - /// - /// Gets or sets the image height (if file is an image) - /// - [DataMember] - public int? Height { get; set; } - - /// - /// Gets or sets the date and time of instance update - /// - [DataMember] - [Index("IX_CreatedOn_IsTransient", 0)] - public DateTime CreatedOnUtc { get; set; } - - /// - /// Gets or sets the date and time of instance update - /// - [DataMember] - public DateTime UpdatedOnUtc { get; set; } - - /// - /// Gets or sets a value indicating whether the file is transient/preliminary - /// - [DataMember] - [Index("IX_CreatedOn_IsTransient", 1)] - public bool IsTransient { get; set; } - - /// - /// Gets or sets a value indicating whether the file has been soft deleted - /// - public bool Deleted { get; set; } - - /// - /// Gets or sets a value indicating whether the file is hidden - /// - public bool Hidden { get; set; } - - /// - /// Internally used for migration stuff only. - /// 0 = needs migration 'cause existed in previous versions already, 1 = was migrated by migrator, 2 = relations has been detected - /// - public int Version { get; set; } = 2; - - /// - /// Gets or sets the media storage identifier - /// - [DataMember] - public int? MediaStorageId { get; set; } - - /// - /// Gets or sets the media storage - /// - public virtual MediaStorage MediaStorage { get; set; } - - /// - /// Gets the associated tags - /// - [DataMember] - public virtual ICollection Tags - { - get { return _tags ?? (_tags = new HashSet()); } - protected set { _tags = value; } - } - - /// - /// Gets the related entity tracks - /// - [DataMember] - public virtual ICollection Tracks - { - get { return _tracks ?? (_tracks = new HashSet()); } - protected set { _tracks = value; } - } - - /// - /// Gets or sets the product pictures - /// - [DataMember] - public virtual ICollection ProductMediaFiles + /// + /// Gets or sets the SEO friendly name of the media file including file extension + /// + [DataMember] + [Index("IX_Media_Name", 1)] + public string Name { get; set; } + + /// + /// Gets or sets the localizable image ALT text + /// + [DataMember] + public string Alt { get; set; } + + /// + /// Gets or sets the localizable media file title text + /// + [DataMember] + public string Title { get; set; } + + /// + /// Gets or sets the (dotless) file extension + /// + [DataMember] + [Index("IX_Media_MediaType", 2)] + [Index("IX_Media_Extension", 1)] + public string Extension { get; set; } + + /// + /// Gets or sets the file MIME type + /// + [DataMember] + public string MimeType { get; set; } + + /// + /// Gets or sets the file media type (image, video, audio, document etc.) + /// + [DataMember] + [Index("IX_Media_MediaType", 1)] + public string MediaType { get; set; } + + /// + /// Gets or sets the file size in bytes + /// + [DataMember] + [Index("IX_Media_Size", 1)] + public int Size { get; set; } + + /// + /// Gets or sets the total pixel size of an image (width * height) + /// + [DataMember] + [Index("IX_Media_MediaType", 3)] + [Index("IX_Media_Extension", 2)] + [Index("IX_Media_PixelSize", 1)] + public int? PixelSize { get; set; } + + /// + /// Gets or sets the file metadata as raw JSON dictionary (width, height, video length, EXIF etc.) + /// + [DataMember] + public string Metadata { get; set; } + + /// + /// Gets or sets the image width (if file is an image) + /// + [DataMember] + public int? Width { get; set; } + + /// + /// Gets or sets the image height (if file is an image) + /// + [DataMember] + public int? Height { get; set; } + + /// + /// Gets or sets the date and time of instance update + /// + [DataMember] + public DateTime CreatedOnUtc { get; set; } + + /// + /// Gets or sets the date and time of instance update + /// + [DataMember] + public DateTime UpdatedOnUtc { get; set; } + + /// + /// Gets or sets a value indicating whether the file is transient/preliminary + /// + [DataMember] + public bool IsTransient { get; set; } + + /// + /// Gets or sets a value indicating whether the file has been soft deleted + /// + [Index("IX_Media_MediaType", 4)] + [Index("IX_Media_Extension", 3)] + [Index("IX_Media_PixelSize", 2)] + [Index("IX_Media_Name", 2)] + [Index("IX_Media_Size", 2)] + [Index("IX_Media_UpdatedOnUtc", 2)] + [Index("IX_Media_FolderId", 1)] + public bool Deleted { get; set; } + + /// + /// Gets or sets a value indicating whether the file is hidden + /// + public bool Hidden { get; set; } + + /// + /// Internally used for migration stuff only. + /// 0 = needs migration 'cause existed in previous versions already, 1 = was migrated by migrator, 2 = relations has been detected + /// + public int Version { get; set; } = 2; + + /// + /// Gets or sets the media storage identifier + /// + [DataMember] + public int? MediaStorageId { get; set; } + + /// + /// Gets or sets the media storage + /// + public virtual MediaStorage MediaStorage { get; set; } + + /// + /// Gets the associated tags + /// + [DataMember] + public virtual ICollection Tags + { + get => _tags ?? (_tags = new HashSet()); + protected set => _tags = value; + } + + /// + /// Gets the related entity tracks + /// + [DataMember] + public virtual ICollection Tracks + { + get => _tracks ?? (_tracks = new HashSet()); + protected set => _tracks = value; + } + + /// + /// Gets or sets the product pictures + /// + [DataMember] + public virtual ICollection ProductMediaFiles { - get { return _productMediaFiles ?? (_productMediaFiles = new HashSet()); } - protected set { _productMediaFiles = value; } - } + get => _productMediaFiles ?? (_productMediaFiles = new HashSet()); + protected set => _productMediaFiles = value; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaFolder.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaFolder.cs index 7776d4211f..3598c1bb8d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaFolder.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaFolder.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; @@ -47,8 +46,8 @@ public partial class MediaFolder : BaseEntity /// public virtual ICollection Children { - get { return _children ?? (_children = new HashSet()); } - protected set { _children = value; } + get => _children ?? (_children = new HashSet()); + protected set => _children = value; } /// @@ -68,8 +67,8 @@ public virtual ICollection Children /// public virtual ICollection Files { - get { return _files ?? (_files = new HashSet()); } - protected set { _files = value; } + get => _files ?? (_files = new HashSet()); + protected set => _files = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs index a2adfa735e..1ed1fbbbea 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs @@ -1,85 +1,171 @@ -using SmartStore.Core.Configuration; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Media { public class MediaSettings : ISettings { - public int AvatarPictureSize { get; set; } = 256; - public int ProductThumbPictureSize { get; set; } = 256; - public int ProductDetailsPictureSize { get; set; } = 600; - public int ProductThumbPictureSizeOnProductDetailsPage { get; set; } = 72; - public int MessageProductThumbPictureSize { get; set; } = 72; - public int AssociatedProductPictureSize { get; set; } = 600; - public int BundledProductPictureSize { get; set; } = 72; - public int CategoryThumbPictureSize { get; set; } = 256; - public int ManufacturerThumbPictureSize { get; set; } = 256; - public int CartThumbPictureSize { get; set; } = 256; - public int CartThumbBundleItemPictureSize { get; set; } = 32; - public int MiniCartThumbPictureSize { get; set; } = 256; - public int VariantValueThumbPictureSize { get; set; } = 72; - public int AttributeOptionThumbPictureSize { get; set; } = 72; - - public bool DefaultPictureZoomEnabled { get; set; } = true; - public string PictureZoomType { get; set; } = "window"; - - public int MaximumImageSize { get; set; } = 2048; - - /// - /// Geta or sets a default quality used for image generation - /// - public int DefaultImageQuality { get; set; } = 90; - - /// - /// Gets or sets the height to width ratio for thumbnails in grid style lists (0.2 - 2) - /// - /// - /// A value greater than 1 indicates, that your product pictures are generally - /// in portrait format, less than 1 indicates landscape format. - /// - public float DefaultThumbnailAspectRatio { get; set; } = 1; - - /// - /// Geta or sets a vaue indicating whether single (/media/thumbs/) or multiple (/media/thumbs/0001/ and /media/thumbs/0002/) directories will used for picture thumbs - /// - public bool MultipleThumbDirectories { get; set; } = true; - - /// - /// Generates absolute media urls based upon current request uri instead of relative urls. - /// - public bool AutoGenerateAbsoluteUrls { get; set; } = true; - - /// - /// Whether orphaned files should automatically be marked as transient so that the daily cleanup task may delete them. - /// - public bool MakeFilesTransientWhenOrphaned { get; set; } = true; - - #region MediaTypes - - /// - /// A space separated list of image type file extensions (dotless) - /// - public string ImageTypes { get; set; } - - /// - /// A space separated list of video type file extensions (dotless) - /// - public string VideoTypes { get; set; } - - /// - /// A space separated list of audio type file extensions (dotless) - /// - public string AudioTypes { get; set; } - - /// - /// A space separated list of document type file extensions (dotless) - /// - public string DocumentTypes { get; set; } - - /// - /// A space separated list of text type file extensions (dotless) - /// - public string TextTypes { get; set; } - - #endregion - } + public bool DefaultPictureZoomEnabled { get; set; } = true; + public string PictureZoomType { get; set; } = "window"; + + /// + /// Geta or sets a default quality used for image generation + /// + public int DefaultImageQuality { get; set; } = 90; + + /// + /// Gets or sets the height to width ratio for thumbnails in grid style lists (0.2 - 2) + /// + /// + /// A value greater than 1 indicates, that your product pictures are generally + /// in portrait format, less than 1 indicates landscape format. + /// + public float DefaultThumbnailAspectRatio { get; set; } = 1; + + /// + /// Geta or sets a vaue indicating whether single (/media/thumbs/) or multiple (/media/thumbs/0001/ and /media/thumbs/0002/) directories will used for picture thumbs + /// + public bool MultipleThumbDirectories { get; set; } = true; + + /// + /// Generates absolute media urls based upon current request uri instead of relative urls. + /// + public bool AutoGenerateAbsoluteUrls { get; set; } = true; + + /// + /// Whether orphaned files should automatically be marked as transient so that the daily cleanup task may delete them. + /// + public bool MakeFilesTransientWhenOrphaned { get; set; } = true; + + /// + /// Gets or sets the maximum size (in KB) of an uploaded media file. The default is 102,400 (100 MB). + /// + public long MaxUploadFileSize { get; set; } = 102400; + + #region Thumb sizes / security + + private HashSet _allowedThumbSizes; + + public const int ThumbnailSizeXxs = 32; + public const int ThumbnailSizeXs = 72; + public const int ThumbnailSizeSm = 128; + public const int ThumbnailSizeMd = 256; + public const int ThumbnailSizeLg = 512; + public const int ThumbnailSizeXl = 600; + public const int ThumbnailSizeXxl = 1024; + public const int MaxImageSize = 2048; + + public int AvatarPictureSize { get; set; } = ThumbnailSizeMd; + public int ProductThumbPictureSize { get; set; } = ThumbnailSizeMd; + public int ProductDetailsPictureSize { get; set; } = ThumbnailSizeXl; + public int ProductThumbPictureSizeOnProductDetailsPage { get; set; } = ThumbnailSizeXs; + public int MessageProductThumbPictureSize { get; set; } = ThumbnailSizeXs; + public int AssociatedProductPictureSize { get; set; } = ThumbnailSizeXl; + public int BundledProductPictureSize { get; set; } = ThumbnailSizeXs; + public int CategoryThumbPictureSize { get; set; } = ThumbnailSizeMd; + public int ManufacturerThumbPictureSize { get; set; } = ThumbnailSizeMd; + public int CartThumbPictureSize { get; set; } = ThumbnailSizeMd; + public int CartThumbBundleItemPictureSize { get; set; } = ThumbnailSizeXxs; + public int MiniCartThumbPictureSize { get; set; } = ThumbnailSizeMd; + public int VariantValueThumbPictureSize { get; set; } = ThumbnailSizeXs; + public int AttributeOptionThumbPictureSize { get; set; } = ThumbnailSizeXs; + + public List AllowedExtraThumbnailSizes { get; set; } + + public int MaximumImageSize { get; set; } = MaxImageSize; + + public int[] GetAllowedThumbnailSizes() + { + EnsureThumbSizeWhitelist(); + return _allowedThumbSizes.OrderBy(x => x).ToArray(); + } + + public bool IsAllowedThumbnailSize(int size) + { + EnsureThumbSizeWhitelist(); + return _allowedThumbSizes.Contains(size); + } + + public int GetNextValidThumbnailSize(int currentSize) + { + var allowedSizes = GetAllowedThumbnailSizes(); + foreach (var size in allowedSizes) + { + if (size >= currentSize) + { + return size; + } + } + + return MaxImageSize; + } + + private void EnsureThumbSizeWhitelist() + { + if (_allowedThumbSizes != null) + return; + + _allowedThumbSizes = new HashSet + { + 48, ThumbnailSizeXxs, ThumbnailSizeXs, ThumbnailSizeSm, ThumbnailSizeMd, ThumbnailSizeLg, ThumbnailSizeXl, ThumbnailSizeXxl, + AvatarPictureSize, + ProductThumbPictureSize, + ProductDetailsPictureSize, + ProductThumbPictureSizeOnProductDetailsPage, + MessageProductThumbPictureSize, + AssociatedProductPictureSize, + BundledProductPictureSize, + CategoryThumbPictureSize, + ManufacturerThumbPictureSize, + CartThumbPictureSize, + CartThumbBundleItemPictureSize, + MiniCartThumbPictureSize, + VariantValueThumbPictureSize, + AttributeOptionThumbPictureSize, + MaxImageSize + }; + + if (AllowedExtraThumbnailSizes?.Count > 0) + { + _allowedThumbSizes.AddRange(AllowedExtraThumbnailSizes); + } + } + + #endregion + + #region Media types + + /// + /// A space separated list of image type file extensions (dotless) + /// + public string ImageTypes { get; set; } + + /// + /// A space separated list of video type file extensions (dotless) + /// + public string VideoTypes { get; set; } + + /// + /// A space separated list of audio type file extensions (dotless) + /// + public string AudioTypes { get; set; } + + /// + /// A space separated list of document type file extensions (dotless) + /// + public string DocumentTypes { get; set; } + + /// + /// A space separated list of text type file extensions (dotless) + /// + public string TextTypes { get; set; } + + /// + /// A space separated list of other types file extensions (dotless) + /// + public string BinTypes { get; set; } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaStorage.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaStorage.cs index f58d750751..687b3274c8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaStorage.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaStorage.cs @@ -1,10 +1,10 @@ namespace SmartStore.Core.Domain.Media { - public partial class MediaStorage : BaseEntity - { - /// - /// Binary data - /// - public byte[] Data { get; set; } - } + public partial class MediaStorage : BaseEntity + { + /// + /// Binary data + /// + public byte[] Data { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaTag.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaTag.cs index 16dd5ee2a1..4b264cd475 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaTag.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaTag.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using System.Collections.Generic; using System.Runtime.Serialization; namespace SmartStore.Core.Domain.Media @@ -22,8 +19,8 @@ public partial class MediaTag : BaseEntity /// public virtual ICollection MediaFiles { - get { return _mediaFiles ?? (_mediaFiles = new HashSet()); } - protected set { _mediaFiles = value; } + get => _mediaFiles ?? (_mediaFiles = new HashSet()); + protected set => _mediaFiles = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs index d39d745ef8..2f3e818181 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs @@ -55,10 +55,10 @@ public string FriendlyName { get { - if (DisplayName.IsEmpty()) - return Email; + if (DisplayName.IsEmpty()) + return Email; - return "{0} ({1})".FormatInvariant(DisplayName, Email); + return "{0} ({1})".FormatInvariant(DisplayName, Email); } } @@ -72,9 +72,9 @@ object ICloneable.Clone() return this.MemberwiseClone(); } - public EmailAddress ToEmailAddress() - { - return new EmailAddress(this.Email, this.DisplayName); - } + public EmailAddress ToEmailAddress() + { + return new EmailAddress(this.Email, this.DisplayName); + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs index 8a4d73f784..5c42f41643 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs @@ -9,10 +9,10 @@ public class EmailAccountSettings : ISettings /// public int DefaultEmailAccountId { get; set; } - /// - /// Gets or sets a folder where mail messages should be saved (instead of sending them). - /// For debug and test purposes only. - /// - public string PickupDirectoryLocation { get; set; } - } + /// + /// Gets or sets a folder where mail messages should be saved (instead of sending them). + /// For debug and test purposes only. + /// + public string PickupDirectoryLocation { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs index 4bc4cbf0f4..3b86155494 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs @@ -2,21 +2,21 @@ namespace SmartStore.Core.Domain.Messages { - public enum EmailAttachmentStorageLocation - { - /// - /// Attachment is embedded as Blob - /// - Blob, + public enum EmailAttachmentStorageLocation + { + /// + /// Attachment is embedded as Blob + /// + Blob, - /// - /// Attachment is a reference to - /// - FileReference, - - /// - /// Attachment is located on disk (physical or virtual path) - /// - Path - } + /// + /// Attachment is a reference to + /// + FileReference, + + /// + /// Attachment is located on disk (physical or virtual path) + /// + Path + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs b/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs index a10f7193c1..8e183eeb14 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs @@ -4,19 +4,19 @@ namespace SmartStore.Core.Domain.Messages { public class EmailSubscribedEvent : IEquatable { - public EmailSubscribedEvent(string email) + public EmailSubscribedEvent(string email) { Email = email; } - public string Email { get; private set; } + public string Email { get; private set; } - public override bool Equals(object obj) - { - return Equals(obj as EmailSubscribedEvent); - } + public override bool Equals(object obj) + { + return Equals(obj as EmailSubscribedEvent); + } - public bool Equals(EmailSubscribedEvent other) + public bool Equals(EmailSubscribedEvent other) { if (ReferenceEquals(null, other)) return false; @@ -33,20 +33,20 @@ public override int GetHashCode() } public class EmailUnsubscribedEvent : IEquatable - { - public EmailUnsubscribedEvent(string email) + { + public EmailUnsubscribedEvent(string email) { Email = email; } - public string Email { get; private set; } + public string Email { get; private set; } - public override bool Equals(object obj) - { - return Equals(obj as EmailUnsubscribedEvent); - } + public override bool Equals(object obj) + { + return Equals(obj as EmailUnsubscribedEvent); + } - public bool Equals(EmailUnsubscribedEvent other) + public bool Equals(EmailUnsubscribedEvent other) { if (ReferenceEquals(null, other)) return false; diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs index d939cc684b..b15cfc8557 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs @@ -1,7 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Messages @@ -11,35 +10,35 @@ namespace SmartStore.Core.Domain.Messages /// public partial class MessageTemplate : BaseEntity, ILocalizedEntity, IStoreMappingSupported { - /// + /// /// Gets or sets the name /// public string Name { get; set; } - [StringLength(500), Required] - public string To { get; set; } + [StringLength(500), Required] + public string To { get; set; } - [StringLength(500)] - public string ReplyTo { get; set; } + [StringLength(500)] + public string ReplyTo { get; set; } - /// - /// A comma separated list of required model types (e.g.: Product, Order, Customer, GiftCard) - /// - [StringLength(500)] - public string ModelTypes { get; set; } + /// + /// A comma separated list of required model types (e.g.: Product, Order, Customer, GiftCard) + /// + [StringLength(500)] + public string ModelTypes { get; set; } - [MaxLength] - public string LastModelTree { get; set; } + [MaxLength] + public string LastModelTree { get; set; } - /// - /// Gets or sets the BCC Email addresses - /// - public string BccEmailAddresses { get; set; } + /// + /// Gets or sets the BCC Email addresses + /// + public string BccEmailAddresses { get; set; } - /// - /// Gets or sets the subject - /// - public string Subject { get; set; } + /// + /// Gets or sets the subject + /// + public string Subject { get; set; } /// /// Gets or sets the body @@ -57,29 +56,29 @@ public partial class MessageTemplate : BaseEntity, ILocalizedEntity, IStoreMappi /// public int EmailAccountId { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + public bool LimitedToStores { get; set; } - /// - /// Gets or sets a value indicating whether emails derived from the template are only send manually - /// - public bool SendManually { get; set; } + /// + /// Gets or sets a value indicating whether emails derived from the template are only send manually + /// + public bool SendManually { get; set; } - /// - /// Gets or sets the attachment 1 file identifier - /// - public int? Attachment1FileId { get; set; } + /// + /// Gets or sets the attachment 1 file identifier + /// + public int? Attachment1FileId { get; set; } - /// - /// Gets or sets the attachment 2 file identifier - /// - public int? Attachment2FileId { get; set; } + /// + /// Gets or sets the attachment 2 file identifier + /// + public int? Attachment2FileId { get; set; } - /// - /// Gets or sets the attachment 3 file identifier - /// - public int? Attachment3FileId { get; set; } + /// + /// Gets or sets the attachment 3 file identifier + /// + public int? Attachment3FileId { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs index d768f0c2dc..83f2dfe962 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs @@ -1,39 +1,38 @@ -using System; - -namespace SmartStore.Core.Domain.Messages +namespace SmartStore.Core.Domain.Messages { - public class MessageTemplateNames - { - public const string CustomerRegistered = "NewCustomer.Notification"; - public const string CustomerWelcome = "Customer.WelcomeMessage"; - public const string CustomerEmailValidation = "Customer.EmailValidationMessage"; - public const string CustomerPasswordRecovery = "Customer.PasswordRecovery"; - public const string OrderPlacedStoreOwner = "OrderPlaced.StoreOwnerNotification"; - public const string OrderPlacedCustomer = "OrderPlaced.CustomerNotification"; - public const string ShipmentSentCustomer = "ShipmentSent.CustomerNotification"; - public const string ShipmentDeliveredCustomer = "ShipmentDelivered.CustomerNotification"; - public const string OrderCompletedCustomer = "OrderCompleted.CustomerNotification"; - public const string OrderCancelledCustomer = "OrderCancelled.CustomerNotification"; - public const string OrderNoteAddedCustomer = "Customer.NewOrderNote"; - public const string RecurringPaymentCancelledStoreOwner = "RecurringPaymentCancelled.StoreOwnerNotification"; - public const string NewsLetterSubscriptionActivation = "NewsLetterSubscription.ActivationMessage"; - public const string NewsLetterSubscriptionDeactivation = "NewsLetterSubscription.DeactivationMessage"; - public const string ShareProduct = "Service.EmailAFriend"; - public const string ShareWishlist = "Wishlist.EmailAFriend"; - public const string ProductQuestion = "Product.AskQuestion"; - public const string NewReturnRequestStoreOwner = "NewReturnRequest.StoreOwnerNotification"; - public const string ReturnRequestStatusChangedCustomer = "ReturnRequestStatusChanged.CustomerNotification"; - public const string NewForumTopic = "Forums.NewForumTopic"; - public const string NewForumPost = "Forums.NewForumPost"; - public const string NewPrivateMessage = "Customer.NewPM"; - public const string GiftCardCustomer = "GiftCard.Notification"; - public const string ProductReviewStoreOwner = "Product.ProductReview"; - public const string QuantityBelowStoreOwner = "QuantityBelow.StoreOwnerNotification"; - public const string NewVatSubmittedStoreOwner = "NewVATSubmitted.StoreOwnerNotification"; - public const string BlogCommentStoreOwner = "Blog.BlogComment"; - public const string NewsCommentStoreOwner = "News.NewsComment"; - public const string BackInStockCustomer = "Customer.BackInStock"; - public const string SystemCampaign = "System.Campaign"; - public const string SystemContactUs = "System.ContactUs"; - } + public class MessageTemplateNames + { + public const string CustomerRegistered = "NewCustomer.Notification"; + public const string CustomerWelcome = "Customer.WelcomeMessage"; + public const string CustomerEmailValidation = "Customer.EmailValidationMessage"; + public const string CustomerPasswordRecovery = "Customer.PasswordRecovery"; + public const string OrderPlacedStoreOwner = "OrderPlaced.StoreOwnerNotification"; + public const string OrderPlacedCustomer = "OrderPlaced.CustomerNotification"; + public const string ShipmentSentCustomer = "ShipmentSent.CustomerNotification"; + public const string ShipmentDeliveredCustomer = "ShipmentDelivered.CustomerNotification"; + public const string OrderCompletedCustomer = "OrderCompleted.CustomerNotification"; + public const string OrderCancelledCustomer = "OrderCancelled.CustomerNotification"; + public const string OrderNoteAddedCustomer = "Customer.NewOrderNote"; + public const string RecurringPaymentCancelledStoreOwner = "RecurringPaymentCancelled.StoreOwnerNotification"; + public const string NewsLetterSubscriptionActivation = "NewsLetterSubscription.ActivationMessage"; + public const string NewsLetterSubscriptionDeactivation = "NewsLetterSubscription.DeactivationMessage"; + public const string ShareProduct = "Service.EmailAFriend"; + public const string ShareWishlist = "Wishlist.EmailAFriend"; + public const string ProductQuestion = "Product.AskQuestion"; + public const string NewReturnRequestStoreOwner = "NewReturnRequest.StoreOwnerNotification"; + public const string ReturnRequestStatusChangedCustomer = "ReturnRequestStatusChanged.CustomerNotification"; + public const string NewForumTopic = "Forums.NewForumTopic"; + public const string NewForumPost = "Forums.NewForumPost"; + public const string NewPrivateMessage = "Customer.NewPM"; + public const string GiftCardCustomer = "GiftCard.Notification"; + public const string ProductReviewStoreOwner = "Product.ProductReview"; + public const string QuantityBelowStoreOwner = "QuantityBelow.StoreOwnerNotification"; + public const string NewVatSubmittedStoreOwner = "NewVATSubmitted.StoreOwnerNotification"; + public const string BlogCommentStoreOwner = "Blog.BlogComment"; + public const string NewsCommentStoreOwner = "News.NewsComment"; + public const string BackInStockCustomer = "Customer.BackInStock"; + public const string SystemCampaign = "System.Campaign"; + public const string SystemContactUs = "System.ContactUs"; + public const string SystemGeneric = "System.Generic"; + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/NewsLetterSubscription.cs b/src/Libraries/SmartStore.Core/Domain/Messages/NewsLetterSubscription.cs index d54f3a1ded..282267126a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/NewsLetterSubscription.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/NewsLetterSubscription.cs @@ -41,7 +41,7 @@ public partial class NewsLetterSubscription : BaseEntity /// [DataMember] [Index("IX_NewsletterSubscription_Email_StoreId", 2)] - public int StoreId { get; set; } + public int StoreId { get; set; } /// /// Gets or sets the language identifier diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs index be65e172b7..14ba2d6067 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs @@ -8,7 +8,7 @@ namespace SmartStore.Core.Domain.Messages /// public partial class QueuedEmail : BaseEntity { - private ICollection _attachments; + private ICollection _attachments; /// /// Gets or sets the priority @@ -25,10 +25,10 @@ public partial class QueuedEmail : BaseEntity /// public string To { get; set; } - /// - /// Gets or sets the ReplyTo property - /// - public string ReplyTo { get; set; } + /// + /// Gets or sets the ReplyTo property + /// + public string ReplyTo { get; set; } /// /// Gets or sets the CC @@ -70,23 +70,23 @@ public partial class QueuedEmail : BaseEntity /// public int EmailAccountId { get; set; } - /// - /// Gets or sets a value indicating whether emails are only send manually - /// - public bool SendManually { get; set; } + /// + /// Gets or sets a value indicating whether emails are only send manually + /// + public bool SendManually { get; set; } /// /// Gets the email account /// public virtual EmailAccount EmailAccount { get; set; } - /// - /// Gets or sets the collection of attachments - /// - public virtual ICollection Attachments - { - get { return _attachments ?? (_attachments = new HashSet()); } - protected set { _attachments = value; } - } + /// + /// Gets or sets the collection of attachments + /// + public virtual ICollection Attachments + { + get => _attachments ?? (_attachments = new HashSet()); + protected set => _attachments = value; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs index 7c7ca60028..1f7a005828 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs @@ -1,64 +1,63 @@ -using System; -using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Media; namespace SmartStore.Core.Domain.Messages { - /// - /// Represents an e-mail attachment - /// - public partial class QueuedEmailAttachment : BaseEntity, IHasMedia - { - /// - /// Gets or sets the queued email identifier - /// - public int QueuedEmailId { get; set; } - - /// - /// Gets or sets the queued email entity instance - /// - public virtual QueuedEmail QueuedEmail { get; set; } - - /// - /// Gets or sets the storage location - /// - public EmailAttachmentStorageLocation StorageLocation { get; set; } - - /// - /// A physical or virtual path to the file (only applicable if location is Path) - /// - public string Path { get; set; } - - /// - /// The id of a record (only applicable if location is FileReference) - /// - public int? MediaFileId { get; set; } - - /// - /// Gets the file object - /// - /// - /// This property is not named Download on purpose, because we're going to rename Download to File in a future release. - /// - public virtual MediaFile MediaFile { get; set; } - - /// - /// The attachment file name (without path) - /// - public string Name { get; set; } - - /// - /// The attachment file's mime type, e.g. application/pdf - /// - public string MimeType { get; set; } - - /// - /// Gets or sets the media storage identifier (when location is BLOB) - /// - public int? MediaStorageId { get; set; } - - /// - /// Gets or sets the media storage (when location is BLOB) - /// - public virtual MediaStorage MediaStorage { get; set; } - } + /// + /// Represents an e-mail attachment + /// + public partial class QueuedEmailAttachment : BaseEntity, IHasMedia + { + /// + /// Gets or sets the queued email identifier + /// + public int QueuedEmailId { get; set; } + + /// + /// Gets or sets the queued email entity instance + /// + public virtual QueuedEmail QueuedEmail { get; set; } + + /// + /// Gets or sets the storage location + /// + public EmailAttachmentStorageLocation StorageLocation { get; set; } + + /// + /// A physical or virtual path to the file (only applicable if location is Path) + /// + public string Path { get; set; } + + /// + /// The id of a record (only applicable if location is FileReference) + /// + public int? MediaFileId { get; set; } + + /// + /// Gets the file object + /// + /// + /// This property is not named Download on purpose, because we're going to rename Download to File in a future release. + /// + public virtual MediaFile MediaFile { get; set; } + + /// + /// The attachment file name (without path) + /// + public string Name { get; set; } + + /// + /// The attachment file's mime type, e.g. application/pdf + /// + public string MimeType { get; set; } + + /// + /// Gets or sets the media storage identifier (when location is BLOB) + /// + public int? MediaStorageId { get; set; } + + /// + /// Gets or sets the media storage (when location is BLOB) + /// + public virtual MediaStorage MediaStorage { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/News/NewsItem.cs b/src/Libraries/SmartStore.Core/Domain/News/NewsItem.cs index 5763f56c8a..954961a9b2 100644 --- a/src/Libraries/SmartStore.Core/Domain/News/NewsItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/News/NewsItem.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Runtime.Serialization; +using System.ComponentModel.DataAnnotations.Schema; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Seo; @@ -11,14 +11,26 @@ namespace SmartStore.Core.Domain.News /// /// Represents a news item /// - public partial class NewsItem : BaseEntity, ISlugSupported, IStoreMappingSupported + public partial class NewsItem : BaseEntity, ISlugSupported, IStoreMappingSupported, ILocalizedEntity { - private ICollection _newsComments; + #region static - /// - /// Gets or sets the language identifier - /// - public int LanguageId { get; set; } + private static readonly List _visibilityAffectingProps = new List + { + nameof(NewsItem.Published), + nameof(NewsItem.StartDateUtc), + nameof(NewsItem.EndDateUtc), + nameof(NewsItem.LimitedToStores) + }; + + public static IReadOnlyCollection GetVisibilityAffectingPropertyNames() + { + return _visibilityAffectingProps; + } + + #endregion + + private ICollection _newsComments; /// /// Gets or sets the news title @@ -81,6 +93,7 @@ public partial class NewsItem : BaseEntity, ISlugSupported, IStoreMappingSupport /// /// public int ApprovedCommentCount { get; set; } + /// /// Gets or sets the total number of not approved comments /// The same as if we run newsItem.NewsComments.Where(n => !n.IsApproved).Count() @@ -89,10 +102,10 @@ public partial class NewsItem : BaseEntity, ISlugSupported, IStoreMappingSupport /// public int NotApprovedCommentCount { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + public bool LimitedToStores { get; set; } /// /// Gets or sets the date and time of entity creation @@ -114,18 +127,24 @@ public partial class NewsItem : BaseEntity, ISlugSupported, IStoreMappingSupport /// public string MetaTitle { get; set; } + /// + /// Gets or sets a language identifier for which the news item should be displayed. + /// + [Index] + public int? LanguageId { get; set; } + + /// + /// Gets or sets the language. + /// + public virtual Language Language { get; set; } + /// /// Gets or sets the news comments /// public virtual ICollection NewsComments { - get { return _newsComments ?? (_newsComments = new HashSet()); } - protected set { _newsComments = value; } + get => _newsComments ?? (_newsComments = new HashSet()); + protected set => _newsComments = value; } - - /// - /// Gets or sets the language - /// - public virtual Language Language { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs b/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs index ffceb3edab..236b10d1c0 100644 --- a/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs @@ -4,25 +4,15 @@ namespace SmartStore.Core.Domain.News { public class NewsSettings : ISettings { - public NewsSettings() - { - Enabled = true; - AllowNotRegisteredUsersToLeaveComments = true; - ShowNewsOnMainPage = true; - MainPageNewsCount = 3; - NewsArchivePageSize = 10; - MaxAgeInDays = 180; - } - - /// + /// /// Gets or sets a value indicating whether news are enabled /// - public bool Enabled { get; set; } + public bool Enabled { get; set; } = true; /// /// Gets or sets a value indicating whether not registered user can leave comments /// - public bool AllowNotRegisteredUsersToLeaveComments { get; set; } + public bool AllowNotRegisteredUsersToLeaveComments { get; set; } = true; /// /// Gets or sets a value indicating whether to notify about new news comments @@ -32,26 +22,41 @@ public NewsSettings() /// /// Gets or sets a value indicating whether to show news on the main page /// - public bool ShowNewsOnMainPage { get; set; } + public bool ShowNewsOnMainPage { get; set; } = true; /// /// Gets or sets a value indicating news count displayed on the main page /// - public int MainPageNewsCount { get; set; } + public int MainPageNewsCount { get; set; } = 4; /// /// Gets or sets the page size for news archive /// - public int NewsArchivePageSize { get; set; } + public int NewsArchivePageSize { get; set; } = 9; - /// - /// The maximum age of news (in days) for RSS feed - /// - public int MaxAgeInDays { get; set; } + /// + /// The maximum age of news (in days) for RSS feed + /// + public int MaxAgeInDays { get; set; } = 180; /// /// Enable the news RSS feed link in customers browser address bar /// public bool ShowHeaderRssUrl { get; set; } + + /// + /// Gets or sets the meta title for news index page + /// + public string MetaTitle { get; set; } + + /// + /// Gets or sets the meta description for news index page + /// + public string MetaDescription { get; set; } + + /// + /// Gets or sets the meta keywords for news index page + /// + public string MetaKeywords { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs index 63cbf58ea8..ca95d9b6df 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs @@ -5,17 +5,17 @@ namespace SmartStore.Core.Domain.Orders { - /// - /// Represents a checkout attribute - /// - public partial class CheckoutAttribute : BaseEntity, ILocalizedEntity, IStoreMappingSupported - { + /// + /// Represents a checkout attribute + /// + public partial class CheckoutAttribute : BaseEntity, ILocalizedEntity, IStoreMappingSupported + { private ICollection _checkoutAttributeValues; - public CheckoutAttribute() - { - this.IsActive = true; - } + public CheckoutAttribute() + { + this.IsActive = true; + } /// /// Gets or sets the name @@ -57,28 +57,36 @@ public CheckoutAttribute() /// public int DisplayOrder { get; set; } - /// - /// Gets or sets whether the checkout attribute is active - /// - public bool IsActive { get; set; } + /// + /// Gets or sets whether the checkout attribute is active + /// + public bool IsActive { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + public bool LimitedToStores { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - public bool LimitedToStores { get; set; } + /// + /// Gets the attribute control type + /// + public AttributeControlType AttributeControlType + { + get => (AttributeControlType)this.AttributeControlTypeId; + set => this.AttributeControlTypeId = (int)value; + } - /// - /// Gets the attribute control type - /// - public AttributeControlType AttributeControlType + public bool IsListTypeAttribute() { - get + switch (AttributeControlType) { - return (AttributeControlType)this.AttributeControlTypeId; - } - set - { - this.AttributeControlTypeId = (int)value; + case AttributeControlType.Checkboxes: + case AttributeControlType.Boxes: + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + return true; + default: + return false; } } @@ -87,9 +95,8 @@ public AttributeControlType AttributeControlType /// public virtual ICollection CheckoutAttributeValues { - get { return _checkoutAttributeValues ?? (_checkoutAttributeValues = new HashSet()); } - protected set { _checkoutAttributeValues = value; } + get => _checkoutAttributeValues ?? (_checkoutAttributeValues = new HashSet()); + protected set => _checkoutAttributeValues = value; } } - } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttributeValue.cs b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttributeValue.cs index 065fd323ad..d0f1e09bc0 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttributeValue.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttributeValue.cs @@ -1,5 +1,6 @@ - +using System.ComponentModel.DataAnnotations; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; namespace SmartStore.Core.Domain.Orders { @@ -38,10 +39,25 @@ public partial class CheckoutAttributeValue : BaseEntity, ILocalizedEntity /// public int DisplayOrder { get; set; } + /// + /// Gets or sets the media file identifier. + /// + public int? MediaFileId { get; set; } + + /// + /// Gets or sets the color RGB value (used with "Boxes" attribute type). + /// + [StringLength(100)] + public string Color { get; set; } + + /// + /// Gets or sets the media file. + /// + public virtual MediaFile MediaFile { get; set; } + /// /// Gets or sets the checkout attribute /// public virtual CheckoutAttribute CheckoutAttribute { get; set; } } - } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs index 4ce792b336..1ba0d1c87e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs @@ -1,45 +1,45 @@ namespace SmartStore.Core.Domain.Orders { - /// - /// Setting for newsletter subscription in checkout - /// - public enum CheckoutNewsLetterSubscription - { - /// - /// No newsletter subscription checkbox - /// - None = 0, + /// + /// Setting for newsletter subscription in checkout + /// + public enum CheckoutNewsLetterSubscription + { + /// + /// No newsletter subscription checkbox + /// + None = 0, - /// - /// Deactivated newsletter subscription checkbox - /// - Deactivated, + /// + /// Deactivated newsletter subscription checkbox + /// + Deactivated, - /// - /// Activated newsletter subscription checkbox - /// - Activated - } + /// + /// Activated newsletter subscription checkbox + /// + Activated + } - /// - /// Setting to hand over customer email to third party - /// - public enum CheckoutThirdPartyEmailHandOver - { - /// - /// No third party email hand over checkbox - /// - None = 0, + /// + /// Setting to hand over customer email to third party + /// + public enum CheckoutThirdPartyEmailHandOver + { + /// + /// No third party email hand over checkbox + /// + None = 0, - /// - /// Deactivated third party email hand over checkbox - /// - Deactivated, + /// + /// Deactivated third party email hand over checkbox + /// + Deactivated, - /// - /// Activated third party email hand over checkbox - /// - Activated - } + /// + /// Activated third party email hand over checkbox + /// + Activated + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/Events.cs b/src/Libraries/SmartStore.Core/Domain/Orders/Events.cs index e7b898754c..e85fe0da04 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/Events.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/Events.cs @@ -11,10 +11,7 @@ public OrderPaidEvent(Order order) this._order = order; } - public Order Order - { - get { return _order; } - } + public Order Order => _order; } public class OrderPlacedEvent @@ -26,53 +23,38 @@ public OrderPlacedEvent(Order order) this._order = order; } - public Order Order - { - get { return _order; } - } + public Order Order => _order; } - public class OrderUpdatedEvent - { - private readonly Order _order; + public class OrderUpdatedEvent + { + private readonly Order _order; - public OrderUpdatedEvent(Order order) - { - this._order = order; - } + public OrderUpdatedEvent(Order order) + { + this._order = order; + } - public Order Order - { - get { return _order; } - } - } + public Order Order => _order; + } - public class MigrateShoppingCartEvent - { - private readonly Customer _fromCustomer; - private readonly Customer _toCustomer; - private readonly int _storeId; + public class MigrateShoppingCartEvent + { + private readonly Customer _fromCustomer; + private readonly Customer _toCustomer; + private readonly int _storeId; - public MigrateShoppingCartEvent(Customer fromCustomer, Customer toCustomer, int storeId) - { - _fromCustomer = fromCustomer; - _toCustomer = toCustomer; - _storeId = storeId; - } + public MigrateShoppingCartEvent(Customer fromCustomer, Customer toCustomer, int storeId) + { + _fromCustomer = fromCustomer; + _toCustomer = toCustomer; + _storeId = storeId; + } - public Customer FromCustomer - { - get { return _fromCustomer; } - } + public Customer FromCustomer => _fromCustomer; - public Customer ToCustomer - { - get { return _toCustomer; } - } + public Customer ToCustomer => _toCustomer; - public int StoreId - { - get { return _storeId; } - } - } + public int StoreId => _storeId; + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/GiftCard.cs b/src/Libraries/SmartStore.Core/Domain/Orders/GiftCard.cs index 3f8de5f300..5d4f7542eb 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/GiftCard.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/GiftCard.cs @@ -10,7 +10,7 @@ namespace SmartStore.Core.Domain.Orders public partial class GiftCard : BaseEntity { private ICollection _giftCardUsageHistory; - + /// /// Gets or sets the associated order item identifier /// @@ -70,31 +70,25 @@ public partial class GiftCard : BaseEntity /// Gets or sets the date and time of instance creation /// public DateTime CreatedOnUtc { get; set; } - + /// /// Gets or sets the gift card usage history /// public virtual ICollection GiftCardUsageHistory { - get { return _giftCardUsageHistory ?? (_giftCardUsageHistory = new HashSet()); } - protected set { _giftCardUsageHistory = value; } + get => _giftCardUsageHistory ?? (_giftCardUsageHistory = new HashSet()); + protected set => _giftCardUsageHistory = value; } - + /// /// Gets or sets the gift card type /// - public GiftCardType GiftCardType + public GiftCardType GiftCardType { - get - { - return (GiftCardType)this.GiftCardTypeId; - } - set - { - this.GiftCardTypeId = (int)value; - } + get => (GiftCardType)this.GiftCardTypeId; + set => this.GiftCardTypeId = (int)value; } - + /// /// Gets or sets the associated order item /// @@ -129,15 +123,15 @@ public bool IsGiftCardValid(int storeId) if (!this.IsGiftCardActivated) return false; - if (storeId != 0 && - PurchasedWithOrderItemId.HasValue && PurchasedWithOrderItem != null && - PurchasedWithOrderItem.Order != null) - { - if (PurchasedWithOrderItem.Order.StoreId != storeId) - return false; - } + if (storeId != 0 && + PurchasedWithOrderItemId.HasValue && PurchasedWithOrderItem != null && + PurchasedWithOrderItem.Order != null) + { + if (PurchasedWithOrderItem.Order.StoreId != storeId) + return false; + } - decimal remainingAmount = GetGiftCardRemainingAmount(); + decimal remainingAmount = GetGiftCardRemainingAmount(); if (remainingAmount > decimal.Zero) return true; diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/GiftCardUsageHistory.cs b/src/Libraries/SmartStore.Core/Domain/Orders/GiftCardUsageHistory.cs index da836ce1e7..c1da377335 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/GiftCardUsageHistory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/GiftCardUsageHistory.cs @@ -26,7 +26,7 @@ public partial class GiftCardUsageHistory : BaseEntity /// Gets or sets the date and time of instance creation /// public DateTime CreatedOnUtc { get; set; } - + /// /// Gets the gift card /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs index 44b1afa410..d32337103c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.Serialization; -using SmartStore.Core.Domain.Affiliates; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; @@ -18,10 +17,10 @@ namespace SmartStore.Core.Domain.Orders /// Represents an order /// [DataContract] - public partial class Order : BaseEntity, IAuditable, ISoftDeletable + public partial class Order : BaseEntity, IAuditable, ISoftDeletable { - private ICollection _walletHistory; - private ICollection _discountUsageHistory; + private ICollection _walletHistory; + private ICollection _discountUsageHistory; private ICollection _giftCardUsageHistory; private ICollection _orderNotes; private ICollection _orderItems; @@ -72,7 +71,7 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets the (formatted) order number /// [DataMember] - public string OrderNumber { get; set; } + public string OrderNumber { get; set; } /// /// Gets or sets the order identifier @@ -80,29 +79,29 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat [DataMember] public Guid OrderGuid { get; set; } - /// - /// Gets or sets the store identifier - /// - [DataMember] - public int StoreId { get; set; } + /// + /// Gets or sets the store identifier + /// + [DataMember] + public int StoreId { get; set; } /// /// Gets or sets the customer identifier /// [DataMember] - public int CustomerId { get; set; } + public int CustomerId { get; set; } /// /// Gets or sets the billing address identifier /// [DataMember] - public int BillingAddressId { get; set; } + public int BillingAddressId { get; set; } /// /// Gets or sets the shipping address identifier /// [DataMember] - public int? ShippingAddressId { get; set; } + public int? ShippingAddressId { get; set; } /// /// Gets or sets an order status identifier @@ -114,13 +113,13 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets the shipping status identifier /// [DataMember] - public int ShippingStatusId { get; set; } + public int ShippingStatusId { get; set; } /// /// Gets or sets the payment status identifier /// [DataMember] - public int PaymentStatusId { get; set; } + public int PaymentStatusId { get; set; } /// /// Gets or sets the payment method system name @@ -144,7 +143,7 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets the customer tax display type identifier /// [DataMember] - public virtual int CustomerTaxDisplayTypeId { get; set; } + public virtual int CustomerTaxDisplayTypeId { get; set; } /// /// Gets or sets the VAT number (the European Union Value Added Tax) @@ -188,11 +187,11 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat [DataMember] public decimal OrderShippingExclTax { get; set; } - /// - /// Gets or sets the tax rate for order shipping - /// - [DataMember] - public decimal OrderShippingTaxRate { get; set; } + /// + /// Gets or sets the tax rate for order shipping + /// + [DataMember] + public decimal OrderShippingTaxRate { get; set; } /// /// Gets or sets the payment method additional fee (incl tax) @@ -206,11 +205,11 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat [DataMember] public decimal PaymentMethodAdditionalFeeExclTax { get; set; } - /// - /// Gets or sets the tax rate for payment method additional fee - /// - [DataMember] - public decimal PaymentMethodAdditionalFeeTaxRate { get; set; } + /// + /// Gets or sets the tax rate for payment method additional fee + /// + [DataMember] + public decimal PaymentMethodAdditionalFeeTaxRate { get; set; } /// /// Gets or sets the tax rates @@ -230,16 +229,16 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat [DataMember] public decimal OrderDiscount { get; set; } - /// - /// Gets or sets the wallet credit amount used to (partially) pay this order. - /// - [DataMember] - public decimal CreditBalance { get; set; } + /// + /// Gets or sets the wallet credit amount used to (partially) pay this order. + /// + [DataMember] + public decimal CreditBalance { get; set; } - /// - /// Gets or sets the order total rounding amount - /// - [DataMember] + /// + /// Gets or sets the order total rounding amount + /// + [DataMember] public decimal OrderTotalRounding { get; set; } /// @@ -254,12 +253,12 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat [DataMember] public decimal RefundedAmount { get; set; } - /// - /// Gets or sets the value indicating whether reward points were earned for this order - /// - [DataMember] + /// + /// Gets or sets the value indicating whether reward points were earned for this order + /// + [DataMember] public bool RewardPointsWereAdded { get; set; } - + /// /// Gets or sets the checkout attribute description /// @@ -282,7 +281,7 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets the affiliate identifier /// [DataMember] - public int AffiliateId { get; set; } + public int AffiliateId { get; set; } /// /// Gets or sets the customer IP address @@ -294,7 +293,7 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets a value indicating whether storing of credit card number is allowed /// [DataMember] - public bool AllowStoringCreditCardNumber { get; set; } + public bool AllowStoringCreditCardNumber { get; set; } /// /// Gets or sets the card type @@ -382,61 +381,61 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets the authorization transaction identifier /// [DataMember] - public string AuthorizationTransactionId { get; set; } + public string AuthorizationTransactionId { get; set; } /// /// Gets or sets the authorization transaction code /// [DataMember] - public string AuthorizationTransactionCode { get; set; } + public string AuthorizationTransactionCode { get; set; } /// /// Gets or sets the authorization transaction result /// [DataMember] - public string AuthorizationTransactionResult { get; set; } + public string AuthorizationTransactionResult { get; set; } /// /// Gets or sets the capture transaction identifier /// [DataMember] - public string CaptureTransactionId { get; set; } + public string CaptureTransactionId { get; set; } /// /// Gets or sets the capture transaction result /// [DataMember] - public string CaptureTransactionResult { get; set; } + public string CaptureTransactionResult { get; set; } /// /// Gets or sets the subscription transaction identifier /// [DataMember] - public string SubscriptionTransactionId { get; set; } + public string SubscriptionTransactionId { get; set; } /// /// Gets or sets the purchase order number /// [DataMember] - public string PurchaseOrderNumber { get; set; } + public string PurchaseOrderNumber { get; set; } /// /// Gets or sets the paid date and time /// [DataMember] - public DateTime? PaidDateUtc { get; set; } - + public DateTime? PaidDateUtc { get; set; } + /// /// Gets or sets the shipping method /// - [DataMember] - public string ShippingMethod { get; set; } + [DataMember] + public string ShippingMethod { get; set; } /// /// Gets or sets the shipping rate computation method identifier /// [DataMember] - public string ShippingRateComputationMethodSystemName { get; set; } + public string ShippingRateComputationMethodSystemName { get; set; } /// /// Gets or sets a value indicating whether the entity has been deleted @@ -448,75 +447,76 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat /// Gets or sets the date and time of order creation /// [DataMember] - public DateTime CreatedOnUtc { get; set; } + public DateTime CreatedOnUtc { get; set; } - /// - /// Gets or sets the date and time when order was updated - /// - [DataMember] - public DateTime UpdatedOnUtc { get; set; } + /// + /// Gets or sets the date and time when order was updated + /// + [DataMember] + public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets or sets the amount of remaing reward points - /// - [DataMember] - public int? RewardPointsRemaining { get; set; } + /// + /// Gets or sets the amount of remaing reward points + /// + [DataMember] + public int? RewardPointsRemaining { get; set; } - /// - /// Gets or sets a value indicating whether a new payment notification arrived (IPN, webhook, callback etc.) - /// - [DataMember] - public bool HasNewPaymentNotification { get; set; } + /// + /// Gets or sets a value indicating whether a new payment notification arrived (IPN, webhook, callback etc.) + /// + [DataMember] + public bool HasNewPaymentNotification { get; set; } - /// - /// Gets or sets a value indicating whether the customer accepted to hand over email address to third party - /// - [DataMember] - public bool AcceptThirdPartyEmailHandOver { get; set; } + /// + /// Gets or sets a value indicating whether the customer accepted to hand over email address to third party + /// + [DataMember] + public bool AcceptThirdPartyEmailHandOver { get; set; } - #endregion + #endregion - #region Navigation properties + #region Navigation properties - /// - /// Gets or sets the customer - /// - [DataMember] + /// + /// Gets or sets the customer + /// + [DataMember] public virtual Customer Customer { get; set; } /// /// Gets or sets the billing address /// [DataMember] - public virtual Address BillingAddress { get; set; } + public virtual Address BillingAddress { get; set; } /// /// Gets or sets the shipping address /// [DataMember] - public virtual Address ShippingAddress { get; set; } - + public virtual Address ShippingAddress { get; set; } + /// /// Gets or sets the reward points history record /// + [DataMember] public virtual RewardPointsHistory RedeemedRewardPointsEntry { get; set; } - /// - /// Gets or sets the wallet history. - /// - public virtual ICollection WalletHistory - { - get { return _walletHistory ?? (_walletHistory = new HashSet()); } - protected set { _walletHistory = value; } - } - - /// - /// Gets or sets discount usage history - /// - public virtual ICollection DiscountUsageHistory + /// + /// Gets or sets the wallet history. + /// + public virtual ICollection WalletHistory + { + get => _walletHistory ?? (_walletHistory = new HashSet()); + protected set => _walletHistory = value; + } + + /// + /// Gets or sets discount usage history + /// + public virtual ICollection DiscountUsageHistory { - get { return _discountUsageHistory ?? (_discountUsageHistory = new HashSet()); } - protected set { _discountUsageHistory = value; } + get => _discountUsageHistory ?? (_discountUsageHistory = new HashSet()); + protected set => _discountUsageHistory = value; } /// @@ -524,38 +524,38 @@ public virtual ICollection DiscountUsageHistory /// public virtual ICollection GiftCardUsageHistory { - get { return _giftCardUsageHistory ?? (_giftCardUsageHistory = new HashSet()); } - protected set { _giftCardUsageHistory = value; } + get => _giftCardUsageHistory ?? (_giftCardUsageHistory = new HashSet()); + protected set => _giftCardUsageHistory = value; } /// /// Gets or sets order notes /// [DataMember] - public virtual ICollection OrderNotes + public virtual ICollection OrderNotes { - get { return _orderNotes ?? (_orderNotes = new HashSet()); } - protected set { _orderNotes = value; } + get => _orderNotes ?? (_orderNotes = new HashSet()); + protected set => _orderNotes = value; } /// /// Gets or sets order items /// [DataMember] - public virtual ICollection OrderItems + public virtual ICollection OrderItems { - get { return _orderItems ?? (_orderItems = new HashSet()); } - protected internal set { _orderItems = value; } + get => _orderItems ?? (_orderItems = new HashSet()); + protected internal set => _orderItems = value; } /// /// Gets or sets shipments /// [DataMember] - public virtual ICollection Shipments + public virtual ICollection Shipments { - get { return _shipments ?? (_shipments = new HashSet()); } - protected set { _shipments = value; } + get => _shipments ?? (_shipments = new HashSet()); + protected set => _shipments = value; } #endregion @@ -566,77 +566,47 @@ public virtual ICollection Shipments /// Gets or sets the order status /// [DataMember] - public OrderStatus OrderStatus + public OrderStatus OrderStatus { - get - { - return (OrderStatus)this.OrderStatusId; - } - set - { - this.OrderStatusId = (int)value; - } + get => (OrderStatus)this.OrderStatusId; + set => this.OrderStatusId = (int)value; } /// /// Gets or sets the payment status /// [DataMember] - public PaymentStatus PaymentStatus + public PaymentStatus PaymentStatus { - get - { - return (PaymentStatus)this.PaymentStatusId; - } - set - { - this.PaymentStatusId = (int)value; - } + get => (PaymentStatus)this.PaymentStatusId; + set => this.PaymentStatusId = (int)value; } /// /// Gets or sets the shipping status /// [DataMember] - public ShippingStatus ShippingStatus + public ShippingStatus ShippingStatus { - get - { - return (ShippingStatus)this.ShippingStatusId; - } - set - { - this.ShippingStatusId = (int)value; - } + get => (ShippingStatus)this.ShippingStatusId; + set => this.ShippingStatusId = (int)value; } /// /// Gets or sets the customer tax display type /// [DataMember] - public TaxDisplayType CustomerTaxDisplayType + public TaxDisplayType CustomerTaxDisplayType { - get - { - return (TaxDisplayType)this.CustomerTaxDisplayTypeId; - } - set - { - this.CustomerTaxDisplayTypeId = (int)value; - } + get => (TaxDisplayType)this.CustomerTaxDisplayTypeId; + set => this.CustomerTaxDisplayTypeId = (int)value; } /// /// Gets the applied tax rates /// - public SortedDictionary TaxRatesDictionary - { - get - { - return ParseTaxRates(this.TaxRates); - } - } - + public SortedDictionary TaxRatesDictionary => ParseTaxRates(this.TaxRates); + #endregion } } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrderItem.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrderItem.cs index 48ca396073..d81a8017f1 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrderItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrderItem.cs @@ -17,116 +17,116 @@ public partial class OrderItem : BaseEntity /// Gets or sets the order product variant identifier /// [DataMember] - public Guid OrderItemGuid { get; set; } + public Guid OrderItemGuid { get; set; } /// /// Gets or sets the order identifier /// [DataMember] - public int OrderId { get; set; } + public int OrderId { get; set; } /// /// Gets or sets the product identifier /// [DataMember] - public int ProductId { get; set; } + public int ProductId { get; set; } /// /// Gets or sets the quantity /// [DataMember] - public int Quantity { get; set; } + public int Quantity { get; set; } /// /// Gets or sets the unit price in primary store currency (incl tax) /// [DataMember] - public decimal UnitPriceInclTax { get; set; } + public decimal UnitPriceInclTax { get; set; } /// /// Gets or sets the unit price in primary store currency (excl tax) /// [DataMember] - public decimal UnitPriceExclTax { get; set; } + public decimal UnitPriceExclTax { get; set; } /// /// Gets or sets the price in primary store currency (incl tax) /// [DataMember] - public decimal PriceInclTax { get; set; } + public decimal PriceInclTax { get; set; } /// /// Gets or sets the price in primary store currency (excl tax) /// [DataMember] - public decimal PriceExclTax { get; set; } + public decimal PriceExclTax { get; set; } - /// - /// Gets or sets the tax rate - /// - [DataMember] - public decimal TaxRate { get; set; } + /// + /// Gets or sets the tax rate + /// + [DataMember] + public decimal TaxRate { get; set; } /// /// Gets or sets the discount amount (incl tax) /// [DataMember] - public decimal DiscountAmountInclTax { get; set; } + public decimal DiscountAmountInclTax { get; set; } /// /// Gets or sets the discount amount (excl tax) /// [DataMember] - public decimal DiscountAmountExclTax { get; set; } + public decimal DiscountAmountExclTax { get; set; } /// /// Gets or sets the attribute description /// [DataMember] - public string AttributeDescription { get; set; } + public string AttributeDescription { get; set; } /// /// Gets or sets the product variant attributes in XML format /// [DataMember] - public string AttributesXml { get; set; } - + public string AttributesXml { get; set; } + /// /// Gets or sets the download count /// - [DataMember] - public int DownloadCount { get; set; } + [DataMember] + public int DownloadCount { get; set; } /// /// Gets or sets a value indicating whether download is activated /// [DataMember] - public bool IsDownloadActivated { get; set; } + public bool IsDownloadActivated { get; set; } /// /// Gets or sets a license download identifier (in case this is a downloadable product) /// [DataMember] - public int? LicenseDownloadId { get; set; } - + public int? LicenseDownloadId { get; set; } + /// /// Gets or sets the total weight of one item /// It's nullable for compatibility with the previous version where was no such property /// - [DataMember] - public decimal? ItemWeight { get; set; } + [DataMember] + public decimal? ItemWeight { get; set; } - /// - /// Gets or sets extra bundle data - /// - [DataMember] - public string BundleData { get; set; } + /// + /// Gets or sets extra bundle data + /// + [DataMember] + public string BundleData { get; set; } - /// - /// Gets or sets the original product cost - /// - [DataMember] - public decimal ProductCost { get; set; } + /// + /// Gets or sets the original product cost + /// + [DataMember] + public decimal ProductCost { get; set; } /// /// Gets or sets the delivery time at the time of purchase. @@ -144,21 +144,21 @@ public partial class OrderItem : BaseEntity /// Gets the order /// [DataMember] - public virtual Order Order { get; set; } + public virtual Order Order { get; set; } /// /// Gets the product /// [DataMember] - public virtual Product Product { get; set; } + public virtual Product Product { get; set; } /// /// Gets or sets the associated gift card /// public virtual ICollection AssociatedGiftCards { - get { return _associatedGiftCards ?? (_associatedGiftCards = new HashSet()); } - protected set { _associatedGiftCards = value; } + get => _associatedGiftCards ?? (_associatedGiftCards = new HashSet()); + protected set => _associatedGiftCards = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrderNote.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrderNote.cs index f191c23b19..f406f42616 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrderNote.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrderNote.cs @@ -7,31 +7,31 @@ namespace SmartStore.Core.Domain.Orders /// Represents an order note /// [DataContract] - public partial class OrderNote : BaseEntity + public partial class OrderNote : BaseEntity { /// /// Gets or sets the order identifier /// [DataMember] - public int OrderId { get; set; } + public int OrderId { get; set; } /// /// Gets or sets the note /// [DataMember] - public string Note { get; set; } + public string Note { get; set; } /// /// Gets or sets a value indicating whether a customer can see a note /// [DataMember] - public bool DisplayToCustomer { get; set; } + public bool DisplayToCustomer { get; set; } /// /// Gets or sets the date and time of order note creation /// [DataMember] - public DateTime CreatedOnUtc { get; set; } + public DateTime CreatedOnUtc { get; set; } /// /// Gets the order diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs index 05e9a49fd4..fce240153c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs @@ -1,38 +1,44 @@ using SmartStore.Core.Configuration; -using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Orders { - public class OrderSettings : BaseEntity, ISettings, ILocalizedEntity + public class OrderSettings : ISettings { - public OrderSettings() - { - IsReOrderAllowed = true; - AnonymousCheckoutAllowed = true; - TermsOfServiceEnabled = true; - ReturnRequestsEnabled = true; - ReturnRequestActions = "Repair,Replacement,Store Credit"; - ReturnRequestReasons = "Received Wrong Product,Wrong Product Ordered,There Was A Problem With The Product"; - NumberOfDaysReturnRequestAvailable = 365; - MinimumOrderPlacementInterval = 30; - OrderListPageSize = 10; - } - - /// + public OrderSettings() + { + IsReOrderAllowed = true; + AnonymousCheckoutAllowed = true; + TermsOfServiceEnabled = true; + ReturnRequestsEnabled = true; + ReturnRequestActions = "Repair,Replacement,Store Credit"; + ReturnRequestReasons = "Received Wrong Product,Wrong Product Ordered,There Was A Problem With The Product"; + NumberOfDaysReturnRequestAvailable = 365; + MinimumOrderPlacementInterval = 30; + OrderListPageSize = 10; + RecurringPaymentListPageSize = 10; + } + + /// /// Gets or sets a value indicating whether customer can make re-order /// public bool IsReOrderAllowed { get; set; } /// - /// Gets or sets a minimum order subtotal amount + /// Gets or sets a minimum order total amount /// - public decimal MinOrderSubtotalAmount { get; set; } + public decimal? OrderTotalMinimum { get; set; } /// - /// Gets or sets a minimum order total amount + /// Gets or sets a maximum order total amount /// - public decimal MinOrderTotalAmount { get; set; } - + public decimal? OrderTotalMaximum { get; set; } + + /// + /// Gets or sets a value indicating how customer group restrictions are applied with each other + /// true - lowest possible total amount span gets applied, false - highest possible total amount span gets applied + /// + public bool MultipleOrderTotalRestrictionsExpandRange { get; set; } + /// /// Gets or sets a value indicating whether anonymous checkout allowed /// @@ -43,11 +49,11 @@ public OrderSettings() /// public bool TermsOfServiceEnabled { get; set; } - /// - /// Gets or sets a value indicating whether "Order completed" page should be skipped - /// - public bool DisableOrderCompletedPage { get; set; } - + /// + /// Gets or sets a value indicating whether "Order completed" page should be skipped + /// + public bool DisableOrderCompletedPage { get; set; } + /// /// Gets or sets a value indicating whether "Return requests" are allowed /// @@ -83,14 +89,19 @@ public OrderSettings() /// public int MinimumOrderPlacementInterval { get; set; } - /// - /// Gets or sets a value indicating whether to display all orders of all stores to a customer - /// - public bool DisplayOrdersOfAllStores { get; set; } + /// + /// Gets or sets a value indicating whether to display all orders of all stores to a customer + /// + public bool DisplayOrdersOfAllStores { get; set; } - /// - /// Page size of the order list - /// - public int OrderListPageSize { get; set; } - } + /// + /// Page size of the order list + /// + public int OrderListPageSize { get; set; } + + /// + /// Page size of the recurring payment list + /// + public int RecurringPaymentListPageSize { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrganizedShoppingCartItem.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrganizedShoppingCartItem.cs index 41d38a8d0f..a052f20006 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrganizedShoppingCartItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrganizedShoppingCartItem.cs @@ -1,28 +1,27 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; namespace SmartStore.Core.Domain.Orders { - public partial class OrganizedShoppingCartItem - { - public OrganizedShoppingCartItem(ShoppingCartItem item) - { - Guard.NotNull(item, nameof(item)); + public partial class OrganizedShoppingCartItem + { + public OrganizedShoppingCartItem(ShoppingCartItem item) + { + Guard.NotNull(item, nameof(item)); - Item = item; // must not be null - ChildItems = new List(); - BundleItemData = new ProductBundleItemData(item.BundleItem); - CustomProperties = new Dictionary(); - } + Item = item; // must not be null + ChildItems = new List(); + BundleItemData = new ProductBundleItemData(item.BundleItem); + CustomProperties = new Dictionary(); + } - public ShoppingCartItem Item { get; private set; } - public IList ChildItems { get; set; } - public ProductBundleItemData BundleItemData { get; set; } + public ShoppingCartItem Item { get; private set; } + public IList ChildItems { get; set; } + public ProductBundleItemData BundleItemData { get; set; } - /// - /// Use this dictionary for any custom data required along cart processing - /// - public Dictionary CustomProperties { get; set; } - } + /// + /// Use this dictionary for any custom data required along cart processing + /// + public Dictionary CustomProperties { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs b/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs index 9861dc9302..02fce4bf05 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs @@ -50,7 +50,7 @@ public partial class RecurringPayment : BaseEntity, ISoftDeletable /// Gets or sets the date and time of payment creation /// public DateTime CreatedOnUtc { get; set; } - + /// /// Gets the next payment date /// @@ -121,31 +121,31 @@ public DateTime? NextPaymentDate //} //else //{ - if (historyCollection.Count > 0) - { - switch (this.CyclePeriod) - { - case RecurringProductCyclePeriod.Days: - result = this.StartDateUtc.AddDays((double)this.CycleLength * historyCollection.Count); - break; - case RecurringProductCyclePeriod.Weeks: - result = this.StartDateUtc.AddDays((double)(7 * this.CycleLength) * historyCollection.Count); - break; - case RecurringProductCyclePeriod.Months: - result = this.StartDateUtc.AddMonths(this.CycleLength * historyCollection.Count); - break; - case RecurringProductCyclePeriod.Years: - result = this.StartDateUtc.AddYears(this.CycleLength * historyCollection.Count); - break; - default: - throw new SmartException("Not supported cycle period"); - } - } - else + if (historyCollection.Count > 0) + { + switch (this.CyclePeriod) { - if (this.TotalCycles > 0) - result = this.StartDateUtc; + case RecurringProductCyclePeriod.Days: + result = this.StartDateUtc.AddDays((double)this.CycleLength * historyCollection.Count); + break; + case RecurringProductCyclePeriod.Weeks: + result = this.StartDateUtc.AddDays((double)(7 * this.CycleLength) * historyCollection.Count); + break; + case RecurringProductCyclePeriod.Months: + result = this.StartDateUtc.AddMonths(this.CycleLength * historyCollection.Count); + break; + case RecurringProductCyclePeriod.Years: + result = this.StartDateUtc.AddYears(this.CycleLength * historyCollection.Count); + break; + default: + throw new SmartException("Not supported cycle period"); } + } + else + { + if (this.TotalCycles > 0) + result = this.StartDateUtc; + } //} return result; @@ -174,16 +174,10 @@ public int CyclesRemaining /// public RecurringProductCyclePeriod CyclePeriod { - get - { - return (RecurringProductCyclePeriod)this.CyclePeriodId; - } - set - { - this.CyclePeriodId = (int)value; - } + get => (RecurringProductCyclePeriod)this.CyclePeriodId; + set => this.CyclePeriodId = (int)value; } - + @@ -192,9 +186,9 @@ public RecurringProductCyclePeriod CyclePeriod /// public virtual ICollection RecurringPaymentHistory { - get { return _recurringPaymentHistory ?? (_recurringPaymentHistory = new List()); } - protected set { _recurringPaymentHistory = value; } - } + get => _recurringPaymentHistory ?? (_recurringPaymentHistory = new List()); + protected set => _recurringPaymentHistory = value; + } /// /// Gets the initial order diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPaymentHistory.cs b/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPaymentHistory.cs index 457bb9806d..4755d07c6a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPaymentHistory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPaymentHistory.cs @@ -16,12 +16,12 @@ public partial class RecurringPaymentHistory : BaseEntity /// Gets or sets the recurring payment identifier /// public int OrderId { get; set; } - + /// /// Gets or sets the date and time of entity creation /// public DateTime CreatedOnUtc { get; set; } - + /// /// Gets the recurring payment /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequest.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequest.cs index 888c2ac83e..c876feff76 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequest.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequest.cs @@ -4,116 +4,110 @@ namespace SmartStore.Core.Domain.Orders { - /// - /// Represents a return request - /// - [DataContract] - public partial class ReturnRequest : BaseEntity, IAuditable - { - /// - /// Gets or sets the store identifier - /// - [DataMember] - public int StoreId { get; set; } + /// + /// Represents a return request + /// + [DataContract] + public partial class ReturnRequest : BaseEntity, IAuditable + { + /// + /// Gets or sets the store identifier + /// + [DataMember] + public int StoreId { get; set; } /// /// Gets or sets the order item identifier /// [DataMember] - public int OrderItemId { get; set; } + public int OrderItemId { get; set; } /// /// Gets or sets the customer identifier /// [DataMember] - public int CustomerId { get; set; } + public int CustomerId { get; set; } /// /// Gets or sets the quantity /// [DataMember] - public int Quantity { get; set; } + public int Quantity { get; set; } /// /// Gets or sets the reason to return /// [DataMember] - public string ReasonForReturn { get; set; } + public string ReasonForReturn { get; set; } /// /// Gets or sets the requested action /// [DataMember] - public string RequestedAction { get; set; } + public string RequestedAction { get; set; } - /// - /// Gets or sets the date and time when requested action was last updated - /// - [DataMember] - public DateTime? RequestedActionUpdatedOnUtc { get; set; } + /// + /// Gets or sets the date and time when requested action was last updated + /// + [DataMember] + public DateTime? RequestedActionUpdatedOnUtc { get; set; } /// /// Gets or sets the customer comments /// [DataMember] - public string CustomerComments { get; set; } + public string CustomerComments { get; set; } /// /// Gets or sets the staff notes /// [DataMember] - public string StaffNotes { get; set; } + public string StaffNotes { get; set; } - /// - /// Gets or sets the admin comment - /// - [DataMember] - public string AdminComment { get; set; } + /// + /// Gets or sets the admin comment + /// + [DataMember] + public string AdminComment { get; set; } /// /// Gets or sets the return status identifier /// [DataMember] - public int ReturnRequestStatusId { get; set; } + public int ReturnRequestStatusId { get; set; } - /// - /// Gets or sets whether to refund to wallet. - /// - [DataMember] - public bool? RefundToWallet { get; set; } + /// + /// Gets or sets whether to refund to wallet. + /// + [DataMember] + public bool? RefundToWallet { get; set; } - /// - /// Gets or sets the date and time of entity creation - /// - [DataMember] - public DateTime CreatedOnUtc { get; set; } + /// + /// Gets or sets the date and time of entity creation + /// + [DataMember] + public DateTime CreatedOnUtc { get; set; } /// /// Gets or sets the date and time of entity update /// [DataMember] - public DateTime UpdatedOnUtc { get; set; } - + public DateTime UpdatedOnUtc { get; set; } + /// /// Gets or sets the return status /// - [DataMember] - public ReturnRequestStatus ReturnRequestStatus + [DataMember] + public ReturnRequestStatus ReturnRequestStatus { - get - { - return (ReturnRequestStatus)this.ReturnRequestStatusId; - } - set - { - this.ReturnRequestStatusId = (int)value; - } + get => (ReturnRequestStatus)this.ReturnRequestStatusId; + set => this.ReturnRequestStatusId = (int)value; } /// /// Gets or sets the customer /// [DataMember] - public virtual Customer Customer { get; set; } + public virtual Customer Customer { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs index a7c3c91273..3ceba8c27d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; @@ -9,21 +8,21 @@ namespace SmartStore.Core.Domain.Orders /// Represents a shopping cart item /// public partial class ShoppingCartItem : BaseEntity, IAuditable - { - /// - /// Gets or sets the store identifier - /// - public int StoreId { get; set; } + { + /// + /// Gets or sets the store identifier + /// + public int StoreId { get; set; } - /// - /// The parent shopping cart item id - /// - public int? ParentItemId { get; set; } + /// + /// The parent shopping cart item id + /// + public int? ParentItemId { get; set; } - /// - /// Gets or sets ths bundle item identifier - /// - public int? BundleItemId { get; set; } + /// + /// Gets or sets ths bundle item identifier + /// + public int? BundleItemId { get; set; } /// /// Gets or sets the shopping cart type identifier @@ -65,19 +64,13 @@ public partial class ShoppingCartItem : BaseEntity, IAuditable /// public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets or sets the shopping cart type - /// - public ShoppingCartType ShoppingCartType + /// + /// Gets or sets the shopping cart type + /// + public ShoppingCartType ShoppingCartType { - get - { - return (ShoppingCartType)this.ShoppingCartTypeId; - } - set - { - this.ShoppingCartTypeId = (int)value; - } + get => (ShoppingCartType)this.ShoppingCartTypeId; + set => this.ShoppingCartTypeId = (int)value; } /// @@ -90,10 +83,10 @@ public ShoppingCartType ShoppingCartType /// public virtual Customer Customer { get; set; } - /// - /// Gets or sets the product bundle item - /// - public virtual ProductBundleItem BundleItem { get; set; } + /// + /// Gets or sets the product bundle item + /// + public virtual ProductBundleItem BundleItem { get; set; } /// /// Gets a value indicating whether the shopping cart item is free shipping @@ -102,9 +95,9 @@ public bool IsFreeShipping { get { - var product = this.Product; - if (product != null) - return product.IsFreeShipping; + var product = this.Product; + if (product != null) + return product.IsFreeShipping; return true; } } @@ -116,9 +109,9 @@ public bool IsShipEnabled { get { - var product = this.Product; - if (product != null) - return product.IsShipEnabled; + var product = this.Product; + if (product != null) + return product.IsShipEnabled; return false; } } @@ -130,9 +123,9 @@ public bool IsTaxExempt { get { - var product = this.Product; - if (product != null) - return product.IsTaxExempt; + var product = this.Product; + if (product != null) + return product.IsTaxExempt; return false; } } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs index e569f371b7..0c9fec1eca 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs @@ -1,32 +1,33 @@ using SmartStore.Core.Configuration; -using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Core.Domain.Orders { - public class ShoppingCartSettings : BaseEntity, ISettings, ILocalizedEntity - { - public ShoppingCartSettings() - { - MaximumShoppingCartItems = 1000; - MaximumWishlistItems = 1000; - MoveItemsFromWishlistToCart = true; - ShowItemsFromWishlistToCartButton = true; - ShowProductImagesOnShoppingCart = true; - ShowProductBundleImagesOnShoppingCart = true; - ShowProductImagesOnWishList = true; - ShowProductBundleImagesOnWishList = true; - ShowDiscountBox = true; - ShowGiftCardBox = true; + public class ShoppingCartSettings : ISettings + { + public ShoppingCartSettings() + { + MaximumShoppingCartItems = 1000; + MaximumWishlistItems = 1000; + MoveItemsFromWishlistToCart = true; + ShowItemsFromWishlistToCartButton = true; + ShowProductImagesOnShoppingCart = true; + ShowProductBundleImagesOnShoppingCart = true; + ShowProductImagesOnWishList = true; + ShowProductBundleImagesOnWishList = true; + ShowDiscountBox = true; + ShowGiftCardBox = true; ShowCommentBox = true; - ShowEsdRevocationWaiverBox = true; - CrossSellsNumber = 8; - EmailWishlistEnabled = true; - MiniShoppingCartEnabled = true; - ShowProductImagesInMiniShoppingCart = true; + ShowEsdRevocationWaiverBox = true; + CrossSellsNumber = 8; + EmailWishlistEnabled = true; + MiniShoppingCartEnabled = true; + ShowProductImagesInMiniShoppingCart = true; ShowBasePrice = true; - ShowDeliveryTimes = true; + DeliveryTimesInShoppingCart = DeliveryTimesPresentation.DateOnly; ShowShortDesc = true; AllowAnonymousUsersToEmailWishlist = false; + AddProductsToBasketInSinglePositions = false; } /// @@ -43,7 +44,7 @@ public ShoppingCartSettings() /// Gets or sets a value indicating maximum number of items in the shopping cart /// public int MaximumShoppingCartItems { get; set; } - + /// /// Gets or sets a value indicating maximum number of items in the wishlist /// @@ -59,30 +60,30 @@ public ShoppingCartSettings() /// public bool MoveItemsFromWishlistToCart { get; set; } - /// - /// Gets or sets a value indicating whether to show the items from wishlist to ahopping cart button. - /// - public bool ShowItemsFromWishlistToCartButton { get; set; } + /// + /// Gets or sets a value indicating whether to show the items from wishlist to ahopping cart button. + /// + public bool ShowItemsFromWishlistToCartButton { get; set; } /// /// Gets or sets a value indicating whether to show product image on shopping cart page /// public bool ShowProductImagesOnShoppingCart { get; set; } - /// - /// Gets or sets a value indicating whether to show product bundle images on shopping cart page - /// - public bool ShowProductBundleImagesOnShoppingCart { get; set; } + /// + /// Gets or sets a value indicating whether to show product bundle images on shopping cart page + /// + public bool ShowProductBundleImagesOnShoppingCart { get; set; } /// /// Gets or sets a value indicating whether to show product image on wishlist page /// public bool ShowProductImagesOnWishList { get; set; } - /// - /// Gets or sets a value indicating whether to show product image on wishlist page - /// - public bool ShowProductBundleImagesOnWishList { get; set; } + /// + /// Gets or sets a value indicating whether to show product image on wishlist page + /// + public bool ShowProductBundleImagesOnWishList { get; set; } /// /// Gets or sets a value indicating whether to show discount box on shopping cart page @@ -99,31 +100,31 @@ public ShoppingCartSettings() /// public bool ShowCommentBox { get; set; } - /// - /// Gets or sets a value indicating whether to show a revocation waiver checkbox box for ESD products - /// - public bool ShowEsdRevocationWaiverBox { get; set; } + /// + /// Gets or sets a value indicating whether to show a revocation waiver checkbox box for ESD products + /// + public bool ShowEsdRevocationWaiverBox { get; set; } - /// - /// Gets or sets a value indicating whether to show a checkbox to subscribe to newsletters - /// - public CheckoutNewsLetterSubscription NewsLetterSubscription { get; set; } + /// + /// Gets or sets a value indicating whether to show a checkbox to subscribe to newsletters + /// + public CheckoutNewsLetterSubscription NewsLetterSubscription { get; set; } - /// - /// Gets or sets a value indicating whether to show a checkbox to let the customer accept to hand over email address to third party - /// - public CheckoutThirdPartyEmailHandOver ThirdPartyEmailHandOver { get; set; } + /// + /// Gets or sets a value indicating whether to show a checkbox to let the customer accept to hand over email address to third party + /// + public CheckoutThirdPartyEmailHandOver ThirdPartyEmailHandOver { get; set; } - /// - /// Gets or sets the label to accept to hand over the email to third party - /// - public string ThirdPartyEmailHandOverLabel { get; set; } + /// + /// Gets or sets the label to accept to hand over the email to third party + /// + public string ThirdPartyEmailHandOverLabel { get; set; } + + /// + /// Gets or sets a number of "Cross-sells" on shopping cart page + /// + public int CrossSellsNumber { get; set; } - /// - /// Gets or sets a number of "Cross-sells" on shopping cart page - /// - public int CrossSellsNumber { get; set; } - /// /// Gets or sets a value indicating whether "email a wishlist" feature is enabled /// @@ -133,7 +134,7 @@ public ShoppingCartSettings() /// Gets or sets a value indicating whether to enabled "email a wishlist" for anonymous users. /// public bool AllowAnonymousUsersToEmailWishlist { get; set; } - + /// Gets or sets a value indicating whether mini-shopping cart is enabled /// public bool MiniShoppingCartEnabled { get; set; } @@ -149,9 +150,9 @@ public ShoppingCartSettings() public bool ShowConfirmOrderLegalHint { get; set; } /// - /// Gets or sets a value indicating whether to show delivery times in the order summary + /// Specifies the presentation of delivery times in shopping cart. /// - public bool ShowDeliveryTimes { get; set; } + public DeliveryTimesPresentation DeliveryTimesInShoppingCart { get; set; } /// /// Gets or sets a value indicating whether to show the product short description in the order summary @@ -168,9 +169,14 @@ public ShoppingCartSettings() /// public bool ShowBasePrice { get; set; } - /// - /// Gets or sets a value indicating whether to show the quantity of linked attribute value product + /// + /// Gets or sets a value indicating whether to show the quantity of linked attribute value product + /// + public bool ShowLinkedAttributeValueQuantity { get; set; } + + /// + /// Gets or sets a value indicating whether to add products to the basket in single positions /// - public bool ShowLinkedAttributeValueQuantity { get; set; } + public bool AddProductsToBasketInSinglePositions { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs b/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs index 39d599f41d..96fa8b0323 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs @@ -1,18 +1,18 @@ namespace SmartStore.Core.Domain.Payments { - /// - /// The reason for automatic capturing of the payment amount. - /// - public enum CapturePaymentReason - { - /// - /// Capture payment because the order has been marked as shipped. - /// - OrderShipped = 0, + /// + /// The reason for automatic capturing of the payment amount. + /// + public enum CapturePaymentReason + { + /// + /// Capture payment because the order has been marked as shipped. + /// + OrderShipped = 0, - /// - /// Capture payment because the order has been marked as delivered. - /// - OrderDelivered - } + /// + /// Capture payment because the order has been marked as delivered. + /// + OrderDelivered + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs index cd51eabedb..5427dcdd9c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs @@ -6,11 +6,11 @@ namespace SmartStore.Core.Domain.Payments { - /// - /// Represents a payment method - /// - [DataContract] - public partial class PaymentMethod : BaseEntity, ILocalizedEntity, IStoreMappingSupported, IRulesContainer + /// + /// Represents a payment method + /// + [DataContract] + public partial class PaymentMethod : BaseEntity, ILocalizedEntity, IStoreMappingSupported, IRulesContainer { private ICollection _ruleSets; @@ -18,13 +18,13 @@ public partial class PaymentMethod : BaseEntity, ILocalizedEntity, IStoreMapping /// Gets or sets the payment method system name /// [DataMember] - public string PaymentMethodSystemName { get; set; } + public string PaymentMethodSystemName { get; set; } - /// - /// Gets or sets the full description - /// - [DataMember] - public string FullDescription { get; set; } + /// + /// Gets or sets the full description + /// + [DataMember] + public string FullDescription { get; set; } /// /// Gets or sets a value indicating whether to round the order total. Also known as "Cash rounding". @@ -33,19 +33,19 @@ public partial class PaymentMethod : BaseEntity, ILocalizedEntity, IStoreMapping [DataMember] public bool RoundOrderTotalEnabled { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } /// /// Gets or sets assigned rule sets. /// public virtual ICollection RuleSets { - get { return _ruleSets ?? (_ruleSets = new HashSet()); } - protected set { _ruleSets = value; } + get => _ruleSets ?? (_ruleSets = new HashSet()); + protected set => _ruleSets = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs index 7aad6a5667..5d3533c27d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs @@ -8,8 +8,8 @@ public class PaymentSettings : ISettings public PaymentSettings() { ActivePaymentMethodSystemNames = new List(); - AllowRePostingPayments = true; - BypassPaymentMethodSelectionIfOnlyOne = false; + AllowRePostingPayments = true; + BypassPaymentMethodSelectionIfOnlyOne = false; } /// @@ -27,19 +27,19 @@ public PaymentSettings() /// public bool BypassPaymentMethodSelectionIfOnlyOne { get; set; } - /// - /// Gets or sets a value indicating whether we should bypass the payment method info page - /// - public bool BypassPaymentMethodInfo { get; set; } + /// + /// Gets or sets a value indicating whether we should bypass the payment method info page + /// + public bool BypassPaymentMethodInfo { get; set; } - /// - /// Gets or sets the reason for automatic payment capturing - /// - public CapturePaymentReason? CapturePaymentReason { get; set; } + /// + /// Gets or sets the reason for automatic payment capturing + /// + public CapturePaymentReason? CapturePaymentReason { get; set; } - /// - /// Gets or sets the identifier of the currency in which the wallet is kept. - /// - public int WalletCurrencyId { get; set; } - } + /// + /// Gets or sets the identifier of the currency in which the wallet is kept. + /// + public int WalletCurrencyId { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Polls/Poll.cs b/src/Libraries/SmartStore.Core/Domain/Polls/Poll.cs index 73c2d2481f..d3d51d806a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Polls/Poll.cs +++ b/src/Libraries/SmartStore.Core/Domain/Polls/Poll.cs @@ -57,20 +57,20 @@ public partial class Poll : BaseEntity, IStoreMappingSupported /// public DateTime? EndDateUtc { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - public bool LimitedToStores { get; set; } - + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + public bool LimitedToStores { get; set; } + /// /// Gets or sets the news comments /// public virtual ICollection PollAnswers { - get { return _pollAnswers ?? (_pollAnswers = new HashSet()); } - protected set { _pollAnswers = value; } + get => _pollAnswers ?? (_pollAnswers = new HashSet()); + protected set => _pollAnswers = value; } - + /// /// Gets or sets the language /// diff --git a/src/Libraries/SmartStore.Core/Domain/Polls/PollAnswer.cs b/src/Libraries/SmartStore.Core/Domain/Polls/PollAnswer.cs index 86fb99cb53..af8dfde475 100644 --- a/src/Libraries/SmartStore.Core/Domain/Polls/PollAnswer.cs +++ b/src/Libraries/SmartStore.Core/Domain/Polls/PollAnswer.cs @@ -28,7 +28,7 @@ public partial class PollAnswer : BaseEntity /// Gets or sets the display order /// public int DisplayOrder { get; set; } - + /// /// Gets or sets the poll /// @@ -39,8 +39,8 @@ public partial class PollAnswer : BaseEntity /// public virtual ICollection PollVotingRecords { - get { return _pollVotingRecords ?? (_pollVotingRecords = new HashSet()); } - protected set { _pollVotingRecords = value; } + get => _pollVotingRecords ?? (_pollVotingRecords = new HashSet()); + protected set => _pollVotingRecords = value; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Security/AclRecord.cs b/src/Libraries/SmartStore.Core/Domain/Security/AclRecord.cs index 7bd0cf40e3..ff8f04dede 100644 --- a/src/Libraries/SmartStore.Core/Domain/Security/AclRecord.cs +++ b/src/Libraries/SmartStore.Core/Domain/Security/AclRecord.cs @@ -21,17 +21,17 @@ public partial class AclRecord : BaseEntity /// public int CustomerRoleId { get; set; } - /// - /// Gets or sets the customer role - /// - public virtual CustomerRole CustomerRole { get; set; } + /// + /// Gets or sets the customer role + /// + public virtual CustomerRole CustomerRole { get; set; } - /// - /// Gets or sets a value indicating whether the entry is idle - /// - /// - /// An entry is idle when it's related entity has been soft-deleted - /// - public bool IsIdle { get; set; } + /// + /// Gets or sets a value indicating whether the entry is idle + /// + /// + /// An entry is idle when it's related entity has been soft-deleted + /// + public bool IsIdle { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Security/DefaultPermissionRecord.cs b/src/Libraries/SmartStore.Core/Domain/Security/DefaultPermissionRecord.cs index ca9665ea9c..1cb5af8b56 100644 --- a/src/Libraries/SmartStore.Core/Domain/Security/DefaultPermissionRecord.cs +++ b/src/Libraries/SmartStore.Core/Domain/Security/DefaultPermissionRecord.cs @@ -7,7 +7,7 @@ namespace SmartStore.Core.Domain.Security /// public class DefaultPermissionRecord { - public DefaultPermissionRecord() + public DefaultPermissionRecord() { PermissionRecords = new List(); } diff --git a/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs b/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs index 872f42384d..60661ea0df 100644 --- a/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs +++ b/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs @@ -5,9 +5,9 @@ namespace SmartStore.Core.Domain.Security /// public partial interface IAclSupported { - /// - /// Gets or sets a value indicating whether the entity is subject to ACL - /// - bool SubjectToAcl { get; set; } + /// + /// Gets or sets a value indicating whether the entity is subject to ACL + /// + bool SubjectToAcl { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Security/PermissionRecord.cs b/src/Libraries/SmartStore.Core/Domain/Security/PermissionRecord.cs index 70edc60ae7..aa2f64a941 100644 --- a/src/Libraries/SmartStore.Core/Domain/Security/PermissionRecord.cs +++ b/src/Libraries/SmartStore.Core/Domain/Security/PermissionRecord.cs @@ -15,14 +15,14 @@ public class PermissionRecord : BaseEntity /// [Index] public string SystemName { get; set; } - + /// /// Gets or sets permission role mappings. /// public virtual ICollection PermissionRoleMappings { - get { return _permissionRoleMappings ?? (_permissionRoleMappings = new HashSet()); } - protected set { _permissionRoleMappings = value; } + get => _permissionRoleMappings ?? (_permissionRoleMappings = new HashSet()); + protected set => _permissionRoleMappings = value; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Security/SecuritySettings.cs b/src/Libraries/SmartStore.Core/Domain/Security/SecuritySettings.cs index 71999bd5ba..66f00dad16 100644 --- a/src/Libraries/SmartStore.Core/Domain/Security/SecuritySettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Security/SecuritySettings.cs @@ -6,21 +6,21 @@ namespace SmartStore.Core.Domain.Security { public class SecuritySettings : ISettings { - public SecuritySettings() - { - EncryptionKey = CommonHelper.GenerateRandomDigitCode(16); - AdminAreaAllowedIpAddresses = new List(); - } + public SecuritySettings() + { + EncryptionKey = CommonHelper.GenerateRandomDigitCode(16); + AdminAreaAllowedIpAddresses = new List(); + } - /// - /// When true, bypasses any SSL redirection on localhost - /// - public bool UseSslOnLocalhost { get; set; } + /// + /// When true, bypasses any SSL redirection on localhost + /// + public bool UseSslOnLocalhost { get; set; } - /// - /// Gets or sets an encryption key - /// - public string EncryptionKey { get; set; } + /// + /// Gets or sets an encryption key + /// + public string EncryptionKey { get; set; } /// /// Gets or sets a list of adminn area allowed IP addresses @@ -32,9 +32,9 @@ public SecuritySettings() /// public bool HideAdminMenuItemsBasedOnPermissions { get; set; } - /// - /// Gets or sets a vaule indicating whether "Honeypot" is enabled to prevent bots from posting forms. - /// - public bool EnableHoneypotProtection { get; set; } + /// + /// Gets or sets a vaule indicating whether "Honeypot" is enabled to prevent bots from posting forms. + /// + public bool EnableHoneypotProtection { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/CanonicalHostNameRule.cs b/src/Libraries/SmartStore.Core/Domain/Seo/CanonicalHostNameRule.cs index 7e64e097aa..19fe7a890d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/CanonicalHostNameRule.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/CanonicalHostNameRule.cs @@ -1,13 +1,12 @@ namespace SmartStore.Core.Domain.Seo { - - public enum CanonicalHostNameRule + public enum CanonicalHostNameRule { /// /// Doesn't matter (as requested) /// NoRule, - /// + /// /// The www prefix is required (www.myshop.com is default host) /// RequireWww, @@ -16,5 +15,4 @@ public enum CanonicalHostNameRule /// OmitWww } - -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/HomePageSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/HomePageSettings.cs new file mode 100644 index 0000000000..418b4ed90f --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Seo/HomePageSettings.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core.Configuration; + +namespace SmartStore.Core.Domain.Seo +{ + public class HomePageSettings : ISettings + { + public string MetaTitle { get; set; } + public string MetaDescription { get; set; } + public string MetaKeywords { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs b/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs index 6b52764cec..154a2c2b32 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs @@ -6,5 +6,5 @@ namespace SmartStore.Core.Domain.Seo /// public interface ISlugSupported { - } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs index 1c98426cee..09c0ca7f5a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs @@ -4,96 +4,105 @@ namespace SmartStore.Core.Domain.Seo { - public class SeoSettings : ISettings + public class SeoSettings : ISettings { - public SeoSettings() - { - ExtraRobotsDisallows = new List { "/blog/tag/", "/blog/month/", "/producttags/" }; - - ReservedUrlRecordSlugs = new List - { - "admin", - "install", - "recentlyviewedproducts", - "newproducts", - "compareproducts", - "clearcomparelist", - "setproductreviewhelpfulness", - "login", - "register", - "logout", - "cart", - "wishlist", - "emailwishlist", - "checkout", - "contactus", - "passwordrecovery", - "subscribenewsletter", - "blog", - "boards", - "inboxupdate", - "sentupdate", - "news", - "sitemap", - "sitemapseo", - "search", - "config", - "api", - "odata" - }; - - SeoNameCharConversion = string.Join(Environment.NewLine, new List - { - "ä;ae", - "ö;oe", - "ü;ue", - "Ä;Ae", - "Ö;Oe", - "Ü;Ue", - "ß;ss" - }); - } - - public string PageTitleSeparator { get; set; } = ". "; - public PageTitleSeoAdjustment PageTitleSeoAdjustment { get; set; } = PageTitleSeoAdjustment.PagenameAfterStorename; - public string DefaultTitle { get; set; } = "Shop"; - - public string DefaultMetaKeywords { get; set; } = string.Empty; - public string DefaultMetaDescription { get; set; } = string.Empty; - public string MetaRobotsContent { get; set; } - - public bool ConvertNonWesternChars { get; set; } + public SeoSettings() + { + ExtraRobotsDisallows = new List { "/blog/tag/", "/blog/month/", "/producttags/" }; + ExtraRobotsAllows = new List(); + + ReservedUrlRecordSlugs = new List + { + "admin", + "install", + "recentlyviewedproducts", + "newproducts", + "compareproducts", + "clearcomparelist", + "setproductreviewhelpfulness", + "login", + "register", + "logout", + "cart", + "wishlist", + "emailwishlist", + "checkout", + "contactus", + "passwordrecovery", + "subscribenewsletter", + "blog", + "boards", + "inboxupdate", + "sentupdate", + "news", + "sitemap", + "sitemapseo", + "search", + "config", + "api", + "odata" + }; + + SeoNameCharConversion = string.Join(Environment.NewLine, new List + { + "ä;ae", + "ö;oe", + "ü;ue", + "Ä;Ae", + "Ö;Oe", + "Ü;Ue", + "ß;ss" + }); + } + + public string PageTitleSeparator { get; set; } = ". "; + public PageTitleSeoAdjustment PageTitleSeoAdjustment { get; set; } = PageTitleSeoAdjustment.PagenameAfterStorename; + + /// + /// Gets or sets the default meta title for the shop. + /// + public string MetaTitle { get; set; } = "Shop"; + public string MetaDescription { get; set; } = string.Empty; + public string MetaKeywords { get; set; } = string.Empty; + + public string MetaRobotsContent { get; set; } + + public bool ConvertNonWesternChars { get; set; } public bool AllowUnicodeCharsInUrls { get; set; } - public string SeoNameCharConversion { get; set; } + public string SeoNameCharConversion { get; set; } - public bool CanonicalUrlsEnabled { get; set; } + public bool CanonicalUrlsEnabled { get; set; } public CanonicalHostNameRule CanonicalHostNameRule { get; set; } = CanonicalHostNameRule.NoRule; - /// - /// Slugs (sename) reserved for some other needs - /// - public List ReservedUrlRecordSlugs { get; set; } + /// + /// Slugs (sename) reserved for some other needs + /// + public List ReservedUrlRecordSlugs { get; set; } public List ExtraRobotsDisallows { get; set; } - /// - /// A value indicating whether to load all URL records and active slugs on application startup - /// - public bool LoadAllUrlAliasesOnStartup { get; set; } = true; + public List ExtraRobotsAllows { get; set; } + + /// + /// A value indicating whether to load all URL records and active slugs on application startup + /// + public bool LoadAllUrlAliasesOnStartup { get; set; } = true; + + public bool RedirectLegacyTopicUrls { get; set; } + + #region XML Sitemap - public bool RedirectLegacyTopicUrls { get; set; } + public bool XmlSitemapEnabled { get; set; } = true; + public bool XmlSitemapIncludesCategories { get; set; } = true; + public bool XmlSitemapIncludesManufacturers { get; set; } = true; + public bool XmlSitemapIncludesProducts { get; set; } = true; + public bool XmlSitemapIncludesTopics { get; set; } = true; + public bool XmlSitemapIncludesBlog { get; set; } = true; + public bool XmlSitemapIncludesNews { get; set; } = true; + public bool XmlSitemapIncludesForum { get; set; } = true; - #region XML Sitemap + #endregion - public bool XmlSitemapEnabled { get; set; } = true; - public bool XmlSitemapIncludesCategories { get; set; } = true; - public bool XmlSitemapIncludesManufacturers { get; set; } = true; - public bool XmlSitemapIncludesProducts { get; set; } = true; - public bool XmlSitemapIncludesTopics { get; set; } = true; - public bool XmlSitemapIncludesBlog { get; set; } = true; - public bool XmlSitemapIncludesNews { get; set; } = true; - public bool XmlSitemapIncludesForum { get; set; } = true; - #endregion - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecord.cs b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecord.cs index 8022e4dcfc..11640bc967 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecord.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecord.cs @@ -6,36 +6,36 @@ namespace SmartStore.Core.Domain.Seo /// Represents an URL record /// [DataContract] - public partial class UrlRecord : BaseEntity + public partial class UrlRecord : BaseEntity { /// /// Gets or sets the entity identifier /// [DataMember] - public int EntityId { get; set; } + public int EntityId { get; set; } /// /// Gets or sets the entity name /// [DataMember] - public string EntityName { get; set; } + public string EntityName { get; set; } /// /// Gets or sets the slug /// [DataMember] - public string Slug { get; set; } + public string Slug { get; set; } /// /// Gets or sets the value indicating whether the record is active /// [DataMember] - public bool IsActive { get; set; } + public bool IsActive { get; set; } /// /// Gets or sets the language identifier /// [DataMember] - public int LanguageId { get; set; } + public int LanguageId { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs index 733e800f7a..3f765dd5d8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/UrlRecordCollection.cs @@ -6,90 +6,97 @@ namespace SmartStore.Core.Domain.Seo { public class UrlRecordCollection : IReadOnlyCollection - { - private readonly string _entityName; - private readonly IDictionary _dict; - private HashSet _requestedSet; - - public UrlRecordCollection(string entityName, int[] requestedSet, IEnumerable items) - { - Guard.NotEmpty(entityName, nameof(entityName)); - Guard.NotNull(items, nameof(items)); - - _entityName = entityName; - _dict = items.ToDictionarySafe(x => CreateKey(x.EntityId, x.LanguageId)); - - if (requestedSet != null && requestedSet.Length > 0) - { - _requestedSet = new HashSet(requestedSet); - } - } - - public void MergeWith(UrlRecordCollection other) - { - Guard.NotNull(other, nameof(other)); - - if (!this._entityName.IsCaseInsensitiveEqual(other._entityName)) - { - throw new InvalidOperationException("Expected group '{0}', but was '{1}'".FormatInvariant(this._entityName, other._entityName)); - } - - // Merge dictionary - other._dict.Merge(this._dict, true); - - // Merge requested set (entity ids) - if (this._requestedSet != null) - { - if (other._requestedSet == null) - { - other._requestedSet = new HashSet(this._requestedSet); - } - else - { - other._requestedSet.AddRange(this._requestedSet); - } - } - } - - public string GetSlug(int languageId, int entityId, bool returnDefaultValue = true) - { - var slug = Find(languageId, entityId)?.Slug; - + { + private readonly string _entityName; + private readonly IDictionary _dict; + private HashSet _requestedSet; + + public UrlRecordCollection(string entityName, int[] requestedSet, IEnumerable items) + { + Guard.NotEmpty(entityName, nameof(entityName)); + Guard.NotNull(items, nameof(items)); + + _entityName = entityName; + _dict = items.ToDictionarySafe(x => CreateKey(x.EntityId, x.LanguageId)); + + if (requestedSet != null && requestedSet.Length > 0) + { + _requestedSet = new HashSet(requestedSet); + } + } + + public void MergeWith(UrlRecordCollection other) + { + Guard.NotNull(other, nameof(other)); + + if (!this._entityName.IsCaseInsensitiveEqual(other._entityName)) + { + throw new InvalidOperationException("Expected group '{0}', but was '{1}'".FormatInvariant(this._entityName, other._entityName)); + } + + // Merge dictionary + other._dict.Merge(this._dict, true); + + // Merge requested set (entity ids) + if (this._requestedSet != null) + { + if (other._requestedSet == null) + { + other._requestedSet = new HashSet(this._requestedSet); + } + else + { + other._requestedSet.AddRange(this._requestedSet); + } + } + } + + public string GetSlug(int languageId, int entityId, bool returnDefaultValue = true) + { + var slug = Find(languageId, entityId)?.Slug; + if (returnDefaultValue && languageId != 0 && string.IsNullOrEmpty(slug)) { slug = Find(0, entityId)?.Slug; } return slug; - } - - public UrlRecord Find(int languageId, int entityId) - { - var item = _dict.Get(CreateKey(entityId, languageId)); - - if (item == null && (_requestedSet == null || _requestedSet.Contains(entityId))) - { - // Although the item does not exist in the local dictionary it has been requested - // from the database, which means it does not exist in the db either. - // Avoid the upcoming roundtrip. - return new UrlRecord - { - EntityName = _entityName, - EntityId = entityId, - LanguageId = languageId - }; - } - - return item; - } - - private string CreateKey(int entityId, int languageId) - { - return string.Concat(entityId.ToString(CultureInfo.InvariantCulture), "-", languageId); - } - - public int Count => _dict.Values.Count; - public IEnumerator GetEnumerator() => _dict.Values.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _dict.Values.GetEnumerator(); - } + } + + public UrlRecord Find(int languageId, int entityId) + { + var item = _dict.Get(CreateKey(entityId, languageId)); + + if (item == null && (_requestedSet == null || _requestedSet.Contains(entityId))) + { + // Although the item does not exist in the local dictionary it has been requested + // from the database, which means it does not exist in the db either. + // Avoid the upcoming roundtrip. + return new UrlRecord + { + EntityName = _entityName, + EntityId = entityId, + LanguageId = languageId + }; + } + + return item; + } + + private string CreateKey(int entityId, int languageId) + { + return string.Concat(entityId.ToString(CultureInfo.InvariantCulture), "-", languageId); + } + + public int Count => _dict.Values.Count; + public IEnumerator GetEnumerator() + { + return _dict.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _dict.Values.GetEnumerator(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs index 5e76bf8d43..d172210689 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using SmartStore.Core.Domain.Orders; -using System.Runtime.Serialization; using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using SmartStore.Core.Domain.Orders; namespace SmartStore.Core.Domain.Shipping { @@ -10,7 +10,7 @@ namespace SmartStore.Core.Domain.Shipping /// Represents a shipment /// [DataContract] - public partial class Shipment : BaseEntity + public partial class Shipment : BaseEntity { private ICollection _shipmentItems; @@ -18,13 +18,13 @@ public partial class Shipment : BaseEntity /// Gets or sets the order identifier /// [DataMember] - public int OrderId { get; set; } - + public int OrderId { get; set; } + /// /// Gets or sets the tracking number of this shipment /// - [DataMember] - public string TrackingNumber { get; set; } + [DataMember] + public string TrackingNumber { get; set; } /// /// Gets or sets the tracking URL. @@ -35,34 +35,34 @@ public partial class Shipment : BaseEntity /// /// Gets or sets the total weight of this shipment - /// It's nullable for compatibility with the previous version of SmartStore.NET where was no such property + /// It's nullable for compatibility with the previous version of Smartstore where was no such property /// [DataMember] - public decimal? TotalWeight { get; set; } + public decimal? TotalWeight { get; set; } /// /// Gets or sets the shipped date and time /// [DataMember] - public DateTime? ShippedDateUtc { get; set; } + public DateTime? ShippedDateUtc { get; set; } /// /// Gets or sets the delivery date and time /// [DataMember] - public DateTime? DeliveryDateUtc { get; set; } + public DateTime? DeliveryDateUtc { get; set; } /// /// Gets or sets the entity creation date /// [DataMember] - public DateTime CreatedOnUtc { get; set; } + public DateTime CreatedOnUtc { get; set; } /// /// Gets the order /// [DataMember] - public virtual Order Order { get; set; } + public virtual Order Order { get; set; } /// /// Gets or sets the shipment items @@ -70,8 +70,8 @@ public partial class Shipment : BaseEntity [DataMember] public virtual ICollection ShipmentItems { - get { return _shipmentItems ?? (_shipmentItems = new HashSet()); } - protected set { _shipmentItems = value; } + get => _shipmentItems ?? (_shipmentItems = new HashSet()); + protected set => _shipmentItems = value; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs index bba860d0b2..09cc57c627 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs @@ -6,11 +6,11 @@ namespace SmartStore.Core.Domain.Shipping { - /// - /// Represents a shipping method (used for offline shipping rate computation methods) - /// - [DataContract] - public partial class ShippingMethod : BaseEntity, ILocalizedEntity, IStoreMappingSupported, IRulesContainer + /// + /// Represents a shipping method (used for offline shipping rate computation methods) + /// + [DataContract] + public partial class ShippingMethod : BaseEntity, ILocalizedEntity, IStoreMappingSupported, IRulesContainer { private ICollection _ruleSets; @@ -18,39 +18,39 @@ public partial class ShippingMethod : BaseEntity, ILocalizedEntity, IStoreMappin /// Gets or sets the name /// [DataMember] - public string Name { get; set; } + public string Name { get; set; } /// /// Gets or sets the description /// [DataMember] - public string Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } - /// - /// Gets or sets whether to ignore charges - /// - [DataMember] - public bool IgnoreCharges { get; set; } + /// + /// Gets or sets whether to ignore charges + /// + [DataMember] + public bool IgnoreCharges { get; set; } - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } /// /// Gets or sets assigned rule sets. /// public virtual ICollection RuleSets { - get { return _ruleSets ?? (_ruleSets = new HashSet()); } - protected set { _ruleSets = value; } + get => _ruleSets ?? (_ruleSets = new HashSet()); + protected set => _ruleSets = value; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs index 5edb06f415..ec5c564e45 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs @@ -1,11 +1,3 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml.Serialization; - namespace SmartStore.Core.Domain.Shipping { /// @@ -13,10 +5,10 @@ namespace SmartStore.Core.Domain.Shipping /// public partial class ShippingOption { - /// - /// Shipping method identifier - /// - public int ShippingMethodId { get; set; } + /// + /// Shipping method identifier + /// + public int ShippingMethodId { get; set; } /// /// Gets or sets the system name of shipping rate computation method diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs index 1dc2bfbf54..2138f1c115 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs @@ -7,9 +7,11 @@ public class ShippingSettings : ISettings { public ShippingSettings() { - ActiveShippingRateComputationMethodSystemNames = new List() { "Shipping.FixedRate" }; - EstimateShippingEnabled = true; - ReturnValidOptionsIfThereAreAny = true; + ActiveShippingRateComputationMethodSystemNames = new List { "Shipping.FixedRate" }; + EstimateShippingEnabled = true; + ReturnValidOptionsIfThereAreAny = true; + TodayShipmentHour = 14; + DeliveryOnWorkweekDaysOnly = true; } /// @@ -62,5 +64,24 @@ public ShippingSettings() /// Gets or sets a value indicating whether to charge only the highest shipping surcharge of products /// public bool ChargeOnlyHighestProductShippingSurcharge { get; set; } + + #region Delivery Time + + /// + /// Specifies a custom formatting pattern for delivery time dates (e.g. "M"). + /// + public string DeliveryTimesDateFormat { get; set; } + + /// + /// Specifies the hour (value between 1 and 24) by which the order will be shipped the same day. + /// + public int? TodayShipmentHour { get; set; } + + /// + /// Specifies whether delivery takes place only on workweek days. + /// + public bool DeliveryOnWorkweekDaysOnly { get; set; } + + #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/StoreInformationSettings.cs b/src/Libraries/SmartStore.Core/Domain/StoreInformationSettings.cs index b486b2acc3..ccab383818 100644 --- a/src/Libraries/SmartStore.Core/Domain/StoreInformationSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/StoreInformationSettings.cs @@ -4,12 +4,12 @@ namespace SmartStore.Core.Domain { public class StoreInformationSettings : ISettings { - public StoreInformationSettings() - { - StoreClosedAllowForAdmins = true; - } - - /// + public StoreInformationSettings() + { + StoreClosedAllowForAdmins = true; + } + + /// /// Gets or sets a value indicating whether store is closed /// public bool StoreClosed { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/IStoreMappingSupported.cs b/src/Libraries/SmartStore.Core/Domain/Stores/IStoreMappingSupported.cs index 9303ccab4c..e3aaf3c44c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/IStoreMappingSupported.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/IStoreMappingSupported.cs @@ -1,13 +1,13 @@ namespace SmartStore.Core.Domain.Stores { - /// - /// Represents an entity which supports store mapping - /// - public partial interface IStoreMappingSupported - { - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - bool LimitedToStores { get; set; } - } + /// + /// Represents an entity which supports store mapping + /// + public partial interface IStoreMappingSupported + { + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + bool LimitedToStores { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs index 2ea353a1c4..52d602cb97 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs @@ -1,159 +1,188 @@ using System.Runtime.Serialization; using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Media; namespace SmartStore.Core.Domain.Stores { - /// - /// Represents a store - /// - [DataContract] - public partial class Store : BaseEntity - { - /// - /// Gets or sets the store name - /// - [DataMember] - public string Name { get; set; } - - /// - /// Gets or sets the store URL - /// - [DataMember] - public string Url { get; set; } - - /// - /// Gets or sets a value indicating whether SSL is enabled - /// - [DataMember] - public bool SslEnabled { get; set; } - - /// - /// Gets or sets the store secure URL (HTTPS) - /// - [DataMember] - public string SecureUrl { get; set; } - - /// - /// Gets or sets a value indicating whether all pages will be forced to use SSL (no matter of a specified [RequireHttpsByConfigAttribute] attribute) - /// - [DataMember] - public bool ForceSslForAllPages { get; set; } - - /// - /// Gets or sets the comma separated list of possible HTTP_HOST values - /// - [DataMember] - public string Hosts { get; set; } - - /// - /// Gets or sets the logo media file id - /// - [DataMember] - public int LogoMediaFileId { get; set; } - - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } - - /// - /// Gets or sets a store specific id for the HTML body - /// - [DataMember] - public string HtmlBodyId { get; set; } - - /// - /// Gets or sets the CDN host name, if static media content should be served through a CDN. - /// - [DataMember] - public string ContentDeliveryNetwork { get; set; } - - /// - /// Gets or sets the primary store currency identifier - /// - [DataMember] - public int PrimaryStoreCurrencyId { get; set; } - - /// - /// Gets or sets the primary exchange rate currency identifier - /// - [DataMember] - public int PrimaryExchangeRateCurrencyId { get; set; } - - /// - /// Gets or sets the primary store currency - /// - [DataMember] - public virtual Currency PrimaryStoreCurrency { get; set; } - - /// - /// Gets or sets the primary exchange rate currency - /// - [DataMember] - public virtual Currency PrimaryExchangeRateCurrency { get; set; } - - - /// - /// Gets the security mode for the store - /// - public virtual HttpSecurityMode GetSecurityMode(bool? useSsl = null) - { - if (useSsl ?? SslEnabled) - { - if (SecureUrl.HasValue() && Url.HasValue() && !Url.StartsWith("https")) - { - return HttpSecurityMode.SharedSsl; - } - else - { - return HttpSecurityMode.Ssl; - } - } - - return HttpSecurityMode.Unsecured; - } - - private string _secureHost; - private string _unsecureHost; - - /// - /// Gets the store host name - /// - /// - /// If false, returns the default unsecured url. - /// If true, returns the secure url, but only if SSL is enabled for the store. - /// - /// The host name - public string GetHost(bool secure) - { - return secure - ? _secureHost ?? (_secureHost = GetHostInternal(true)) - : _unsecureHost ?? (_unsecureHost = GetHostInternal(false)); - } - - private string GetHostInternal(bool secure) - { - var host = string.Empty; - - if (secure && SslEnabled) - { - if (SecureUrl.HasValue()) - { - host = SecureUrl; - } - else - { - host = Url.Replace("http:/", "https:/"); - } - } - else - { - host = Url; - } - - return host.EnsureEndsWith("/"); - } - } + /// + /// Represents a store + /// + [DataContract] + public partial class Store : BaseEntity + { + /// + /// Gets or sets the store name + /// + [DataMember] + public string Name { get; set; } + + /// + /// Gets or sets the store URL + /// + [DataMember] + public string Url { get; set; } + + /// + /// Gets or sets a value indicating whether SSL is enabled + /// + [DataMember] + public bool SslEnabled { get; set; } + + /// + /// Gets or sets the store secure URL (HTTPS) + /// + [DataMember] + public string SecureUrl { get; set; } + + /// + /// Gets or sets a value indicating whether all pages will be forced to use SSL (no matter of a specified [RequireHttpsByConfigAttribute] attribute) + /// + [DataMember] + public bool ForceSslForAllPages { get; set; } + + /// + /// Gets or sets the comma separated list of possible HTTP_HOST values + /// + [DataMember] + public string Hosts { get; set; } + + /// + /// Gets or sets the logo media file id + /// + [DataMember] + public int LogoMediaFileId { get; set; } + + /// + /// Gets or sets the png icon media file id + /// + [DataMember] + public int? FavIconMediaFileId { get; set; } + + /// + /// Gets or sets the png icon media file id + /// + [DataMember] + public int? PngIconMediaFileId { get; set; } + + /// + /// Gets or sets the apple touch icon media file id + /// + [DataMember] + public int? AppleTouchIconMediaFileId { get; set; } + + /// + /// Gets or sets the ms tile image media file id + /// + [DataMember] + public int? MsTileImageMediaFileId { get; set; } + + /// + /// Gets or sets the ms tile color + /// + [DataMember] + public string MsTileColor { get; set; } + + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } + + /// + /// Gets or sets a store specific id for the HTML body + /// + [DataMember] + public string HtmlBodyId { get; set; } + + /// + /// Gets or sets the CDN host name, if static media content should be served through a CDN. + /// + [DataMember] + public string ContentDeliveryNetwork { get; set; } + + /// + /// Gets or sets the primary store currency identifier + /// + [DataMember] + public int PrimaryStoreCurrencyId { get; set; } + + /// + /// Gets or sets the primary exchange rate currency identifier + /// + [DataMember] + public int PrimaryExchangeRateCurrencyId { get; set; } + + /// + /// Gets or sets the primary store currency + /// + [DataMember] + public virtual Currency PrimaryStoreCurrency { get; set; } + + /// + /// Gets or sets the primary exchange rate currency + /// + [DataMember] + public virtual Currency PrimaryExchangeRateCurrency { get; set; } + + + /// + /// Gets the security mode for the store + /// + public virtual HttpSecurityMode GetSecurityMode(bool? useSsl = null) + { + if (useSsl ?? SslEnabled) + { + if (SecureUrl.HasValue() && Url.HasValue() && !Url.StartsWith("https")) + { + return HttpSecurityMode.SharedSsl; + } + else + { + return HttpSecurityMode.Ssl; + } + } + + return HttpSecurityMode.Unsecured; + } + + private string _secureHost; + private string _unsecureHost; + + /// + /// Gets the store host name + /// + /// + /// If false, returns the default unsecured url. + /// If true, returns the secure url, but only if SSL is enabled for the store. + /// + /// The host name + public string GetHost(bool secure) + { + return secure + ? _secureHost ?? (_secureHost = GetHostInternal(true)) + : _unsecureHost ?? (_unsecureHost = GetHostInternal(false)); + } + + private string GetHostInternal(bool secure) + { + var host = string.Empty; + + if (secure && SslEnabled) + { + if (SecureUrl.HasValue()) + { + host = SecureUrl; + } + else + { + host = Url.Replace("http:/", "https:/"); + } + } + else + { + host = Url; + } + + return host.EnsureEndsWith("/"); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs index 1185f4b399..80e71808e7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs @@ -1,50 +1,49 @@ using System; using System.Linq; -using System.Collections.Generic; namespace SmartStore.Core.Domain.Stores { - public static class StoreExtensions - { - /// - /// Indicates whether a store contains a specified host - /// - /// Store - /// Host - /// true - contains, false - no - public static bool ContainsHostValue(this Store store, string host) - { - Guard.NotNull(store, nameof(store)); + public static class StoreExtensions + { + /// + /// Indicates whether a store contains a specified host + /// + /// Store + /// Host + /// true - contains, false - no + public static bool ContainsHostValue(this Store store, string host) + { + Guard.NotNull(store, nameof(store)); - if (string.IsNullOrEmpty(host)) - { - return false; - } + if (string.IsNullOrEmpty(host)) + { + return false; + } - var contains = store.ParseHostValues() - .FirstOrDefault(x => x.Equals(host, StringComparison.InvariantCultureIgnoreCase)) != null; - return contains; - } + var contains = store.ParseHostValues() + .FirstOrDefault(x => x.Equals(host, StringComparison.InvariantCultureIgnoreCase)) != null; + return contains; + } - /// - /// Parse comma-separated hosts - /// - /// Store - /// Comma-separated hosts - public static string[] ParseHostValues(this Store store) - { - Guard.NotNull(store, nameof(store)); + /// + /// Parse comma-separated hosts + /// + /// Store + /// Comma-separated hosts + public static string[] ParseHostValues(this Store store) + { + Guard.NotNull(store, nameof(store)); - if (string.IsNullOrWhiteSpace(store.Hosts)) - { - return Array.Empty(); - } + if (string.IsNullOrWhiteSpace(store.Hosts)) + { + return Array.Empty(); + } - return store.Hosts - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(host => host.Trim()) - .Where(host => !string.IsNullOrWhiteSpace(host)) - .ToArray(); - } - } + return store.Hosts + .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(host => host.Trim()) + .Where(host => !string.IsNullOrWhiteSpace(host)) + .ToArray(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/StoreMapping.cs b/src/Libraries/SmartStore.Core/Domain/Stores/StoreMapping.cs index 6867cbf737..6f14db3afe 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/StoreMapping.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/StoreMapping.cs @@ -2,28 +2,28 @@ namespace SmartStore.Core.Domain.Stores { - /// - /// Represents a store mapping record - /// - [DataContract] - public partial class StoreMapping : BaseEntity - { - /// - /// Gets or sets the entity identifier - /// - [DataMember] - public virtual int EntityId { get; set; } + /// + /// Represents a store mapping record + /// + [DataContract] + public partial class StoreMapping : BaseEntity + { + /// + /// Gets or sets the entity identifier + /// + [DataMember] + public virtual int EntityId { get; set; } - /// - /// Gets or sets the entity name - /// - [DataMember] - public virtual string EntityName { get; set; } + /// + /// Gets or sets the entity name + /// + [DataMember] + public virtual string EntityName { get; set; } - /// - /// Gets or sets the store identifier - /// - [DataMember] - public virtual int StoreId { get; set; } - } + /// + /// Gets or sets the store identifier + /// + [DataMember] + public virtual int StoreId { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs index 235f54f617..bf04b013c6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs @@ -12,10 +12,10 @@ public enum TaskPriority Normal = 0, High = 1 } - + [DebuggerDisplay("{Name} (Type: {Type})")] - [Hookable(false)] - public class ScheduleTask : BaseEntity, ICloneable + [Hookable(false)] + public class ScheduleTask : BaseEntity, ICloneable { private ICollection _scheduleTaskHistory; @@ -29,10 +29,10 @@ public class ScheduleTask : BaseEntity, ICloneable /// public string Alias { get; set; } - /// - /// Gets or sets the CRON expression used to calculate future schedules - /// - public string CronExpression { get; set; } + /// + /// Gets or sets the CRON expression used to calculate future schedules + /// + public string CronExpression { get; set; } /// /// Gets or sets the type of appropriate ITask class @@ -69,17 +69,17 @@ public class ScheduleTask : BaseEntity, ICloneable /// public bool RunPerMachine { get; set; } - /// - /// Gets a value indicating whether a task is scheduled for execution (Enabled = true and NextRunUtc <= UtcNow and is not running). - /// - public bool IsPending - { - get - { + /// + /// Gets a value indicating whether a task is scheduled for execution (Enabled = true and NextRunUtc <= UtcNow and is not running). + /// + public bool IsPending + { + get + { var result = Enabled && NextRunUtc.HasValue && NextRunUtc <= DateTime.UtcNow && (LastHistoryEntry == null || !LastHistoryEntry.IsRunning); - return result; - } - } + return result; + } + } public ScheduleTaskHistory LastHistoryEntry { get; set; } @@ -88,18 +88,12 @@ public bool IsPending /// public virtual ICollection ScheduleTaskHistory { - get - { - return _scheduleTaskHistory ?? (_scheduleTaskHistory = new HashSet()); - } - protected set - { - _scheduleTaskHistory = value; - } + get => _scheduleTaskHistory ?? (_scheduleTaskHistory = new HashSet()); + protected set => _scheduleTaskHistory = value; } public ScheduleTask Clone() - { + { var task = new ScheduleTask { Name = Name, @@ -114,11 +108,11 @@ public ScheduleTask Clone() }; return task; - } + } - object ICloneable.Clone() - { - return this.Clone(); - } - } + object ICloneable.Clone() + { + return this.Clone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/AuxiliaryServicesTaxType.cs b/src/Libraries/SmartStore.Core/Domain/Tax/AuxiliaryServicesTaxType.cs index a215f4160c..9ea9566b5f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/AuxiliaryServicesTaxType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/AuxiliaryServicesTaxType.cs @@ -1,29 +1,29 @@ namespace SmartStore.Core.Domain.Tax { - /// - /// Specifies how to calculate the tax of auxiliary services like shipping and payment fees - /// - public enum AuxiliaryServicesTaxType - { - /// - /// Calculate tax of auxiliary services with the tax category specified in settings - /// - SpecifiedTaxCategory = 0, + /// + /// Specifies how to calculate the tax of auxiliary services like shipping and payment fees + /// + public enum AuxiliaryServicesTaxType + { + /// + /// Calculate tax of auxiliary services with the tax category specified in settings + /// + SpecifiedTaxCategory = 0, - /// - /// Calculate tax with the tax rate that has the highest amount in the cart - /// - HighestCartAmount = 10, + /// + /// Calculate tax with the tax rate that has the highest amount in the cart + /// + HighestCartAmount = 10, - /// - /// Calculate tax by the highest tax rate in the cart - /// - HighestTaxRate = 15, + /// + /// Calculate tax by the highest tax rate in the cart + /// + HighestTaxRate = 15, - /// - /// Calculate tax pro rata in accordance with main service (proportion of line subtotal and sum of all line subtotals) - /// - /// commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate - ///ProRata = 20 - } + /// + /// Calculate tax pro rata in accordance with main service (proportion of line subtotal and sum of all line subtotals) + /// + /// commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate + ///ProRata = 20 + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxCategory.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxCategory.cs index 7fd0279410..ab09afe7bc 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxCategory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxCategory.cs @@ -2,23 +2,23 @@ namespace SmartStore.Core.Domain.Tax { - /// - /// Represents a tax category - /// - [DataContract] - public partial class TaxCategory : BaseEntity + /// + /// Represents a tax category + /// + [DataContract] + public partial class TaxCategory : BaseEntity { - /// - /// Gets or sets the name - /// - [DataMember] - public string Name { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } - /// - /// Gets or sets the display order - /// - [DataMember] - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs index 28fd36b269..13ad247d4c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs @@ -4,22 +4,22 @@ namespace SmartStore.Core.Domain.Tax { public class TaxSettings : ISettings { - public TaxSettings() - { - TaxBasedOn = TaxBasedOn.BillingAddress; - TaxDisplayType = TaxDisplayType.ExcludingTax; - ActiveTaxProviderSystemName = "Tax.FixedRate"; - DisplayTaxSuffix = true; - PricesIncludeTax = true; - EuVatAllowVatExemption = true; + public TaxSettings() + { + TaxBasedOn = TaxBasedOn.BillingAddress; + TaxDisplayType = TaxDisplayType.ExcludingTax; + ActiveTaxProviderSystemName = "Tax.FixedRate"; + DisplayTaxSuffix = true; + PricesIncludeTax = true; + EuVatAllowVatExemption = true; ShippingPriceIncludesTax = true; - ShowLegalHintsInFooter = true; - ShowLegalHintsInProductDetails = true; - ShowLegalHintsInProductList = true; - AuxiliaryServicesTaxingType = AuxiliaryServicesTaxType.SpecifiedTaxCategory; - } - - /// + ShowLegalHintsInFooter = true; + ShowLegalHintsInProductDetails = true; + ShowLegalHintsInProductList = true; + AuxiliaryServicesTaxingType = AuxiliaryServicesTaxType.SpecifiedTaxCategory; + } + + /// /// Tax based on /// public TaxBasedOn TaxBasedOn { get; set; } @@ -114,15 +114,15 @@ public TaxSettings() /// public int PaymentMethodAdditionalFeeTaxClassId { get; set; } - /// - /// Taxing type for auxiliary services like shipping and payment fees - /// - public AuxiliaryServicesTaxType AuxiliaryServicesTaxingType { get; set; } + /// + /// Taxing type for auxiliary services like shipping and payment fees + /// + public AuxiliaryServicesTaxType AuxiliaryServicesTaxingType { get; set; } - /// - /// Gets or sets a value indicating whether EU VAT (Eupore Union Value Added Tax) is enabled - /// - public bool EuVatEnabled { get; set; } + /// + /// Gets or sets a value indicating whether EU VAT (Eupore Union Value Added Tax) is enabled + /// + public bool EuVatEnabled { get; set; } /// /// Gets or sets a shop country identifier diff --git a/src/Libraries/SmartStore.Core/Domain/Themes/ThemeSettings.cs b/src/Libraries/SmartStore.Core/Domain/Themes/ThemeSettings.cs index aff5ac1f4b..4b1b127369 100644 --- a/src/Libraries/SmartStore.Core/Domain/Themes/ThemeSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Themes/ThemeSettings.cs @@ -1,16 +1,15 @@ -using System; -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Themes { public class ThemeSettings : ISettings { - public ThemeSettings() - { - DefaultTheme = "Flex"; - AllowCustomerToSelectTheme = true; - AssetCachingEnabled = 2; - } + public ThemeSettings() + { + DefaultTheme = "Flex"; + AllowCustomerToSelectTheme = true; + AssetCachingEnabled = 2; + } /// /// Gets or sets a value indicating whether @@ -23,35 +22,35 @@ public ThemeSettings() /// public int BundleOptimizationEnabled { get; set; } - /// - /// Gets or sets a value indicating whether - /// asset caching is enabled - /// - /// - /// 0: Auto (decide based on debug mode) - /// 1: Disabled - /// 2: Enabled > default - /// - public int AssetCachingEnabled { get; set; } + /// + /// Gets or sets a value indicating whether + /// asset caching is enabled + /// + /// + /// 0: Auto (decide based on debug mode) + /// 1: Disabled + /// 2: Enabled > default + /// + public int AssetCachingEnabled { get; set; } - /// - /// Gets or sets a default store theme for desktops - /// - public string DefaultTheme { get; set; } + /// + /// Gets or sets a default store theme for desktops + /// + public string DefaultTheme { get; set; } /// /// Gets or sets a value indicating whether customers are allowed to select a theme /// public bool AllowCustomerToSelectTheme { get; set; } - /// - /// Gets or sets a value indicating whether user's theme choice should be saved in a cookie - /// - /// - /// If false, user's theme choice is associated to the account, - /// which may be undesirable when, for example, multiple users share a guest account. - /// - public bool SaveThemeChoiceInCookie { get; set; } + /// + /// Gets or sets a value indicating whether user's theme choice should be saved in a cookie + /// + /// + /// If false, user's theme choice is associated to the account, + /// which may be undesirable when, for example, multiple users share a guest account. + /// + public bool SaveThemeChoiceInCookie { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs b/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs index 2befa5532e..14540d65d6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs +++ b/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; // codehint: sm-add (whole file) namespace SmartStore.Core.Domain.Themes -{ +{ public class ThemeVariable : BaseEntity { /// @@ -24,10 +21,10 @@ public class ThemeVariable : BaseEntity /// public string Value { get; set; } - /// - /// Gets or sets the store identifier - /// - public int StoreId { get; set; } + /// + /// Gets or sets the store identifier + /// + public int StoreId { get; set; } protected override bool Equals(BaseEntity other) { diff --git a/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs b/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs index d05d74c975..a5e7171f8e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs +++ b/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs @@ -6,31 +6,32 @@ using SmartStore.Core.Domain.Security; using SmartStore.Core.Domain.Seo; using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Plugins; namespace SmartStore.Core.Domain.Topics { - /// - /// Represents a topic - /// - [DataContract] - public partial class Topic : BaseEntity, ILocalizedEntity, ISlugSupported, IStoreMappingSupported, IAclSupported + /// + /// Represents a topic + /// + [DataContract] + public partial class Topic : BaseEntity, ILocalizedEntity, ISlugSupported, IStoreMappingSupported, IAclSupported { - public Topic() - { - IsPublished = true; - } + public Topic() + { + IsPublished = true; + } - /// - /// Gets or sets the name - /// - [DataMember] - public string SystemName { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string SystemName { get; set; } - /// - /// Gets or sets the value indicating whether this topic is deleteable by a user - /// - [DataMember] - public bool IsSystemTopic { get; set; } + /// + /// Gets or sets the value indicating whether this topic is deleteable by a user + /// + [DataMember] + public bool IsSystemTopic { get; set; } /// /// Gets or sets the html id @@ -48,133 +49,138 @@ public Topic() /// Gets or sets the value indicating whether this topic should be included in sitemap /// [DataMember] - public bool IncludeInSitemap { get; set; } - - /// - /// Gets or sets the value indicating whether this topic is password protected - /// - [DataMember] - public bool IsPasswordProtected { get; set; } - - /// - /// Gets or sets the password - /// - [DataMember] - public string Password { get; set; } - - /// - /// Gets or sets the title - /// - [DataMember] - public string Title { get; set; } - - /// - /// Gets or sets the short title (for links) - /// - [DataMember] - public string ShortTitle { get; set; } - - /// - /// Gets or sets the intro - /// - [DataMember] - public string Intro { get; set; } - - /// - /// Gets or sets the body - /// - [DataMember] - public string Body { get; set; } - - /// - /// Gets or sets the meta keywords - /// - [DataMember] - public string MetaKeywords { get; set; } - - /// - /// Gets or sets the meta description - /// - [DataMember] - public string MetaDescription { get; set; } - - /// - /// Gets or sets the meta title - /// - [DataMember] - public string MetaTitle { get; set; } - - /// - /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores - /// - [DataMember] - public bool LimitedToStores { get; set; } - - /// - /// Gets or sets a value indicating whether the topic should also be rendered as a generic html widget - /// - [DataMember] - public bool RenderAsWidget { get; set; } - - /// - /// Gets or sets the widget zone name - /// - [DataMember] - public string WidgetZone { get; set; } - - /// - /// Gets or sets a value indicating whether the content should be surrounded by a topic block wrapper - /// - [DataMember] - public bool? WidgetWrapContent { get; set; } - - /// - /// Gets or sets a value indicating whether the title should be displayed in the widget block - /// - [DataMember] - public bool WidgetShowTitle { get; set; } - - /// - /// Gets or sets a value indicating whether the widget block should have borders - /// - [DataMember] - public bool WidgetBordered { get; set; } - - /// - /// Gets or sets the sort order (relevant for widgets) - /// - [DataMember] - public int Priority { get; set; } - - /// - /// Gets or sets the title tag - /// - [DataMember] - public string TitleTag { get; set; } - - /// - /// Gets or sets a value indicating whether the entity is subject to ACL - /// - [DataMember] - public bool SubjectToAcl { get; set; } - - /// - /// Gets or sets a value indicating whether the topic page is published - /// - [DataMember] - public bool IsPublished { get; set; } - - /// - /// Helper function which gets the comma-separated WidgetZone property as list of strings - /// - /// - public IEnumerable GetWidgetZones() + public bool IncludeInSitemap { get; set; } + + /// + /// Gets or sets the value indicating whether this topic is password protected + /// + [DataMember] + public bool IsPasswordProtected { get; set; } + + /// + /// Gets or sets the password + /// + [DataMember] + public string Password { get; set; } + + /// + /// Gets or sets the title + /// + [DataMember] + public string Title { get; set; } + + /// + /// Gets or sets the short title (for links) + /// + [DataMember] + public string ShortTitle { get; set; } + + /// + /// Gets or sets the intro + /// + [DataMember] + public string Intro { get; set; } + + /// + /// Gets or sets the body + /// + [DataMember] + public string Body { get; set; } + + /// + /// Gets or sets the meta keywords + /// + [DataMember] + public string MetaKeywords { get; set; } + + /// + /// Gets or sets the meta description + /// + [DataMember] + public string MetaDescription { get; set; } + + /// + /// Gets or sets the meta title + /// + [DataMember] + public string MetaTitle { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } + + /// + /// Gets or sets a value indicating whether the topic should also be rendered as a generic html widget + /// + [DataMember] + public bool RenderAsWidget { get; set; } + + /// + /// Gets or sets the widget zone name + /// + [DataMember] + public string WidgetZone { get; set; } + + /// + /// Gets or sets a value indicating whether the content should be surrounded by a topic block wrapper + /// + [DataMember] + public bool? WidgetWrapContent { get; set; } + + /// + /// Gets or sets a value indicating whether the title should be displayed in the widget block + /// + [DataMember] + public bool WidgetShowTitle { get; set; } + + /// + /// Gets or sets a value indicating whether the widget block should have borders + /// + [DataMember] + public bool WidgetBordered { get; set; } + + /// + /// Gets or sets the sort order (relevant for widgets) + /// + [DataMember] + public int Priority { get; set; } + + /// + /// Gets or sets the title tag + /// + [DataMember] + public string TitleTag { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is subject to ACL + /// + [DataMember] + public bool SubjectToAcl { get; set; } + + /// + /// Gets or sets a value indicating whether the topic page is published + /// + [DataMember] + public bool IsPublished { get; set; } + + /// + /// Gets or sets a value indicating whether the topic set a cookie and the cookie type + /// + public CookieType? CookieType { get; set; } + + /// + /// Helper function which gets the comma-separated WidgetZone property as list of strings + /// + /// + public IEnumerable GetWidgetZones() { if (this.WidgetZone.IsEmpty()) { return Enumerable.Empty(); } - + return this.WidgetZone.Trim().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); } } diff --git a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs index 5492f4631f..8973a9f3c9 100644 --- a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs +++ b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text; using System.Net.Mail; using System.Net.Mime; -using System.IO; using System.Threading.Tasks; using SmartStore.Core.Domain.Messages; @@ -12,34 +10,34 @@ namespace SmartStore.Core.Email { public class DefaultEmailSender : IEmailSender { - private readonly EmailAccountSettings _emailAccountSettings; - - public DefaultEmailSender(EmailAccountSettings emailAccountSettings) - { - _emailAccountSettings = emailAccountSettings; - } - - /// - /// Builds System.Net.Mail.Message - /// - /// SmartStore.Email.Message - /// System.Net.Mail.Message - protected virtual MailMessage BuildMailMessage(EmailMessage original) + private readonly EmailAccountSettings _emailAccountSettings; + + public DefaultEmailSender(EmailAccountSettings emailAccountSettings) + { + _emailAccountSettings = emailAccountSettings; + } + + /// + /// Builds System.Net.Mail.Message + /// + /// SmartStore.Email.Message + /// System.Net.Mail.Message + protected virtual MailMessage BuildMailMessage(EmailMessage original) { MailMessage msg = new MailMessage(); - if (String.IsNullOrEmpty(original.Subject)) - { - throw new MailSenderException("Required subject is missing!"); - } - - msg.Subject = original.Subject; - msg.IsBodyHtml = original.BodyFormat == MailBodyFormat.Html; + if (String.IsNullOrEmpty(original.Subject)) + { + throw new MailSenderException("Required subject is missing!"); + } + + msg.Subject = original.Subject; + msg.IsBodyHtml = original.BodyFormat == MailBodyFormat.Html; if (original.AltText.HasValue()) { - msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(original.AltText, new ContentType("text/html"))); - msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(original.Body, new ContentType("text/plain"))); + msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(original.Body, new ContentType("text/html"))); + msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(original.AltText, new ContentType("text/plain"))); } else { @@ -50,65 +48,65 @@ protected virtual MailMessage BuildMailMessage(EmailMessage original) msg.From = original.From.ToMailAddress(); - msg.To.AddRange(original.To.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); - msg.CC.AddRange(original.Cc.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); - msg.Bcc.AddRange(original.Bcc.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); - msg.ReplyToList.AddRange(original.ReplyTo.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); + msg.To.AddRange(original.To.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); + msg.CC.AddRange(original.Cc.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); + msg.Bcc.AddRange(original.Bcc.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); + msg.ReplyToList.AddRange(original.ReplyTo.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); - msg.Attachments.AddRange(original.Attachments); + msg.Attachments.AddRange(original.Attachments); if (original.Headers != null) - msg.Headers.AddRange(original.Headers); - + msg.Headers.AddRange(original.Headers); + msg.Priority = original.Priority; return msg; } - #region IMailSender Members + #region IMailSender Members + + public void SendEmail(SmtpContext context, EmailMessage message) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(message, nameof(message)); + + using (var msg = this.BuildMailMessage(message)) + { + using (var client = context.ToSmtpClient()) + { + ApplySettings(client); + client.Send(msg); + } + } + } + + public async Task SendEmailAsync(SmtpContext context, EmailMessage message) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(message, nameof(message)); + + using (var msg = this.BuildMailMessage(message)) + { + using (var client = context.ToSmtpClient()) + { + ApplySettings(client); + await client.SendMailAsync(msg); + } + } + } - public void SendEmail(SmtpContext context, EmailMessage message) + private void ApplySettings(SmtpClient client) { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(message, nameof(message)); - - using (var msg = this.BuildMailMessage(message)) - { - using (var client = context.ToSmtpClient()) - { - ApplySettings(client); - client.Send(msg); - } - } + var pickupDirLocation = _emailAccountSettings.PickupDirectoryLocation; + if (pickupDirLocation.HasValue() && client.DeliveryMethod != SmtpDeliveryMethod.SpecifiedPickupDirectory && Path.IsPathRooted(pickupDirLocation)) + { + client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; + client.PickupDirectoryLocation = pickupDirLocation; + client.EnableSsl = false; + } } - public async Task SendEmailAsync(SmtpContext context, EmailMessage message) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(message, nameof(message)); - - using (var msg = this.BuildMailMessage(message)) - { - using (var client = context.ToSmtpClient()) - { - ApplySettings(client); - await client.SendMailAsync(msg); - } - } - } - - private void ApplySettings(SmtpClient client) - { - var pickupDirLocation = _emailAccountSettings.PickupDirectoryLocation; - if (pickupDirLocation.HasValue() && client.DeliveryMethod != SmtpDeliveryMethod.SpecifiedPickupDirectory && Path.IsPathRooted(pickupDirLocation)) - { - client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; - client.PickupDirectoryLocation = pickupDirLocation; - client.EnableSsl = false; - } - } - - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Email/EmailAddress.cs b/src/Libraries/SmartStore.Core/Email/EmailAddress.cs index 8699174caf..b8820eb0d2 100644 --- a/src/Libraries/SmartStore.Core/Email/EmailAddress.cs +++ b/src/Libraries/SmartStore.Core/Email/EmailAddress.cs @@ -1,56 +1,52 @@ -using System; -using System.Net.Mail; +using System.Net.Mail; namespace SmartStore.Core.Email { public class EmailAddress { - private readonly MailAddress _inner; + private readonly MailAddress _inner; - public EmailAddress(string address) + public EmailAddress(string address) { - _inner = new MailAddress(address); + _inner = new MailAddress(address); } public EmailAddress(string address, string displayName) { - _inner = new MailAddress(address, displayName); - } + _inner = new MailAddress(address, displayName); + } - public EmailAddress(MailAddress address) - { - _inner = address; - } + public EmailAddress(MailAddress address) + { + _inner = address; + } - public string Address - { - get { return _inner.Address; } - } + public string Address => _inner.Address; - public string DisplayName - { - get { return _inner.DisplayName; } - } + public string DisplayName => _inner.DisplayName; - public string User - { - get { return _inner.User; } - } + public string User => _inner.User; - public string Host - { - get { return _inner.Host; } - } + public string Host => _inner.Host; - public override int GetHashCode() => _inner.GetHashCode(); + public override int GetHashCode() + { + return _inner.GetHashCode(); + } - public override string ToString() => _inner.ToString(); + public override string ToString() + { + return _inner.ToString(); + } - public MailAddress ToMailAddress() => _inner; + public MailAddress ToMailAddress() + { + return _inner; + } - public static implicit operator string(EmailAddress obj) - { - return obj.ToString(); - } - } + public static implicit operator string(EmailAddress obj) + { + return obj.ToString(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs index cdc83e6efb..0249348cfe 100644 --- a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs +++ b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs @@ -1,72 +1,72 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Net.Mail; using System.IO; -using System.Text; using System.Net; +using System.Net.Mail; +using System.Text; namespace SmartStore.Core.Email { public class EmailMessage : ICloneable { - public EmailMessage() - { - this.BodyFormat = MailBodyFormat.Html; - this.Priority = MailPriority.Normal; - - this.To = new List(); - this.Cc = new List(); - this.Bcc = new List(); - this.ReplyTo = new List(); - - this.Attachments = new List(); - - this.Headers = new NameValueCollection(); - } - - public EmailMessage(string to, string subject, string body, string from) - : this() - { - Guard.NotEmpty(to, nameof(to)); - Guard.NotEmpty(from, nameof(from)); - Guard.NotEmpty(subject, nameof(subject)); - Guard.NotEmpty(body, nameof(body)); - - this.To.Add(new EmailAddress(to)); - this.Subject = subject; - this.Body = body; - this.From = new EmailAddress(from); - } - - public EmailMessage(EmailAddress to, string subject, string body, EmailAddress from) - : this() - { - Guard.NotNull(to, nameof(to)); - Guard.NotNull(from, nameof(from)); - Guard.NotEmpty(subject, nameof(subject)); - Guard.NotEmpty(body, nameof(body)); - - this.To.Add(to); - this.Subject = subject; - this.Body = body; - this.From = from; - } - - public EmailAddress From { get; set; } + public EmailMessage() + { + this.BodyFormat = MailBodyFormat.Html; + this.Priority = MailPriority.Normal; + + this.To = new List(); + this.Cc = new List(); + this.Bcc = new List(); + this.ReplyTo = new List(); + + this.Attachments = new List(); + + this.Headers = new NameValueCollection(); + } + + public EmailMessage(string to, string subject, string body, string from) + : this() + { + Guard.NotEmpty(to, nameof(to)); + Guard.NotEmpty(from, nameof(from)); + Guard.NotEmpty(subject, nameof(subject)); + Guard.NotEmpty(body, nameof(body)); + + this.To.Add(new EmailAddress(to)); + this.Subject = subject; + this.Body = body; + this.From = new EmailAddress(from); + } + + public EmailMessage(EmailAddress to, string subject, string body, EmailAddress from) + : this() + { + Guard.NotNull(to, nameof(to)); + Guard.NotNull(from, nameof(from)); + Guard.NotEmpty(subject, nameof(subject)); + Guard.NotEmpty(body, nameof(body)); + + this.To.Add(to); + this.Subject = subject; + this.Body = body; + this.From = from; + } + + public EmailAddress From { get; set; } public string Subject { get; set; } public string Body { get; set; } public string AltText { get; set; } - public MailBodyFormat BodyFormat { get; set; } + public MailBodyFormat BodyFormat { get; set; } public MailPriority Priority { get; set; } public ICollection To { get; private set; } - public ICollection Cc { get; private set; } - public ICollection Bcc { get; private set; } - public ICollection ReplyTo { get; private set; } + public ICollection Cc { get; private set; } + public ICollection Bcc { get; private set; } + public ICollection ReplyTo { get; private set; } - public ICollection Attachments { get; private set; } + public ICollection Attachments { get; private set; } public NameValueCollection Headers { get; private set; } @@ -95,19 +95,19 @@ public EmailMessage Clone() { var clone = new EmailMessage(); - clone.Attachments.AddRange(this.Attachments); - clone.To.AddRange(this.To); - clone.Cc.AddRange(this.Cc); - clone.Bcc.AddRange(this.Bcc); - clone.ReplyTo.AddRange(this.ReplyTo); - clone.Headers.AddRange(this.Headers); + clone.Attachments.AddRange(this.Attachments); + clone.To.AddRange(this.To); + clone.Cc.AddRange(this.Cc); + clone.Bcc.AddRange(this.Bcc); + clone.ReplyTo.AddRange(this.ReplyTo); + clone.Headers.AddRange(this.Headers); - clone.AltText = this.AltText; - clone.Body = this.Body; - clone.BodyFormat = this.BodyFormat; - clone.From = this.From; - clone.Priority = this.Priority; - clone.Subject = this.Subject; + clone.AltText = this.AltText; + clone.Body = this.Body; + clone.BodyFormat = this.BodyFormat; + clone.From = this.From; + clone.Priority = this.Priority; + clone.Subject = this.Subject; return clone; } @@ -120,9 +120,9 @@ object ICloneable.Clone() #endregion } - public enum MailBodyFormat - { - Text, - Html - } + public enum MailBodyFormat + { + Text, + Html + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Email/IEmailSender.cs b/src/Libraries/SmartStore.Core/Email/IEmailSender.cs index a2740cb877..9850acfa80 100644 --- a/src/Libraries/SmartStore.Core/Email/IEmailSender.cs +++ b/src/Libraries/SmartStore.Core/Email/IEmailSender.cs @@ -1,14 +1,13 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace SmartStore.Core.Email -{ +{ /// /// contract for email sender /// public interface IEmailSender { void SendEmail(SmtpContext smtpContext, EmailMessage message); - Task SendEmailAsync(SmtpContext smtpContext, EmailMessage message); + Task SendEmailAsync(SmtpContext smtpContext, EmailMessage message); } } diff --git a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs index e1fde3c68a..ba9b71014d 100644 --- a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs +++ b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs @@ -1,7 +1,5 @@ using System; using System.Net; -using System.Net.Configuration; -using System.Configuration; using System.Net.Mail; using SmartStore.Core.Domain.Messages; @@ -12,81 +10,81 @@ public class SmtpContext public SmtpContext(string host, int port = 25) { - Guard.NotEmpty(host, nameof(host)); - Guard.IsPositive(port, nameof(port)); - - this.Host = host; + Guard.NotEmpty(host, nameof(host)); + Guard.IsPositive(port, nameof(port)); + + this.Host = host; this.Port = port; } - public SmtpContext(EmailAccount account) - { - Guard.NotNull(account, nameof(account)); - - this.Host = account.Host; - this.Port = account.Port; - this.EnableSsl = account.EnableSsl; - this.Password = account.Password; - this.UseDefaultCredentials = account.UseDefaultCredentials; - this.Username = account.Username; - } - - public bool UseDefaultCredentials - { - get; - set; - } - - public string Host - { - get; - set; - } - - public int Port - { - get; - set; - } - - public string Username - { - get; - set; - } - - public string Password - { - get; - set; - } - - public bool EnableSsl - { - get; - set; - } - - public SmtpClient ToSmtpClient() - { - var smtpClient = new SmtpClient(this.Host, this.Port); - - smtpClient.UseDefaultCredentials = this.UseDefaultCredentials; - smtpClient.EnableSsl = this.EnableSsl; - smtpClient.Timeout = 10000; - - if (this.UseDefaultCredentials) - { - smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials; - } - else - { - if (!String.IsNullOrEmpty(this.Username)) - smtpClient.Credentials = new NetworkCredential(this.Username, this.Password); - } - - return smtpClient; - } + public SmtpContext(EmailAccount account) + { + Guard.NotNull(account, nameof(account)); + + this.Host = account.Host; + this.Port = account.Port; + this.EnableSsl = account.EnableSsl; + this.Password = account.Password; + this.UseDefaultCredentials = account.UseDefaultCredentials; + this.Username = account.Username; + } + + public bool UseDefaultCredentials + { + get; + set; + } + + public string Host + { + get; + set; + } + + public int Port + { + get; + set; + } + + public string Username + { + get; + set; + } + + public string Password + { + get; + set; + } + + public bool EnableSsl + { + get; + set; + } + + public SmtpClient ToSmtpClient() + { + var smtpClient = new SmtpClient(this.Host, this.Port); + + smtpClient.UseDefaultCredentials = this.UseDefaultCredentials; + smtpClient.EnableSsl = this.EnableSsl; + smtpClient.Timeout = 10000; + + if (this.UseDefaultCredentials) + { + smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials; + } + else + { + if (!String.IsNullOrEmpty(this.Username)) + smtpClient.Credentials = new NetworkCredential(this.Username, this.Password); + } + + return smtpClient; + } } } diff --git a/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs b/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs index 18b72d94da..453ddd7406 100644 --- a/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs +++ b/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs @@ -3,11 +3,11 @@ namespace SmartStore.Core.Events { - public sealed class AmbigousConsumerException : Exception - { - public AmbigousConsumerException(MethodInfo method1, MethodInfo method2) - : base("Ambigous consumer methods detected in '{0}'. Method 1: '{1}', Method 2: '{2}'.".FormatInvariant(method1.DeclaringType.FullName, method1.ToString(), method2.ToString())) - { - } - } + public sealed class AmbigousConsumerException : Exception + { + public AmbigousConsumerException(MethodInfo method1, MethodInfo method2) + : base("Ambigous consumer methods detected in '{0}'. Method 1: '{1}', Method 2: '{2}'.".FormatInvariant(method1.DeclaringType.FullName, method1.ToString(), method2.ToString())) + { + } + } } diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs index 348b5fac89..5058307cfb 100644 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs +++ b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; +using System.Collections.Generic; using SmartStore.Core.Domain.Tasks; namespace SmartStore.Core.Events { - /// - /// to initialize scheduled tasks in Application_Start - /// - public class AppInitScheduledTasksEvent - { - public IList ScheduledTasks { get; set; } - } + /// + /// to initialize scheduled tasks in Application_Start + /// + public class AppInitScheduledTasksEvent + { + public IList ScheduledTasks { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs index d825cc079d..6fd6e8d6fe 100644 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs +++ b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs @@ -1,14 +1,12 @@ -using System; -using System.Web; -using System.Web.Mvc; +using System.Web.Mvc; namespace SmartStore.Core.Events { - /// - /// to register global filters in Application_Start - /// - public class AppRegisterGlobalFiltersEvent - { - public GlobalFilterCollection Filters { get; set; } - } + /// + /// to register global filters in Application_Start + /// + public class AppRegisterGlobalFiltersEvent + { + public GlobalFilterCollection Filters { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppStartedEvent.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppStartedEvent.cs index c1f2880780..95576aea9f 100644 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppStartedEvent.cs +++ b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppStartedEvent.cs @@ -1,10 +1,9 @@ -using System; -using System.Web; +using System.Web; namespace SmartStore.Core.Events { - public class AppStartedEvent - { - public HttpContextBase HttpContext { get; set; } - } + public class AppStartedEvent + { + public HttpContextBase HttpContext { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs b/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs index d52607845a..dae3684721 100644 --- a/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs +++ b/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs @@ -1,23 +1,21 @@ -using System; - -namespace SmartStore.Core.Events +namespace SmartStore.Core.Events { - /// - /// Wrapper/Envelope for event message objects. For now, only wraps - /// the message. There may be other context data in future. - /// - /// Type of message. - public class ConsumeContext - { - public ConsumeContext(TMessage message) - { - Message = message; - } + /// + /// Wrapper/Envelope for event message objects. For now, only wraps + /// the message. There may be other context data in future. + /// + /// Type of message. + public class ConsumeContext + { + public ConsumeContext(TMessage message) + { + Message = message; + } - public TMessage Message - { - get; - private set; - } - } + public TMessage Message + { + get; + private set; + } + } } diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs index f990c1e61c..25302b3771 100644 --- a/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs @@ -1,50 +1,44 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using SmartStore.ComponentModel; using SmartStore.Core.Plugins; namespace SmartStore.Core.Events { - /// - /// Contains metadata about a consumer / event handler method. - /// - public class ConsumerDescriptor - { - public ConsumerDescriptor() - { - } + /// + /// Contains metadata about a consumer / event handler method. + /// + public class ConsumerDescriptor + { + public ConsumerDescriptor() + { + } - public ConsumerDescriptor(EventConsumerMetadata metadata) - { - ContainerType = metadata.ContainerType; - PluginDescriptor = metadata.PluginDescriptor; - } + public ConsumerDescriptor(EventConsumerMetadata metadata) + { + ContainerType = metadata.ContainerType; + PluginDescriptor = metadata.PluginDescriptor; + } - public bool WithEnvelope { get; set; } - public bool IsAsync { get; set; } - public bool FireForget { get; set; } - public PluginDescriptor PluginDescriptor { get; set; } + public bool WithEnvelope { get; set; } + public bool IsAsync { get; set; } + public bool FireForget { get; set; } + public PluginDescriptor PluginDescriptor { get; set; } - public Type MessageType { get; set; } - public Type ContainerType { get; set; } + public Type MessageType { get; set; } + public Type ContainerType { get; set; } - public MethodInfo Method { get; set; } - public ParameterInfo MessageParameter { get; set; } + public MethodInfo Method { get; set; } + public ParameterInfo MessageParameter { get; set; } - /// - /// All method parameters except - /// or empty array if is the only parameter. - /// - public ParameterInfo[] Parameters { get; set; } + /// + /// All method parameters except + /// or empty array if is the only parameter. + /// + public ParameterInfo[] Parameters { get; set; } - public override string ToString() - { - return "MessageType: {0} - Consumer: {1}.{2}".FormatCurrent(MessageType.Name, ContainerType.FullName, Method.Name); - } - } + public override string ToString() + { + return "MessageType: {0} - Consumer: {1}.{2}".FormatCurrent(MessageType.Name, ContainerType.FullName, Method.Name); + } + } } diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs index 9eb50ecb00..e382cae1b3 100644 --- a/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs +++ b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Web; using Autofac; using SmartStore.ComponentModel; using SmartStore.Core.Async; @@ -11,183 +10,183 @@ namespace SmartStore.Core.Events { - public class ConsumerInvoker : IConsumerInvoker - { - private readonly IConsumerResolver _resolver; - - public ConsumerInvoker(IConsumerResolver resolver) - { - _resolver = resolver; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, ConsumeContext envelope) where TMessage : class - { - var d = descriptor; - var p = descriptor.WithEnvelope ? (object)envelope : envelope.Message; - var fastInvoker = FastInvoker.GetInvoker(d.Method); - - if (!d.FireForget && !d.IsAsync) - { - // The all synch case - try - { - InvokeCore(); - } - catch (Exception ex) - { - HandleException(ex, d); - } - } - else if (!d.FireForget && d.IsAsync) - { - //// The awaitable Task case - //BeginInvoke((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken), EndInvoke, d); - - // For now we must go with the AsyncRunner, the above call to BeginInvoke (APM > TPL) does not always - // guarantee that the task is awaited and throws exceptions especially when EF is involved. - using (var runner = AsyncRunner.Create()) - { - try - { - runner.Run((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken)); - } - catch (Exception ex) - { - HandleException(ex, d); - } - } - } - else if (d.FireForget && !d.IsAsync) - { - // A synch method should be executed async (without awaiting) - AsyncRunner.Run((c, ct, state) => InvokeCore(c, ct), d) - .ContinueWith(t => EndInvoke(t), /*TaskContinuationOptions.OnlyOnFaulted*/ TaskContinuationOptions.None) - .ConfigureAwait(false); - } - else if (d.FireForget && d.IsAsync) - { - // An async (Task) method should be executed without awaiting - AsyncRunner.Run((c, ct) => (Task)InvokeCore(c, ct), d) - .ContinueWith(t => EndInvoke(t), TaskContinuationOptions.OnlyOnFaulted) - .ConfigureAwait(false); - } - - object InvokeCore(IComponentContext c = null, CancellationToken cancelToken = default(CancellationToken)) - { - if (d.Parameters.Length == 0) - { - // Only one method param: the message! - return fastInvoker.Invoke(consumer, p); - } - - var parameters = new object[d.Parameters.Length + 1]; - parameters[0] = p; - - int i = 0; - foreach (var obj in ResolveParameters(c, d, cancelToken).ToArray()) - { - i++; - parameters[i] = obj; - } - - return fastInvoker.Invoke(consumer, parameters); - } - } - - private void HandleException(Exception ex, ConsumerDescriptor descriptor) - { - if (ex is AggregateException ae) - { - ae.InnerExceptions.Each(x => Logger.Error(x)); - } - else - { - Logger.Error(ex); - } - - if (!descriptor.FireForget) - { - throw ex; - } - } - - protected internal virtual IEnumerable ResolveParameters( - IComponentContext container, - ConsumerDescriptor descriptor, - CancellationToken cancelToken) - { - foreach (var p in descriptor.Parameters) - { - if (p.ParameterType == typeof(CancellationToken)) - { - yield return cancelToken; - } - else - { - yield return _resolver.ResolveParameter(p, container); - } - } - } - - #region APM > TPL pattern (does not always work stable) - - /// - /// Wraps a into the Begin method of an APM pattern. - /// - /// The task to wrap. - /// The callback method passed into the Begin method of the APM pattern. - /// The state passed into the Begin method of the APM pattern. - /// The asynchronous operation, to be returned by the Begin method of the APM pattern. - protected IAsyncResult BeginInvoke(Task task, AsyncCallback callback, ConsumerDescriptor descriptor) - { - var options = TaskCreationOptions.RunContinuationsAsynchronously; - var tcs = new TaskCompletionSource(descriptor, options); - - // "_ =" to discard 'async/await' compiler warning - _ = AwaitCompletionAsync(task, callback, tcs, descriptor); - - return tcs.Task; - } - - private async Task AwaitCompletionAsync( - Task task, - AsyncCallback callback, - TaskCompletionSource tcs, - ConsumerDescriptor descriptor) - { - try - { - await task; - tcs.TrySetResult(null); - } - catch (OperationCanceledException ex) - { - tcs.TrySetCanceled(ex.CancellationToken); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - finally - { - callback?.Invoke(tcs.Task); - } - } - - protected virtual void EndInvoke(IAsyncResult asyncResult) - { - var task = (Task)asyncResult; - - if (task.IsFaulted && task.Exception != null) - { - HandleException(task.Exception, (ConsumerDescriptor)asyncResult.AsyncState); - } - } - - #endregion - } + public class ConsumerInvoker : IConsumerInvoker + { + private readonly IConsumerResolver _resolver; + + public ConsumerInvoker(IConsumerResolver resolver) + { + _resolver = resolver; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, ConsumeContext envelope) where TMessage : class + { + var d = descriptor; + var p = descriptor.WithEnvelope ? (object)envelope : envelope.Message; + var fastInvoker = FastInvoker.GetInvoker(d.Method); + + if (!d.FireForget && !d.IsAsync) + { + // The all synch case + try + { + InvokeCore(); + } + catch (Exception ex) + { + HandleException(ex, d); + } + } + else if (!d.FireForget && d.IsAsync) + { + //// The awaitable Task case + //BeginInvoke((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken), EndInvoke, d); + + // For now we must go with the AsyncRunner, the above call to BeginInvoke (APM > TPL) does not always + // guarantee that the task is awaited and throws exceptions especially when EF is involved. + using (var runner = AsyncRunner.Create()) + { + try + { + runner.Run((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken)); + } + catch (Exception ex) + { + HandleException(ex, d); + } + } + } + else if (d.FireForget && !d.IsAsync) + { + // A synch method should be executed async (without awaiting) + AsyncRunner.Run((c, ct, state) => InvokeCore(c, ct), d) + .ContinueWith(t => EndInvoke(t), /*TaskContinuationOptions.OnlyOnFaulted*/ TaskContinuationOptions.None) + .ConfigureAwait(false); + } + else if (d.FireForget && d.IsAsync) + { + // An async (Task) method should be executed without awaiting + AsyncRunner.Run((c, ct) => (Task)InvokeCore(c, ct), d) + .ContinueWith(t => EndInvoke(t), TaskContinuationOptions.OnlyOnFaulted) + .ConfigureAwait(false); + } + + object InvokeCore(IComponentContext c = null, CancellationToken cancelToken = default(CancellationToken)) + { + if (d.Parameters.Length == 0) + { + // Only one method param: the message! + return fastInvoker.Invoke(consumer, p); + } + + var parameters = new object[d.Parameters.Length + 1]; + parameters[0] = p; + + int i = 0; + foreach (var obj in ResolveParameters(c, d, cancelToken).ToArray()) + { + i++; + parameters[i] = obj; + } + + return fastInvoker.Invoke(consumer, parameters); + } + } + + private void HandleException(Exception ex, ConsumerDescriptor descriptor) + { + if (ex is AggregateException ae) + { + ae.InnerExceptions.Each(x => Logger.Error(x, $"Error invoking event consumer '{descriptor}'.")); + } + else + { + Logger.Error(ex, $"Error invoking event consumer '{descriptor}'."); + } + + if (!descriptor.FireForget) + { + throw ex; + } + } + + protected internal virtual IEnumerable ResolveParameters( + IComponentContext container, + ConsumerDescriptor descriptor, + CancellationToken cancelToken) + { + foreach (var p in descriptor.Parameters) + { + if (p.ParameterType == typeof(CancellationToken)) + { + yield return cancelToken; + } + else + { + yield return _resolver.ResolveParameter(p, container); + } + } + } + + #region APM > TPL pattern (does not always work stable) + + /// + /// Wraps a into the Begin method of an APM pattern. + /// + /// The task to wrap. + /// The callback method passed into the Begin method of the APM pattern. + /// The state passed into the Begin method of the APM pattern. + /// The asynchronous operation, to be returned by the Begin method of the APM pattern. + protected IAsyncResult BeginInvoke(Task task, AsyncCallback callback, ConsumerDescriptor descriptor) + { + var options = TaskCreationOptions.RunContinuationsAsynchronously; + var tcs = new TaskCompletionSource(descriptor, options); + + // "_ =" to discard 'async/await' compiler warning + _ = AwaitCompletionAsync(task, callback, tcs, descriptor); + + return tcs.Task; + } + + private async Task AwaitCompletionAsync( + Task task, + AsyncCallback callback, + TaskCompletionSource tcs, + ConsumerDescriptor descriptor) + { + try + { + await task; + tcs.TrySetResult(null); + } + catch (OperationCanceledException ex) + { + tcs.TrySetCanceled(ex.CancellationToken); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + finally + { + callback?.Invoke(tcs.Task); + } + } + + protected virtual void EndInvoke(IAsyncResult asyncResult) + { + var task = (Task)asyncResult; + + if (task.IsFaulted && task.Exception != null) + { + HandleException(task.Exception, (ConsumerDescriptor)asyncResult.AsyncState); + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs b/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs index 0c233e9da1..d6e5927392 100644 --- a/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs +++ b/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs @@ -3,10 +3,10 @@ namespace SmartStore.Core.Events { - public class EventConsumerMetadata - { - public bool IsActive { get; set; } - public Type ContainerType { get; set; } - public PluginDescriptor PluginDescriptor { get; set; } - } + public class EventConsumerMetadata + { + public bool IsActive { get; set; } + public Type ContainerType { get; set; } + public PluginDescriptor PluginDescriptor { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs b/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs index a5076473c9..f5992a9bd9 100644 --- a/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs +++ b/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs @@ -2,19 +2,19 @@ namespace SmartStore.Core.Events { - /// - /// Specifies whether an event consumer method should be invoked asynchronously without awaiting. - /// This can be advantegous in long running processes, because the current request thread will not 'block' - /// until method completion. - /// - /// - /// Use this with caution and only if you know what you are doing :-) - /// A class containing a fire & forget consumer should NOT take dependencies on request scoped services, because task continuation - /// happens on another thread and context gets lost. Pass required dependencies as method parameters instead, the consumer invoker - /// will spawn a new private context for the unit of work and resolve dependencies from this context. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class FireForgetAttribute : Attribute - { - } + /// + /// Specifies whether an event consumer method should be invoked asynchronously without awaiting. + /// This can be advantegous in long running processes, because the current request thread will not 'block' + /// until method completion. + /// + /// + /// Use this with caution and only if you know what you are doing :-) + /// A class containing a fire & forget consumer should NOT take dependencies on request scoped services, because task continuation + /// happens on another thread and context gets lost. Pass required dependencies as method parameters instead, the consumer invoker + /// will spawn a new private context for the unit of work and resolve dependencies from this context. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class FireForgetAttribute : Attribute + { + } } diff --git a/src/Libraries/SmartStore.Core/Events/IConsumer.cs b/src/Libraries/SmartStore.Core/Events/IConsumer.cs index da2ff66d89..f5fa00be1d 100644 --- a/src/Libraries/SmartStore.Core/Events/IConsumer.cs +++ b/src/Libraries/SmartStore.Core/Events/IConsumer.cs @@ -2,44 +2,44 @@ namespace SmartStore.Core.Events { - /// - /// Marker interface for classes that contain one or more - /// event message handlers. A handler is a public instance method following some conventions. - /// The name of the handler method must be one of: - /// - /// void Handle(TMessage msg) - /// void HandleEvent(TMessage msg) - /// void Consume(TMessage msg) - /// Task HandleAsync(TMessage msg) - /// Task HandleEventAsync(TMessage msg) - /// Task ConsumeAsync(TMessage msg) - /// - /// Alternatively, the TMessage param can be the generic type. - /// - /// The decides how to call the method based on its signature: - /// - /// void method are invoked synchronously. - /// methods are invoked asynchronously and awaited. - /// The makes the method run in the background without awaiting. - /// - /// - /// - /// You can declare additional dependency parameters in the handler method, e.g.: - /// - /// Task HandleEventAsync(SomeEvent message, IDbContext dbContext, ICacheManager cache, CancellationToken cancelToken) - /// - /// Order does not matter. The invoker will auto-resolve appropriate instances and pass them to the method. But any unregistered - /// dependency or a primitive type will result in an exception (except of which always resolves to - /// ). - /// - /// - public interface IConsumer - { - } + /// + /// Marker interface for classes that contain one or more + /// event message handlers. A handler is a public instance method following some conventions. + /// The name of the handler method must be one of: + /// + /// void Handle(TMessage msg) + /// void HandleEvent(TMessage msg) + /// void Consume(TMessage msg) + /// Task HandleAsync(TMessage msg) + /// Task HandleEventAsync(TMessage msg) + /// Task ConsumeAsync(TMessage msg) + /// + /// Alternatively, the TMessage param can be the generic type. + /// + /// The decides how to call the method based on its signature: + /// + /// void method are invoked synchronously. + /// methods are invoked asynchronously and awaited. + /// The makes the method run in the background without awaiting. + /// + /// + /// + /// You can declare additional dependency parameters in the handler method, e.g.: + /// + /// Task HandleEventAsync(SomeEvent message, IDbContext dbContext, ICacheManager cache, CancellationToken cancelToken) + /// + /// Order does not matter. The invoker will auto-resolve appropriate instances and pass them to the method. But any unregistered + /// dependency or a primitive type will result in an exception (except of which always resolves to + /// ). + /// + /// + public interface IConsumer + { + } - [Obsolete("Implement the non-generic 'IConsumer' interface instead.")] - public interface IConsumer : IConsumer - { - void HandleEvent(T message); - } + [Obsolete("Implement the non-generic 'IConsumer' interface instead.")] + public interface IConsumer : IConsumer + { + void HandleEvent(T message); + } } diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs b/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs index 85036e7528..18a6fd082a 100644 --- a/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs +++ b/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs @@ -1,12 +1,10 @@ -using System; - -namespace SmartStore.Core.Events +namespace SmartStore.Core.Events { - /// - /// Responsible for invoking event message handler methods. - /// - public interface IConsumerInvoker - { - void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, ConsumeContext envelope) where TMessage : class; - } + /// + /// Responsible for invoking event message handler methods. + /// + public interface IConsumerInvoker + { + void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, ConsumeContext envelope) where TMessage : class; + } } diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs b/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs index 4339d6fb4f..a23e6e97de 100644 --- a/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs +++ b/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Core.Events { - /// - /// A registry for fast lookup. - /// - public interface IConsumerRegistry - { - IEnumerable GetConsumers(object message); - } + /// + /// A registry for fast lookup. + /// + public interface IConsumerRegistry + { + IEnumerable GetConsumers(object message); + } } diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs b/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs index 7a7be293d7..48d940e611 100644 --- a/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs +++ b/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs @@ -1,16 +1,15 @@ -using System; -using System.Reflection; +using System.Reflection; using Autofac; namespace SmartStore.Core.Events { - /// - /// Resolves the concrete class instance which contains - /// the requested consumer. - /// - public interface IConsumerResolver - { - IConsumer Resolve(ConsumerDescriptor descriptor); - object ResolveParameter(ParameterInfo p, IComponentContext c = null); - } + /// + /// Resolves the concrete class instance which contains + /// the requested consumer. + /// + public interface IConsumerResolver + { + IConsumer Resolve(ConsumerDescriptor descriptor); + object ResolveParameter(ParameterInfo p, IComponentContext c = null); + } } diff --git a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs index 21f0f6ebe6..f4cbbd9724 100644 --- a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs +++ b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs @@ -1,9 +1,7 @@ -using System; - -namespace SmartStore.Core.Events +namespace SmartStore.Core.Events { - public interface IEventPublisher - { - void Publish(T message) where T : class; - } + public interface IEventPublisher + { + void Publish(T message) where T : class; + } } diff --git a/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs b/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs index 07d7ae6e11..18c6cea799 100644 --- a/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs +++ b/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs @@ -1,18 +1,15 @@  namespace SmartStore.Core.Events { - public class NullEventPublisher : IEventPublisher - { - private readonly static IEventPublisher s_instance = new NullEventPublisher(); + public class NullEventPublisher : IEventPublisher + { + private readonly static IEventPublisher s_instance = new NullEventPublisher(); - public static IEventPublisher Instance - { - get { return s_instance; } - } + public static IEventPublisher Instance => s_instance; - public void Publish(T eventMessage) where T : class - { - // Noop - } - } + public void Publish(T eventMessage) where T : class + { + // Noop + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs index 8d15fef773..bae926e8e1 100644 --- a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.CompilerServices; using SmartStore.Collections; @@ -12,32 +11,32 @@ public static void AddRange(this ICollection initial, IEnumerable other if (other == null) return; - if (initial is List list) - { - list.AddRange(other); - return; - } + if (initial is List list) + { + list.AddRange(other); + return; + } - foreach (var local in other) - { - initial.Add(local); - } + foreach (var local in other) + { + initial.Add(local); + } } - public static SyncedCollection AsSynchronized(this ICollection source) - { - return AsSynchronized(source, new object()); - } + public static SyncedCollection AsSynchronized(this ICollection source) + { + return AsSynchronized(source, new object()); + } - public static SyncedCollection AsSynchronized(this ICollection source, object syncRoot) - { - if (source is SyncedCollection sc) - { - return sc; - } + public static SyncedCollection AsSynchronized(this ICollection source, object syncRoot) + { + if (source is SyncedCollection sc) + { + return sc; + } - return new SyncedCollection(source, syncRoot); - } + return new SyncedCollection(source, syncRoot); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNullOrEmpty(this ICollection source) diff --git a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs index 324b084e6f..4689401f35 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs @@ -10,13 +10,12 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -using SmartStore.ComponentModel; using SmartStore.Utilities; using SmartStore.Utilities.ObjectPools; namespace SmartStore { - public static class ConversionExtensions + public static class ConversionExtensions { #region Object @@ -33,7 +32,7 @@ public static T Convert(this object value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Convert(this object value, T defaultValue) - { + { if (!CommonHelper.TryConvert(value, typeof(T), CultureInfo.InvariantCulture, out object result)) { return defaultValue; @@ -55,7 +54,7 @@ public static T Convert(this object value, CultureInfo culture) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Convert(this object value, T defaultValue, CultureInfo culture) - { + { if (!CommonHelper.TryConvert(value, typeof(T), culture, out object result)) { return defaultValue; @@ -78,7 +77,7 @@ public static object Convert(this object value, Type to) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static object Convert(this object value, Type to, CultureInfo culture) - { + { if (!CommonHelper.TryConvert(value, to, culture, out object result)) { return null; @@ -86,7 +85,7 @@ public static object Convert(this object value, Type to, CultureInfo culture) } return result; - } + } #endregion @@ -110,45 +109,45 @@ public static char ToHex(this int value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ToInt(this string value, int defaultValue = 0) { - if (CommonHelper.TryConvert(value, typeof(int), CultureInfo.InvariantCulture, out object result)) - { - return (int)result; - } + if (CommonHelper.TryConvert(value, typeof(int), CultureInfo.InvariantCulture, out object result)) + { + return (int)result; + } - return defaultValue; + return defaultValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char ToChar(this string value, bool unescape = false, char defaultValue = '\0') - { - if (value.HasValue() && char.TryParse(unescape ? Regex.Unescape(value) : value, out char result)) - { - return result; - } + { + if (value.HasValue() && char.TryParse(unescape ? Regex.Unescape(value) : value, out char result)) + { + return result; + } - return defaultValue; - } + return defaultValue; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ToFloat(this string value, float defaultValue = 0) { - if (CommonHelper.TryConvert(value, typeof(float), CultureInfo.InvariantCulture, out object result)) - { - return (float)result; - } + if (CommonHelper.TryConvert(value, typeof(float), CultureInfo.InvariantCulture, out object result)) + { + return (float)result; + } - return defaultValue; + return defaultValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ToBool(this string value, bool defaultValue = false) { - if (CommonHelper.TryConvert(value, typeof(bool), out object result)) - { - return (bool)result; - } + if (CommonHelper.TryConvert(value, typeof(bool), out object result)) + { + return (bool)result; + } - return defaultValue; + return defaultValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -183,25 +182,25 @@ public static bool ToBool(this string value, bool defaultValue = false) return defaultValue; } - /// - /// Parse ISO-8601 UTC timestamp including milliseconds. - /// - /// - /// Dublicate can be found in HmacAuthentication class. - /// - public static DateTime? ToDateTimeIso8601(this string value) - { - if (value.HasValue()) - { - if (DateTime.TryParseExact(value, "o", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out DateTime dt)) - return dt; - - if (DateTime.TryParseExact(value, "yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt)) - return dt; - } - - return null; - } + /// + /// Parse ISO-8601 UTC timestamp including milliseconds. + /// + /// + /// Dublicate can be found in HmacAuthentication class. + /// + public static DateTime? ToDateTimeIso8601(this string value) + { + if (value.HasValue()) + { + if (DateTime.TryParseExact(value, "o", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out DateTime dt)) + return dt; + + if (DateTime.TryParseExact(value, "yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt)) + return dt; + } + + return null; + } [DebuggerStepThrough] public static Version ToVersion(this string value, Version defaultVersion = null) @@ -222,19 +221,19 @@ public static Version ToVersion(this string value, Version defaultVersion = null public static byte[] ToByteArray(this Stream stream) { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - if (stream is MemoryStream mem) - { - if (mem.TryGetBuffer(out var buffer)) - { - return buffer.Array; - } - return mem.ToArray(); - } - else - { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (stream is MemoryStream mem) + { + if (mem.TryGetBuffer(out var buffer)) + { + return buffer.Array; + } + return mem.ToArray(); + } + else + { try { var len = stream.Length; @@ -252,23 +251,23 @@ public static byte[] ToByteArray(this Stream stream) return ToByteArrayCopy(stream); } } - } - - public static async Task ToByteArrayAsync(this Stream stream) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - if (stream is MemoryStream mem) - { - if (mem.TryGetBuffer(out var buffer)) - { - return buffer.Array; - } - return mem.ToArray(); - } - else - { + } + + public static async Task ToByteArrayAsync(this Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (stream is MemoryStream mem) + { + if (mem.TryGetBuffer(out var buffer)) + { + return buffer.Array; + } + return mem.ToArray(); + } + else + { try { var len = stream.Length; @@ -286,7 +285,7 @@ public static async Task ToByteArrayAsync(this Stream stream) return await ToByteArrayCopyAsync(stream); } } - } + } private static byte[] ToByteArrayCopy(Stream stream, int capacity = 0) { @@ -308,28 +307,28 @@ private static async Task ToByteArrayCopyAsync(Stream stream, int capaci [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string AsString(this Stream stream) - { - return stream.AsString(Encoding.UTF8); - } + { + return stream.AsString(Encoding.UTF8); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task AsStringAsync(this Stream stream) - { - return stream.AsStringAsync(Encoding.UTF8); - } + { + return stream.AsStringAsync(Encoding.UTF8); + } - public static string AsString(this Stream stream, Encoding encoding) + public static string AsString(this Stream stream, Encoding encoding) { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); - // convert stream to string - string result; + // convert stream to string + string result; - if (stream.CanSeek) - { - stream.Position = 0; - } + if (stream.CanSeek) + { + stream.Position = 0; + } using (StreamReader sr = new StreamReader(stream, encoding)) { @@ -339,37 +338,37 @@ public static string AsString(this Stream stream, Encoding encoding) return result; } - public static Task AsStringAsync(this Stream stream, Encoding encoding) - { - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); + public static Task AsStringAsync(this Stream stream, Encoding encoding) + { + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); - // convert stream to string - Task result; + // convert stream to string + Task result; - if (stream.CanSeek) - { - stream.Position = 0; - } + if (stream.CanSeek) + { + stream.Position = 0; + } - using (StreamReader sr = new StreamReader(stream, encoding)) - { - result = sr.ReadToEndAsync(); - } + using (StreamReader sr = new StreamReader(stream, encoding)) + { + result = sr.ReadToEndAsync(); + } - return result; - } + return result; + } - #endregion + #endregion - #region ByteArray + #region ByteArray - /// - /// Converts a byte array into an object. - /// - /// Object to deserialize. May be null. - /// Deserialized object, or null if input was null. - public static object ToObject(this byte[] bytes) + /// + /// Converts a byte array into an object. + /// + /// Object to deserialize. May be null. + /// Deserialized object, or null if input was null. + public static object ToObject(this byte[] bytes) { if (bytes == null) return null; @@ -385,18 +384,18 @@ public static Stream ToStream(this byte[] bytes) return new MemoryStream(bytes); } - /// - /// Computes the MD5 hash of a byte array - /// - /// The byte array to compute the hash for - /// The hash value - [DebuggerStepThrough] - public static string Hash(this byte[] value, bool toBase64 = false) + /// + /// Computes the MD5 hash of a byte array + /// + /// The byte array to compute the hash for + /// The hash value + [DebuggerStepThrough] + public static string Hash(this byte[] value, bool toBase64 = false) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + if (value == null) + throw new ArgumentNullException(nameof(value)); - using (MD5 md5 = MD5.Create()) + using (MD5 md5 = MD5.Create()) { if (toBase64) @@ -420,43 +419,43 @@ public static string Hash(this byte[] value, bool toBase64 = false) } } - /// - /// Compresses the input buffer with - /// - /// Decompressed input - /// The compressed result - public static byte[] Zip(this byte[] buffer) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - using (var compressedStream = new MemoryStream()) - using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Close(); - return compressedStream.ToArray(); - } - } - - /// - /// Decompresses the input buffer with decompression - /// - /// Compressed input - /// The decompressed result - public static byte[] UnZip(this byte[] buffer) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - using (var compressedStream = new MemoryStream(buffer)) - using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress)) - using (var resultStream = new MemoryStream()) - { - zipStream.CopyTo(resultStream); - return resultStream.ToArray(); - } - } + /// + /// Compresses the input buffer with + /// + /// Decompressed input + /// The compressed result + public static byte[] Zip(this byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + using (var compressedStream = new MemoryStream()) + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress)) + { + zipStream.Write(buffer, 0, buffer.Length); + zipStream.Close(); + return compressedStream.ToArray(); + } + } + + /// + /// Decompresses the input buffer with decompression + /// + /// Compressed input + /// The decompressed result + public static byte[] UnZip(this byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + using (var compressedStream = new MemoryStream(buffer)) + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress)) + using (var resultStream = new MemoryStream()) + { + zipStream.CopyTo(resultStream); + return resultStream.ToArray(); + } + } #endregion diff --git a/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs index 847572155a..517bab695a 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs @@ -1,15 +1,12 @@ using System; -using System.Xml; using System.Globalization; -using TimeZone = System.TimeZoneInfo; -using System.Text.RegularExpressions; using System.Runtime.CompilerServices; namespace SmartStore { public static class DateTimeExtensions { - public static readonly DateTime BeginOfEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + public static readonly DateTime BeginOfEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// /// Converts a nullable date/time value to UTC. @@ -105,7 +102,7 @@ public static DateTime GetEvenMinuteDateBefore(this DateTime? value) public static long ToJavaScriptTicks(this DateTime value) { DateTimeOffset utcDateTime = value.ToUniversalTime(); - long javaScriptTicks = (utcDateTime.Ticks - BeginOfEpoch.Ticks) / (long)10000; + long javaScriptTicks = (utcDateTime.Ticks - BeginOfEpoch.Ticks) / (long)10000; return javaScriptTicks; } @@ -136,71 +133,79 @@ public static DateTime GetLastDayOfMonth(this DateTime value) return dtTo; } - public static DateTime ToEndOfTheDay(this DateTime value) - { - if (value != null) - return new DateTime(value.Year, value.Month, value.Day, 23, 59, 59); - return value; - } + public static DateTime ToEndOfTheDay(this DateTime value) + { + if (value != null) + return new DateTime(value.Year, value.Month, value.Day, 23, 59, 59); + return value; + } - public static DateTime? ToEndOfTheDay(this DateTime? value) - { - return (value.HasValue ? value.Value.ToEndOfTheDay() : value); - } + public static DateTime? ToEndOfTheDay(this DateTime? value) + { + return (value.HasValue ? value.Value.ToEndOfTheDay() : value); + } /// /// Epoch time. Number of seconds since midnight (UTC) on 1st January 1970. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ToUnixTime(this DateTime value) - { - return Convert.ToInt64((value.ToUniversalTime() - BeginOfEpoch).TotalSeconds); - } + { + return Convert.ToInt64((value.ToUniversalTime() - BeginOfEpoch).TotalSeconds); + } /// /// UTC date based on number of seconds since midnight (UTC) on 1st January 1970. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTime FromUnixTime(this long unixTime) - { - return BeginOfEpoch.AddSeconds(unixTime); - } + { + return BeginOfEpoch.AddSeconds(unixTime); + } /// /// Converts a DateTime to a string with native digits /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToNativeString(this DateTime value) - { - return value.ToNativeString(null, null); - } + { + return value.ToNativeString(null, null); + } /// /// Converts a DateTime to a string with native digits /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToNativeString(this DateTime value, IFormatProvider provider) - { - return value.ToNativeString(null, provider); - } + { + return value.ToNativeString(null, provider); + } /// /// Converts a DateTime to a string with native digits /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToNativeString(this DateTime value, string format) - { - return value.ToNativeString(format, null); - } + { + 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); - } + { + provider = provider ?? CultureInfo.CurrentCulture; + return value.ToString(format, provider).ReplaceNativeDigits(provider); + } + + /// + /// Converts a DateTime to ISO 8601 string + /// + public static string ToIso8601String(this DateTime value) + { + return value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + } } } diff --git a/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs index d2b51fb506..24b46dddf9 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Dynamic; -using SmartStore.Utilities; +using System.Linq; using System.Runtime.CompilerServices; -using System.Web.Routing; +using SmartStore.Utilities; namespace SmartStore { @@ -35,7 +34,7 @@ public static IDictionary Merge(this IDictionary [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDictionary Merge(this IDictionary instance, object values, bool replaceExisting = true) { - return instance.Merge(CommonHelper.ObjectToDictionary(values), replaceExisting); + return instance.Merge(CommonHelper.ObjectToDictionary(values), replaceExisting); } public static IDictionary Merge(this IDictionary instance, IDictionary from, bool replaceExisting = true) @@ -54,30 +53,30 @@ public static IDictionary Merge(this IDictionary AppendInValue(this IDictionary instance, string key, string separator, string value) { - return AddInValue(instance, key, separator, value, false); - } + return AddInValue(instance, key, separator, value, false); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDictionary PrependInValue(this IDictionary instance, string key, string separator, string value) { - return AddInValue(instance, key, separator, value, true); + return AddInValue(instance, key, separator, value, true); } - private static IDictionary AddInValue(IDictionary instance, string key, string separator, string value, bool prepend = false) - { - if (!instance.TryGetValue(key, out var obj)) - { - instance[key] = value; - } - else - { - var arr = obj.ToString().Trim().Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries).AsEnumerable(); - var arrValue = value.Trim().Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries).AsEnumerable(); + private static IDictionary AddInValue(IDictionary instance, string key, string separator, string value, bool prepend = false) + { + if (!instance.TryGetValue(key, out var obj)) + { + instance[key] = value; + } + else + { + var arr = obj.ToString().Trim().Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries).AsEnumerable(); + var arrValue = value.Trim().Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries).AsEnumerable(); - arr = prepend ? arrValue.Union(arr) : arr.Union(arrValue); + arr = prepend ? arrValue.Union(arr) : arr.Union(arrValue); - instance[key] = string.Join(separator, arr); - } + instance[key] = string.Join(separator, arr); + } return instance; @@ -85,20 +84,20 @@ private static IDictionary AddInValue(IDictionary(this IDictionary instance, TKey key) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); + { + if (instance == null) + throw new ArgumentNullException(nameof(instance)); - instance.TryGetValue(key, out var val); - return val; - } + instance.TryGetValue(key, out var val); + return val; + } - public static ExpandoObject ToExpandoObject(this IDictionary source, bool castIfPossible = false) + public static ExpandoObject ToExpandoObject(this IDictionary source, bool castIfPossible = false) { - if (source == null) - throw new ArgumentNullException(nameof(source)); + if (source == null) + throw new ArgumentNullException(nameof(source)); - if (castIfPossible && source is ExpandoObject) + if (castIfPossible && source is ExpandoObject) { return source as ExpandoObject; } @@ -110,29 +109,29 @@ public static ExpandoObject ToExpandoObject(this IDictionary sou } - public static bool TryAdd(this IDictionary source, TKey key, TValue value, bool updateIfExists = false) - { - if (source == null || key == null) - { - return false; - } - - if (source.ContainsKey(key)) - { - if (updateIfExists) - { - source[key] = value; - return true; - } - } - else - { - source.Add(key, value); - return true; - } - - return false; - } + public static bool TryAdd(this IDictionary source, TKey key, TValue value, bool updateIfExists = false) + { + if (source == null || key == null) + { + return false; + } + + if (source.ContainsKey(key)) + { + if (updateIfExists) + { + source[key] = value; + return true; + } + } + else + { + source.Add(key, value); + return true; + } + + return false; + } public static bool TryRemove(this IDictionary source, TKey key, out TValue value) { diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs index 8039088462..f867372efa 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -2,148 +2,145 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Web; using SmartStore.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using SmartStore.Core; -using System.Runtime.CompilerServices; using SmartStore.Utilities.ObjectPools; namespace SmartStore { - public static class CollectionSlicer - { - /// - /// Slices the iteration over an enumerable by the given slice sizes. - /// - /// - /// The source sequence to slice - /// - /// Slice sizes. At least one size is required. Multiple sizes result in differently sized slices, - /// whereat the last size is used for the "rest" (if any) - /// - /// The sliced enumerable - public static IEnumerable> Slice(this IEnumerable source, params int[] sizes) - { - if (!sizes.Any(step => step != 0)) - { - throw new InvalidOperationException("Can't slice a collection with step length 0."); - } - - return new Slicer(source.GetEnumerator(), sizes).Slice(); - } - } - - internal sealed class Slicer - { - private readonly IEnumerator _iterator; - private readonly int[] _sizes; - private volatile bool _hasNext; - private volatile int _currentSize; - private volatile int _index; - - public Slicer(IEnumerator iterator, int[] sizes) - { - _iterator = iterator; - _sizes = sizes; - _index = 0; - _currentSize = 0; - _hasNext = true; - } - - public int Index - { - get { return _index; } - } - - public IEnumerable> Slice() - { - var length = _sizes.Length; - var index = 1; - var size = 0; - - for (var i = 0; _hasNext; ++i) - { - if (i < length) - { - size = _sizes[i]; - _currentSize = size - 1; - } - - while (_index < index && _hasNext) - { - _hasNext = MoveNext(); - } - - if (_hasNext) - { - yield return new List(SliceInternal()); - index += size; - } - } - } - - private IEnumerable SliceInternal() - { - if (_currentSize == -1) yield break; - yield return _iterator.Current; - - for (var count = 0; count < _currentSize && _hasNext; ++count) - { - _hasNext = MoveNext(); - - if (_hasNext) - { - yield return _iterator.Current; - } - } - } - - private bool MoveNext() - { - ++_index; - return _iterator.MoveNext(); - } - } - - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - public static class EnumerableExtensions - { - #region Nested classes - - private static class DefaultReadOnlyCollection - { - private static ReadOnlyCollection defaultCollection; - - [SuppressMessage("ReSharper", "ConvertIfStatementToNullCoalescingExpression")] - internal static ReadOnlyCollection Empty - { - get - { - if (defaultCollection == null) - { - defaultCollection = new ReadOnlyCollection(new T[0]); - } - return defaultCollection; - } - } - } - - #endregion - - #region IEnumerable - - /// - /// Performs an action on each item while iterating through a list. - /// This is a handy shortcut for foreach(item in list) { ... } - /// - /// The type of the items. - /// The list, which holds the objects. - /// The action delegate which is called on each item while iterating. - [DebuggerStepThrough] + public static class CollectionSlicer + { + /// + /// Slices the iteration over an enumerable by the given slice sizes. + /// + /// + /// The source sequence to slice + /// + /// Slice sizes. At least one size is required. Multiple sizes result in differently sized slices, + /// whereat the last size is used for the "rest" (if any) + /// + /// The sliced enumerable + public static IEnumerable> Slice(this IEnumerable source, params int[] sizes) + { + if (!sizes.Any(step => step != 0)) + { + throw new InvalidOperationException("Can't slice a collection with step length 0."); + } + + return new Slicer(source.GetEnumerator(), sizes).Slice(); + } + } + + internal sealed class Slicer + { + private readonly IEnumerator _iterator; + private readonly int[] _sizes; + private volatile bool _hasNext; + private volatile int _currentSize; + private volatile int _index; + + public Slicer(IEnumerator iterator, int[] sizes) + { + _iterator = iterator; + _sizes = sizes; + _index = 0; + _currentSize = 0; + _hasNext = true; + } + + public int Index => _index; + + public IEnumerable> Slice() + { + var length = _sizes.Length; + var index = 1; + var size = 0; + + for (var i = 0; _hasNext; ++i) + { + if (i < length) + { + size = _sizes[i]; + _currentSize = size - 1; + } + + while (_index < index && _hasNext) + { + _hasNext = MoveNext(); + } + + if (_hasNext) + { + yield return new List(SliceInternal()); + index += size; + } + } + } + + private IEnumerable SliceInternal() + { + if (_currentSize == -1) yield break; + yield return _iterator.Current; + + for (var count = 0; count < _currentSize && _hasNext; ++count) + { + _hasNext = MoveNext(); + + if (_hasNext) + { + yield return _iterator.Current; + } + } + } + + private bool MoveNext() + { + ++_index; + return _iterator.MoveNext(); + } + } + + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + public static class EnumerableExtensions + { + #region Nested classes + + private static class DefaultReadOnlyCollection + { + private static ReadOnlyCollection defaultCollection; + + [SuppressMessage("ReSharper", "ConvertIfStatementToNullCoalescingExpression")] + internal static ReadOnlyCollection Empty + { + get + { + if (defaultCollection == null) + { + defaultCollection = new ReadOnlyCollection(new T[0]); + } + return defaultCollection; + } + } + } + + #endregion + + #region IEnumerable + + /// + /// Performs an action on each item while iterating through a list. + /// This is a handy shortcut for foreach(item in list) { ... } + /// + /// The type of the items. + /// The list, which holds the objects. + /// The action delegate which is called on each item while iterating. + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Each(this IEnumerable source, Action action) { @@ -153,39 +150,39 @@ public static void Each(this IEnumerable source, Action action) } } - /// - /// Performs an action on each item while iterating through a list. - /// This is a handy shortcut for foreach(item in list) { ... } - /// - /// The type of the items. - /// The list, which holds the objects. - /// The action delegate which is called on each item while iterating. - [DebuggerStepThrough] + /// + /// Performs an action on each item while iterating through a list. + /// This is a handy shortcut for foreach(item in list) { ... } + /// + /// The type of the items. + /// The list, which holds the objects. + /// The action delegate which is called on each item while iterating. + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Each(this IEnumerable source, Action action) - { - int i = 0; - foreach (T t in source) - { - action(t, i++); - } - } + { + int i = 0; + foreach (T t in source) + { + action(t, i++); + } + } public static ReadOnlyCollection AsReadOnly(this IEnumerable source) { - if (source == null || !source.Any()) - return DefaultReadOnlyCollection.Empty; - - if (source is ReadOnlyCollection readOnly) - { - return readOnly; - } - else if (source is List list) - { - return list.AsReadOnly(); - } - - return new ReadOnlyCollection(source.ToList()); + if (source == null || !source.Any()) + return DefaultReadOnlyCollection.Empty; + + if (source is ReadOnlyCollection readOnly) + { + return readOnly; + } + else if (source is List list) + { + return list.AsReadOnly(); + } + + return new ReadOnlyCollection(source.ToList()); } /// @@ -196,11 +193,11 @@ public static ReadOnlyCollection AsReadOnly(this IEnumerable source) /// Result as dictionary [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Dictionary ToDictionarySafe( - this IEnumerable source, - Func keySelector) - { - return source.ToDictionarySafe(keySelector, new Func(src => src), null); - } + this IEnumerable source, + Func keySelector) + { + return source.ToDictionarySafe(keySelector, new Func(src => src), null); + } /// /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) @@ -211,12 +208,12 @@ public static Dictionary ToDictionarySafe( /// Result as dictionary [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Dictionary ToDictionarySafe( - this IEnumerable source, - Func keySelector, - IEqualityComparer comparer) - { - return source.ToDictionarySafe(keySelector, new Func(src => src), comparer); - } + this IEnumerable source, + Func keySelector, + IEqualityComparer comparer) + { + return source.ToDictionarySafe(keySelector, new Func(src => src), comparer); + } /// /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) @@ -227,85 +224,85 @@ public static Dictionary ToDictionarySafe( /// Result as dictionary [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Dictionary ToDictionarySafe( - this IEnumerable source, - Func keySelector, - Func elementSelector) - { - return source.ToDictionarySafe(keySelector, elementSelector, null); - } - - /// - /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) - /// - /// source - /// keySelector - /// elementSelector - /// comparer - /// Result as dictionary - public static Dictionary ToDictionarySafe( - this IEnumerable source, - Func keySelector, - Func elementSelector, - IEqualityComparer comparer) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - if (keySelector == null) - throw new ArgumentNullException(nameof(keySelector)); - - if (elementSelector == null) - throw new ArgumentNullException(nameof(elementSelector)); - - var dictionary = new Dictionary(comparer); - - foreach (var local in source) - { - dictionary[keySelector(local)] = elementSelector(local); - } - - return dictionary; - } - - /// The distinct by. - /// The source. - /// The key selector. - /// Source type - /// Key type - /// the unique list - public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) - where TKey : IEquatable - { - return source.Distinct(GenericEqualityComparer.CompareMember(keySelector)); - } - - /// - /// Orders a collection of entities by a specific ID sequence - /// - /// Entity type - /// The entity collection to sort - /// The IDs to order by - /// The sorted entity collection - public static IEnumerable OrderBySequence(this IEnumerable source, IEnumerable ids) where TEntity : BaseEntity - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - if (ids == null) - throw new ArgumentNullException(nameof(ids)); - - var sorted = from id in ids - join entity in source on id equals entity.Id - select entity; - - return sorted; - } + this IEnumerable source, + Func keySelector, + Func elementSelector) + { + return source.ToDictionarySafe(keySelector, elementSelector, null); + } + + /// + /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) + /// + /// source + /// keySelector + /// elementSelector + /// comparer + /// Result as dictionary + public static Dictionary ToDictionarySafe( + this IEnumerable source, + Func keySelector, + Func elementSelector, + IEqualityComparer comparer) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (keySelector == null) + throw new ArgumentNullException(nameof(keySelector)); + + if (elementSelector == null) + throw new ArgumentNullException(nameof(elementSelector)); + + var dictionary = new Dictionary(comparer); + + foreach (var local in source) + { + dictionary[keySelector(local)] = elementSelector(local); + } + + return dictionary; + } + + /// The distinct by. + /// The source. + /// The key selector. + /// Source type + /// Key type + /// the unique list + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) + where TKey : IEquatable + { + return source.Distinct(GenericEqualityComparer.CompareMember(keySelector)); + } + + /// + /// Orders a collection of entities by a specific ID sequence + /// + /// Entity type + /// The entity collection to sort + /// The IDs to order by + /// The sorted entity collection + public static IEnumerable OrderBySequence(this IEnumerable source, IEnumerable ids) where TEntity : BaseEntity + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (ids == null) + throw new ArgumentNullException(nameof(ids)); + + var sorted = from id in ids + join entity in source on id equals entity.Id + select entity; + + return sorted; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string StrJoin(this IEnumerable source, string separator) - { - return string.Join(separator, source); - } + { + return string.Join(separator, source); + } #endregion @@ -317,44 +314,44 @@ public static Multimap ToMultimap( Func keySelector, Func valueSelector) { - return source.ToMultimap(keySelector, valueSelector, null); + return source.ToMultimap(keySelector, valueSelector, null); } - public static Multimap ToMultimap( - this IEnumerable source, - Func keySelector, - Func valueSelector, - IEqualityComparer comparer) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); + public static Multimap ToMultimap( + this IEnumerable source, + Func keySelector, + Func valueSelector, + IEqualityComparer comparer) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); - if (keySelector == null) - throw new ArgumentNullException(nameof(keySelector)); + if (keySelector == null) + throw new ArgumentNullException(nameof(keySelector)); - if (valueSelector == null) - throw new ArgumentNullException(nameof(valueSelector)); + if (valueSelector == null) + throw new ArgumentNullException(nameof(valueSelector)); - var map = new Multimap(comparer); + var map = new Multimap(comparer); - foreach (var item in source) - { - map.Add(keySelector(item), valueSelector(item)); - } + foreach (var item in source) + { + map.Add(keySelector(item), valueSelector(item)); + } - return map; - } + return map; + } - #endregion + #endregion - #region NameValueCollection + #region NameValueCollection - public static void AddRange(this NameValueCollection initial, NameValueCollection other) + public static void AddRange(this NameValueCollection initial, NameValueCollection other) { - if (initial == null) - throw new ArgumentNullException(nameof(initial)); + if (initial == null) + throw new ArgumentNullException(nameof(initial)); - if (other == null) + if (other == null) return; foreach (var item in other.AllKeys) @@ -363,15 +360,15 @@ public static void AddRange(this NameValueCollection initial, NameValueCollectio } } - /// - /// Builds an URL query string - /// - /// Name value collection - /// Encoding type. Can be null. - /// Whether to encode keys and values - /// The query string without leading a question mark - public static string BuildQueryString(this NameValueCollection nvc, Encoding encoding, bool encode = true) - { + /// + /// Builds an URL query string + /// + /// Name value collection + /// Encoding type. Can be null. + /// Whether to encode keys and values + /// The query string without leading a question mark + public static string BuildQueryString(this NameValueCollection nvc, Encoding encoding, bool encode = true) + { var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; @@ -401,7 +398,7 @@ public static string BuildQueryString(this NameValueCollection nvc, Encoding enc } return psb.ToStringAndReturn(); - } + } #endregion @@ -433,36 +430,36 @@ public static int Remove(this IList list, Func selector) return count; } - #endregion + #endregion - #region Stack + #region Stack - public static bool TryPeek(this Stack stack, out T value) - { - value = default(T); + public static bool TryPeek(this Stack stack, out T value) + { + value = default(T); - if (stack.Count > 0) - { - value = stack.Peek(); - return true; - } + if (stack.Count > 0) + { + value = stack.Peek(); + return true; + } - return false; - } + return false; + } - public static bool TryPop(this Stack stack, out T value) - { - value = default(T); + public static bool TryPop(this Stack stack, out T value) + { + value = default(T); - if (stack.Count > 0) - { - value = stack.Pop(); - return true; - } + if (stack.Count > 0) + { + value = stack.Pop(); + return true; + } - return false; - } + return false; + } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/ExceptionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ExceptionExtensions.cs index 4f674a61cf..70f1e966e0 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ExceptionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ExceptionExtensions.cs @@ -5,17 +5,17 @@ namespace SmartStore { - public static class ExceptionExtensions - { - public static bool IsFatal(this Exception ex) - { - return ex is StackOverflowException || - ex is OutOfMemoryException || - ex is AccessViolationException || - ex is AppDomainUnloadedException || - ex is ThreadAbortException || - ex is SecurityException || - ex is SEHException; - } - } + public static class ExceptionExtensions + { + public static bool IsFatal(this Exception ex) + { + return ex is StackOverflowException || + ex is OutOfMemoryException || + ex is AccessViolationException || + ex is AppDomainUnloadedException || + ex is ThreadAbortException || + ex is SecurityException || + ex is SEHException; + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/ExpressionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ExpressionExtensions.cs index ceeb53466a..4d27f78734 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ExpressionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ExpressionExtensions.cs @@ -8,7 +8,7 @@ namespace SmartStore public static class ExpressionExtensions { public static FastPropertyInvoker CompileFast( - this Expression> expression, + this Expression> expression, PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) { if (!(expression.Body is MemberExpression member)) diff --git a/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs index acd2f75109..df752203cd 100644 --- a/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Web.UI; namespace SmartStore -{ +{ public static class HtmlTextWriterExtensions { public static void AddAttributes(this HtmlTextWriter writer, IDictionary attributes) diff --git a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs index 5d105b0165..a2618ee834 100644 --- a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs @@ -2,35 +2,34 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Net; using System.Linq; -using System.Text; +using System.Net; +using System.Runtime.CompilerServices; using System.Web; using System.Web.Caching; +using System.Web.Mvc; using System.Web.Security; -using SmartStore.Core.Infrastructure; using SmartStore.Core; -using System.Web.Mvc; using SmartStore.Core.Fakes; -using System.Runtime.CompilerServices; +using SmartStore.Core.Infrastructure; namespace SmartStore { - public static class HttpExtensions - { - const string CacheRegionName = "SmartStoreNET:"; - const string RememberPathKey = "AppRelativeCurrentExecutionFilePath.Original"; - - private static readonly List> _sslHeaders = new List> - { - new Tuple("HTTP_CLUSTER_HTTPS", "on"), + public static class HttpExtensions + { + const string CacheRegionName = "SmartStoreNET:"; + const string RememberPathKey = "AppRelativeCurrentExecutionFilePath.Original"; + + private static readonly List> _sslHeaders = new List> + { + new Tuple("HTTP_CLUSTER_HTTPS", "on"), new Tuple("HTTP_X_FORWARDED_PROTO", "https"), new Tuple("X-Forwarded-Proto", "https"), - new Tuple("x-arr-ssl", null), - new Tuple("X-Forwarded-Protocol", "https"), - new Tuple("X-Forwarded-Ssl", "on"), - new Tuple("X-Url-Scheme", "https") - }; + new Tuple("x-arr-ssl", null), + new Tuple("X-Forwarded-Protocol", "https"), + new Tuple("X-Forwarded-Ssl", "on"), + new Tuple("X-Url-Scheme", "https") + }; /// /// Tries to get the instance without throwing exceptions @@ -38,161 +37,180 @@ public static class HttpExtensions /// The instance or null. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HttpRequestBase SafeGetHttpRequest(this HttpContext httpContext) - { - if (httpContext == null) - { - return null; - } - - return SafeGetHttpRequest(new HttpContextWrapper(httpContext)); - } - - /// - /// Tries to get the instance without throwing exceptions - /// - /// The instance or null. - public static HttpRequestBase SafeGetHttpRequest(this HttpContextBase httpContext) - { - if (httpContext == null) - { - return null; - } - - if (httpContext.Handler != null || httpContext is FakeHttpContext) - { - // Having a handler means we're most likely in the MVC routing pipeline. - return httpContext.Request; - } - - try - { - return httpContext.Request; - } - catch - { - return null; - } - } - - /// - /// Returns wether the specified url is local to the host or not - /// - /// - /// - /// - public static bool IsAppLocalUrl(this HttpRequestBase request, string url) - { - if (string.IsNullOrWhiteSpace(url)) - { - return false; - } - - url = url.Trim(); - - if (url.StartsWith("~/")) - { - return true; - } - - if (url.StartsWith("//") || url.StartsWith("/\\")) - { - return false; - } - - // At this point when the url starts with "/" it is local - if (url.StartsWith("/")) - { - return true; - } - - // At this point, check for a fully qualified url - try - { - var uri = new Uri(url); - - if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (uri.Authority.Equals(request.Headers["Host"], StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Finally, check the base url from the settings - var storeContext = EngineContext.Current.Resolve(); - if (storeContext != null) - { - var baseUrl = storeContext.CurrentStore.Url; - if (baseUrl.HasValue()) - { - if (uri.Authority.Equals(new Uri(baseUrl).Authority, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - catch - { - // mall-formed url e.g, "abcdef" - return false; - } - } + { + if (httpContext == null) + { + return null; + } + + return SafeGetHttpRequest(new HttpContextWrapper(httpContext)); + } /// - /// Gets a value which indicates whether the HTTP connection uses secure sockets (HTTPS protocol). - /// Works with Cloud's load balancers. + /// Tries to get the instance without throwing exceptions + /// + /// The instance or null. + public static HttpRequestBase SafeGetHttpRequest(this HttpContextBase httpContext) + { + if (httpContext == null) + { + return null; + } + + if (httpContext.Handler != null || httpContext is FakeHttpContext) + { + // Having a handler means we're most likely in the MVC routing pipeline. + return httpContext.Request; + } + + try + { + return httpContext.Request; + } + catch + { + return null; + } + } + + /// + /// Returns wether the specified url is local to the host or not /// /// + /// /// + public static bool IsAppLocalUrl(this HttpRequestBase request, string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return false; + } + + url = url.Trim(); + + if (url.StartsWith("~/")) + { + return true; + } + + if (url.StartsWith("//") || url.StartsWith("/\\")) + { + return false; + } + + // At this point when the url starts with "/" it is local + if (url.StartsWith("/")) + { + return true; + } + + // At this point, check for a fully qualified url + try + { + var uri = new Uri(url); + + if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (uri.Authority.Equals(request.Headers["Host"], StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Finally, check the base url from the settings + var storeContext = EngineContext.Current.Resolve(); + if (storeContext != null) + { + var baseUrl = storeContext.CurrentStore.Url; + if (baseUrl.HasValue()) + { + if (uri.Authority.Equals(new Uri(baseUrl).Authority, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + catch + { + // mall-formed url e.g, "abcdef" + return false; + } + } + + /// + /// Gets a value which indicates whether the HTTP connection uses secure sockets (HTTPS protocol). + /// Works with Cloud's load balancers. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSecureConnection(this HttpRequest request) + public static bool IsHttps(this HttpRequest request) { - return IsSecureConnection(new HttpRequestWrapper(request)); + return IsHttps(new HttpRequestWrapper(request)); } - /// - /// Gets a value which indicates whether the HTTP connection uses secure sockets (HTTPS protocol). - /// Works with Cloud's load balancers. - /// - /// - /// - public static bool IsSecureConnection(this HttpRequestBase request) - { - if (request.IsSecureConnection) - { - return true; - } - - foreach (var tuple in _sslHeaders) - { - var serverVar = request.ServerVariables[tuple.Item1]; - if (serverVar != null) - { - return tuple.Item2 == null || tuple.Item2.Equals(serverVar, StringComparison.OrdinalIgnoreCase); - } - } - - return false; - } - - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + /// + /// Gets a value which indicates whether the HTTP connection uses secure sockets (HTTPS protocol). + /// Works with Cloud's load balancers. + /// + public static bool IsHttps(this HttpRequestBase request) + { + if (request.IsSecureConnection) + { + return true; + } + + foreach (var tuple in _sslHeaders) + { + var serverVar = request.ServerVariables[tuple.Item1]; + if (serverVar != null) + { + return tuple.Item2 == null || tuple.Item2.Equals(serverVar, StringComparison.OrdinalIgnoreCase); + } + } + + return false; + } + + /// + /// Gets a value which indicates whether the current request requests a static resource, like .txt, .pdf, .js, .css etc. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsStaticResourceRequested(this HttpContext context) + { + return IsStaticResourceRequested(new HttpContextWrapper(context)); + } + + /// + /// Gets a value which indicates whether the current request requests a static resource, like .txt, .pdf, .js, .css etc. + /// + public static bool IsStaticResourceRequested(this HttpContextBase context) + { + if (context?.Request == null) + return false; + + return context.GetItem( + "IsStaticResourceRequested", + () => WebHelper.IsStaticResourceRequested(context.Request), + true); + } + + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetFormsAuthenticationCookie(this HttpWebRequest webRequest, HttpRequestBase httpRequest) - { - CopyCookie(webRequest, httpRequest, FormsAuthentication.FormsCookieName); - } + { + CopyCookie(webRequest, httpRequest, FormsAuthentication.FormsCookieName); + } - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetAnonymousIdentCookie(this HttpWebRequest webRequest, HttpRequestBase httpRequest) - { - CopyCookie(webRequest, httpRequest, "SMARTSTORE.ANONYMOUS"); - } + { + CopyCookie(webRequest, httpRequest, "SMARTSTORE.ANONYMOUS"); + } [SuppressMessage("ReSharper", "PossibleNullReferenceException")] [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -202,124 +220,124 @@ public static void SetVisitorCookie(this HttpWebRequest webRequest, HttpRequestB } private static void CopyCookie(HttpWebRequest webRequest, HttpRequestBase sourceHttpRequest, string cookieName) - { - Guard.NotNull(webRequest, nameof(webRequest)); - Guard.NotNull(sourceHttpRequest, nameof(sourceHttpRequest)); - Guard.NotEmpty(cookieName, nameof(cookieName)); + { + Guard.NotNull(webRequest, nameof(webRequest)); + Guard.NotNull(sourceHttpRequest, nameof(sourceHttpRequest)); + Guard.NotEmpty(cookieName, nameof(cookieName)); - var sourceCookie = sourceHttpRequest.Cookies[cookieName]; - if (sourceCookie == null) - return; + var sourceCookie = sourceHttpRequest.Cookies[cookieName]; + if (sourceCookie == null) + return; - var sendCookie = new Cookie(sourceCookie.Name, sourceCookie.Value, sourceCookie.Path, sourceHttpRequest.Url.Host); + var sendCookie = new Cookie(sourceCookie.Name, sourceCookie.Value, sourceCookie.Path, sourceHttpRequest.Url.Host); - if (webRequest.CookieContainer == null) - { - webRequest.CookieContainer = new CookieContainer(); - } + if (webRequest.CookieContainer == null) + { + webRequest.CookieContainer = new CookieContainer(); + } - webRequest.CookieContainer.Add(sendCookie); - } + webRequest.CookieContainer.Add(sendCookie); + } - public static string BuildScopedKey(this Cache cache, string key) - { - return key.HasValue() ? CacheRegionName + key : null; - } + public static string BuildScopedKey(this Cache cache, string key) + { + return key.HasValue() ? CacheRegionName + key : null; + } - public static T GetOrAdd(this Cache cache, string key, Func acquirer, TimeSpan? duration = null) - { - Guard.NotEmpty(key, nameof(key)); - Guard.NotNull(acquirer, nameof(acquirer)); + public static T GetOrAdd(this Cache cache, string key, Func acquirer, TimeSpan? duration = null) + { + Guard.NotEmpty(key, nameof(key)); + Guard.NotNull(acquirer, nameof(acquirer)); - object obj = cache.Get(key); + object obj = cache.Get(key); - if (obj != null) - { - return (T)obj; - } + if (obj != null) + { + return (T)obj; + } - var value = acquirer(); + var value = acquirer(); - var absoluteExpiration = Cache.NoAbsoluteExpiration; - if (duration.HasValue) - { - absoluteExpiration = DateTime.UtcNow + duration.Value; - } + var absoluteExpiration = Cache.NoAbsoluteExpiration; + if (duration.HasValue) + { + absoluteExpiration = DateTime.UtcNow + duration.Value; + } - cache.Insert(key, value, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration); + cache.Insert(key, value, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration); - return value; - } + return value; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RememberAppRelativePath(this HttpContextBase httpContext) - { - httpContext.Items[RememberPathKey] = httpContext.Request.AppRelativeCurrentExecutionFilePath; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RememberAppRelativePath(this HttpContextBase httpContext) + { + httpContext.Items[RememberPathKey] = httpContext.Request.AppRelativeCurrentExecutionFilePath; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetOriginalAppRelativePath(this HttpContextBase httpContext) - { - return GetItem(httpContext, RememberPathKey, forceCreation: false) ?? httpContext.Request.AppRelativeCurrentExecutionFilePath; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetOriginalAppRelativePath(this HttpContextBase httpContext) + { + return GetItem(httpContext, RememberPathKey, forceCreation: false) ?? httpContext.Request.AppRelativeCurrentExecutionFilePath; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T GetItem(this HttpContext httpContext, string key, Func factory = null, bool forceCreation = true) - { - return GetItem(new HttpContextWrapper(httpContext), key, factory, forceCreation); - } - - public static T GetItem(this HttpContextBase httpContext, string key, Func factory = null, bool forceCreation = true) - { - Guard.NotEmpty(key, nameof(key)); - - var items = httpContext?.Items; - if (items == null) - { - return default(T); - } - - if (items.Contains(key)) - { - return (T)items[key]; - } - else - { - if (forceCreation) - { - var item = items[key] = (factory ?? (() => Activator.CreateInstance())).Invoke(); - return (T)item; - } - else - { - return default(T); - } - } - } - - public static void RemoveByPattern(this Cache cache, string pattern) - { - var keys = cache.AllKeys(pattern); - - foreach (var key in keys.ToArray()) - { - cache.Remove(key); - } - } - - public static string[] AllKeys(this Cache cache, string pattern) - { - pattern = pattern == "*" ? CacheRegionName : pattern; - - var keys = from entry in HttpRuntime.Cache.AsParallel().Cast() - let key = entry.Key.ToString() - where key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase) - select key; - - return keys.ToArray(); - } - - public static ControllerContext GetRootControllerContext(this ControllerContext controllerContext) + { + return GetItem(new HttpContextWrapper(httpContext), key, factory, forceCreation); + } + + public static T GetItem(this HttpContextBase httpContext, string key, Func factory = null, bool forceCreation = true) + { + Guard.NotEmpty(key, nameof(key)); + + var items = httpContext?.Items; + if (items == null) + { + return default(T); + } + + if (items.Contains(key)) + { + return (T)items[key]; + } + else + { + if (forceCreation) + { + var item = items[key] = (factory ?? (() => Activator.CreateInstance())).Invoke(); + return (T)item; + } + else + { + return default(T); + } + } + } + + public static void RemoveByPattern(this Cache cache, string pattern) + { + var keys = cache.AllKeys(pattern); + + foreach (var key in keys.ToArray()) + { + cache.Remove(key); + } + } + + public static string[] AllKeys(this Cache cache, string pattern) + { + pattern = pattern == "*" ? CacheRegionName : pattern; + + var keys = from entry in HttpRuntime.Cache.AsParallel().Cast() + let key = entry.Key.ToString() + where key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase) + select key; + + return keys.ToArray(); + } + + public static ControllerContext GetRootControllerContext(this ControllerContext controllerContext) { Guard.NotNull(controllerContext, nameof(controllerContext)); @@ -348,5 +366,5 @@ public static bool IsBareBonePage(this ControllerContext controllerContext) return false; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs index 707dd26914..c4a8f10848 100644 --- a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs @@ -1,66 +1,65 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace SmartStore { - public static class IOExtensions - { - public static bool WaitForUnlock(this FileInfo file, int timeoutMs = 1000) - { - Guard.NotNull(file, nameof(file)); + public static class IOExtensions + { + public static bool WaitForUnlock(this FileInfo file, int timeoutMs = 1000) + { + Guard.NotNull(file, nameof(file)); - var wait = TimeSpan.FromMilliseconds(50); - var attempts = Math.Floor(timeoutMs / wait.TotalMilliseconds); + var wait = TimeSpan.FromMilliseconds(50); + var attempts = Math.Floor(timeoutMs / wait.TotalMilliseconds); - try - { - for (var i = 0; i < attempts; i++) - { - if (!IsFileLocked(file)) - { - return true; - } + try + { + for (var i = 0; i < attempts; i++) + { + if (!IsFileLocked(file)) + { + return true; + } - Task.Delay(wait).Wait(); - } + Task.Delay(wait).Wait(); + } - return false; - } - catch - { - return false; - } - } - - public static bool IsFileLocked(this FileInfo file) - { - if (file == null) - return false; + return false; + } + catch + { + return false; + } + } - FileStream stream = null; + public static bool IsFileLocked(this FileInfo file) + { + if (file == null) + return false; - try - { - stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None); - } - 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) - return true; - } - finally - { - if (stream != null) - stream.Close(); - } + FileStream stream = null; - // File is not locked - return false; - } - } + try + { + stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None); + } + 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) + return true; + } + finally + { + if (stream != null) + stream.Close(); + } + + // File is not locked + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs index 8c8df01019..0fb5786eaa 100644 --- a/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; @@ -22,34 +20,34 @@ public static FieldInfo ExtractFieldInfo(this LambdaExpression propertyAccessor) return propertyAccessor.ExtractMemberInfo() as FieldInfo; } - [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] - public static MemberInfo ExtractMemberInfo(this LambdaExpression propertyAccessor) + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public static MemberInfo ExtractMemberInfo(this LambdaExpression propertyAccessor) { - if (propertyAccessor == null) - throw new ArgumentNullException(nameof(propertyAccessor)); + if (propertyAccessor == null) + throw new ArgumentNullException(nameof(propertyAccessor)); - MemberInfo info; + MemberInfo info; try { MemberExpression operand; // o => o.PropertyOrField LambdaExpression expression = propertyAccessor; - // If the property is not an Object, then the member access expression will be wrapped in a conversion expression - // (object)o.PropertyOrField - - if (expression.Body is UnaryExpression body) - { - // o.PropertyOrField - operand = (MemberExpression)body.Operand; - } - else - { - // o.PropertyOrField - operand = (MemberExpression)expression.Body; - } - - // Member - MemberInfo member = operand.Member; + // If the property is not an Object, then the member access expression will be wrapped in a conversion expression + // (object)o.PropertyOrField + + if (expression.Body is UnaryExpression body) + { + // o.PropertyOrField + operand = (MemberExpression)body.Operand; + } + else + { + // o.PropertyOrField + operand = (MemberExpression)expression.Body; + } + + // Member + MemberInfo member = operand.Member; info = member; } catch (Exception e) diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index d5e3eb1685..dc7cac2c34 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -8,18 +8,18 @@ namespace SmartStore { public static class MiscExtensions { - public static void Dump(this Exception exception) - { - try - { - exception.StackTrace.Dump(); - exception.Message.Dump(); - } - catch { } - } - - public static string ToAllMessages(this Exception exception, bool includeStackTrace = false) - { + public static void Dump(this Exception exception) + { + try + { + exception.StackTrace.Dump(); + exception.Message.Dump(); + } + catch { } + } + + public static string ToAllMessages(this Exception exception, bool includeStackTrace = false) + { var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; @@ -46,19 +46,19 @@ public static string ToAllMessages(this Exception exception, bool includeStackTr } return psb.ToStringAndReturn(); - } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ToElapsedMinutes(this Stopwatch watch) + public static string ToElapsedMinutes(this Stopwatch watch) { - return "{0:0.0}".FormatWith(TimeSpan.FromMilliseconds(watch.ElapsedMilliseconds).TotalMinutes); - } + return "{0:0.0}".FormatWith(TimeSpan.FromMilliseconds(watch.ElapsedMilliseconds).TotalMinutes); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ToElapsedSeconds(this Stopwatch watch) + public static string ToElapsedSeconds(this Stopwatch watch) { - return "{0:0.0}".FormatWith(TimeSpan.FromMilliseconds(watch.ElapsedMilliseconds).TotalSeconds); - } + return "{0:0.0}".FormatWith(TimeSpan.FromMilliseconds(watch.ElapsedMilliseconds).TotalSeconds); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNullOrDefault(this T? value) where T : struct @@ -66,43 +66,43 @@ public static bool IsNullOrDefault(this T? value) where T : struct return default(T).Equals(value.GetValueOrDefault()); } - /// - /// Converts bytes into a hex string. - /// - public static string ToHexString(this byte[] bytes, int length = 0) - { + /// + /// Converts bytes into a hex string. + /// + public static string ToHexString(this byte[] bytes, int length = 0) + { if (bytes == null || bytes.Length <= 0) { return ""; } - var sb = new StringBuilder(); + var sb = new StringBuilder(); - foreach (byte b in bytes) - { - sb.Append(b.ToString("x2")); + foreach (byte b in bytes) + { + sb.Append(b.ToString("x2")); if (length > 0 && sb.Length >= length) { break; } - } - - return sb.ToString(); - } - - /// - /// Append grow if string builder is empty. Append delimiter and grow otherwise. - /// - /// Target string builder - /// Value to append - /// Delimiter to use - public static void Grow(this StringBuilder sb, string grow, string delimiter) - { + } + + return sb.ToString(); + } + + /// + /// Append grow if string builder is empty. Append delimiter and grow otherwise. + /// + /// Target string builder + /// Value to append + /// Delimiter to use + public static void Grow(this StringBuilder sb, string grow, string delimiter) + { Guard.NotNull(delimiter, nameof(delimiter)); - if (!string.IsNullOrWhiteSpace(grow)) - { + if (!string.IsNullOrWhiteSpace(grow)) + { if (sb.Length <= 0) { sb.Append(grow); @@ -111,13 +111,13 @@ public static void Grow(this StringBuilder sb, string grow, string delimiter) { sb.AppendFormat("{0}{1}", delimiter, grow); } - } - } + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string SafeGet(this string[] arr, int index) - { - return arr != null && index < arr.Length ? arr[index] : ""; - } + { + return arr != null && index < arr.Length ? arr[index] : ""; + } } } diff --git a/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs index e31eed7d2c..16d04db508 100644 --- a/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs @@ -6,18 +6,17 @@ namespace SmartStore { public static class NumericExtensions - { - #region int + { + #region int - public static int GetRange(this int id, int size, out int lower) - { - // max 1000 values per cache item - var range = (int)Math.Ceiling((decimal)id / size) * size; - - lower = range - (size - 1); + public static (int lower, int upper) GetRange(this int id, int size = 500) + { + // Max [size] values per cache item + var lower = (int)Math.Floor((decimal)id / size) * size; + var upper = lower + (size - 1); - return range; - } + return (lower, upper); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int? ZeroToNull(this int? value) @@ -37,16 +36,16 @@ public static int GetRange(this int id, int size, out int lower) /// Rounding decimal number /// Tax percentage public static decimal ToTaxPercentage(this decimal inclTax, decimal exclTax, int? decimals = null) - { - if (exclTax == decimal.Zero) - { - return decimal.Zero; - } + { + if (exclTax == decimal.Zero) + { + return decimal.Zero; + } - var result = ((inclTax / exclTax) - 1.0M) * 100.0M; + var result = ((inclTax / exclTax) - 1.0M) * 100.0M; - return (decimals.HasValue ? Math.Round(result, decimals.Value) : result); - } + return (decimals.HasValue ? Math.Round(result, decimals.Value) : result); + } /// /// Converts to smallest currency uint, e.g. cents @@ -55,9 +54,9 @@ public static decimal ToTaxPercentage(this decimal inclTax, decimal exclTax, int /// Smallest currency unit [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ToSmallestCurrencyUnit(this decimal value, MidpointRounding midpoint = MidpointRounding.AwayFromZero) - { - return Convert.ToInt32(Math.Round(value * 100, 0, midpoint)); - } + { + return Convert.ToInt32(Math.Round(value * 100, 0, midpoint)); + } /// /// Round decimal to the nearest multiple of denomination @@ -68,14 +67,14 @@ public static int ToSmallestCurrencyUnit(this decimal value, MidpointRounding mi /// Rounded value [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal RoundToNearest(this decimal value, decimal denomination, MidpointRounding midpoint = MidpointRounding.AwayFromZero) - { - if (denomination == decimal.Zero) - { - return value; - } + { + if (denomination == decimal.Zero) + { + return value; + } - return Math.Round(value / denomination, midpoint) * denomination; - } + return Math.Round(value / denomination, midpoint) * denomination; + } /// /// Round decimal up or down to the nearest multiple of denomination @@ -140,10 +139,10 @@ public static decimal RoundToNearest(this decimal value, Currency currency, out /// Rounded value public static decimal RoundIfEnabledFor(this decimal value, Currency currency) { - if (currency == null) - throw new ArgumentNullException(nameof(currency)); + if (currency == null) + throw new ArgumentNullException(nameof(currency)); - if (currency.RoundOrderItemsEnabled) + if (currency.RoundOrderItemsEnabled) { return Math.Round(value, currency.RoundNumDecimals); } @@ -159,10 +158,10 @@ public static decimal RoundIfEnabledFor(this decimal value, Currency currency) /// Rounded and formated value [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FormatInvariant(this decimal value, int decimals = 2) - { - return Math.Round(value, decimals).ToString("0.00", CultureInfo.InvariantCulture); - } + { + return Math.Round(value, decimals).ToString("0.00", CultureInfo.InvariantCulture); + } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/ProcessExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ProcessExtensions.cs index ae4d99a145..a14e6e5213 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ProcessExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ProcessExtensions.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -30,7 +26,7 @@ public static class ProcessExtensions { tcs.TrySetResult(process.ExitCode); }; - + if (process.HasExited) { return process.ExitCode; @@ -39,7 +35,7 @@ public static class ProcessExtensions using (var registration = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken))) { return await tcs.Task.ConfigureAwait(false); - } + } } } } diff --git a/src/Libraries/SmartStore.Core/Extensions/RegexExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/RegexExtensions.cs index 02bc3d5ab5..0e29ac7926 100644 --- a/src/Libraries/SmartStore.Core/Extensions/RegexExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/RegexExtensions.cs @@ -5,41 +5,41 @@ namespace SmartStore { - public static class RegexExtensions - { - public static string ReplaceGroup(this Regex regex, string input, string groupName, string replacement) - { - return ReplaceGroupInternal(regex, input, replacement, m => m.Groups[groupName]); - } - - public static string ReplaceGroup(this Regex regex, string input, int groupNum, string replacement) - { - return ReplaceGroupInternal(regex, input, replacement, m => m.Groups[groupNum]); - } - - private static string ReplaceGroupInternal(this Regex regex, string input, string replacement, Func groupGetter) - { - return regex.Replace(input, match => - { - var group = groupGetter(match); - var sb = new StringBuilder(); - var previousCaptureEnd = 0; - - foreach (var capture in group.Captures.Cast()) - { - var currentCaptureEnd = capture.Index + capture.Length - match.Index; - var currentCaptureLength = capture.Index - match.Index - previousCaptureEnd; - - sb.Append(match.Value.Substring(previousCaptureEnd, currentCaptureLength)); - sb.Append(replacement); - - previousCaptureEnd = currentCaptureEnd; - } - - sb.Append(match.Value.Substring(previousCaptureEnd)); - - return sb.ToString(); - }); - } - } + public static class RegexExtensions + { + public static string ReplaceGroup(this Regex regex, string input, string groupName, string replacement) + { + return ReplaceGroupInternal(regex, input, replacement, m => m.Groups[groupName]); + } + + public static string ReplaceGroup(this Regex regex, string input, int groupNum, string replacement) + { + return ReplaceGroupInternal(regex, input, replacement, m => m.Groups[groupNum]); + } + + private static string ReplaceGroupInternal(this Regex regex, string input, string replacement, Func groupGetter) + { + return regex.Replace(input, match => + { + var group = groupGetter(match); + var sb = new StringBuilder(); + var previousCaptureEnd = 0; + + foreach (var capture in group.Captures.Cast()) + { + var currentCaptureEnd = capture.Index + capture.Length - match.Index; + var currentCaptureLength = capture.Index - match.Index - previousCaptureEnd; + + sb.Append(match.Value.Substring(previousCaptureEnd, currentCaptureLength)); + sb.Append(replacement); + + previousCaptureEnd = currentCaptureEnd; + } + + sb.Append(match.Value.Substring(previousCaptureEnd)); + + return sb.ToString(); + }); + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs index 845d7a9a79..94ad48d31e 100644 --- a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs @@ -1,58 +1,57 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; using System.Web.Routing; // use base SmartStore Namespace to ensure the extension methods are always available namespace SmartStore { - public static class RouteExtensions - { - public static string GetAreaName(this RouteData routeData) - { - if (routeData.DataTokens.TryGetValue("area", out object area)) - { - return (area as string); - } - - return routeData.Route.GetAreaName(); - } - - public static string GetAreaName(this RouteBase route) - { - var area = route as IRouteWithArea; - if (area != null) - { - return area.Area; - } - - var route2 = route as Route; - if ((route2 != null) && (route2.DataTokens != null)) - { - return (route2.DataTokens["area"] as string); - } - - return null; - } + public static class RouteExtensions + { + public static string GetAreaName(this RouteData routeData) + { + if (routeData.DataTokens.TryGetValue("area", out object area)) + { + return (area as string); + } + + return routeData.Route.GetAreaName(); + } + + public static string GetAreaName(this RouteBase route) + { + var area = route as IRouteWithArea; + if (area != null) + { + return area.Area; + } + + var route2 = route as Route; + if ((route2 != null) && (route2.DataTokens != null)) + { + return (route2.DataTokens["area"] as string); + } + + return null; + } /// /// Generates an identifier for the given route in the form "[{area}.]{controller}.{action}" /// public static string GenerateRouteIdentifier(this RouteData routeData) - { - string area = routeData.GetAreaName(); - string controller = routeData.GetRequiredString("controller"); - string action = routeData.GetRequiredString("action"); + { + string area = routeData.GetAreaName(); + string controller = routeData.GetRequiredString("controller"); + string action = routeData.GetRequiredString("action"); - return "{0}{1}.{2}".FormatInvariant(area.HasValue() ? area + "." : "", controller, action); - } + return "{0}{1}.{2}".FormatInvariant(area.HasValue() ? area + "." : "", controller, action); + } - public static bool IsRouteEqual(this RouteData routeData, string controller, string action) - { - if (routeData == null) - return false; + public static bool IsRouteEqual(this RouteData routeData, string controller, string action) + { + if (routeData == null) + return false; - return routeData.GetRequiredString("controller").IsCaseInsensitiveEqual(controller) && routeData.GetRequiredString("action").IsCaseInsensitiveEqual(action); - } + return routeData.GetRequiredString("controller").IsCaseInsensitiveEqual(controller) && routeData.GetRequiredString("action").IsCaseInsensitiveEqual(action); + } - } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs index ba62a83fe2..2e3a6e94ba 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs @@ -1,25 +1,25 @@ using System; -using System.Text; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using SmartStore.Core.IO; -using System.Diagnostics; namespace SmartStore { - public static class StreamExtensions - { + public static class StreamExtensions + { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static StreamReader ToStreamReader(this Stream stream, bool leaveOpen) - { - return new StreamReader(stream, Encoding.UTF8, true, 0x400, leaveOpen); - } + { + return new StreamReader(stream, Encoding.UTF8, true, 0x400, leaveOpen); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static StreamReader ToStreamReader(this Stream stream, Encoding encoding, bool detectEncoding, int bufferSize, bool leaveOpen) - { - return new StreamReader(stream, encoding, detectEncoding, bufferSize, leaveOpen); - } + { + return new StreamReader(stream, encoding, detectEncoding, bufferSize, leaveOpen); + } public static Stream MakeSeekable(this Stream stream) { @@ -31,50 +31,50 @@ public static Stream MakeSeekable(this Stream stream) return new SeekableReadOnlyStream(stream, (int)stream.Length); } - public static bool ToFile(this Stream srcStream, string path) + public static bool ToFile(this Stream srcStream, string path) { - if (srcStream == null) - return false; + if (srcStream == null) + return false; - const int BuffSize = 32768; - var result = true; - Stream dstStream = null; - var buffer = new byte[BuffSize]; + const int BuffSize = 32768; + var result = true; + Stream dstStream = null; + var buffer = new byte[BuffSize]; - try + try { using (dstStream = File.Open(path, FileMode.Create)) { - int len; + int len; while ((len = srcStream.Read(buffer, 0, BuffSize)) > 0) { dstStream.Write(buffer, 0, len); } - } + } } - catch + catch { - result = false; - } - finally - { - if (dstStream != null) - { - dstStream.Close(); - dstStream.Dispose(); - } - } - - return (result && File.Exists(path)); - } - - public static bool ContentsEqual(this Stream src, Stream other, bool? forceLengthCompare = null) + result = false; + } + finally + { + if (dstStream != null) + { + dstStream.Close(); + dstStream.Dispose(); + } + } + + return (result && File.Exists(path)); + } + + public static bool ContentsEqual(this Stream src, Stream other, bool? forceLengthCompare = null) { - if (src == null) - throw new ArgumentNullException(nameof(src)); + if (src == null) + throw new ArgumentNullException(nameof(src)); - if (other == null) - throw new ArgumentNullException(nameof(other)); + if (other == null) + throw new ArgumentNullException(nameof(other)); if (src == other) { @@ -98,13 +98,13 @@ public static bool ContentsEqual(this Stream src, Stream other, bool? forceLengt const int bufferSize = 1024 * intSize; // 2048; var buffer1 = new byte[bufferSize]; var buffer2 = new byte[bufferSize]; - + while (true) { int len1 = src.Read(buffer1, 0, bufferSize); int len2 = other.Read(buffer2, 0, bufferSize); - if (len1 != len2) + if (len1 != len2) return false; if (len1 == 0) @@ -120,7 +120,7 @@ public static bool ContentsEqual(this Stream src, Stream other, bool? forceLengt } } - return true; + return true; } } } diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 68a9f39b64..efea7d0778 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -1,29 +1,29 @@ using System; using System.Collections.Generic; -using System.Text; -using System.Globalization; -using System.Security.Cryptography; -using System.Web; -using System.Text.RegularExpressions; -using System.IO; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; using System.Linq; -using SmartStore.Core.Html; using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; +using SmartStore.Core.Html; using SmartStore.Utilities.ObjectPools; namespace SmartStore { public static class StringExtensions { - public const string CarriageReturnLineFeed = "\r\n"; + public const string CarriageReturnLineFeed = "\r\n"; public const string Empty = ""; public const char CarriageReturn = '\r'; public const char LineFeed = '\n'; public const char Tab = '\t'; - private delegate void ActionLine(TextWriter textWriter, string line); + private delegate void ActionLine(TextWriter textWriter, string line); #region Char extensions @@ -73,22 +73,22 @@ internal static void WriteCharAsUnicode(char c, TextWriter writer) writer.Write(h4); } - public static char TryRemoveDiacritic(this char c) - { - var normalized = c.ToString().Normalize(NormalizationForm.FormD); - if (normalized.Length > 1) - { - return normalized[0]; - } + public static char TryRemoveDiacritic(this char c) + { + var normalized = c.ToString().Normalize(NormalizationForm.FormD); + if (normalized.Length > 1) + { + return normalized[0]; + } - return c; - } + return c; + } - #endregion + #endregion - #region String extensions + #region String extensions - [DebuggerStepThrough] + [DebuggerStepThrough] public static T ToEnum(this string value, T defaultValue) { if (!value.HasValue()) @@ -130,39 +130,39 @@ public static string NullEmpty(this string value) return (string.IsNullOrEmpty(value)) ? null : value; } - /// - /// Formats a string to an invariant culture - /// - /// The format string. - /// The objects. - /// - [DebuggerStepThrough] + /// + /// Formats a string to an invariant culture + /// + /// The format string. + /// The objects. + /// + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FormatInvariant(this string format, params object[] objects) { return string.Format(CultureInfo.InvariantCulture, format, objects); } - /// - /// Formats a string to the current culture. - /// - /// The format string. - /// The objects. - /// - [DebuggerStepThrough] + /// + /// Formats a string to the current culture. + /// + /// The format string. + /// The objects. + /// + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FormatCurrent(this string format, params object[] objects) { return string.Format(CultureInfo.CurrentCulture, format, objects); } - /// - /// Formats a string to the current UI culture. - /// - /// The format string. - /// The objects. - /// - [DebuggerStepThrough] + /// + /// Formats a string to the current UI culture. + /// + /// The format string. + /// The objects. + /// + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FormatCurrentUI(this string format, params object[] objects) { @@ -182,30 +182,30 @@ public static string FormatWith(this string format, IFormatProvider provider, pa return string.Format(provider, format, args); } - /// - /// Determines whether this instance and another specified System.String object have the same value. - /// - /// The string to check equality. - /// The comparing with string. - /// - /// true if the value of the comparing parameter is the same as this string; otherwise, false. - /// - [DebuggerStepThrough] + /// + /// Determines whether this instance and another specified System.String object have the same value. + /// + /// The string to check equality. + /// The comparing with string. + /// + /// true if the value of the comparing parameter is the same as this string; otherwise, false. + /// + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsCaseSensitiveEqual(this string value, string comparing) { return string.CompareOrdinal(value, comparing) == 0; } - /// - /// Determines whether this instance and another specified System.String object have the same value. - /// - /// The string to check equality. - /// The comparing with string. - /// - /// true if the value of the comparing parameter is the same as this string; otherwise, false. - /// - [DebuggerStepThrough] + /// + /// Determines whether this instance and another specified System.String object have the same value. + /// + /// The string to check equality. + /// The comparing with string. + /// + /// true if the value of the comparing parameter is the same as this string; otherwise, false. + /// + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsCaseInsensitiveEqual(this string value, string comparing) { @@ -219,23 +219,23 @@ public static bool IsCaseInsensitiveEqual(this string value, string comparing) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsEmpty(this string value) { - return string.IsNullOrWhiteSpace(value); + return string.IsNullOrWhiteSpace(value); } - /// - /// Determines whether the string is all white space. Empty string will return false. - /// - /// The string to test whether it is all white space. - /// - /// true if the string is all white space; otherwise, false. - /// - [DebuggerStepThrough] + /// + /// Determines whether the string is all white space. Empty string will return false. + /// + /// The string to test whether it is all white space. + /// + /// true if the string is all white space; otherwise, false. + /// + [DebuggerStepThrough] public static bool IsWhiteSpace(this string value) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + if (value == null) + throw new ArgumentNullException(nameof(value)); - if (value.Length == 0) + if (value.Length == 0) return false; for (int i = 0; i < value.Length; i++) @@ -254,73 +254,73 @@ public static bool HasValue(this string value) return !string.IsNullOrWhiteSpace(value); } - /// to get equivalent result to PHPs md5 function call Hash("my value", Encoding.ASCII, false). + /// to get equivalent result to PHPs md5 function call Hash("my value", Encoding.ASCII, false). [DebuggerStepThrough] - public static string Hash(this string value, Encoding encoding, bool toBase64 = false) + public static string Hash(this string value, Encoding encoding, bool toBase64 = false) { - if (value.IsEmpty()) - return value; + if (value.IsEmpty()) + return value; using (var md5 = MD5.Create()) { - byte[] data = encoding.GetBytes(value); + byte[] data = encoding.GetBytes(value); - if (toBase64) + if (toBase64) { - byte[] hash = md5.ComputeHash(data); - return Convert.ToBase64String(hash); - } - else + byte[] hash = md5.ComputeHash(data); + return Convert.ToBase64String(hash); + } + else { - return md5.ComputeHash(data).ToHexString().ToLower(); - } + return md5.ComputeHash(data).ToHexString().ToLower(); + } } } - /// - /// Mask by replacing characters with asterisks. - /// - /// The string - /// Number of characters to leave untouched. - /// The mask string - [DebuggerStepThrough] - public static string Mask(this string value, int length) - { - if (value.HasValue()) - return value.Substring(0, length) + new String('*', value.Length - length); + /// + /// Mask by replacing characters with asterisks. + /// + /// The string + /// Number of characters to leave untouched. + /// The mask string + [DebuggerStepThrough] + public static string Mask(this string value, int length) + { + if (value.HasValue()) + return value.Substring(0, length) + new String('*', value.Length - length); - return value; - } + return value; + } private static bool IsWebUrlInternal(string value, bool schemeIsOptional) { - if (String.IsNullOrEmpty(value)) - return false; + if (String.IsNullOrEmpty(value)) + return false; - value = value.Trim().ToLowerInvariant(); + value = value.Trim().ToLowerInvariant(); - if (schemeIsOptional && value.StartsWith("//")) - { - value = "http:" + value; - } + if (schemeIsOptional && value.StartsWith("//")) + { + value = "http:" + value; + } - return Uri.IsWellFormedUriString(value, UriKind.Absolute) && - (value.StartsWith("http://") || value.StartsWith("https://") || value.StartsWith("ftp://")); - } + return Uri.IsWellFormedUriString(value, UriKind.Absolute) && + (value.StartsWith("http://") || value.StartsWith("https://") || value.StartsWith("ftp://")); + } - [DebuggerStepThrough] - public static bool IsWebUrl(this string value) - { - return IsWebUrlInternal(value, false); - } + [DebuggerStepThrough] + public static bool IsWebUrl(this string value) + { + return IsWebUrlInternal(value, false); + } - [DebuggerStepThrough] - public static bool IsWebUrl(this string value, bool schemeIsOptional) - { - return IsWebUrlInternal(value, schemeIsOptional); - } + [DebuggerStepThrough] + public static bool IsWebUrl(this string value, bool schemeIsOptional) + { + return IsWebUrlInternal(value, schemeIsOptional); + } - [DebuggerStepThrough] + [DebuggerStepThrough] public static bool IsEmail(this string value) { return !String.IsNullOrEmpty(value) && RegularExpressions.IsEmail.IsMatch(value.Trim()); @@ -338,19 +338,19 @@ public static bool IsNumeric(this string value) RegularExpressions.IsNumeric.IsMatch(value); } - /// - /// Ensures that a string only contains numeric values - /// - /// Input string - /// Input string with only numeric values, empty string if input is null or empty - [DebuggerStepThrough] - public static string EnsureNumericOnly(this string str) - { - if (String.IsNullOrEmpty(str)) - return string.Empty; + /// + /// Ensures that a string only contains numeric values + /// + /// Input string + /// Input string with only numeric values, empty string if input is null or empty + [DebuggerStepThrough] + public static string EnsureNumericOnly(this string str) + { + if (String.IsNullOrEmpty(str)) + return string.Empty; - return new String(str.Where(c => Char.IsDigit(c)).ToArray()); - } + return new String(str.Where(c => Char.IsDigit(c)).ToArray()); + } [DebuggerStepThrough] public static bool IsAlpha(this string value) @@ -367,12 +367,12 @@ public static bool IsAlphaNumeric(this string value) [DebuggerStepThrough] public static string Truncate(this string value, int maxLength, string suffix = "") { - if (suffix == null) - throw new ArgumentNullException(nameof(suffix)); + if (suffix == null) + throw new ArgumentNullException(nameof(suffix)); Guard.IsPositive(maxLength, nameof(maxLength)); - int subStringLength = maxLength - suffix.Length; + int subStringLength = maxLength - suffix.Length; if (subStringLength <= 0) throw Error.Argument(nameof(maxLength), "Length of suffix string is greater or equal to maximumLength"); @@ -392,142 +392,142 @@ public static string Truncate(this string value, int maxLength, string suffix = } } - /// - /// Removes all redundant whitespace (empty lines, double space etc.). - /// Use ~! literal to keep whitespace wherever necessary. - /// - /// Input - /// The compacted string - public static string Compact(this string input, bool removeEmptyLines = false) - { - Guard.NotNull(input, nameof(input)); + /// + /// Removes all redundant whitespace (empty lines, double space etc.). + /// Use ~! literal to keep whitespace wherever necessary. + /// + /// Input + /// The compacted string + public static string Compact(this string input, bool removeEmptyLines = false) + { + Guard.NotNull(input, nameof(input)); var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; var lines = GetLines(input.Trim(), true, removeEmptyLines).ToArray(); - foreach (var line in lines) - { - var len = line.Length; + foreach (var line in lines) + { + var len = line.Length; var psbLine = PooledStringBuilder.Rent(); var sbLine = (StringBuilder)psbLine; - var isChar = false; - var isLiteral = false; // When we detect the ~! literal - int i = 0; - var eof = false; - - for (i = 0; i < len; i++) - { - var c = line[i]; - - eof = i == len - 1; - - if (Char.IsWhiteSpace(c)) - { - // Space, Tab etc. - if (isChar) - { - // If last char not empty, append the space. - sbLine.Append(' '); - } - - isLiteral = false; - isChar = false; - } - else - { - // Char or Literal (~!) - - isLiteral = c == '~' && !eof && line[i + 1] == '!'; - isChar = true; - - if (isLiteral) - { - sbLine.Append(' '); - i++; // skip next "!" char - } - else - { - sbLine.Append(c); - } - } - } - - // Append the compacted and trimmed line - sb.AppendLine(sbLine.ToString().Trim().Trim(',')); + var isChar = false; + var isLiteral = false; // When we detect the ~! literal + int i = 0; + var eof = false; + + for (i = 0; i < len; i++) + { + var c = line[i]; + + eof = i == len - 1; + + if (Char.IsWhiteSpace(c)) + { + // Space, Tab etc. + if (isChar) + { + // If last char not empty, append the space. + sbLine.Append(' '); + } + + isLiteral = false; + isChar = false; + } + else + { + // Char or Literal (~!) + + isLiteral = c == '~' && !eof && line[i + 1] == '!'; + isChar = true; + + if (isLiteral) + { + sbLine.Append(' '); + i++; // skip next "!" char + } + else + { + sbLine.Append(c); + } + } + } + + // Append the compacted and trimmed line + sb.AppendLine(sbLine.ToString().Trim().Trim(',')); psbLine.Return(); } - return psb.ToStringAndReturn().Trim(); - } - - /// - /// Splits the input string by carriage return. - /// - /// The string to split - /// A sequence with string items per line - public static IEnumerable GetLines(this string input, bool trimLines = false, bool removeEmptyLines = false) - { - if (input.IsEmpty()) - { - yield break; - } - - using (var sr = new StringReader(input)) - { - string line; - while ((line = sr.ReadLine()) != null) - { - if (trimLines) - { - line = line.Trim(); - } - - if (removeEmptyLines && IsEmpty(line)) - { - continue; - } - - yield return line; - } - } - } - - /// - /// Ensure that a string starts with a string. - /// - /// The target string - /// The string the target string should start with - /// The resulting string - [DebuggerStepThrough] + return psb.ToStringAndReturn().Trim(); + } + + /// + /// Splits the input string by carriage return. + /// + /// The string to split + /// A sequence with string items per line + public static IEnumerable GetLines(this string input, bool trimLines = false, bool removeEmptyLines = false) + { + if (input.IsEmpty()) + { + yield break; + } + + using (var sr = new StringReader(input)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + if (trimLines) + { + line = line.Trim(); + } + + if (removeEmptyLines && IsEmpty(line)) + { + continue; + } + + yield return line; + } + } + } + + /// + /// Ensure that a string starts with a string. + /// + /// The target string + /// The string the target string should start with + /// The resulting string + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string EnsureStartsWith(this string value, string startsWith) - { + { if (value == null) - throw new ArgumentNullException(nameof(value)); + throw new ArgumentNullException(nameof(value)); - if (startsWith == null) - throw new ArgumentNullException(nameof(startsWith)); + if (startsWith == null) + throw new ArgumentNullException(nameof(startsWith)); - return value.StartsWith(startsWith) ? value : (startsWith + value); - } + return value.StartsWith(startsWith) ? value : (startsWith + value); + } - /// - /// Ensures the target string ends with the specified string. - /// - /// The target. - /// The value. - /// The target string with the value string at the end. - [DebuggerStepThrough] + /// + /// Ensures the target string ends with the specified string. + /// + /// The target. + /// The value. + /// The target string with the value string at the end. + [DebuggerStepThrough] public static string EnsureEndsWith(this string value, string endWith) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + if (value == null) + throw new ArgumentNullException(nameof(value)); - if (endWith == null) - throw new ArgumentNullException(nameof(endWith)); + if (endWith == null) + throw new ArgumentNullException(nameof(endWith)); - if (value.Length >= endWith.Length) + if (value.Length >= endWith.Length) { if (string.Compare(value, value.Length - endWith.Length, endWith, 0, endWith.Length, StringComparison.OrdinalIgnoreCase) == 0) return value; @@ -576,25 +576,25 @@ public static string HtmlDecode(this string value) return HttpUtility.HtmlDecode(value); } - [Obsolete("The 'removeTags' parameter is not supported anymore. Use the parameterless method instead.")] - public static string RemoveHtml(this string source, ICollection removeTags) - { - return RemoveHtml(source); - } + [Obsolete("The 'removeTags' parameter is not supported anymore. Use the parameterless method instead.")] + public static string RemoveHtml(this string source, ICollection removeTags) + { + return RemoveHtml(source); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string RemoveHtml(this string source) - { - return HtmlUtils.StripTags(source).Trim().HtmlDecode(); - } - - /// - /// Replaces pascal casing with spaces. For example "CustomerId" would become "Customer Id". - /// Strings that already contain spaces are ignored. - /// - /// String to split - /// The string after being split - [DebuggerStepThrough] + { + return HtmlUtils.StripTags(source).Trim().HtmlDecode(); + } + + /// + /// Replaces pascal casing with spaces. For example "CustomerId" would become "Customer Id". + /// Strings that already contain spaces are ignored. + /// + /// String to split + /// The string after being split + [DebuggerStepThrough] public static string SplitPascalCase(this string value) { var psb = PooledStringBuilder.Rent(); @@ -621,71 +621,71 @@ public static string SplitPascalCase(this string value) return psb.ToStringAndReturn(); } - /// - /// Splits a string into a string array - /// - /// String value to split - /// If null then value is searched for a common delimiter like pipe, semicolon or comma - /// String array - [DebuggerStepThrough] - public static string[] SplitSafe(this string value, string separator) - { - if (string.IsNullOrEmpty(value)) - return new string[0]; - - // Do not use separator.IsEmpty() here because whitespace like " " is a valid separator. - // an empty separator "" returns array with value. - if (separator == null) - { - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - if (c == ';' || c == ',' || c == '|') - { - return value.Split(new char[] { c }, StringSplitOptions.RemoveEmptyEntries); - } - if (c == '\r' && (i + 1) < value.Length & value[i + 1] == '\n') - { - return value.GetLines(false, true).ToArray(); - } - } - - return new string[] { value }; - } - else - { - return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); - } - } - - /// Splits a string into two strings - /// true: success, false: failure - [DebuggerStepThrough] + /// + /// Splits a string into a string array + /// + /// String value to split + /// If null then value is searched for a common delimiter like pipe, semicolon or comma + /// String array + [DebuggerStepThrough] + public static string[] SplitSafe(this string value, string separator) + { + if (string.IsNullOrEmpty(value)) + return new string[0]; + + // Do not use separator.IsEmpty() here because whitespace like " " is a valid separator. + // an empty separator "" returns array with value. + if (separator == null) + { + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + if (c == ';' || c == ',' || c == '|') + { + return value.Split(new char[] { c }, StringSplitOptions.RemoveEmptyEntries); + } + if (c == '\r' && (i + 1) < value.Length & value[i + 1] == '\n') + { + return value.GetLines(false, true).ToArray(); + } + } + + return new string[] { value }; + } + else + { + return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); + } + } + + /// Splits a string into two strings + /// true: success, false: failure + [DebuggerStepThrough] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - public static bool SplitToPair(this string value, out string leftPart, out string rightPart, string delimiter, bool splitAfterLast = false) - { - leftPart = value; - rightPart = ""; + public static bool SplitToPair(this string value, out string leftPart, out string rightPart, string delimiter, bool splitAfterLast = false) + { + leftPart = value; + rightPart = ""; - if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter)) - { - return false; - } + if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter)) + { + return false; + } - var idx = splitAfterLast - ? value.LastIndexOf(delimiter) - : value.IndexOf(delimiter); + var idx = splitAfterLast + ? value.LastIndexOf(delimiter) + : value.IndexOf(delimiter); - if (idx == -1) - { - return false; - } + if (idx == -1) + { + return false; + } - leftPart = value.Substring(0, idx); - rightPart = value.Substring(idx + delimiter.Length); + leftPart = value.Substring(0, idx); + rightPart = value.Substring(idx + delimiter.Length); - return true; - } + return true; + } [DebuggerStepThrough] public static string EncodeJsString(this string value) @@ -785,58 +785,58 @@ public static string RemoveEncloser(this string value, string start, string end, return value; } - /// Debug.WriteLine + /// Debug.WriteLine [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Dump(this string value, bool appendMarks = false) + public static void Dump(this string value, bool appendMarks = false) { - Debug.WriteLine(value); - Debug.WriteLineIf(appendMarks, "------------------------------------------------"); - } + Debug.WriteLine(value); + Debug.WriteLineIf(appendMarks, "------------------------------------------------"); + } - /// Smart way to create a HTML attribute with a leading space. - /// Name of the attribute. - /// - /// - [SuppressMessage("ReSharper", "StringCompareIsCultureSpecific.3")] - public static string ToAttribute(this string value, string name, bool htmlEncode = true) + /// Smart way to create a HTML attribute with a leading space. + /// Name of the attribute. + /// + /// + [SuppressMessage("ReSharper", "StringCompareIsCultureSpecific.3")] + public static string ToAttribute(this string value, string name, bool htmlEncode = true) { - if (name.IsEmpty()) - return ""; + if (name.IsEmpty()) + return ""; - if (value == "" && name != "value" && !name.StartsWith("data")) - return ""; + if (value == "" && name != "value" && !name.StartsWith("data")) + return ""; - if (name == "maxlength" && (value == "" || value == "0")) - return ""; + if (name == "maxlength" && (value == "" || value == "0")) + return ""; - if (name == "checked" || name == "disabled" || name == "multiple") + if (name == "checked" || name == "disabled" || name == "multiple") { - if (value == "" || string.Compare(value, "false", true) == 0) - return ""; - value = (string.Compare(value, "true", true) == 0 ? name : value); - } - - if (name.StartsWith("data")) - name = name.Insert(4, "-"); - - return string.Format(" {0}=\"{1}\"", name, htmlEncode ? HttpUtility.HtmlEncode(value) : value); - } - - /// - /// Appends grow and uses delimiter if the string is not empty. - /// + if (value == "" || string.Compare(value, "false", true) == 0) + return ""; + value = (string.Compare(value, "true", true) == 0 ? name : value); + } + + if (name.StartsWith("data")) + name = name.Insert(4, "-"); + + return string.Format(" {0}=\"{1}\"", name, htmlEncode ? HttpUtility.HtmlEncode(value) : value); + } + + /// + /// Appends grow and uses delimiter if the string is not empty. + /// [DebuggerStepThrough] - public static string Grow(this string value, string grow, string delimiter) + public static string Grow(this string value, string grow, string delimiter) { - if (string.IsNullOrEmpty(value)) - return (string.IsNullOrEmpty(grow) ? "" : grow); + if (string.IsNullOrEmpty(value)) + return (string.IsNullOrEmpty(grow) ? "" : grow); - if (string.IsNullOrEmpty(grow)) - return (string.IsNullOrEmpty(value) ? "" : value); + if (string.IsNullOrEmpty(grow)) + return (string.IsNullOrEmpty(value) ? "" : value); - return value + delimiter + grow; - } + return value + delimiter + grow; + } /// /// Left-pads a string. Always returns empty string if source is null or empty. @@ -868,187 +868,173 @@ public static string LeftPad(this string value, string format = null, char pad = /// [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string NaIfEmpty(this string value) + public static string NaIfEmpty(this string value) + { + return (string.IsNullOrWhiteSpace(value) ? "n/a" : value); + } + + /// + /// Replaces substring with position x1 to x2 by replaceBy. + /// + [DebuggerStepThrough] + public static string Replace(this string value, int x1, int x2, string replaceBy = null) + { + if (!string.IsNullOrWhiteSpace(value) && x1 > 0 && x2 > x1 && x2 < value.Length) + { + return value.Substring(0, x1) + (replaceBy.EmptyNull()) + value.Substring(x2 + 1); + } + + return value; + } + + [DebuggerStepThrough] + public static string Replace(this string value, string oldValue, string newValue, StringComparison comparisonType) { - return (string.IsNullOrWhiteSpace(value) ? "n/a" : value); - } + try + { + int startIndex = 0; + while (true) + { + startIndex = value.IndexOf(oldValue, startIndex, comparisonType); + if (startIndex == -1) + break; + + value = value.Substring(0, startIndex) + newValue + value.Substring(startIndex + oldValue.Length); + + startIndex += newValue.Length; + } + } + catch (Exception exc) + { + exc.Dump(); + } + + return value; + } - /// - /// Replaces substring with position x1 to x2 by replaceBy. - /// + /// + /// Replaces digits in a string with culture native digits (if digit substitution for culture is required) + /// [DebuggerStepThrough] - public static string Replace(this string value, int x1, int x2, string replaceBy = null) + public static string ReplaceNativeDigits(this string value, IFormatProvider provider = null) { - if (!string.IsNullOrWhiteSpace(value) && x1 > 0 && x2 > x1 && x2 < value.Length) + if (value == null) + throw new ArgumentNullException(nameof(value)); + + provider = provider ?? NumberFormatInfo.CurrentInfo; + var nfi = NumberFormatInfo.GetInstance(provider); + + if (nfi.DigitSubstitution == DigitShapes.None) { - return value.Substring(0, x1) + (replaceBy.EmptyNull()) + value.Substring(x2 + 1); - } - - return value; - } - - [DebuggerStepThrough] - public static string Replace(this string value, string oldValue, string newValue, StringComparison comparisonType) - { - try - { - int startIndex = 0; - while (true) - { - startIndex = value.IndexOf(oldValue, startIndex, comparisonType); - if (startIndex == -1) - break; - - value = value.Substring(0, startIndex) + newValue + value.Substring(startIndex + oldValue.Length); - - startIndex += newValue.Length; - } - } - catch (Exception exc) - { - exc.Dump(); - } - - 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) - { - if (value == null) - throw new ArgumentNullException(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] + return value; + } + + var nativeDigits = nfi.NativeDigits; + var rg = new Regex(@"\d"); + + var result = rg.Replace(value, m => nativeDigits[m.Value.ToInt()]); + return result; + } + + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string TrimSafe(this string value) + public static string TrimSafe(this string value) { - return (value.HasValue() ? value.Trim() : value); - } + return (value.HasValue() ? value.Trim() : value); + } [DebuggerStepThrough] - public static string Slugify(this string value, bool allowSpace = false, char[] allowChars = null) + public static string Slugify(this string value, bool allowSpace = false, char[] allowChars = null) { - string res = string.Empty; + string res = string.Empty; var psb = PooledStringBuilder.Rent(); - try + try { - if (!string.IsNullOrWhiteSpace(value)) + if (!string.IsNullOrWhiteSpace(value)) { var sb = (StringBuilder)psb; bool space = false; - char ch; + char ch; - for (int i = 0; i < value.Length; ++i) + for (int i = 0; i < value.Length; ++i) { - ch = value[i]; + ch = value[i]; - if (ch == ' ' || ch == '-') + if (ch == ' ' || ch == '-') { - if (allowSpace && ch == ' ') - sb.Append(' '); - else if (!space) - sb.Append('-'); - space = true; - continue; - } - - space = false; - - if ((ch >= 48 && ch <= 57) || (ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122) || ch == '_') - { - sb.Append(ch); - continue; - } - - if (allowChars != null && allowChars.Contains(ch)) + if (allowSpace && ch == ' ') + sb.Append(' '); + else if (!space) + sb.Append('-'); + space = true; + continue; + } + + space = false; + + if ((ch >= 48 && ch <= 57) || (ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122) || ch == '_') { - sb.Append(ch); - continue; - } - - if ((int)ch >= 128) - { - switch (ch) - { - case 'ä': sb.Append("ae"); break; - case 'ö': sb.Append("oe"); break; - case 'ü': sb.Append("ue"); break; - case 'ß': sb.Append("ss"); break; - case 'Ä': sb.Append("AE"); break; - case 'Ö': sb.Append("OE"); break; - case 'Ü': sb.Append("UE"); break; - default: - var c2 = ch.TryRemoveDiacritic(); - if ((c2 >= 'a' && c2 <= 'z') || (c2 >= 'A' && c2 <= 'Z')) - { - sb.Append(c2); - } - break; - } - } - } // for - - if (sb.Length > 0) + sb.Append(ch); + continue; + } + + if (allowChars != null && allowChars.Contains(ch)) + { + sb.Append(ch); + continue; + } + + if ((int)ch >= 128) + { + switch (ch) + { + case 'ä': sb.Append("ae"); break; + case 'ö': sb.Append("oe"); break; + case 'ü': sb.Append("ue"); break; + case 'ß': sb.Append("ss"); break; + case 'Ä': sb.Append("AE"); break; + case 'Ö': sb.Append("OE"); break; + case 'Ü': sb.Append("UE"); break; + default: + var c2 = ch.TryRemoveDiacritic(); + if ((c2 >= 'a' && c2 <= 'z') || (c2 >= 'A' && c2 <= 'Z')) + { + sb.Append(c2); + } + break; + } + } + } // for + + if (sb.Length > 0) { - res = sb.ToString().Trim(new char[] { ' ', '-' }); - - Regex pat = new Regex(@"(-{2,})"); // remove double SpaceChar - res = pat.Replace(res, "-"); - res = res.Replace("__", "_"); - } - } - } - catch (Exception ex) + res = sb.ToString().Trim(new char[] { ' ', '-' }); + + Regex pat = new Regex(@"(-{2,})"); // remove double SpaceChar + res = pat.Replace(res, "-"); + res = res.Replace("__", "_"); + } + } + } + catch (Exception ex) { - ex.Dump(); - } + ex.Dump(); + } finally { psb.Return(); } - return (res.Length > 0 ? res : "null"); - } + return (res.Length > 0 ? res : "null"); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string SanitizeHtmlId(this string value) { - return System.Web.Mvc.TagBuilder.CreateSanitizedId(value); + return System.Web.Mvc.TagBuilder.CreateSanitizedId(value); } - public static string Sha(this string value, Encoding encoding) - { - if (!string.IsNullOrWhiteSpace(value)) - { - using (var sha1 = new SHA1CryptoServiceProvider()) - { - byte[] data = encoding.GetBytes(value); - return sha1.ComputeHash(data).ToHexString(); - } - } - - return string.Empty; - } - [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsMatch(this string input, string pattern, RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.Multiline) @@ -1076,112 +1062,96 @@ public static string RegexReplace(this string input, string pattern, string repl } [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToValidFileName(this string input, string replacement = "-") { - return input.ToValidPathInternal(false, replacement); + return string.Join( + replacement ?? "-", + input.ToSafe().Split(Path.GetInvalidFileNameChars())); } [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToValidPath(this string input, string replacement = "-") { - return input.ToValidPathInternal(true, replacement); + return string.Join( + replacement ?? "-", + input.ToSafe().Split(Path.GetInvalidPathChars())); } - private static string ToValidPathInternal(this string input, bool isPath, string replacement) + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int[] ToIntArray(this string s) { - var invalidChars = new HashSet(isPath ? Path.GetInvalidPathChars() : Path.GetInvalidFileNameChars()); + return Array.ConvertAll(s.SplitSafe(","), v => int.Parse(v.Trim())); + } - replacement = replacement ?? "-"; + [DebuggerStepThrough] + public static bool ToIntArrayContains(this string s, int value, bool defaultValue) + { + if (s == null) + return defaultValue; + var arr = s.ToIntArray(); + if (arr == null || arr.Count() <= 0) + return defaultValue; - var sb = new StringBuilder(); - foreach (var c in input.ToSafe()) - { - if (invalidChars.Contains(c)) - { - sb.Append(replacement); - } - else - { - sb.Append(c); - } - } + return arr.Contains(value); + } - return sb.ToString(); + [DebuggerStepThrough] + public static string RemoveInvalidXmlChars(this string s) + { + if (s.IsEmpty()) + return s; + + return Regex.Replace(s, @"[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]", "", RegexOptions.Compiled); } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int[] ToIntArray(this string s) - { - return Array.ConvertAll(s.SplitSafe(","), v => int.Parse(v.Trim())); - } - - [DebuggerStepThrough] - public static bool ToIntArrayContains(this string s, int value, bool defaultValue) - { - if (s == null) - return defaultValue; - var arr = s.ToIntArray(); - if (arr == null || arr.Count() <= 0) - return defaultValue; - - return arr.Contains(value); - } - - [DebuggerStepThrough] - public static string RemoveInvalidXmlChars(this string s) - { - if (s.IsEmpty()) - return s; - - return Regex.Replace(s, @"[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]", "", RegexOptions.Compiled); - } - - [DebuggerStepThrough] - public static string ReplaceCsvChars(this string s) - { - if (s.IsEmpty()) - { - return ""; - } - - s = s.Replace(';', ','); - s = s.Replace('\r', ' '); - s = s.Replace('\n', ' '); - return s.Replace("'", ""); - } - - [DebuggerStepThrough] - public static string HighlightKeywords(this string input, string keywords, string preMatch = "", string postMatch = "") - { + [DebuggerStepThrough] + public static string ReplaceCsvChars(this string s) + { + if (s.IsEmpty()) + { + return ""; + } + + s = s.Replace(';', ','); + s = s.Replace('\r', ' '); + s = s.Replace('\n', ' '); + return s.Replace("'", ""); + } + + [DebuggerStepThrough] + public static string HighlightKeywords(this string input, string keywords, string preMatch = "", string postMatch = "") + { Guard.NotNull(preMatch, nameof(preMatch)); Guard.NotNull(postMatch, nameof(postMatch)); - if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(keywords)) - { - return input; - } + if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(keywords)) + { + return input; + } - var pattern = String.Join("|", keywords.Trim().Split(' ', '-') - .Select(x => x.Trim()) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => Regex.Escape(x)) - .Distinct()); + var pattern = String.Join("|", keywords.Trim().Split(' ', '-') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => Regex.Escape(x)) + .Distinct()); - if (!string.IsNullOrWhiteSpace(pattern)) - { - var rg = new Regex(pattern, RegexOptions.IgnoreCase); - input = rg.Replace(input, m => preMatch + m.Value.EmptyNull().HtmlEncode() + postMatch); - } + if (!string.IsNullOrWhiteSpace(pattern)) + { + var rg = new Regex(pattern, RegexOptions.IgnoreCase); + input = rg.Replace(input, m => preMatch + m.Value.EmptyNull().HtmlEncode() + postMatch); + } - return input; - } + return input; + } - #endregion + #endregion - #region Helper + #region Helper - private static void EncodeJsChar(TextWriter writer, char c, char delimiter) + private static void EncodeJsChar(TextWriter writer, char c, char delimiter) { switch (c) { diff --git a/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs index 07e188fbff..5308d447ff 100644 --- a/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Collections; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; namespace SmartStore @@ -15,51 +15,51 @@ public static class TypeExtensions public static string AssemblyQualifiedNameWithoutVersion(this Type type) { - if (type == null) - throw new ArgumentNullException(nameof(type)); + if (type == null) + throw new ArgumentNullException(nameof(type)); - if (type.AssemblyQualifiedName != null) - { - var strArray = type.AssemblyQualifiedName.Split(new char[] { ',' }); - return string.Format("{0}, {1}", strArray[0].Trim(), strArray[1].Trim()); - } + if (type.AssemblyQualifiedName != null) + { + var strArray = type.AssemblyQualifiedName.Split(new char[] { ',' }); + return string.Format("{0}, {1}", strArray[0].Trim(), strArray[1].Trim()); + } - return null; + return null; } - public static bool IsNumericType(this Type type) - { - switch (Type.GetTypeCode(type)) - { - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Single: - return true; - case TypeCode.Object: - if (type.IsNullable(out var innerType)) - { - return innerType.IsNumericType(); - } - return false; - default: - return false; - } - } - - public static bool IsSequenceType(this Type type) + public static bool IsNumericType(this Type type) { - if (type == typeof(string)) - return false; + switch (Type.GetTypeCode(type)) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + case TypeCode.Object: + if (type.IsNullable(out var innerType)) + { + return innerType.IsNumericType(); + } + return false; + default: + return false; + } + } + + public static bool IsSequenceType(this Type type) + { + if (type == typeof(string)) + return false; - return type.IsArray || typeof(IEnumerable).IsAssignableFrom(type); + return type.IsArray || typeof(IEnumerable).IsAssignableFrom(type); } public static bool IsSequenceType(this Type type, out Type elementType) @@ -130,17 +130,17 @@ public static bool IsPredefinedType(this Type type) { return (string.Compare(type.FullName, "System.Xml.Linq.XElement", StringComparison.Ordinal) == 0); } - + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPlainObjectType(this Type type) - { - return type.IsClass && !type.IsSequenceType() && !type.IsPredefinedType(); - } + { + return type.IsClass && !type.IsSequenceType() && !type.IsPredefinedType(); + } - public static bool IsInteger(this Type type) + public static bool IsInteger(this Type type) { switch (Type.GetTypeCode(type)) { @@ -174,11 +174,11 @@ public static Type GetNonNullableType(this Type type) public static bool IsNullable(this Type type, out Type elementType) { if (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - elementType = type.GetGenericArguments()[0]; + elementType = type.GetGenericArguments()[0]; else elementType = type; - return elementType != type; + return elementType != type; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -189,10 +189,10 @@ public static bool IsEnumType(this Type type) public static bool IsConstructable(this Type type) { - if (type == null) - throw new ArgumentNullException(nameof(type)); + if (type == null) + throw new ArgumentNullException(nameof(type)); - if (type.IsAbstract || type.IsInterface || type.IsArray || type.IsGenericTypeDefinition || type == typeof(void)) + if (type.IsAbstract || type.IsInterface || type.IsArray || type.IsGenericTypeDefinition || type == typeof(void)) return false; if (!HasDefaultConstructor(type)) @@ -224,10 +224,10 @@ public static bool IsAnonymous(this Type type) [DebuggerStepThrough] public static bool HasDefaultConstructor(this Type type) { - if (type == null) - throw new ArgumentNullException(nameof(type)); + if (type == null) + throw new ArgumentNullException(nameof(type)); - if (type.IsValueType) + if (type.IsValueType) return true; return type.GetConstructors(BindingFlags.Instance | BindingFlags.Public) @@ -237,18 +237,19 @@ public static bool HasDefaultConstructor(this Type type) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsSubClass(this Type type, Type check) { - return IsSubClass(type, check, out Type _); - } + return IsSubClass(type, check, out _); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsSubClass(this Type type, Type check, out Type implementingType) { - if (type == null) - throw new ArgumentNullException(nameof(type)); + if (type == null) + throw new ArgumentNullException(nameof(type)); - if (check == null) - throw new ArgumentNullException(nameof(check)); + if (check == null) + throw new ArgumentNullException(nameof(check)); - return IsSubClassInternal(type, type, check, out implementingType); + return IsSubClassInternal(type, type, check, out implementingType); } private static bool IsSubClassInternal(Type initialType, Type currentType, Type check, out Type implementingType) @@ -279,6 +280,7 @@ private static bool IsSubClassInternal(Type initialType, Type currentType, Type { if (IsSubClassInternal(initialType, currentType.GetGenericTypeDefinition(), check, out implementingType)) { + // INFO: this seems to be wrong! Don't we need the implementing base type (?!?) implementingType = currentType; return true; } @@ -468,15 +470,15 @@ public static bool HasAttribute(this ICustomAttributeProvider target return target.IsDefined(typeof(TAttribute), inherits); } - /// - /// Given a particular MemberInfo, return the custom attributes of the - /// given type on that member. - /// - /// Type of attribute to retrieve. - /// The member to look at. - /// True to include attributes inherited from base classes. - /// Array of found attributes. - public static TAttribute[] GetAttributes(this ICustomAttributeProvider target, bool inherits) where TAttribute : Attribute + /// + /// Given a particular MemberInfo, return the custom attributes of the + /// given type on that member. + /// + /// Type of attribute to retrieve. + /// The member to look at. + /// True to include attributes inherited from base classes. + /// Array of found attributes. + public static TAttribute[] GetAttributes(this ICustomAttributeProvider target, bool inherits) where TAttribute : Attribute { if (target.IsDefined(typeof(TAttribute), inherits)) { @@ -523,8 +525,8 @@ public static TAttribute[] GetAllAttributes(this MemberInfo member, return attributes.ToArray(); } - [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] - internal static IEnumerable SortAttributesIfPossible(IEnumerable attributes) + [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] + internal static IEnumerable SortAttributesIfPossible(IEnumerable attributes) where TAttribute : Attribute { if (typeof(IOrdered).IsAssignableFrom(typeof(TAttribute))) @@ -572,7 +574,7 @@ internal static Type FindIEnumerable(this Type seqType) if (seqType.IsGenericType) { - var args = seqType.GetGenericArguments(); + var args = seqType.GetGenericArguments(); foreach (var arg in args) { var ienum = typeof(IEnumerable<>).MakeGenericType(arg); diff --git a/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs index d44109c7f2..d22ed21181 100644 --- a/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs @@ -1,98 +1,97 @@ using System; -using System.ComponentModel; using System.Globalization; using System.Xml.XPath; using SmartStore.Core.Domain.Localization; namespace SmartStore { - public static class XPathExtensions - { - /// - /// Safe way to get the value of a xpath navigator item - /// - /// Type - /// The node - /// Optional xpath - /// Optional default value - /// For localized values. Xpath scheme: Localized/xpath[@culture = 'language.LanguageCulture'] - /// If null is passed, CultureInfo.InvariantCulture is used. - /// The value - public static T GetValue(this XPathNavigator node, string xpath = null, T defaultValue = default(T), Language language = null, CultureInfo culture = null) - { - try - { - if (node != null) - { - if (xpath.IsEmpty()) - { - if (node.Value.HasValue()) - return node.Value.Convert(culture); + public static class XPathExtensions + { + /// + /// Safe way to get the value of a xpath navigator item + /// + /// Type + /// The node + /// Optional xpath + /// Optional default value + /// For localized values. Xpath scheme: Localized/xpath[@culture = 'language.LanguageCulture'] + /// If null is passed, CultureInfo.InvariantCulture is used. + /// The value + public static T GetValue(this XPathNavigator node, string xpath = null, T defaultValue = default(T), Language language = null, CultureInfo culture = null) + { + try + { + if (node != null) + { + if (xpath.IsEmpty()) + { + if (node.Value.HasValue()) + return node.Value.Convert(culture); - return defaultValue; - } + return defaultValue; + } - if (language != null) - xpath = "Localized/{0}[@culture = '{1}']".FormatWith(xpath, language.LanguageCulture.EmptyNull().ToLower()); + if (language != null) + xpath = "Localized/{0}[@culture = '{1}']".FormatWith(xpath, language.LanguageCulture.EmptyNull().ToLower()); - var n = node.SelectSingleNode(xpath); + var n = node.SelectSingleNode(xpath); - if (n != null && n.Value.HasValue()) - return n.Value.Convert(culture); - } - } - catch (Exception exc) - { - exc.Dump(); - } + if (n != null && n.Value.HasValue()) + return n.Value.Convert(culture); + } + } + catch (Exception exc) + { + exc.Dump(); + } - return defaultValue; - } + return defaultValue; + } - /// - /// Safe way to get a string value of a xpath navigator item - /// - /// The node - /// Optional xpath - /// Optional default value - /// The value - public static string GetString(this XPathNavigator node, string xpath = null, string defaultValue = null) - { - // note: better not merge GetString(...) and GetValue(...) - try - { - if (node != null) - { - if (xpath.IsEmpty()) - return node.Value; + /// + /// Safe way to get a string value of a xpath navigator item + /// + /// The node + /// Optional xpath + /// Optional default value + /// The value + public static string GetString(this XPathNavigator node, string xpath = null, string defaultValue = null) + { + // note: better not merge GetString(...) and GetValue(...) + try + { + if (node != null) + { + if (xpath.IsEmpty()) + return node.Value; - var n = node.SelectSingleNode(xpath); + var n = node.SelectSingleNode(xpath); - if (n != null) - return n.Value; - } - } - catch (Exception exc) - { - exc.Dump(); - } + if (n != null) + return n.Value; + } + } + catch (Exception exc) + { + exc.Dump(); + } - return defaultValue; - } + return defaultValue; + } - /// - /// Safe way to get a localized string value of a xpath navigator item - /// - /// The node - /// For localized values. Xpath scheme: Localized/xpath[@culture = 'language.LanguageCulture'] - /// Optional xpath - /// Optional default value - /// The value - public static string GetString(this XPathNavigator node, Language language, string xpath, string defaultValue = null) - { - return node.GetString( - "Localized/{0}[@culture = '{1}']".FormatWith(xpath.EmptyNull(), language.LanguageCulture.EmptyNull().ToLower()), - defaultValue); - } - } + /// + /// Safe way to get a localized string value of a xpath navigator item + /// + /// The node + /// For localized values. Xpath scheme: Localized/xpath[@culture = 'language.LanguageCulture'] + /// Optional xpath + /// Optional default value + /// The value + public static string GetString(this XPathNavigator node, Language language, string xpath, string defaultValue = null) + { + return node.GetString( + "Localized/{0}[@culture = '{1}']".FormatWith(xpath.EmptyNull(), language.LanguageCulture.EmptyNull().ToLower()), + defaultValue); + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs index 601c4cef6d..8450b5f8f1 100644 --- a/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs @@ -1,68 +1,68 @@ using System; -using System.ComponentModel; using System.Globalization; using System.Xml; namespace SmartStore { - public static class XmlNodeExtensions - { - /// Safe way to get inner text of an attribute. - public static T GetAttributeText(this XmlNode node, string attributeName, T defaultValue = default(T)) { - try + public static class XmlNodeExtensions + { + /// Safe way to get inner text of an attribute. + public static T GetAttributeText(this XmlNode node, string attributeName, T defaultValue = default(T)) + { + try { - if (node != null && attributeName.HasValue()) + if (node != null && attributeName.HasValue()) { - XmlAttribute attr = node.Attributes[attributeName]; - if (attr != null) + XmlAttribute attr = node.Attributes[attributeName]; + if (attr != null) { - return attr.InnerText.Convert(); - } - } - } - catch (Exception exc) + return attr.InnerText.Convert(); + } + } + } + catch (Exception exc) { - exc.Dump(); - } + exc.Dump(); + } - return defaultValue; - } + return defaultValue; + } - /// Safe way to get inner text of an attribute. - public static string GetAttributeText(this XmlNode node, string attributeName) + /// Safe way to get inner text of an attribute. + public static string GetAttributeText(this XmlNode node, string attributeName) { - return node.GetAttributeText(attributeName, null); - } + return node.GetAttributeText(attributeName, null); + } - /// Safe way to get inner text of a node. - public static T GetText(this XmlNode node, string xpath = null, T defaultValue = default(T), CultureInfo culture = null) + /// Safe way to get inner text of a node. + public static T GetText(this XmlNode node, string xpath = null, T defaultValue = default(T), CultureInfo culture = null) { - try + try { - if (node != null) + if (node != null) { - if (xpath.IsEmpty()) - return node.InnerText.Convert(); + if (xpath.IsEmpty()) + return node.InnerText.Convert(); - var n = node.SelectSingleNode(xpath); + var n = node.SelectSingleNode(xpath); - if (n != null && n.InnerText.HasValue()) - return n.InnerText.Convert(culture); + if (n != null && n.InnerText.HasValue()) + return n.InnerText.Convert(culture); } - } - catch (Exception exc) + } + catch (Exception exc) { - exc.Dump(); - } + exc.Dump(); + } - return defaultValue; - } + return defaultValue; + } - /// Safe way to get inner text of a node. - public static string GetText(this XmlNode node, string xpath = null, string defaultValue = default(string)) + /// Safe way to get inner text of a node. + public static string GetText(this XmlNode node, string xpath = null, string defaultValue = default(string)) { - return node.GetText(xpath, defaultValue); - } - } + return node.GetText(xpath, defaultValue); + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs index ca4e1c2c71..da9685432b 100644 --- a/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs @@ -1,53 +1,52 @@ -using System; -using System.Xml; +using System.Xml; using SmartStore.Core.Domain.Localization; namespace SmartStore { - public static class XmlWriterExtensions - { - public static void WriteCData(this XmlWriter writer, string name, string value, string prefix = null, string ns = null) + public static class XmlWriterExtensions + { + public static void WriteCData(this XmlWriter writer, string name, string value, string prefix = null, string ns = null) { - if (name.HasValue() && value != null) + if (name.HasValue() && value != null) { - if (prefix == null && ns == null) - writer.WriteStartElement(name); - else - writer.WriteStartElement(prefix, name, ns); - - writer.WriteCData(value.RemoveInvalidXmlChars()); - - writer.WriteEndElement(); - } - } - - /// - /// Created a simple or CData node element - /// - /// The writer - /// Node name - /// Node value - /// The language. Its culture is always converted to lowercase! - /// Whether to create simple or CData node - public static void Write(this XmlWriter writer, string name, string value, Language language = null, bool asCData = false) - { - if (name.HasValue() && value != null) - { - if (language != null && value.IsEmpty()) - return; // do not create too many empty nodes for empty localized values - - writer.WriteStartElement(name); - - if (language != null) - writer.WriteAttributeString("culture", language.LanguageCulture.EmptyNull().ToLower()); - - if (asCData) - writer.WriteCData(value.RemoveInvalidXmlChars()); - else - writer.WriteString(value.RemoveInvalidXmlChars()); - - writer.WriteEndElement(); - } - } - } + if (prefix == null && ns == null) + writer.WriteStartElement(name); + else + writer.WriteStartElement(prefix, name, ns); + + writer.WriteCData(value.RemoveInvalidXmlChars()); + + writer.WriteEndElement(); + } + } + + /// + /// Created a simple or CData node element + /// + /// The writer + /// Node name + /// Node value + /// The language. Its culture is always converted to lowercase! + /// Whether to create simple or CData node + public static void Write(this XmlWriter writer, string name, string value, Language language = null, bool asCData = false) + { + if (name.HasValue() && value != null) + { + if (language != null && value.IsEmpty()) + return; // do not create too many empty nodes for empty localized values + + writer.WriteStartElement(name); + + if (language != null) + writer.WriteAttributeString("culture", language.LanguageCulture.EmptyNull().ToLower()); + + if (asCData) + writer.WriteCData(value.RemoveInvalidXmlChars()); + else + writer.WriteString(value.RemoveInvalidXmlChars()); + + writer.WriteEndElement(); + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeController.cs b/src/Libraries/SmartStore.Core/Fakes/FakeController.cs index 1ff087bb77..2e2e4ee851 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeController.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeController.cs @@ -2,7 +2,7 @@ namespace SmartStore.Core.Fakes { - public class FakeController : Controller - { - } + public class FakeController : Controller + { + } } diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs index 68302e9a61..6da257d71a 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs @@ -26,35 +26,35 @@ public static FakeHttpContext Root() return new FakeHttpContext("~/"); } - public FakeHttpContext(string relativeUrl) - : this(relativeUrl, null, null, null, null, null, null) - { - } + public FakeHttpContext(string relativeUrl) + : this(relativeUrl, null, null, null, null, null, null) + { + } public FakeHttpContext(string relativeUrl, string method) : this(relativeUrl, method, null, null, null, null, null, null) { } - public FakeHttpContext(string relativeUrl, - IPrincipal principal, - NameValueCollection formParams, - NameValueCollection queryStringParams, - HttpCookieCollection cookies, - SessionStateItemCollection sessionItems, - NameValueCollection serverVariables) + public FakeHttpContext(string relativeUrl, + IPrincipal principal, + NameValueCollection formParams, + NameValueCollection queryStringParams, + HttpCookieCollection cookies, + SessionStateItemCollection sessionItems, + NameValueCollection serverVariables) : this(relativeUrl, null, principal, formParams, queryStringParams, cookies, sessionItems, serverVariables) { } - public FakeHttpContext(string relativeUrl, - string method, - IPrincipal principal, - NameValueCollection formParams, - NameValueCollection queryStringParams, - HttpCookieCollection cookies, - SessionStateItemCollection sessionItems, - NameValueCollection serverVariables) + public FakeHttpContext(string relativeUrl, + string method, + IPrincipal principal, + NameValueCollection formParams, + NameValueCollection queryStringParams, + HttpCookieCollection cookies, + SessionStateItemCollection sessionItems, + NameValueCollection serverVariables) { _relativeUrl = relativeUrl; _method = method; @@ -70,28 +70,16 @@ public FakeHttpContext(string relativeUrl, Handler = new FakeHttpHandler(); } - public override IHttpHandler Handler { get; set; } + public override IHttpHandler Handler { get; set; } - public override HttpRequestBase Request - { - get - { - return _request ?? new FakeHttpRequest(_relativeUrl, _method, _formParams, _queryStringParams, _cookies, _serverVariables); - } - } + public override HttpRequestBase Request => _request ?? new FakeHttpRequest(_relativeUrl, _method, _formParams, _queryStringParams, _cookies, _serverVariables); public void SetRequest(HttpRequestBase request) { _request = request; } - public override HttpResponseBase Response - { - get - { - return _response ?? new FakeHttpResponse(); - } - } + public override HttpResponseBase Response => _response ?? new FakeHttpResponse(); public void SetResponse(HttpResponseBase response) { @@ -100,22 +88,13 @@ public void SetResponse(HttpResponseBase response) public override IPrincipal User { - get { return _principal; } - set { _principal = value; } + get => _principal; + set => _principal = value; } - public override HttpSessionStateBase Session - { - get { return new FakeHttpSessionState(_sessionItems ?? new SessionStateItemCollection()); } - } + public override HttpSessionStateBase Session => new FakeHttpSessionState(_sessionItems ?? new SessionStateItemCollection()); - public override System.Collections.IDictionary Items - { - get - { - return _items; - } - } + public override System.Collections.IDictionary Items => _items; public override bool SkipAuthorization { get; set; } diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs index bf87081f57..5bc4db024a 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs @@ -16,19 +16,19 @@ public class FakeHttpRequest : HttpRequestBase private readonly Uri _url; private readonly Uri _urlReferrer; private readonly string _httpMethod; - private RequestContext _requestContext; + private RequestContext _requestContext; - public FakeHttpRequest(string relativeUrl, Uri url, Uri urlReferrer) - : this(relativeUrl, HttpVerbs.Get.ToString("g"), url, urlReferrer, null, null, null, null) - { - } + public FakeHttpRequest(string relativeUrl, Uri url, Uri urlReferrer) + : this(relativeUrl, HttpVerbs.Get.ToString("g"), url, urlReferrer, null, null, null, null) + { + } - public FakeHttpRequest(string relativeUrl, - string method, + public FakeHttpRequest(string relativeUrl, + string method, NameValueCollection formParams, - NameValueCollection queryStringParams, - HttpCookieCollection cookies, - NameValueCollection serverVariables) + NameValueCollection queryStringParams, + HttpCookieCollection cookies, + NameValueCollection serverVariables) { _httpMethod = method; _relativeUrl = relativeUrl; @@ -39,68 +39,37 @@ public FakeHttpRequest(string relativeUrl, } - public FakeHttpRequest(string relativeUrl, - string method, - Uri url, - Uri urlReferrer, - NameValueCollection formParams, - NameValueCollection queryStringParams, - HttpCookieCollection cookies, - NameValueCollection serverVariables) - : this(relativeUrl, method, formParams, queryStringParams, cookies, serverVariables) - { - _url = url; - _urlReferrer = urlReferrer; - } - - public override NameValueCollection ServerVariables + public FakeHttpRequest(string relativeUrl, + string method, + Uri url, + Uri urlReferrer, + NameValueCollection formParams, + NameValueCollection queryStringParams, + HttpCookieCollection cookies, + NameValueCollection serverVariables) + : this(relativeUrl, method, formParams, queryStringParams, cookies, serverVariables) { - get - { - return _serverVariables; - } + _url = url; + _urlReferrer = urlReferrer; } - public override NameValueCollection Form - { - get { return _formParams; } - } + public override NameValueCollection ServerVariables => _serverVariables; - public override NameValueCollection QueryString - { - get { return _queryStringParams; } - } + public override NameValueCollection Form => _formParams; - public override HttpCookieCollection Cookies - { - get { return _cookies; } - } + public override NameValueCollection QueryString => _queryStringParams; - public override string AppRelativeCurrentExecutionFilePath - { - get { return _relativeUrl; } - } + public override HttpCookieCollection Cookies => _cookies; - public override Uri Url - { - get - { - return _url ?? new Uri("http://tempuri.org"); - } - } + public override string AppRelativeCurrentExecutionFilePath => _relativeUrl; - public override Uri UrlReferrer - { - get - { - return _urlReferrer ?? new Uri("http://tempuri.org"); - } - } + public override Uri Url => _url ?? new Uri("http://tempuri.org"); - public override string PathInfo - { - get { return ""; } - } + public override Uri UrlReferrer => _urlReferrer ?? new Uri("http://tempuri.org"); + + public override string PathInfo => ""; + + public override string Path => _url?.AbsolutePath ?? ApplicationPath ?? "/"; public override string ApplicationPath { @@ -114,37 +83,22 @@ public override string ApplicationPath } } - public override string HttpMethod - { - get - { - return _httpMethod; - } - } + public override string HttpMethod => _httpMethod; - public override string UserHostAddress + public override string UserHostAddress => null; + + public override string RawUrl => this.ApplicationPath; + public override bool IsSecureConnection => _url?.Scheme?.EmptyNull().StartsWith("https", StringComparison.OrdinalIgnoreCase) == true; + public override bool IsAuthenticated => false; + public override string[] UserLanguages => new string[] { }; + public override string UserAgent => "Smartstore"; + public override bool IsLocal => false; + + public override RequestContext RequestContext { - get { return null; } - } + get => _requestContext ?? new RequestContext(); - public override string RawUrl => this.ApplicationPath; - public override bool IsSecureConnection => _url?.Scheme?.EmptyNull().StartsWith("https", StringComparison.OrdinalIgnoreCase) == true; - public override bool IsAuthenticated => false; - public override string[] UserLanguages => new string[] { }; - public override string UserAgent => "SmartStore.NET"; - public override bool IsLocal => false; - - public override RequestContext RequestContext - { - get - { - return _requestContext ?? new RequestContext(); - } - - set - { - _requestContext = value; - } - } - } + set => _requestContext = value; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpResponse.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpResponse.cs index 299b379037..4b5719ed6e 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpResponse.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpResponse.cs @@ -6,18 +6,15 @@ namespace SmartStore.Core.Fakes public class FakeHttpResponse : HttpResponseBase { private readonly HttpCookieCollection _cookies; - private readonly StringBuilder _outputString = new StringBuilder(); + private readonly StringBuilder _outputString = new StringBuilder(); public FakeHttpResponse() { this._cookies = new HttpCookieCollection(); - } - - public string ResponseOutput - { - get { return _outputString.ToString(); } } + public string ResponseOutput => _outputString.ToString(); + public override int StatusCode { get; set; } public override string RedirectLocation { get; set; } @@ -32,12 +29,6 @@ public override string ApplyAppPathModifier(string virtualPath) return virtualPath; } - public override HttpCookieCollection Cookies - { - get - { - return _cookies; - } - } + public override HttpCookieCollection Cookies => _cookies; } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpSessionState.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpSessionState.cs index 97bd46062e..6d45307e06 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpSessionState.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpSessionState.cs @@ -14,20 +14,14 @@ public FakeHttpSessionState(SessionStateItemCollection sessionItems) _sessionItems = sessionItems; } - public override int Count - { - get { return _sessionItems.Count; } - } + public override int Count => _sessionItems.Count; - public override NameObjectCollectionBase.KeysCollection Keys - { - get { return _sessionItems.Keys; } - } + public override NameObjectCollectionBase.KeysCollection Keys => _sessionItems.Keys; public override object this[string name] { - get { return _sessionItems[name]; } - set { _sessionItems[name] = value; } + get => _sessionItems[name]; + set => _sessionItems[name] = value; } public bool Exists(string key) @@ -37,8 +31,8 @@ public bool Exists(string key) public override object this[int index] { - get { return _sessionItems[index]; } - set { _sessionItems[index] = value; } + get => _sessionItems[index]; + set => _sessionItems[index] = value; } public override void Add(string name, object value) diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeIdentity.cs b/src/Libraries/SmartStore.Core/Fakes/FakeIdentity.cs index 2ba265b28a..a4d9ba0d6c 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeIdentity.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeIdentity.cs @@ -12,20 +12,11 @@ public FakeIdentity(string userName) _name = userName; } - public string AuthenticationType - { - get { throw new NotImplementedException(); } - } + public string AuthenticationType => throw new NotImplementedException(); - public bool IsAuthenticated - { - get { return !String.IsNullOrEmpty(_name); } - } + public bool IsAuthenticated => !String.IsNullOrEmpty(_name); - public string Name - { - get { return _name; } - } + public string Name => _name; } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Fakes/FakePrincipal.cs b/src/Libraries/SmartStore.Core/Fakes/FakePrincipal.cs index 276931651e..6205f1ca4b 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakePrincipal.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakePrincipal.cs @@ -15,10 +15,7 @@ public FakePrincipal(IIdentity identity, string[] roles) } - public IIdentity Identity - { - get { return _identity; } - } + public IIdentity Identity => _identity; public bool IsInRole(string role) { diff --git a/src/Libraries/SmartStore.Core/GenericEqualityComparer.cs b/src/Libraries/SmartStore.Core/GenericEqualityComparer.cs index dd66b7b1a7..e96d02f721 100644 --- a/src/Libraries/SmartStore.Core/GenericEqualityComparer.cs +++ b/src/Libraries/SmartStore.Core/GenericEqualityComparer.cs @@ -5,48 +5,48 @@ namespace SmartStore.Core { public sealed partial class GenericEqualityComparer : IEqualityComparer { - private readonly Func _equals; - private readonly Func _getHashcode; + private readonly Func _equals; + private readonly Func _getHashcode; - public GenericEqualityComparer(Func equals, Func getHashcode) - { - _getHashcode = getHashcode; - _equals = equals; - } + public GenericEqualityComparer(Func equals, Func getHashcode) + { + _getHashcode = getHashcode; + _equals = equals; + } - public static GenericEqualityComparer CompareMember(Func memberExpression) where TMember : IEquatable - { - return new GenericEqualityComparer( - (x, y) => memberExpression.Invoke(x).Equals((TMember)memberExpression.Invoke(y)), - x => - { - var invoked = memberExpression.Invoke(x); - return !ReferenceEquals(invoked, default(TMember)) ? invoked.GetHashCode() : 0; - }); - } + public static GenericEqualityComparer CompareMember(Func memberExpression) where TMember : IEquatable + { + return new GenericEqualityComparer( + (x, y) => memberExpression.Invoke(x).Equals((TMember)memberExpression.Invoke(y)), + x => + { + var invoked = memberExpression.Invoke(x); + return !ReferenceEquals(invoked, default(TMember)) ? invoked.GetHashCode() : 0; + }); + } - /// - /// Determines whether the specified objects are equal. - /// - /// - /// true if the specified objects are equal; otherwise, false. - /// - /// The first object of type to compare.The second object of type to compare. - public bool Equals(T x, T y) - { - return _equals.Invoke(x, y); - } + /// + /// Determines whether the specified objects are equal. + /// + /// + /// true if the specified objects are equal; otherwise, false. + /// + /// The first object of type to compare.The second object of type to compare. + public bool Equals(T x, T y) + { + return _equals.Invoke(x, y); + } - /// - /// Returns a hash code for the specified object. - /// - /// - /// A hash code for the specified object. - /// - /// The for which a hash code is to be returned.The type of is a reference type and is null. - public int GetHashCode(T obj) - { - return _getHashcode.Invoke(obj); - } - } + /// + /// Returns a hash code for the specified object. + /// + /// + /// A hash code for the specified object. + /// + /// The for which a hash code is to be returned.The type of is a reference type and is null. + public int GetHashCode(T obj) + { + return _getHashcode.Invoke(obj); + } + } } diff --git a/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs b/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs index b4e9c92eb0..268e22b441 100644 --- a/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs +++ b/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs @@ -11,7 +11,7 @@ public partial class BBCodeHelper private static readonly Regex regexUnderLine = new Regex(@"\[u\](.+?)\[/u\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex regexUrl1 = new Regex(@"\[url\=([^\]]+)\]([^\]]+)\[/url\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex regexUrl2 = new Regex(@"\[url\](.+?)\[/url\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex regexQuote = new Regex(@"\[quote(=.+?)?\](.+?)\[/quote\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex regexQuote = new Regex(@"\[quote(=.+?)?\](.+?)\[/quote\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// /// Formats the text @@ -25,13 +25,13 @@ public partial class BBCodeHelper /// A value indicating whether to replace Quote /// Formatted text public static string ToHtml( - string text, - bool replaceBold = true, - bool replaceItalic = true, + string text, + bool replaceBold = true, + bool replaceItalic = true, bool replaceUnderline = true, - bool replaceUrl = true, - bool replaceCode = true, - bool replaceQuote = true) + bool replaceUrl = true, + bool replaceCode = true, + bool replaceQuote = true) { if (String.IsNullOrEmpty(text)) return string.Empty; @@ -70,29 +70,29 @@ public static string ToHtml( if (replaceQuote) { - while (regexQuote.IsMatch(text)) - { - text = regexQuote.Replace(text, (m) => - { - var from = m.Groups[1].Value; - var quote = m.Groups[2].Value; - - if (quote.IsEmpty()) - { - return ""; - } - - string result = ""; - if (from.HasValue()) - { - result += "{0}:".FormatCurrent(from.Substring(1)); - } - - result += "
{0}
".FormatCurrent(quote); - - return result; - }); - } + while (regexQuote.IsMatch(text)) + { + text = regexQuote.Replace(text, (m) => + { + var from = m.Groups[1].Value; + var quote = m.Groups[2].Value; + + if (quote.IsEmpty()) + { + return ""; + } + + string result = ""; + if (from.HasValue()) + { + result += "{0}:".FormatCurrent(from.Substring(1)); + } + + result += "
{0}
".FormatCurrent(quote); + + return result; + }); + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CLikeFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CLikeFormat.cs index 4a4f948734..122f9a2c43 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CLikeFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CLikeFormat.cs @@ -17,32 +17,26 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Provides a base class for formatting languages similar to C. - /// + /// + /// Provides a base class for formatting languages similar to C. + /// public abstract partial class CLikeFormat : CodeFormat - { - /// - /// Regular expression string to match single line and multi-line - /// comments (// and /* */). - /// - protected override string CommentRegex - { - get { return @"/\*.*?\*/|//.*?(?=\r|\n)"; } - } + { + /// + /// Regular expression string to match single line and multi-line + /// comments (// and /* */). + /// + protected override string CommentRegex => @"/\*.*?\*/|//.*?(?=\r|\n)"; - /// - /// Regular expression string to match string and character literals. - /// - protected override string StringRegex - { - get { return @"@?""""|@?"".*?(?!\\).""|''|'.*?(?!\\).'"; } - } - } + /// + /// Regular expression string to match string and character literals. + /// + protected override string StringRegex => @"@?""""|@?"".*?(?!\\).""|''|'.*?(?!\\).'"; + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CSharpFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CSharpFormat.cs index 1822293a6d..85fb7ed828 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CSharpFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CSharpFormat.cs @@ -17,7 +17,7 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion namespace SmartStore.Core.Html.CodeFormatter @@ -26,36 +26,24 @@ namespace SmartStore.Core.Html.CodeFormatter /// Generates color-coded HTML 4.01 from C# source code. ///
public partial class CSharpFormat : CLikeFormat - { - /// - /// The list of C# keywords. - /// - protected override string Keywords - { - get - { - return "abstract as base bool break byte case catch char " - + "checked class const continue decimal default delegate do double else " - + "enum event explicit extern false finally fixed float for foreach goto " - + "if implicit in int interface internal is lock long namespace new null " - + "object operator out override partial params private protected public readonly " - + "ref return sbyte sealed short sizeof stackalloc static string struct " - + "switch this throw true try typeof uint ulong unchecked unsafe ushort " - + "using value virtual void volatile where while yield"; - } - } + { + /// + /// The list of C# keywords. + /// + protected override string Keywords => "abstract as base bool break byte case catch char " + + "checked class const continue decimal default delegate do double else " + + "enum event explicit extern false finally fixed float for foreach goto " + + "if implicit in int interface internal is lock long namespace new null " + + "object operator out override partial params private protected public readonly " + + "ref return sbyte sealed short sizeof stackalloc static string struct " + + "switch this throw true try typeof uint ulong unchecked unsafe ushort " + + "using value virtual void volatile where while yield"; - /// - /// The list of C# preprocessors. - /// - protected override string Preprocessors - { - get - { - return "#if #else #elif #endif #define #undef #warning " - + "#error #line #region #endregion #pragma"; - } - } - } + /// + /// The list of C# preprocessors. + /// + protected override string Preprocessors => "#if #else #elif #endif #define #undef #warning " + + "#error #line #region #endregion #pragma"; + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormat.cs index efaef284bc..9fb8944681 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormat.cs @@ -17,7 +17,7 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion using System.IO; @@ -26,144 +26,138 @@ namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Provides a base class for formatting most programming languages. - /// + /// + /// Provides a base class for formatting most programming languages. + /// public abstract partial class CodeFormat : SourceFormat - { - /// - /// Must be overridden to provide a list of keywords defined in - /// each language. - /// - /// - /// Keywords must be separated with spaces. - /// - protected abstract string Keywords - { - get; - } + { + /// + /// Must be overridden to provide a list of keywords defined in + /// each language. + /// + /// + /// Keywords must be separated with spaces. + /// + protected abstract string Keywords + { + get; + } - /// - /// Can be overridden to provide a list of preprocessors defined in - /// each language. - /// - /// - /// Preprocessors must be separated with spaces. - /// - protected virtual string Preprocessors - { - get { return ""; } - } + /// + /// Can be overridden to provide a list of preprocessors defined in + /// each language. + /// + /// + /// Preprocessors must be separated with spaces. + /// + protected virtual string Preprocessors => ""; - /// - /// Must be overridden to provide a regular expression string - /// to match strings literals. - /// - protected abstract string StringRegex - { - get; - } + /// + /// Must be overridden to provide a regular expression string + /// to match strings literals. + /// + protected abstract string StringRegex + { + get; + } - /// - /// Must be overridden to provide a regular expression string - /// to match comments. - /// - protected abstract string CommentRegex - { - get; - } + /// + /// Must be overridden to provide a regular expression string + /// to match comments. + /// + protected abstract string CommentRegex + { + get; + } - /// - /// Determines if the language is case sensitive. - /// - /// true if the language is case sensitive, false - /// otherwise. The default is true. - /// - /// A case-insensitive language formatter must override this - /// property to return false. - /// - public virtual bool CaseSensitive - { - get { return true; } - } + /// + /// Determines if the language is case sensitive. + /// + /// true if the language is case sensitive, false + /// otherwise. The default is true. + /// + /// A case-insensitive language formatter must override this + /// property to return false. + /// + public virtual bool CaseSensitive => true; - /// - protected CodeFormat() - { - //generate the keyword and preprocessor regexes from the keyword lists - Regex r; - r = new Regex(@"\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+"); - string regKeyword = r.Replace(Keywords, @"(?<=^|\W)$0(?=\W)"); - string regPreproc = r.Replace(Preprocessors, @"(?<=^|\s)$0(?=\s|$)"); - r = new Regex(@" +"); - regKeyword = r.Replace(regKeyword, @"|"); - regPreproc = r.Replace(regPreproc, @"|"); + /// + protected CodeFormat() + { + //generate the keyword and preprocessor regexes from the keyword lists + Regex r; + r = new Regex(@"\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+"); + string regKeyword = r.Replace(Keywords, @"(?<=^|\W)$0(?=\W)"); + string regPreproc = r.Replace(Preprocessors, @"(?<=^|\s)$0(?=\s|$)"); + r = new Regex(@" +"); + regKeyword = r.Replace(regKeyword, @"|"); + regPreproc = r.Replace(regPreproc, @"|"); - if (regPreproc.Length == 0) - { - regPreproc = "(?!.*)_{37}(? 0) - { - regAll.Append(")|("); - regAll.Append(regPreproc); - } - regAll.Append(")|("); - regAll.Append(regKeyword); - regAll.Append(")"); + regAll.Append("("); + regAll.Append(CommentRegex); + regAll.Append(")|("); + regAll.Append(StringRegex); + if (regPreproc.Length > 0) + { + regAll.Append(")|("); + regAll.Append(regPreproc); + } + regAll.Append(")|("); + regAll.Append(regKeyword); + regAll.Append(")"); - RegexOptions caseInsensitive = CaseSensitive ? 0 : RegexOptions.IgnoreCase; - CodeRegex = new Regex(regAll.ToString(), RegexOptions.Singleline | caseInsensitive); - } + RegexOptions caseInsensitive = CaseSensitive ? 0 : RegexOptions.IgnoreCase; + CodeRegex = new Regex(regAll.ToString(), RegexOptions.Singleline | caseInsensitive); + } - /// - /// Called to evaluate the HTML fragment corresponding to each - /// matching token in the code. - /// - /// The resulting from a - /// single regular expression match. - /// A string containing the HTML code fragment. - protected override string MatchEval(Match match) - { - if(match.Groups[1].Success) //comment - { + /// + /// Called to evaluate the HTML fragment corresponding to each + /// matching token in the code. + /// + /// The resulting from a + /// single regular expression match. + /// A string containing the HTML code fragment. + protected override string MatchEval(Match match) + { + if (match.Groups[1].Success) //comment + { var reader = new StringReader(match.ToString()); - string line; + string line; var sb = new StringBuilder(); - while ((line = reader.ReadLine()) != null) - { - if(sb.Length > 0) - { - sb.Append("\n"); - } - sb.Append(""); - sb.Append(line); - sb.Append(""); - } - return sb.ToString(); - } - if(match.Groups[2].Success) //string literal - { - return "" + match.ToString() + ""; - } - if(match.Groups[3].Success) //preprocessor keyword - { - return "" + match.ToString() + ""; - } - if(match.Groups[4].Success) //keyword - { - return "" + match.ToString() + ""; - } - System.Diagnostics.Debug.Assert(false, "None of the above!"); - return ""; //none of the above - } - } + while ((line = reader.ReadLine()) != null) + { + if (sb.Length > 0) + { + sb.Append("\n"); + } + sb.Append(""); + sb.Append(line); + sb.Append(""); + } + return sb.ToString(); + } + if (match.Groups[2].Success) //string literal + { + return "" + match.ToString() + ""; + } + if (match.Groups[3].Success) //preprocessor keyword + { + return "" + match.ToString() + ""; + } + if (match.Groups[4].Success) //keyword + { + return "" + match.ToString() + ""; + } + System.Diagnostics.Debug.Assert(false, "None of the above!"); + return ""; //none of the above + } + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs index e58e51d428..ff56b0f0d5 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs @@ -4,9 +4,9 @@ namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Represents a code format helper - /// + /// + /// Represents a code format helper + /// public partial class CodeFormatHelper { #region Fields @@ -32,8 +32,8 @@ private static string CodeEvaluatorSimple(Match match) options.Language = "c#"; options.Code = match.Groups["inner"].Value; options.DisplayLineNumbers = false; - options.Title =string.Empty; - options.AlternateLineNumbers =false; + options.Title = string.Empty; + options.AlternateLineNumbers = false; string result = match.Value; result = Highlight(options, result); diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/HtmlFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/HtmlFormat.cs index 718ce0db1e..3592cb6354 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/HtmlFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/HtmlFormat.cs @@ -17,7 +17,7 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion using System.IO; @@ -26,124 +26,124 @@ namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Generates color-coded HTML 4.01 from HTML/XML/ASPX source code. - /// - /// - /// - /// This implementation assumes that code inside <script> blocks - /// is JavaScript, and code inside <% %> blocks is C#. - /// - /// The default tab width is set to 2 characters in this class. - /// + /// + /// Generates color-coded HTML 4.01 from HTML/XML/ASPX source code. + /// + /// + /// + /// This implementation assumes that code inside <script> blocks + /// is JavaScript, and code inside <% %> blocks is C#. + /// + /// The default tab width is set to 2 characters in this class. + /// public partial class HtmlFormat : SourceFormat - { - private readonly CSharpFormat csf; //to format embedded C# code + { + private readonly CSharpFormat csf; //to format embedded C# code private readonly JavaScriptFormat jsf; //to format client-side JavaScript code private readonly Regex attribRegex; - /// - public HtmlFormat() - { - const string regJavaScript = @"(?<=<script(?:\s.*?)?>).+?(?=</script>)"; - const string regComment = @"<!--.*?-->"; - const string regAspTag = @"<%@.*?%>|<%|%>"; - const string regAspCode = @"(?<=<%).*?(?=%>)"; - const string regTagDelimiter = @"(?:</?!?\??(?!%)|(? + public HtmlFormat() + { + const string regJavaScript = @"(?<=<script(?:\s.*?)?>).+?(?=</script>)"; + const string regComment = @"<!--.*?-->"; + const string regAspTag = @"<%@.*?%>|<%|%>"; + const string regAspCode = @"(?<=<%).*?(?=%>)"; + const string regTagDelimiter = @"(?:</?!?\??(?!%)|(? - /// Called to evaluate the HTML fragment corresponding to each - /// attribute's name/value in the code. - /// - /// The resulting from a - /// single regular expression match. - /// A string containing the HTML code fragment. - private string AttributeMatchEval(Match match) - { - if(match.Groups[1].Success) //attribute value - return "" + match.ToString() + ""; + csf = new CSharpFormat(); + jsf = new JavaScriptFormat(); + } - if(match.Groups[2].Success) //attribute name - return "" + match.ToString() + ""; + /// + /// Called to evaluate the HTML fragment corresponding to each + /// attribute's name/value in the code. + /// + /// The resulting from a + /// single regular expression match. + /// A string containing the HTML code fragment. + private string AttributeMatchEval(Match match) + { + if (match.Groups[1].Success) //attribute value + return "" + match.ToString() + ""; - return match.ToString(); - } + if (match.Groups[2].Success) //attribute name + return "" + match.ToString() + ""; - /// - /// Called to evaluate the HTML fragment corresponding to each - /// matching token in the code. - /// - /// The resulting from a - /// single regular expression match. - /// A string containing the HTML code fragment. - protected override string MatchEval(Match match) - { - if(match.Groups[1].Success) //JavaScript code - { - //string s = match.ToString(); - return jsf.FormatSubCode(match.ToString()); - } - if(match.Groups[2].Success) //comment - { + return match.ToString(); + } + + /// + /// Called to evaluate the HTML fragment corresponding to each + /// matching token in the code. + /// + /// The resulting from a + /// single regular expression match. + /// A string containing the HTML code fragment. + protected override string MatchEval(Match match) + { + if (match.Groups[1].Success) //JavaScript code + { + //string s = match.ToString(); + return jsf.FormatSubCode(match.ToString()); + } + if (match.Groups[2].Success) //comment + { var reader = new StringReader(match.ToString()); - string line; + string line; var sb = new StringBuilder(); - while ((line = reader.ReadLine()) != null) - { - if(sb.Length > 0) - { - sb.Append("\n"); - } - sb.Append(""); - sb.Append(line); - sb.Append(""); - } - return sb.ToString(); - } - if(match.Groups[3].Success) //asp tag - { - return "" + match.ToString() + ""; - } - if(match.Groups[4].Success) //asp C# code - { - return csf.FormatSubCode(match.ToString()); - } - if(match.Groups[5].Success) //tag delimiter - { - return "" + match.ToString() + ""; - } - if(match.Groups[6].Success) //html tagname - { - return "" + match.ToString() + ""; - } - if(match.Groups[7].Success) //attributes - { - return attribRegex.Replace(match.ToString(), - new MatchEvaluator(this.AttributeMatchEval)); - } - if(match.Groups[8].Success) //entity - { - return "" + match.ToString() + ""; - } - return match.ToString(); - } - } + while ((line = reader.ReadLine()) != null) + { + if (sb.Length > 0) + { + sb.Append("\n"); + } + sb.Append(""); + sb.Append(line); + sb.Append(""); + } + return sb.ToString(); + } + if (match.Groups[3].Success) //asp tag + { + return "" + match.ToString() + ""; + } + if (match.Groups[4].Success) //asp C# code + { + return csf.FormatSubCode(match.ToString()); + } + if (match.Groups[5].Success) //tag delimiter + { + return "" + match.ToString() + ""; + } + if (match.Groups[6].Success) //html tagname + { + return "" + match.ToString() + ""; + } + if (match.Groups[7].Success) //attributes + { + return attribRegex.Replace(match.ToString(), + new MatchEvaluator(this.AttributeMatchEval)); + } + if (match.Groups[8].Success) //entity + { + return "" + match.ToString() + ""; + } + return match.ToString(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/JavaScriptFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/JavaScriptFormat.cs index 9f65a2bd50..6319d104c2 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/JavaScriptFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/JavaScriptFormat.cs @@ -17,7 +17,7 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion namespace SmartStore.Core.Html.CodeFormatter @@ -26,35 +26,23 @@ namespace SmartStore.Core.Html.CodeFormatter /// Generates color-coded HTML 4.01 from JavaSctript source code. /// public partial class JavaScriptFormat : CLikeFormat - { - /// - /// The list of JavaScript keywords. - /// - protected override string Keywords - { - get - { - return "var function abstract as base bool break byte case catch char " - + "checked class const continue decimal default delegate do double else " - + "enum event explicit extern false finally fixed float for foreach goto " - + "if implicit in int interface internal is lock long namespace new null " - + "object operator out override params private protected public readonly " - + "ref return sbyte sealed short sizeof stackalloc static string struct " - + "switch this throw true try typeof uint ulong unchecked unsafe ushort " - + "using virtual void while"; - } - } + { + /// + /// The list of JavaScript keywords. + /// + protected override string Keywords => "var function abstract as base bool break byte case catch char " + + "checked class const continue decimal default delegate do double else " + + "enum event explicit extern false finally fixed float for foreach goto " + + "if implicit in int interface internal is lock long namespace new null " + + "object operator out override params private protected public readonly " + + "ref return sbyte sealed short sizeof stackalloc static string struct " + + "switch this throw true try typeof uint ulong unchecked unsafe ushort " + + "using virtual void while"; - /// - /// Use the pre-processor color to mark directives that start with @. - /// - protected override string Preprocessors - { - get - { - return @"@\w*"; - } - } - } + /// + /// Use the pre-processor color to mark directives that start with @. + /// + protected override string Preprocessors => @"@\w*"; + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/MshFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/MshFormat.cs index fbe7acbee1..17964eb843 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/MshFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/MshFormat.cs @@ -1,52 +1,34 @@ namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Generates color-coded HTML 4.01 from MSH (code name Monad) source code. - /// + /// + /// Generates color-coded HTML 4.01 from MSH (code name Monad) source code. + /// public partial class MshFormat : CodeFormat - { - /// - /// Regular expression string to match single line comments (#). - /// - protected override string CommentRegex - { - get { return @"#.*?(?=\r|\n)"; } - } + { + /// + /// Regular expression string to match single line comments (#). + /// + protected override string CommentRegex => @"#.*?(?=\r|\n)"; - /// - /// Regular expression string to match string and character literals. - /// - protected override string StringRegex - { - get { return @"@?""""|@?"".*?(?!\\).""|''|'.*?(?!\\).'"; } - } + /// + /// Regular expression string to match string and character literals. + /// + protected override string StringRegex => @"@?""""|@?"".*?(?!\\).""|''|'.*?(?!\\).'"; - /// - /// The list of MSH keywords. - /// - protected override string Keywords - { - get - { - return "function filter global script local private if else" - + " elseif for foreach in while switch continue break" - + " return default param begin process end throw trap"; - } - } + /// + /// The list of MSH keywords. + /// + protected override string Keywords => "function filter global script local private if else" + + " elseif for foreach in while switch continue break" + + " return default param begin process end throw trap"; - /// - /// Use preprocessors property to hilight operators. - /// - protected override string Preprocessors - { - get - { - return "-band -bor -match -notmatch -like -notlike -eq -ne" - + " -gt -ge -lt -le -is -imatch -inotmatch -ilike" - + " -inotlike -ieq -ine -igt -ige -ilt -ile"; - } - } + /// + /// Use preprocessors property to hilight operators. + /// + protected override string Preprocessors => "-band -bor -match -notmatch -like -notlike -eq -ne" + + " -gt -ge -lt -le -is -imatch -inotmatch -ilike" + + " -inotlike -ieq -ine -igt -ige -ilt -ile"; - } + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/SourceFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/SourceFormat.cs index a92c4d20f4..2b5b232219 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/SourceFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/SourceFormat.cs @@ -17,7 +17,7 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion using System; @@ -28,238 +28,238 @@ namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Provides a base implementation for all code formatters. - /// - /// - /// - /// To display the formatted code on your web site, the web page must - /// refer to a stylesheet that defines the formatting for the different - /// CSS classes generated by CSharpFormat: - /// .csharpcode, pre, .rem, .kwrd, .str, .op, .preproc, .alt, .lnum. - /// - /// - /// Note that if you have multi-line comments in your source code - /// (like /* ... */), the "line numbers" or "alternate line background" - /// options will generate code that is not strictly HTML 4.01 compliant. - /// The code will still look good with IE5+ or Mozilla 0.8+. - /// - /// + /// + /// Provides a base implementation for all code formatters. + /// + /// + /// + /// To display the formatted code on your web site, the web page must + /// refer to a stylesheet that defines the formatting for the different + /// CSS classes generated by CSharpFormat: + /// .csharpcode, pre, .rem, .kwrd, .str, .op, .preproc, .alt, .lnum. + /// + /// + /// Note that if you have multi-line comments in your source code + /// (like /* ... */), the "line numbers" or "alternate line background" + /// options will generate code that is not strictly HTML 4.01 compliant. + /// The code will still look good with IE5+ or Mozilla 0.8+. + /// + /// public abstract partial class SourceFormat - { - /// - protected SourceFormat() - { - _tabSpaces = 4; - _lineNumbers = false; - _alternate = false; - _embedStyleSheet = false; - } + { + /// + protected SourceFormat() + { + _tabSpaces = 4; + _lineNumbers = false; + _alternate = false; + _embedStyleSheet = false; + } - private byte _tabSpaces; + private byte _tabSpaces; - /// - /// Gets or sets the tabs width. - /// - /// The number of space characters to substitute for tab - /// characters. The default is 4, unless overridden is a - /// derived class. - public byte TabSpaces - { - get { return _tabSpaces; } - set { _tabSpaces = value; } - } + /// + /// Gets or sets the tabs width. + /// + /// The number of space characters to substitute for tab + /// characters. The default is 4, unless overridden is a + /// derived class. + public byte TabSpaces + { + get => _tabSpaces; + set => _tabSpaces = value; + } - private bool _lineNumbers; + private bool _lineNumbers; - /// - /// Enables or disables line numbers in output. - /// - /// When true, line numbers are generated. - /// The default is false. - public bool LineNumbers - { - get { return _lineNumbers; } - set { _lineNumbers = value; } - } + /// + /// Enables or disables line numbers in output. + /// + /// When true, line numbers are generated. + /// The default is false. + public bool LineNumbers + { + get => _lineNumbers; + set => _lineNumbers = value; + } - private bool _alternate; + private bool _alternate; - /// - /// Enables or disables alternating line background. - /// - /// When true, lines background is alternated. - /// The default is false. - public bool Alternate - { - get { return _alternate; } - set { _alternate = value; } - } + /// + /// Enables or disables alternating line background. + /// + /// When true, lines background is alternated. + /// The default is false. + public bool Alternate + { + get => _alternate; + set => _alternate = value; + } - private bool _embedStyleSheet; + private bool _embedStyleSheet; - /// - /// Enables or disables the embedded CSS style sheet. - /// - /// When true, the CSS <style> element is included - /// in the HTML output. The default is false. - public bool EmbedStyleSheet - { - get { return _embedStyleSheet; } - set { _embedStyleSheet = value; } - } + /// + /// Enables or disables the embedded CSS style sheet. + /// + /// When true, the CSS <style> element is included + /// in the HTML output. The default is false. + public bool EmbedStyleSheet + { + get => _embedStyleSheet; + set => _embedStyleSheet = value; + } - /// Transform source code to HTML 4.01. - /// - /// - /// Transforms a source code stream to HTML 4.01. - /// - /// Source code stream. - /// A string containing the HTML formatted code. - public string FormatCode(Stream source) - { + /// Transform source code to HTML 4.01. + /// + /// + /// Transforms a source code stream to HTML 4.01. + /// + /// Source code stream. + /// A string containing the HTML formatted code. + public string FormatCode(Stream source) + { var reader = new StreamReader(source); - string s = reader.ReadToEnd(); - reader.Close(); - return FormatCode(s, _lineNumbers, _alternate, _embedStyleSheet, false); - } + string s = reader.ReadToEnd(); + reader.Close(); + return FormatCode(s, _lineNumbers, _alternate, _embedStyleSheet, false); + } - /// - /// Transforms a source code string to HTML 4.01. - /// - /// A string containing the HTML formatted code. - public string FormatCode(string source) - { - return FormatCode(source, _lineNumbers, _alternate, _embedStyleSheet, false); - } + /// + /// Transforms a source code string to HTML 4.01. + /// + /// A string containing the HTML formatted code. + public string FormatCode(string source) + { + return FormatCode(source, _lineNumbers, _alternate, _embedStyleSheet, false); + } - /// - /// Allows formatting a part of the code in a different language, - /// for example a JavaScript block inside an HTML file. - /// - public string FormatSubCode(string source) - { - return FormatCode(source, false, false, false, true); - } + /// + /// Allows formatting a part of the code in a different language, + /// for example a JavaScript block inside an HTML file. + /// + public string FormatSubCode(string source) + { + return FormatCode(source, false, false, false, true); + } - /// - /// Gets the CSS stylesheet as a stream. - /// - /// A text of the CSS definitions. - public static Stream GetCssStream() - { - return Assembly.GetExecutingAssembly().GetManifestResourceStream( - "Manoli.Utils.CSharpFormat.csharp.css"); - } + /// + /// Gets the CSS stylesheet as a stream. + /// + /// A text of the CSS definitions. + public static Stream GetCssStream() + { + return Assembly.GetExecutingAssembly().GetManifestResourceStream( + "Manoli.Utils.CSharpFormat.csharp.css"); + } - /// - /// Gets the CSS stylesheet as a string. - /// - /// A string containing the CSS definitions. - public static string GetCssString() - { + /// + /// Gets the CSS stylesheet as a string. + /// + /// A string containing the CSS definitions. + public static string GetCssString() + { var reader = new StreamReader(GetCssStream()); - return reader.ReadToEnd(); - } + return reader.ReadToEnd(); + } - private Regex codeRegex; + private Regex codeRegex; - /// - /// The regular expression used to capture language tokens. - /// - protected Regex CodeRegex - { - get { return codeRegex; } - set { codeRegex = value; } - } + /// + /// The regular expression used to capture language tokens. + /// + protected Regex CodeRegex + { + get => codeRegex; + set => codeRegex = value; + } - /// - /// Called to evaluate the HTML fragment corresponding to each - /// matching token in the code. - /// - /// The resulting from a - /// single regular expression match. - /// A string containing the HTML code fragment. - protected abstract string MatchEval(Match match); + /// + /// Called to evaluate the HTML fragment corresponding to each + /// matching token in the code. + /// + /// The resulting from a + /// single regular expression match. + /// A string containing the HTML code fragment. + protected abstract string MatchEval(Match match); - //does the formatting job - private string FormatCode(string source, bool lineNumbers, - bool alternate, bool embedStyleSheet, bool subCode) - { - //replace special characters + //does the formatting job + private string FormatCode(string source, bool lineNumbers, + bool alternate, bool embedStyleSheet, bool subCode) + { + //replace special characters var sb = new StringBuilder(source); - if(!subCode) - { - sb.Replace("&", "&"); - sb.Replace("<", "<"); - sb.Replace(">", ">"); - sb.Replace("\t", string.Empty.PadRight(_tabSpaces)); - } - - //color the code - source = codeRegex.Replace(sb.ToString(), new MatchEvaluator(this.MatchEval)); + if (!subCode) + { + sb.Replace("&", "&"); + sb.Replace("<", "<"); + sb.Replace(">", ">"); + sb.Replace("\t", string.Empty.PadRight(_tabSpaces)); + } - sb = new StringBuilder(); - - if (embedStyleSheet) - { - sb.Append("\n"); - } + //color the code + source = codeRegex.Replace(sb.ToString(), new MatchEvaluator(this.MatchEval)); - if (lineNumbers || alternate) //we have to process the code line by line - { - if(!subCode) - sb.Append("
\n"); + sb = new StringBuilder(); + + if (embedStyleSheet) + { + sb.Append("\n"); + } + + if (lineNumbers || alternate) //we have to process the code line by line + { + if (!subCode) + sb.Append("
\n"); var reader = new StringReader(source); - int i = 0; - string spaces = " "; - string line; - while ((line = reader.ReadLine()) != null) - { - i++; - if (alternate && ((i % 2) == 1)) - { - sb.Append("
");
-					}
-					else
-					{
-						sb.Append("
");
-					}
+                int i = 0;
+                string spaces = "    ";
+                string line;
+                while ((line = reader.ReadLine()) != null)
+                {
+                    i++;
+                    if (alternate && ((i % 2) == 1))
+                    {
+                        sb.Append("
");
+                    }
+                    else
+                    {
+                        sb.Append("
");
+                    }
+
+                    if (lineNumbers)
+                    {
+                        var order = (int)Math.Log10(i);
+                        sb.Append(""
+                            + spaces.Substring(0, 3 - order) + i.ToString()
+                            + ":  ");
+                    }
+
+                    if (line.Length == 0)
+                        sb.Append(" ");
+                    else
+                        sb.Append(line);
+                    sb.Append("
\n"); + } + reader.Close(); + if (!subCode) + sb.Append("
"); + } + else + { + //have to use a
 because IE below ver 6 does not understand 
+                //the "white-space: pre" CSS value
+                if (!subCode)
+                    sb.Append("
\n");
+                sb.Append(source);
+                if (!subCode)
+                    sb.Append("
"); + } - if(lineNumbers) - { - var order = (int)Math.Log10(i); - sb.Append("" - + spaces.Substring(0, 3 - order) + i.ToString() - + ": "); - } - - if(line.Length == 0) - sb.Append(" "); - else - sb.Append(line); - sb.Append("
\n"); - } - reader.Close(); - if(!subCode) - sb.Append("
"); - } - else - { - //have to use a
 because IE below ver 6 does not understand 
-				//the "white-space: pre" CSS value
-				if(!subCode)
-					sb.Append("
\n");
-				sb.Append(source);
-				if(!subCode)
-					sb.Append("
"); - } - - return sb.ToString(); - } + return sb.ToString(); + } - } + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/TsqlFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/TsqlFormat.cs index 74f58beed1..6aee1b6e6a 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/TsqlFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/TsqlFormat.cs @@ -19,127 +19,100 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Generates color-coded T-SQL source code. - /// + /// + /// Generates color-coded T-SQL source code. + /// public partial class TsqlFormat : CodeFormat - { - /// - /// Regular expression string to match single line - /// comments (--). - /// - protected override string CommentRegex - { - get - { - return @"(?:--\s).*?(?=\r|\n)"; - } - } + { + /// + /// Regular expression string to match single line + /// comments (--). + /// + protected override string CommentRegex => @"(?:--\s).*?(?=\r|\n)"; - /// - /// Regular expression string to match string literals. - /// - protected override string StringRegex - { - get - { - return @"''|'.*?'"; - } - } + /// + /// Regular expression string to match string literals. + /// + protected override string StringRegex => @"''|'.*?'"; - /// - /// Returns false, since T-SQL is not case sensitive. - /// - public override bool CaseSensitive - { - get { return false; } - } + /// + /// Returns false, since T-SQL is not case sensitive. + /// + public override bool CaseSensitive => false; - /// - /// The list of T-SQL keywords. - /// - protected override string Keywords - { - get - { - return "absolute action ada add admin after aggregate " - + "alias all allocate alter and any are array as asc " - + "assertion at authorization avg backup before begin " - + "between binary bit bit_length blob boolean both breadth " - + "break browse bulk by call cascade cascaded case cast " - + "catalog char char_length character character_length " - + "check checkpoint class clob close clustered coalesce " - + "collate collation column commit completion compute " - + "connect connection constraint constraints constructor " - + "contains containstable continue convert corresponding " - + "count create cross cube current current_date current_path " - + "current_role current_time current_timestamp current_user " - + "cursor cycle data database date day dbcc deallocate dec " - + "decimal declare default deferrable deferred delete deny " - + "depth deref desc describe descriptor destroy destructor " - + "deterministic diagnostics dictionary disconnect disk " - + "distinct distributed domain double drop dummy dump " - + "dynamic each else end end-exec equals errlvl escape " - + "every except exception exec execute exists exit external " - + "extract false fetch file fillfactor first float for " - + "foreign fortran found free freetext freetexttable from " - + "full function general get global go goto grant group " - + "grouping having holdlock host hour identity identity_insert " - + "identitycol if ignore immediate in include index indicator " - + "initialize initially inner inout input insensitive insert " - + "int integer intersect interval into is isolation iterate " - + "join key kill language large last lateral leading left " - + "less level like limit lineno load local localtime localtimestamp " - + "locator lower map match max min minute modifies modify " - + "module month names national natural nchar nclob new next " - + "no nocheck nonclustered none not null nullif numeric object " - + "octet_length of off offsets old on only open opendatasource " - + "openquery openrowset openxml operation option or order " - + "ordinality out outer output over overlaps pad parameter " - + "parameters partial pascal path percent plan position " - + "postfix precision prefix preorder prepare preserve " - + "primary print prior privileges proc procedure " - + "public raiserror read reads readtext real reconfigure " - + "recursive ref references referencing relative replication " - + "restore restrict result return returns revoke right role " - + "rollback rollup routine row rowcount rowguidcol rows rule " - + "save savepoint schema scope scroll search second section " - + "select sequence session session_user set sets setuser " - + "shutdown size smallint some space specific specifictype " - + "sql sqlca sqlcode sqlerror sqlexception sqlstate sqlwarning " - + "start state statement static statistics structure substring " - + "sum system_user table temporary terminate textsize than then " - + "time timestamp timezone_hour timezone_minute to top trailing " - + "tran transaction translate translation treat trigger trim " - + "true truncate tsequal under union unique unknown unnest " - + "update updatetext upper usage use user using value values " - + "varchar variable varying view waitfor when whenever where " - + "while with without work write writetext year zone"; - } - } + /// + /// The list of T-SQL keywords. + /// + protected override string Keywords => "absolute action ada add admin after aggregate " + + "alias all allocate alter and any are array as asc " + + "assertion at authorization avg backup before begin " + + "between binary bit bit_length blob boolean both breadth " + + "break browse bulk by call cascade cascaded case cast " + + "catalog char char_length character character_length " + + "check checkpoint class clob close clustered coalesce " + + "collate collation column commit completion compute " + + "connect connection constraint constraints constructor " + + "contains containstable continue convert corresponding " + + "count create cross cube current current_date current_path " + + "current_role current_time current_timestamp current_user " + + "cursor cycle data database date day dbcc deallocate dec " + + "decimal declare default deferrable deferred delete deny " + + "depth deref desc describe descriptor destroy destructor " + + "deterministic diagnostics dictionary disconnect disk " + + "distinct distributed domain double drop dummy dump " + + "dynamic each else end end-exec equals errlvl escape " + + "every except exception exec execute exists exit external " + + "extract false fetch file fillfactor first float for " + + "foreign fortran found free freetext freetexttable from " + + "full function general get global go goto grant group " + + "grouping having holdlock host hour identity identity_insert " + + "identitycol if ignore immediate in include index indicator " + + "initialize initially inner inout input insensitive insert " + + "int integer intersect interval into is isolation iterate " + + "join key kill language large last lateral leading left " + + "less level like limit lineno load local localtime localtimestamp " + + "locator lower map match max min minute modifies modify " + + "module month names national natural nchar nclob new next " + + "no nocheck nonclustered none not null nullif numeric object " + + "octet_length of off offsets old on only open opendatasource " + + "openquery openrowset openxml operation option or order " + + "ordinality out outer output over overlaps pad parameter " + + "parameters partial pascal path percent plan position " + + "postfix precision prefix preorder prepare preserve " + + "primary print prior privileges proc procedure " + + "public raiserror read reads readtext real reconfigure " + + "recursive ref references referencing relative replication " + + "restore restrict result return returns revoke right role " + + "rollback rollup routine row rowcount rowguidcol rows rule " + + "save savepoint schema scope scroll search second section " + + "select sequence session session_user set sets setuser " + + "shutdown size smallint some space specific specifictype " + + "sql sqlca sqlcode sqlerror sqlexception sqlstate sqlwarning " + + "start state statement static statistics structure substring " + + "sum system_user table temporary terminate textsize than then " + + "time timestamp timezone_hour timezone_minute to top trailing " + + "tran transaction translate translation treat trigger trim " + + "true truncate tsequal under union unique unknown unnest " + + "update updatetext upper usage use user using value values " + + "varchar variable varying view waitfor when whenever where " + + "while with without work write writetext year zone"; - /// - /// Use the pre-processor color to mark keywords that start with @@. - /// - protected override string Preprocessors - { - get - { - return @"@@CONNECTIONS @@CPU_BUSY @@CURSOR_ROWS @@DATEFIRST " - + "@@DBTS @@ERROR @@FETCH_STATUS @@IDENTITY @@IDLE " - + "@@IO_BUSY @@LANGID @@LANGUAGE @@LOCK_TIMEOUT " - + "@@MAX_CONNECTIONS @@MAX_PRECISION @@NESTLEVEL @@OPTIONS " - + "@@PACK_RECEIVED @@PACK_SENT @@PACKET_ERRORS @@PROCID " - + "@@REMSERVER @@ROWCOUNT @@SERVERNAME @@SERVICENAME @@SPID " - + "@@TEXTSIZE @@TIMETICKS @@TOTAL_ERRORS @@TOTAL_READ " - + "@@TOTAL_WRITE @@TRANCOUNT @@VERSION"; - } - } - } + /// + /// Use the pre-processor color to mark keywords that start with @@. + /// + protected override string Preprocessors => @"@@CONNECTIONS @@CPU_BUSY @@CURSOR_ROWS @@DATEFIRST " + + "@@DBTS @@ERROR @@FETCH_STATUS @@IDENTITY @@IDLE " + + "@@IO_BUSY @@LANGID @@LANGUAGE @@LOCK_TIMEOUT " + + "@@MAX_CONNECTIONS @@MAX_PRECISION @@NESTLEVEL @@OPTIONS " + + "@@PACK_RECEIVED @@PACK_SENT @@PACKET_ERRORS @@PROCID " + + "@@REMSERVER @@ROWCOUNT @@SERVERNAME @@SERVICENAME @@SPID " + + "@@TEXTSIZE @@TIMETICKS @@TOTAL_ERRORS @@TOTAL_READ " + + "@@TOTAL_WRITE @@TRANCOUNT @@VERSION"; + } } diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/VisualBasicFormat.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/VisualBasicFormat.cs index 36fa1da139..b4a28e8503 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/VisualBasicFormat.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/VisualBasicFormat.cs @@ -17,80 +17,59 @@ * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. - */ + */ #endregion namespace SmartStore.Core.Html.CodeFormatter { - /// - /// Generates color-coded HTML 4.01 from Visual Basic source code. - /// + /// + /// Generates color-coded HTML 4.01 from Visual Basic source code. + /// public partial class VisualBasicFormat : CodeFormat - { - /// - /// Determines if the language is case sensitive. - /// - /// Always true, since VB is not case sensitive. - public override bool CaseSensitive - { - get { return false; } - } + { + /// + /// Determines if the language is case sensitive. + /// + /// Always true, since VB is not case sensitive. + public override bool CaseSensitive => false; - /// - /// Regular expression string to match comments (' and REM). - /// - protected override string CommentRegex - { - get { return @"(?:'|REM\s).*?(?=\r|\n)"; } - } + /// + /// Regular expression string to match comments (' and REM). + /// + protected override string CommentRegex => @"(?:'|REM\s).*?(?=\r|\n)"; - /// - /// Regular expression string to match string and character literals. - /// - protected override string StringRegex - { - get { return @"""""|"".*?"""; } - } + /// + /// Regular expression string to match string and character literals. + /// + protected override string StringRegex => @"""""|"".*?"""; - /// - /// The list of VB keywords. - /// - protected override string Keywords - { - get - { - return "AddHandler AddressOf AndAlso Alias And Ansi As Assembly " - + "Auto Boolean ByRef Byte ByVal Call Case Catch " - + "CBool CByte CChar CDate CDec CDbl Char CInt " - + "Class CLng CObj Const CShort CSng CStr CType " - + "Date Decimal Declare Default Delegate Dim DirectCast Do " - + "Double Each Else ElseIf End EndIf Enum Erase Error " - + "Event Exit False Finally For Friend Function Get " - + "GetType Global GoSub GoTo Handles If Implements Imports In Inherits " - + "Integer Interface Is Let Lib Like Long Loop " - + "Me Mod Module MustInherit MustOverride MyBase MyClass Namespace " - + "Narrowing New Next Not Nothing NotInheritable NotOverridable Object On " - + "Operator Option Optional Or OrElse Overloads Overridable Overrides ParamArray " - + "Preserve Private Property Protected Public RaiseEvent ReadOnly ReDim " - + "REM RemoveHandler Resume Return Select Set Shadows Shared " - + "Short Single Static Step Stop String Structure Sub " - + "SyncLock Then Throw To True Try TryCast TypeOf Unicode " - + "Until Variant Wend When While With Widening WithEvents WriteOnly Xor"; - } - } + /// + /// The list of VB keywords. + /// + protected override string Keywords => "AddHandler AddressOf AndAlso Alias And Ansi As Assembly " + + "Auto Boolean ByRef Byte ByVal Call Case Catch " + + "CBool CByte CChar CDate CDec CDbl Char CInt " + + "Class CLng CObj Const CShort CSng CStr CType " + + "Date Decimal Declare Default Delegate Dim DirectCast Do " + + "Double Each Else ElseIf End EndIf Enum Erase Error " + + "Event Exit False Finally For Friend Function Get " + + "GetType Global GoSub GoTo Handles If Implements Imports In Inherits " + + "Integer Interface Is Let Lib Like Long Loop " + + "Me Mod Module MustInherit MustOverride MyBase MyClass Namespace " + + "Narrowing New Next Not Nothing NotInheritable NotOverridable Object On " + + "Operator Option Optional Or OrElse Overloads Overridable Overrides ParamArray " + + "Preserve Private Property Protected Public RaiseEvent ReadOnly ReDim " + + "REM RemoveHandler Resume Return Select Set Shadows Shared " + + "Short Single Static Step Stop String Structure Sub " + + "SyncLock Then Throw To True Try TryCast TypeOf Unicode " + + "Until Variant Wend When While With Widening WithEvents WriteOnly Xor"; - /// - /// The list of VB preprocessors. - /// - protected override string Preprocessors - { - get - { - return @"#\s*Const #\s*If #\s*Else #\s*ElseIf #\s*End\s+If " - + @"#\s*ExternalSource #\s*End\s+ExternalSource " - + @"#\s*Region #\s*End\s+Region"; - } - } - } + /// + /// The list of VB preprocessors. + /// + protected override string Preprocessors => @"#\s*Const #\s*If #\s*Else #\s*ElseIf #\s*End\s+If " + + @"#\s*ExternalSource #\s*End\s+ExternalSource " + + @"#\s*Region #\s*End\s+Region"; + } } diff --git a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs index 1318d5c861..2428d3c4aa 100644 --- a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs +++ b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs @@ -1,14 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Web; -using System.Linq; using AngleSharp; -using AngleSharp.Parser.Html; -using AngleSharp.Parser.Css; -using System.Collections.Generic; using AngleSharp.Dom; -using AngleSharp.Dom.Html; +using AngleSharp.Extensions; +using AngleSharp.Parser.Html; using Ganss.XSS; using SmartStore.Utilities.ObjectPools; @@ -19,96 +18,96 @@ namespace SmartStore.Core.Html ///
public partial class HtmlUtils { - private readonly static Regex _paragraphStartRegex = new Regex("

", RegexOptions.IgnoreCase); + private readonly static Regex _paragraphStartRegex = new Regex("

", RegexOptions.IgnoreCase); private readonly static Regex _paragraphEndRegex = new Regex("

", RegexOptions.IgnoreCase); - //private static Regex ampRegex = new Regex("&(?!(?:#[0-9]{2,4};|[a-z0-9]+;))", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /// - /// Formats the text - /// - /// Text - /// A value indicating whether to strip tags - /// A value indicating whether HTML is allowed - /// A value indicating whether HTML is allowed - /// A value indicating whether BBCode is allowed - /// A value indicating whether to resolve links - /// A value indicating whether to add "noFollow" tag - /// Formatted text - [Obsolete("Use specific formatter methods instead, e.g. StripTags(), SanitizeHtml() etc.")] - public static string FormatText( - string text, - bool stripTags, - bool convertPlainTextToHtml, - bool allowHtml, - bool allowBBCode, - bool resolveLinks, - bool addNoFollowTag) - { - - if (String.IsNullOrEmpty(text)) - return string.Empty; - - try - { - if (stripTags) - { - text = StripTags(text); - } - - if (allowHtml) - { - text = SanitizeHtml(text); - } - else - { - text = HttpUtility.HtmlEncode(text); - } - - if (convertPlainTextToHtml) - { - text = ConvertPlainTextToHtml(text); - } - - if (allowBBCode) - { - text = BBCodeHelper.ToHtml(text, true, true, true, true, true, true); - } - - if (resolveLinks) - { - text = ResolveLinksHelper.FormatText(text); - } - - if (addNoFollowTag) - { - //add noFollow tag. not implemented - } - } - catch (Exception exc) - { - text = string.Format("Text cannot be formatted. Error: {0}", exc.Message); - } - - return text; - } - - public static string SanitizeHtml(string html, bool isFragment = true) + //private static Regex ampRegex = new Regex("&(?!(?:#[0-9]{2,4};|[a-z0-9]+;))", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// + /// Formats the text + /// + /// Text + /// A value indicating whether to strip tags + /// A value indicating whether HTML is allowed + /// A value indicating whether HTML is allowed + /// A value indicating whether BBCode is allowed + /// A value indicating whether to resolve links + /// A value indicating whether to add "noFollow" tag + /// Formatted text + [Obsolete("Use specific formatter methods instead, e.g. StripTags(), SanitizeHtml() etc.")] + public static string FormatText( + string text, + bool stripTags, + bool convertPlainTextToHtml, + bool allowHtml, + bool allowBBCode, + bool resolveLinks, + bool addNoFollowTag) + { + + if (String.IsNullOrEmpty(text)) + return string.Empty; + + try + { + if (stripTags) + { + text = StripTags(text); + } + + if (allowHtml) + { + text = SanitizeHtml(text); + } + else + { + text = HttpUtility.HtmlEncode(text); + } + + if (convertPlainTextToHtml) + { + text = ConvertPlainTextToHtml(text); + } + + if (allowBBCode) + { + text = BBCodeHelper.ToHtml(text, true, true, true, true, true, true); + } + + if (resolveLinks) + { + text = ResolveLinksHelper.FormatText(text); + } + + if (addNoFollowTag) + { + //add noFollow tag. not implemented + } + } + catch (Exception exc) + { + text = string.Format("Text cannot be formatted. Error: {0}", exc.Message); + } + + return text; + } + + public static string SanitizeHtml(string html, bool isFragment = true) { if (string.IsNullOrEmpty(html)) return string.Empty; - var sanitizer = new HtmlSanitizer(); + var sanitizer = new HtmlSanitizer(); - if (isFragment) - { - return sanitizer.Sanitize(html); - } - else - { - return sanitizer.SanitizeDocument(html); - } + if (isFragment) + { + return sanitizer.Sanitize(html); + } + else + { + return sanitizer.SanitizeDocument(html); + } } - + /// /// Strips tags /// @@ -116,27 +115,89 @@ public static string SanitizeHtml(string html, bool isFragment = true) /// Formatted text public static string StripTags(string html) { - if (html.IsEmpty()) - return string.Empty; + if (html.IsEmpty()) + return string.Empty; - var ignoreTags = new HashSet(StringComparer.OrdinalIgnoreCase) { "script", "style", "svg", "img" }; - var parser = new HtmlParser(); - var doc = parser.Parse(html); + var removeTags = new HashSet(StringComparer.OrdinalIgnoreCase) { "script", "style", "svg", "img" }; + var parser = new HtmlParser(); - List removeElements = new List(); + using (var doc = parser.Parse(html)) + { + List removeElements = new List(); - foreach (var ignoreTag in ignoreTags) - { - removeElements.AddRange(doc.QuerySelectorAll(ignoreTag)); - } + foreach (var el in doc.All) + { + if (removeTags.Contains(el.TagName)) + { + removeElements.Add(el); + } + } - foreach (var el in removeElements) - { - el.Remove(); - } + foreach (var el in removeElements) + { + el.Remove(); + } - return doc.Body.TextContent; - } + return doc.Body.TextContent; + } + } + + /// + /// Checks whether HTML code only contains whitespace stuff ( 

]]>) + ///
+ public static bool IsEmptyHtml(string html) + { + if (html.IsEmpty()) + { + return true; + } + + if (html.Length > 500) + { + // (perf) we simply assume content if length is larger + return false; + } + + var parser = new HtmlParser(); + using (var doc = parser.Parse(html)) + { + foreach (var el in doc.All) + { + switch (el.TagName.ToLower()) + { + case "html": + case "head": + case "br": + continue; + case "body": + if (el.ChildElementCount > 0) + { + continue; + } + else + { + return el.Text().Trim().IsEmpty(); + } + case "p": + case "div": + case "span": + var text = el.Text().Trim(); + if (text.IsEmpty() || text == " ") + { + continue; + } + else + { + return false; + } + default: + return false; + } + } + + return true; + } + } /// /// Replace anchor text (remove a tag from the following url Name and output only the string "Name") @@ -170,7 +231,7 @@ public static string ConvertPlainTextToHtml(string text) return text; } - + /// /// Converts HTML to plain text /// @@ -223,24 +284,24 @@ public static string ConvertPlainTextToTable(string text, string tableCssClass = builder.AppendFormat("", tableCssClass.HasValue() ? "class='" + tableCssClass + "'" : ""); - lines.Where(x => x.HasValue()).Each(x => - { - builder.Append(""); - var tokens = x.Split(new char[] { ':' }, 2); - - if (tokens.Length > 1) - { - builder.AppendFormat("{0}", tokens[0]); - builder.AppendFormat("{0}", tokens[1]); - } - else - { - builder.Append(" "); - builder.AppendFormat("{0}",tokens[0]); - } - - builder.Append(""); - }); + lines.Where(x => x.HasValue()).Each(x => + { + builder.Append(""); + var tokens = x.Split(new char[] { ':' }, 2); + + if (tokens.Length > 1) + { + builder.AppendFormat("{0}", tokens[0]); + builder.AppendFormat("{0}", tokens[1]); + } + else + { + builder.Append(" "); + builder.AppendFormat("{0}", tokens[0]); + } + + builder.Append(""); + }); builder.Append(""); @@ -275,35 +336,35 @@ public static string ConvertPlainTextToParagraph(string text) return builder.ToString(); } - /// - /// Converts all occurences of pixel-based inline font-size expression to relative 'em' - /// - /// - /// - /// - public static string RelativizeFontSizes(string html, int baseFontSizePx = 16) - { - Guard.NotEmpty(html, nameof(html)); - Guard.IsPositive(baseFontSizePx, nameof(baseFontSizePx)); - - var parser = new HtmlParser(new AngleSharp.Configuration().WithCss()); - var doc = parser.Parse(html); - - var nodes = doc.QuerySelectorAll("*[style]"); - foreach (var node in nodes) - { - if (node.Style.FontSize is string s && s.EndsWith("px")) - { - var size = s.Substring(0, s.Length - 2).Convert(); - if (size > 0) - { - //node.Style.FontSize = Math.Round(((double)size / (double)baseFontSizePx), 4) + "em"; - node.Style.FontSize = "{0}em".FormatInvariant(Math.Round(((double)size / (double)baseFontSizePx), 4)); - } - } - } - - return doc.Body.InnerHtml; - } + /// + /// Converts all occurences of pixel-based inline font-size expression to relative 'em' + /// + /// + /// + /// + public static string RelativizeFontSizes(string html, int baseFontSizePx = 16) + { + Guard.NotEmpty(html, nameof(html)); + Guard.IsPositive(baseFontSizePx, nameof(baseFontSizePx)); + + var parser = new HtmlParser(new AngleSharp.Configuration().WithCss()); + var doc = parser.Parse(html); + + var nodes = doc.QuerySelectorAll("*[style]"); + foreach (var node in nodes) + { + if (node.Style.FontSize is string s && s.EndsWith("px")) + { + var size = s.Substring(0, s.Length - 2).Convert(); + if (size > 0) + { + //node.Style.FontSize = Math.Round(((double)size / (double)baseFontSizePx), 4) + "em"; + node.Style.FontSize = "{0}em".FormatInvariant(Math.Round(((double)size / (double)baseFontSizePx), 4)); + } + } + } + + return doc.Body.InnerHtml; + } } } diff --git a/src/Libraries/SmartStore.Core/HttpSecurityMode.cs b/src/Libraries/SmartStore.Core/HttpSecurityMode.cs index 5317183015..f159ce2466 100644 --- a/src/Libraries/SmartStore.Core/HttpSecurityMode.cs +++ b/src/Libraries/SmartStore.Core/HttpSecurityMode.cs @@ -1,6 +1,4 @@ -using System; - -namespace SmartStore.Core +namespace SmartStore.Core { public enum HttpSecurityMode { diff --git a/src/Libraries/SmartStore.Core/IActivatable.cs b/src/Libraries/SmartStore.Core/IActivatable.cs index b42cb554fc..48f59e9969 100644 --- a/src/Libraries/SmartStore.Core/IActivatable.cs +++ b/src/Libraries/SmartStore.Core/IActivatable.cs @@ -1,9 +1,7 @@ -using System; - -namespace SmartStore.Core +namespace SmartStore.Core { - public interface IActivatable - { - bool IsActive { get; } - } + public interface IActivatable + { + bool IsActive { get; } + } } diff --git a/src/Libraries/SmartStore.Core/IAuditable.cs b/src/Libraries/SmartStore.Core/IAuditable.cs index 60f4342efc..cb6c94ac78 100644 --- a/src/Libraries/SmartStore.Core/IAuditable.cs +++ b/src/Libraries/SmartStore.Core/IAuditable.cs @@ -2,16 +2,16 @@ namespace SmartStore.Core { - public interface IAuditable - { - /// - /// Gets or sets the date and time of entity creation - /// - DateTime CreatedOnUtc { get; set; } + public interface IAuditable + { + /// + /// Gets or sets the date and time of entity creation + /// + DateTime CreatedOnUtc { get; set; } - /// - /// Gets or sets the date and time of entity update - /// - DateTime UpdatedOnUtc { get; set; } - } + /// + /// Gets or sets the date and time of entity update + /// + DateTime UpdatedOnUtc { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/IMergedData.cs b/src/Libraries/SmartStore.Core/IMergedData.cs index 8233a55987..2cf39de6dd 100644 --- a/src/Libraries/SmartStore.Core/IMergedData.cs +++ b/src/Libraries/SmartStore.Core/IMergedData.cs @@ -5,43 +5,43 @@ namespace SmartStore.Core { - public interface IMergedData - { - bool MergedDataIgnore { get; set; } - Dictionary MergedDataValues { get; } - } + public interface IMergedData + { + bool MergedDataIgnore { get; set; } + Dictionary MergedDataValues { get; } + } - public static class IMergedDataExtensions - { - public static T GetMergedDataValue(this IMergedData mergedData, string key, T defaultValue) - { - if (mergedData.MergedDataValues == null) - return defaultValue; + public static class IMergedDataExtensions + { + public static T GetMergedDataValue(this IMergedData mergedData, string key, T defaultValue) + { + if (mergedData.MergedDataValues == null) + return defaultValue; - if (mergedData.MergedDataIgnore) - return defaultValue; + if (mergedData.MergedDataIgnore) + return defaultValue; - if (mergedData is BaseEntity && HostingEnvironment.IsHosted) - { - // This is absolutely bad coding! But I don't see any alternatives. - // When the passed object is a (EF)-trackable entity, - // we cannot return the merged value while EF performs - // change detection, because entity properties could be set to modified, - // where in fact nothing has changed. - var dbContext = EngineContext.Current.Resolve(); - if (dbContext.IsDetectingChanges()) - { - return defaultValue; - } - } + if (mergedData is BaseEntity && HostingEnvironment.IsHosted) + { + // This is absolutely bad coding! But I don't see any alternatives. + // When the passed object is a (EF)-trackable entity, + // we cannot return the merged value while EF performs + // change detection, because entity properties could be set to modified, + // where in fact nothing has changed. + var dbContext = EngineContext.Current.Resolve(); + if (dbContext.IsDetectingChanges()) + { + return defaultValue; + } + } - object value; - if (mergedData.MergedDataValues.TryGetValue(key, out value)) - { - return (T)value; - } + object value; + if (mergedData.MergedDataValues.TryGetValue(key, out value)) + { + return (T)value; + } - return defaultValue; - } - } + return defaultValue; + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/DirectoryHasher.cs b/src/Libraries/SmartStore.Core/IO/DirectoryHasher.cs new file mode 100644 index 0000000000..91e96b0ed5 --- /dev/null +++ b/src/Libraries/SmartStore.Core/IO/DirectoryHasher.cs @@ -0,0 +1,154 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; +using System.Web.Hosting; +using SmartStore.Core.Data; +using SmartStore.Utilities; + +namespace SmartStore.Core.IO +{ + public class DirectoryHasher + { + private static readonly string _appRootPath; + private static readonly string _defaultStoragePath; + + private readonly string _path; + private readonly string _searchPattern; + private readonly bool _deep; + private readonly string _storagePath; + + private int? _lastHash; + private int? _currentHash; + private string _lookupKey; + + static DirectoryHasher() + { + _appRootPath = HostingEnvironment.ApplicationPhysicalPath; + _defaultStoragePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "Hash"); + Directory.CreateDirectory(_defaultStoragePath); + } + + public DirectoryHasher(string path) + : this(path, "*", false, null) + { + } + + public DirectoryHasher(string path, string searchPattern, bool deep = false, string storagePath = null) + { + Guard.NotEmpty(path, nameof(path)); + + if (!path.StartsWith(_appRootPath, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Path must be contained within current app root path '{_appRootPath}'", nameof(path)); + } + + _path = path; + _searchPattern = searchPattern ?? "*"; + _deep = deep; + _storagePath = storagePath ?? _defaultStoragePath; + } + + public bool HasChanged => LastHash != CurrentHash; + + public int? LastHash + { + get + { + if (_lastHash == null) + { + _lastHash = ReadLastHash(); + } + + return _lastHash == -1 ? (int?)null : _lastHash.Value; + } + } + + public int CurrentHash => (_currentHash ?? (_currentHash = ComputeHash())).Value; + + public string LookupKey => _lookupKey ?? (_lookupKey = BuildLookupKey()); + + public void Refresh() + { + _currentHash = null; + } + + //public void Reset() + //{ + // _lastHash = -1; + + // var path = Path.Combine(_storagePath, LookupKey + ".hash"); + // if (File.Exists(path)) + // { + // File.Delete(path); + // } + //} + + public void Persist() + { + if (LastHash == CurrentHash) + return; + + var path = Path.Combine(_storagePath, LookupKey + ".hash"); + File.WriteAllText(path, CurrentHash.ToString(CultureInfo.InvariantCulture), Encoding.UTF8); + _lastHash = CurrentHash; + } + + protected virtual int ComputeHash() + { + var hash = 0; + var di = new DirectoryInfo(_path); + + if (di.Exists) + { + var files = di.GetFiles(_searchPattern, _deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + if (files.Length > 0) + { + // Calculate current hash + var hashCombiner = new HashCodeCombiner(); + foreach (var file in files) + { + hashCombiner.Add(file); + } + + hash = hashCombiner.CombinedHash; + } + } + + return hash; + } + + protected virtual int ReadLastHash() + { + var path = Path.Combine(_storagePath, LookupKey + ".hash"); + var hash = File.Exists(path) + ? ConvertHash(File.ReadAllText(path, Encoding.UTF8)) + : -1; + + return hash; + } + + protected virtual string BuildLookupKey() + { + var key = PathHelper.MakeRelativePath(_appRootPath, _path, "_").ToLower(); + + if (_deep) + key += "_d"; + + if (_searchPattern.HasValue() && _searchPattern != "*") + key += "_" + _searchPattern.ToLower().ToValidFileName("x"); + + return key.Trim(new char[] { '/', '\\' }); + } + + private static int ConvertHash(string val) + { + if (Int32.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outVal)) + { + return outVal; + } + + return 0; + } + } +} diff --git a/src/Libraries/SmartStore.Core/IO/IFile.cs b/src/Libraries/SmartStore.Core/IO/IFile.cs index 6d86299fc8..ca64f714e6 100644 --- a/src/Libraries/SmartStore.Core/IO/IFile.cs +++ b/src/Libraries/SmartStore.Core/IO/IFile.cs @@ -7,66 +7,66 @@ namespace SmartStore.Core.IO { public interface IFile { - /// - /// The path relative to the storage root - /// - string Path { get; } + /// + /// The path relative to the storage root + /// + string Path { get; } - /// - /// The path without the file part, but with trailing slash - /// - string Directory { get; } + /// + /// The path without the file part, but with trailing slash + /// + string Directory { get; } - /// - /// File name including extension - /// - string Name { get; } + /// + /// File name including extension + /// + string Name { get; } - /// - /// File name excluding extension - /// - string Title { get; } + /// + /// File name excluding extension + /// + string Title { get; } - /// - /// Size in bytes - /// - long Size { get; } + /// + /// Size in bytes + /// + long Size { get; } - /// - /// Expressed as UTC time - /// - DateTime LastUpdated { get; } + /// + /// Expressed as UTC time + /// + DateTime LastUpdated { get; } - /// - /// File extension including dot - /// - string Extension { get; } + /// + /// File extension including dot + /// + string Extension { get; } - /// - /// Dimensions, if the file is an image. - /// - Size Dimensions { get; } + /// + /// Dimensions, if the file is an image. + /// + Size Dimensions { get; } - bool Exists { get; } + bool Exists { get; } - /// - /// Creates a stream for reading from the file. - /// - Stream OpenRead(); + /// + /// Creates a stream for reading from the file. + /// + Stream OpenRead(); /// /// Creates a stream for writing to the file. /// Stream OpenWrite(); - /// - /// Creates a stream for writing to the file, and truncates the existing content. - /// - Stream CreateFile(); + /// + /// Creates a stream for writing to the file, and truncates the existing content. + /// + Stream CreateFile(); - /// - /// Asynchronously creates a stream for writing to the file, and truncates the existing content. - /// - Task CreateFileAsync(); - } + /// + /// Asynchronously creates a stream for writing to the file, and truncates the existing content. + /// + Task CreateFileAsync(); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs index 7fcb6fc793..74598c9391 100644 --- a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs +++ b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs @@ -7,108 +7,108 @@ namespace SmartStore.Core.IO { public interface IFileSystem { - /// - /// Checks whether the underlying storage is remote, like 'Azure' for example. - /// - bool IsCloudStorage { get; } - - /// - /// Gets the root path - /// - string Root { get; } - - /// - /// Retrieves the public URL for a given file within the storage provider. - /// - /// The file to resolve the public url for. - /// - /// 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(IFile file, bool forCloud = false); - - /// - /// 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, bool forCloud = false); - - /// - /// Retrieves the path within the storage provider for a given public url. - /// - /// The virtual or public url of a file. - /// The storage path or null if the media is not in a correct format. - string GetStoragePath(string url); - - /// - /// Checks if the given file exists within the storage provider. - /// - /// The relative path within the storage provider. - /// True if the file exists; False otherwise. - bool FileExists(string path); - - /// - /// Checks if the given folder exists within the storage provider. - /// - /// The relative path within the storage provider. - /// True if the folder exists; False otherwise. - bool FolderExists(string path); - - /// - /// Retrieves a file within the storage provider. - /// - /// The relative path to the file within the storage provider. - /// The file. - /// If the file is not found. - IFile GetFile(string path); - - /// - /// Retrieves a folder within the storage provider. - /// - /// The relative path to the folder within the storage provider. - /// The folder. - /// If the folder is not found. - IFolder GetFolder(string path); - - /// - /// Retrieves a folder for file path within the storage provider. - /// - /// The relative path to the file within the storage provider. - /// The folder for the file. - /// 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 not 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, bool deep = true); - - /// - /// Lists the files within a storage provider's path. - /// - /// The relative path to the folder which files to list. - /// The list of files in the folder. - IEnumerable ListFiles(string path); + /// + /// Checks whether the underlying storage is remote, like 'Azure' for example. + /// + bool IsCloudStorage { get; } + + /// + /// Gets the root path + /// + string Root { get; } + + /// + /// Retrieves the public URL for a given file within the storage provider. + /// + /// The file to resolve the public url for. + /// + /// 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(IFile file, bool forCloud = false); + + /// + /// 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, bool forCloud = false); + + /// + /// Retrieves the path within the storage provider for a given public url. + /// + /// The virtual or public url of a file. + /// The storage path or null if the media is not in a correct format. + string GetStoragePath(string url); + + /// + /// Checks if the given file exists within the storage provider. + /// + /// The relative path within the storage provider. + /// True if the file exists; False otherwise. + bool FileExists(string path); + + /// + /// Checks if the given folder exists within the storage provider. + /// + /// The relative path within the storage provider. + /// True if the folder exists; False otherwise. + bool FolderExists(string path); + + /// + /// Retrieves a file within the storage provider. + /// + /// The relative path to the file within the storage provider. + /// The file. + /// If the file is not found. + IFile GetFile(string path); + + /// + /// Retrieves a folder within the storage provider. + /// + /// The relative path to the folder within the storage provider. + /// The folder. + /// If the folder is not found. + IFolder GetFolder(string path); + + /// + /// Retrieves a folder for file path within the storage provider. + /// + /// The relative path to the file within the storage provider. + /// The folder for the file. + /// 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 not 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, bool deep = true); + + /// + /// Lists the files within a storage provider's path. + /// + /// The relative path to the folder which files to list. + /// The list of files in the folder. + IEnumerable ListFiles(string path); /// /// Lists the folders within a storage provider's path. @@ -131,46 +131,46 @@ public interface IFileSystem /// If the folder doesn't exist. void DeleteFolder(string path); - /// - /// Renames a folder in the storage provider. - /// - /// The relative path to the folder to be renamed. - /// The relative path to the new folder. - void RenameFolder(string path, string newPath); - - /// - /// Copies a folder and all its content to another folder. - /// - /// Path of source folder - /// Path of destination folder - /// Whether to overwrite existing files - void CopyFolder(string path, string destinationPath, bool overwrite = 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 - /// The new unique path, or null if method returns false - /// - /// false when does not exist yet. true otherwise. - /// - bool CheckUniqueFileName(string path, out string newPath); - - /// - /// Deletes a file in the storage provider. - /// - /// The relative path to the file to be deleted. - /// If the file doesn't exist. - void DeleteFile(string path); - - /// - /// Renames a file in the storage provider. - /// - /// The relative path to the file to be renamed. - /// The relative path to the new file. - void RenameFile(string path, string newPath); + /// + /// Renames a folder in the storage provider. + /// + /// The relative path to the folder to be renamed. + /// The relative path to the new folder. + void RenameFolder(string path, string newPath); + + /// + /// Copies a folder and all its content to another folder. + /// + /// Path of source folder + /// Path of destination folder + /// Whether to overwrite existing files + void CopyFolder(string path, string destinationPath, bool overwrite = 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 + /// The new unique path, or null if method returns false + /// + /// false when does not exist yet. true otherwise. + /// + bool CheckUniqueFileName(string path, out string newPath); + + /// + /// Deletes a file in the storage provider. + /// + /// The relative path to the file to be deleted. + /// If the file doesn't exist. + void DeleteFile(string path); + + /// + /// Renames a file in the storage provider. + /// + /// The relative path to the file to be renamed. + /// The relative path to the new file. + void RenameFile(string path, string newPath); /// /// Creates a file in the storage provider. @@ -180,44 +180,44 @@ public interface IFileSystem /// The created file. IFile CreateFile(string path); - /// - /// Asynchronously creates a file in the storage provider. - /// - /// The relative path to the file to be created. - /// If the file already exists. - /// The created file. - Task CreateFileAsync(string path); - - /// - /// Copies a file in the storage provider. - /// - /// The relative path to the file to be copied. - /// The relative path to the new file. - /// Whether to overwrite a file with same name. - void CopyFile(string path, string newPath, bool overwrite = false); - - /// - /// Saves a stream in the storage provider. If the file already exists, it will be overwritten. - /// - /// The relative path to the file to be created. - /// The stream to be saved. - /// If the stream can't be saved due to access permissions. - void SaveStream(string path, Stream inputStream); - - /// - /// Asynchronously saves a stream in the storage provider. If the file already exists, it will be overwritten. - /// - /// The relative path to the file to be created. - /// The stream to be saved. - /// If the stream can't be saved due to access permissions. - Task SaveStreamAsync(string path, Stream inputStream); - - /// - /// Combines two paths. - /// - /// The parent path. - /// The child path. - /// The combined path. - string Combine(string path1, string path2); - } + /// + /// Asynchronously creates a file in the storage provider. + /// + /// The relative path to the file to be created. + /// If the file already exists. + /// The created file. + Task CreateFileAsync(string path); + + /// + /// Copies a file in the storage provider. + /// + /// The relative path to the file to be copied. + /// The relative path to the new file. + /// Whether to overwrite a file with same name. + void CopyFile(string path, string newPath, bool overwrite = false); + + /// + /// Saves a stream in the storage provider. If the file already exists, it will be overwritten. + /// + /// The relative path to the file to be created. + /// The stream to be saved. + /// If the stream can't be saved due to access permissions. + void SaveStream(string path, Stream inputStream); + + /// + /// Asynchronously saves a stream in the storage provider. If the file already exists, it will be overwritten. + /// + /// The relative path to the file to be created. + /// The stream to be saved. + /// If the stream can't be saved due to access permissions. + Task SaveStreamAsync(string path, Stream inputStream); + + /// + /// Combines two paths. + /// + /// The parent path. + /// The child path. + /// The combined path. + string Combine(string path1, string path2); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs b/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs index 0d1659bad1..ee815cc1e8 100644 --- a/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs +++ b/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs @@ -1,26 +1,24 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace SmartStore.Core.IO { - public static class IFileSystemExtensions - { - public static void WriteAllText(this IFileSystem fileSystem, string path, string contents, Encoding encoding = null) - { - Guard.NotEmpty(path, nameof(path)); - Guard.NotEmpty(contents, nameof(contents)); - - if (fileSystem.FileExists(path)) - { - fileSystem.DeleteFile(path); - } + public static class IFileSystemExtensions + { + public static void WriteAllText(this IFileSystem fileSystem, string path, string contents, Encoding encoding = null) + { + Guard.NotEmpty(path, nameof(path)); + Guard.NotEmpty(contents, nameof(contents)); + + if (fileSystem.FileExists(path)) + { + fileSystem.DeleteFile(path); + } - var file = fileSystem.CreateFile(path); + var file = fileSystem.CreateFile(path); using (var stream = file.OpenWrite()) using (var streamWriter = new StreamWriter(stream, encoding ?? new UTF8Encoding(false, true))) @@ -29,175 +27,175 @@ public static void WriteAllText(this IFileSystem fileSystem, string path, string } } - public static async Task WriteAllTextAsync(this IFileSystem fileSystem, string path, string contents, Encoding encoding = null) - { - Guard.NotEmpty(path, nameof(path)); - Guard.NotEmpty(contents, nameof(contents)); - - if (fileSystem.FileExists(path)) - { - fileSystem.DeleteFile(path); - } - - var file = fileSystem.CreateFile(path); - using (var stream = file.OpenWrite()) - using (var streamWriter = new StreamWriter(stream, encoding ?? new UTF8Encoding(false, true))) - { - await streamWriter.WriteAsync(contents); - } - } - - public static string ReadAllText(this IFileSystem fileSystem, string path) - { - Guard.NotEmpty(path, nameof(path)); - - if (!fileSystem.FileExists(path)) - { - return String.Empty; - } - - var file = fileSystem.GetFile(path); - using (var stream = file.OpenRead()) - using (var streamReader = new StreamReader(stream, Encoding.UTF8)) - { - return streamReader.ReadToEnd(); - } - } - - public static async Task ReadAllTextAsync(this IFileSystem fileSystem, string path) - { - Guard.NotEmpty(path, nameof(path)); - - if (!fileSystem.FileExists(path)) - { - return String.Empty; - } - - var file = fileSystem.GetFile(path); - using (var stream = file.OpenRead()) - using (var streamReader = new StreamReader(stream, Encoding.UTF8)) - { - return await streamReader.ReadToEndAsync(); - } - } - - - public static void WriteAllBytes(this IFileSystem fileSystem, string path, byte[] contents) - { - Guard.NotEmpty(path, nameof(path)); - Guard.NotNull(contents, nameof(contents)); - - if (fileSystem.FileExists(path)) - { - fileSystem.DeleteFile(path); - } - - var file = fileSystem.CreateFile(path); - using (var stream = file.OpenWrite()) - { - stream.Write(contents, 0, contents.Length); - } - } - - public static async Task WriteAllBytesAsync(this IFileSystem fileSystem, string path, byte[] contents) - { - Guard.NotEmpty(path, nameof(path)); - Guard.NotNull(contents, nameof(contents)); - - if (fileSystem.FileExists(path)) - { - fileSystem.DeleteFile(path); - } - - var file = await fileSystem.CreateFileAsync(path); - using (var stream = file.OpenWrite()) - { - await stream.WriteAsync(contents, 0, contents.Length); - } - } - - public static byte[] ReadAllBytes(this IFileSystem fileSystem, string path) - { - Guard.NotEmpty(path, nameof(path)); - - if (!fileSystem.FileExists(path)) - { - return null; - } - - var file = fileSystem.GetFile(path); - using (var stream = file.OpenRead()) - { - return stream.ToByteArray(); - } - } - - public static async Task ReadAllBytesAsync(this IFileSystem fileSystem, string path) - { - Guard.NotEmpty(path, nameof(path)); - - if (!fileSystem.FileExists(path)) - { - return null; - } - - var file = fileSystem.GetFile(path); - using (var stream = file.OpenRead()) - { - return await stream.ToByteArrayAsync(); - } - } - - - /// - /// Tries to save a stream in the storage provider. - /// - /// The relative path to the file to be created. - /// The stream to be saved. - /// True if success; False otherwise. - public static bool TrySaveStream(this IFileSystem fileSystem, string path, Stream inputStream) - { - try - { - if (fileSystem.FileExists(path)) - { - return false; - } - - fileSystem.SaveStream(path, inputStream); - } - catch - { - return false; - } - - return true; - } - - /// - /// Asynchronously tries to save a stream in the storage provider. - /// - /// The relative path to the file to be created. - /// The stream to be saved. - /// True if success; False otherwise. - public static async Task TrySaveStreamAsync(this IFileSystem fileSystem, string path, Stream inputStream) - { - try - { - if (fileSystem.FileExists(path)) - { - return false; - } - - await fileSystem.SaveStreamAsync(path, inputStream); - } - catch - { - return false; - } - - return true; - } + public static async Task WriteAllTextAsync(this IFileSystem fileSystem, string path, string contents, Encoding encoding = null) + { + Guard.NotEmpty(path, nameof(path)); + Guard.NotEmpty(contents, nameof(contents)); + + if (fileSystem.FileExists(path)) + { + fileSystem.DeleteFile(path); + } + + var file = fileSystem.CreateFile(path); + using (var stream = file.OpenWrite()) + using (var streamWriter = new StreamWriter(stream, encoding ?? new UTF8Encoding(false, true))) + { + await streamWriter.WriteAsync(contents); + } + } + + public static string ReadAllText(this IFileSystem fileSystem, string path) + { + Guard.NotEmpty(path, nameof(path)); + + if (!fileSystem.FileExists(path)) + { + return String.Empty; + } + + var file = fileSystem.GetFile(path); + using (var stream = file.OpenRead()) + using (var streamReader = new StreamReader(stream, Encoding.UTF8)) + { + return streamReader.ReadToEnd(); + } + } + + public static async Task ReadAllTextAsync(this IFileSystem fileSystem, string path) + { + Guard.NotEmpty(path, nameof(path)); + + if (!fileSystem.FileExists(path)) + { + return String.Empty; + } + + var file = fileSystem.GetFile(path); + using (var stream = file.OpenRead()) + using (var streamReader = new StreamReader(stream, Encoding.UTF8)) + { + return await streamReader.ReadToEndAsync(); + } + } + + + public static void WriteAllBytes(this IFileSystem fileSystem, string path, byte[] contents) + { + Guard.NotEmpty(path, nameof(path)); + Guard.NotNull(contents, nameof(contents)); + + if (fileSystem.FileExists(path)) + { + fileSystem.DeleteFile(path); + } + + var file = fileSystem.CreateFile(path); + using (var stream = file.OpenWrite()) + { + stream.Write(contents, 0, contents.Length); + } + } + + public static async Task WriteAllBytesAsync(this IFileSystem fileSystem, string path, byte[] contents) + { + Guard.NotEmpty(path, nameof(path)); + Guard.NotNull(contents, nameof(contents)); + + if (fileSystem.FileExists(path)) + { + fileSystem.DeleteFile(path); + } + + var file = await fileSystem.CreateFileAsync(path); + using (var stream = file.OpenWrite()) + { + await stream.WriteAsync(contents, 0, contents.Length); + } + } + + public static byte[] ReadAllBytes(this IFileSystem fileSystem, string path) + { + Guard.NotEmpty(path, nameof(path)); + + if (!fileSystem.FileExists(path)) + { + return null; + } + + var file = fileSystem.GetFile(path); + using (var stream = file.OpenRead()) + { + return stream.ToByteArray(); + } + } + + public static async Task ReadAllBytesAsync(this IFileSystem fileSystem, string path) + { + Guard.NotEmpty(path, nameof(path)); + + if (!fileSystem.FileExists(path)) + { + return null; + } + + var file = fileSystem.GetFile(path); + using (var stream = file.OpenRead()) + { + return await stream.ToByteArrayAsync(); + } + } + + + /// + /// Tries to save a stream in the storage provider. + /// + /// The relative path to the file to be created. + /// The stream to be saved. + /// True if success; False otherwise. + public static bool TrySaveStream(this IFileSystem fileSystem, string path, Stream inputStream) + { + try + { + if (fileSystem.FileExists(path)) + { + return false; + } + + fileSystem.SaveStream(path, inputStream); + } + catch + { + return false; + } + + return true; + } + + /// + /// Asynchronously tries to save a stream in the storage provider. + /// + /// The relative path to the file to be created. + /// The stream to be saved. + /// True if success; False otherwise. + public static async Task TrySaveStreamAsync(this IFileSystem fileSystem, string path, Stream inputStream) + { + try + { + if (fileSystem.FileExists(path)) + { + return false; + } + + await fileSystem.SaveStreamAsync(path, inputStream); + } + catch + { + return false; + } + + return true; + } /// /// Retrieves the count of files within a path. @@ -208,9 +206,9 @@ public static async Task TrySaveStreamAsync(this IFileSystem fileSystem, s /// Total count of files. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long CountFiles(this IFileSystem fileSystem, string path, string pattern, bool deep = true) - { - return fileSystem.CountFiles(path, pattern, null, deep); - } + { + return fileSystem.CountFiles(path, pattern, null, deep); + } /// /// Retrieves the count of files within a path. @@ -220,8 +218,8 @@ public static long CountFiles(this IFileSystem fileSystem, string path, string p /// Total count of files. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long CountFiles(this IFileSystem fileSystem, string path, bool deep = true) - { - return fileSystem.CountFiles(path, "*", null, deep); - } - } + { + return fileSystem.CountFiles(path, "*", null, deep); + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/IFolder.cs b/src/Libraries/SmartStore.Core/IO/IFolder.cs index 77f46d0546..49eda06377 100644 --- a/src/Libraries/SmartStore.Core/IO/IFolder.cs +++ b/src/Libraries/SmartStore.Core/IO/IFolder.cs @@ -2,36 +2,36 @@ namespace SmartStore.Core.IO { - public interface IFolder + public interface IFolder { - /// - /// The path relative to the storage root - /// - string Path { get; } + /// + /// The path relative to the storage root + /// + string Path { get; } - /// - /// The foldername - /// - string Name { get; } + /// + /// The foldername + /// + string Name { get; } - /// - /// Size sum of all containing files - including those in subfolders - in bytes - /// - long Size { get; } + /// + /// Size sum of all containing files - including those in subfolders - in bytes + /// + long Size { get; } - /// - /// Whether the folder exists - /// - bool Exists { get; } + /// + /// Whether the folder exists + /// + bool Exists { get; } - /// - /// Expressed as UTC time - /// - DateTime LastUpdated { get; } + /// + /// Expressed as UTC time + /// + DateTime LastUpdated { get; } - /// - /// The parent folder - /// - IFolder Parent { get; } - } + /// + /// The parent folder + /// + IFolder Parent { get; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/IO/ImageHeader.cs b/src/Libraries/SmartStore.Core/IO/ImageHeader.cs index 82b0c95f4e..231e5b3fe6 100644 --- a/src/Libraries/SmartStore.Core/IO/ImageHeader.cs +++ b/src/Libraries/SmartStore.Core/IO/ImageHeader.cs @@ -8,181 +8,189 @@ 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); - } - else if (mime == "image/svg+xml") + /// + /// 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 readonly 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 }, DecodeJpeg }, + //{ new byte[] { 0xff, 0xd8, 0xff, 0xe0 }, DecodeJpeg }, + //{ new byte[] { 0xff }, DecodeJpeg2 } + }; + + private static readonly 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); + //} + if (mime == "image/svg+xml") { return GetDimensionsFromSvg(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(); - } - else - { - input.Seek(0, SeekOrigin.Begin); - } - } - } - - /// - /// 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); - } - } + { + 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(); + } + else + { + input.Seek(0, SeekOrigin.Begin); + } + } + } + + /// + /// 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)) + { + var size = kvPair.Value(binaryReader); + if (size.IsEmpty) + { + break; + } + else + { + return size; + } + } + } + } + + 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 Size GetDimensionsFromSvg(Stream input) { @@ -221,198 +229,215 @@ private static Size GetDimensionsFromSvg(Stream input) } 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 - } + { + 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); + } + + private static Size DecodeJpeg(BinaryReader reader) + { + string state = "started"; + while (true) + { + byte[] c; + if (state == "started") + { + c = reader.ReadBytes(1); + state = (c[0] == 0xFF) ? "sof" : "started"; + } + else if (state == "sof") + { + c = reader.ReadBytes(1); + if (c[0] >= 0xe0 && c[0] <= 0xef) + { + state = "skipframe"; + } + else if ((c[0] >= 0xC0 && c[0] <= 0xC3) || (c[0] >= 0xC5 && c[0] <= 0xC7) || (c[0] >= 0xC9 && c[0] <= 0xCB) || (c[0] >= 0xCD && c[0] <= 0xCF)) + { + state = "readsize"; + } + else if (c[0] == 0xFF) + { + state = "sof"; + } + else + { + state = "skipframe"; + } + } + else if (state == "skipframe") + { + c = reader.ReadBytes(2); + int skip = ReadInt(c) - 2; + reader.ReadBytes(skip); + state = "started"; + } + else if (state == "readsize") + { + c = reader.ReadBytes(7); + var width = ReadInt(new[] { c[5], c[6] }); + var height = ReadInt(new[] { c[3], c[4] }); + return new Size(width, height); + } + } + + throw new UnknownImageFormatException(); + } + + private static int ReadInt(byte[] chars) + { + return (chars[0] << 8) + chars[1]; + } + + #region Experiments + + //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 eb50af6fa8..45bb9e8c77 100644 --- a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs +++ b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs @@ -10,526 +10,520 @@ namespace SmartStore.Core.IO { - public class LocalFileSystem : IFileSystem - { - private string _root; - private string _publicPath; // /Shop/base - private string _storagePath; // C:\SMNET\base - private bool _isCloudStorage; // When public URL is outside of current app - - public LocalFileSystem() - : this(string.Empty, string.Empty) - { - } - - protected internal LocalFileSystem(string basePath) - : this(basePath, string.Empty) - { - } - - protected internal LocalFileSystem(string basePath, string publicPath) - { - basePath = basePath.EmptyNull(); - - var pathIsAbsolute = PathHelper.IsAbsolutePhysicalPath(basePath); - - NormalizeStoragePath(ref basePath, pathIsAbsolute); - - _publicPath = NormalizePublicPath(publicPath, basePath, pathIsAbsolute); - - _root = basePath; - } - - public bool IsCloudStorage - { - get { return _isCloudStorage; } - } - - private void NormalizeStoragePath(ref string basePath, bool basePathIsAbsolute) - { - if (basePathIsAbsolute) - { - // Path is fully qualified (UNC or absolute with drive letter) - - // In this case this is our root path... - basePath = basePath.TrimEnd(Path.DirectorySeparatorChar); - - // ...AND our physical storage path - _storagePath = basePath; - } - else - { - // Path is relative to the app root - basePath = basePath.TrimEnd('/').EnsureStartsWith("/"); - _storagePath = CommonHelper.MapPath("~" + basePath, false); - } - } - - private string NormalizePublicPath(string publicPath, string basePath, bool basePathIsAbsolute) - { - publicPath = publicPath.EmptyNull(); - - if (basePathIsAbsolute) - { - if (publicPath.IsEmpty() || (!publicPath.StartsWith("~/") && !publicPath.IsWebUrl(true))) - { - var streamMedia = CommonHelper.GetAppSetting("sm:StreamRemoteMedia", true); - if (!streamMedia) - { - throw new ArgumentException(@"When the base path is a fully qualified path and remote media streaming is disabled, + public class LocalFileSystem : IFileSystem + { + private readonly string _root; + private readonly string _publicPath; // /Shop/base + private string _storagePath; // C:\SMNET\base + private bool _isCloudStorage; // When public URL is outside of current app + + public LocalFileSystem() + : this(string.Empty, string.Empty) + { + } + + protected internal LocalFileSystem(string basePath) + : this(basePath, string.Empty) + { + } + + protected internal LocalFileSystem(string basePath, string publicPath) + { + basePath = basePath.EmptyNull(); + + var pathIsAbsolute = PathHelper.IsAbsolutePhysicalPath(basePath); + + NormalizeStoragePath(ref basePath, pathIsAbsolute); + + _publicPath = NormalizePublicPath(publicPath, basePath, pathIsAbsolute); + + _root = basePath; + } + + public bool IsCloudStorage => _isCloudStorage; + + private void NormalizeStoragePath(ref string basePath, bool basePathIsAbsolute) + { + if (basePathIsAbsolute) + { + // Path is fully qualified (UNC or absolute with drive letter) + + // In this case this is our root path... + basePath = basePath.TrimEnd(Path.DirectorySeparatorChar); + + // ...AND our physical storage path + _storagePath = basePath; + } + else + { + // Path is relative to the app root + basePath = basePath.TrimEnd('/').EnsureStartsWith("/"); + _storagePath = CommonHelper.MapPath("~" + basePath, false); + } + } + + private string NormalizePublicPath(string publicPath, string basePath, bool basePathIsAbsolute) + { + publicPath = publicPath.EmptyNull(); + + if (basePathIsAbsolute) + { + if (publicPath.IsEmpty() || (!publicPath.StartsWith("~/") && !publicPath.IsWebUrl(true))) + { + var streamMedia = CommonHelper.GetAppSetting("sm:StreamRemoteMedia", true); + if (!streamMedia) + { + throw new ArgumentException(@"When the base path is a fully qualified path and remote media streaming is disabled, the public path must not be empty, and either be a fully qualified URL or a virtual path (e.g.: ~/Media)", nameof(publicPath)); - } - } - } - - var appVirtualPath = HostingEnvironment.IsHosted - ? HostingEnvironment.ApplicationVirtualPath.TrimEnd('/') - : string.Empty; - - if (publicPath.StartsWith("~/")) - { - // prepend application virtual path - return appVirtualPath + publicPath.Substring(1); - } - - if (publicPath.IsEmpty() && !basePathIsAbsolute) - { - // > /MyAppRoot/Media - return appVirtualPath + basePath; - } - - _isCloudStorage = true; - - return publicPath; - } - - public string Root - { - get { return _root; } - } - - /// - /// Maps a relative path into the storage path. - /// - /// The relative path to be mapped. - /// The relative path combined with the storage path. - protected virtual string MapStorage(string path) - { - var mappedPath = string.IsNullOrEmpty(path) ? _storagePath : Path.Combine(_storagePath, Fix(path)); - return ValidatePath(_storagePath, mappedPath); - } - - /// - /// Maps a relative path into the public path. - /// - /// The relative path to be mapped. - /// The relative path combined with the public path in an URL friendly format ('/' character for directory separator). - protected virtual string MapPublic(string path) - { - return string.IsNullOrEmpty(path) ? _publicPath : HttpUtility.UrlDecode(Path.Combine(_publicPath, path).Replace(Path.DirectorySeparatorChar, '/')); - } - - static string Fix(string path) - { - return string.IsNullOrEmpty(path) - ? "" - : Path.DirectorySeparatorChar != '/' - ? path.Replace('/', Path.DirectorySeparatorChar).TrimStart('/', '\\') - : path.TrimStart('/', '\\'); - } - - public string GetPublicUrl(IFile file, bool forCloud = false) - { - return MapPublic(file.Path); - } - - public string GetPublicUrl(string path, bool forCloud = false) - { - return MapPublic(path); - } - - public virtual string GetStoragePath(string url) - { - if (url.HasValue()) - { - if (url.StartsWith("~/")) - { - url = VirtualPathUtility.ToAbsolute(url); - } - - if (url.StartsWith(_publicPath)) - { - return HttpUtility.UrlDecode(url.Substring(_publicPath.Length).Replace('/', Path.DirectorySeparatorChar)); - } - } - - return null; - } - - public bool FileExists(string path) - { - return File.Exists(MapStorage(path)); - } - - public bool FolderExists(string path) - { - return Directory.Exists(MapStorage(path)); - } - - public IFile GetFile(string path) - { - return new LocalFile(path, MapStorage(path)); - } - - public IFolder GetFolder(string path) - { - return new LocalFolder(path, MapStorage(path)); - } - - public IFolder GetFolderForFile(string path) - { - var fileInfo = new FileInfo(MapStorage(path)); - if (!fileInfo.Exists) - { - throw new FileNotFoundException("File " + path + " does not exist"); - } - - if (!fileInfo.Directory.Exists) - { - throw new DirectoryNotFoundException("Folder " + path + " does not exist"); - } - - // Get relative path of the folder - var folderPath = Path.GetDirectoryName(path); - - return new LocalFolder(folderPath, fileInfo.Directory); - } - - public long CountFiles(string path, string pattern, Func predicate, bool deep = true) - { - 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, deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) - .Select(x => x.Substring(index)); - } - - public IEnumerable ListFiles(string path) - { - var directoryInfo = new DirectoryInfo(MapStorage(path)); - - if (!directoryInfo.Exists) - { - throw new DirectoryNotFoundException("Directory " + path + " does not exist"); - } - - return directoryInfo - .EnumerateFiles() - .Where(fi => !IsHidden(fi)) - .Select(fi => new LocalFile(Path.Combine(path, fi.Name), fi)); - } - - public IEnumerable ListFolders(string path) - { - var directoryInfo = new DirectoryInfo(MapStorage(path)); - - if (!directoryInfo.Exists) - { - try - { - directoryInfo.Create(); - } - catch (Exception ex) - { - if (ex.IsFatal()) - { - throw; - } - throw new ArgumentException(string.Format("The folder could not be created at path: {0}. {1}", path, ex)); - } - } - - return directoryInfo - .EnumerateDirectories() - .Where(di => !IsHidden(di)) - .Select(di => new LocalFolder(Path.Combine(path, di.Name), di)); - } - - private static bool IsHidden(FileSystemInfo di) - { - return (di.Attributes & FileAttributes.Hidden) != 0; - } - - public IFolder CreateFolder(string path) - { - return new LocalFolder(path, Directory.CreateDirectory(MapStorage(path))); - } - - public void DeleteFolder(string path) - { - var directoryInfo = new DirectoryInfo(MapStorage(path)); - - if (!directoryInfo.Exists) - { - throw new DirectoryNotFoundException("Directory " + path + " does not exist."); - } - - directoryInfo.Delete(true); - } - - public void RenameFolder(string path, string newPath) - { - var sourceDirectory = new DirectoryInfo(MapStorage(path)); - if (!sourceDirectory.Exists) - { - throw new DirectoryNotFoundException("Directory " + path + "does not exist."); - } - - var targetDirectory = new DirectoryInfo(MapStorage(newPath)); - if (targetDirectory.Exists) - { - throw new ArgumentException("Directory " + newPath + " already exists."); - } - - Directory.Move(sourceDirectory.FullName, targetDirectory.FullName); - } - - public void CopyFolder(string path, string destinationPath, bool overwrite = true) - { - var sourceDirectory = new DirectoryInfo(MapStorage(path)); - if (!sourceDirectory.Exists) - { - throw new DirectoryNotFoundException("Directory " + path + "does not exist."); - } - - var targetPath = Combine(MapStorage(destinationPath), sourceDirectory.Name); - var destDirectory = new DirectoryInfo(targetPath); - if (!overwrite && destDirectory.Exists) - { - throw new ArgumentException("Directory " + destinationPath + " already exists."); - } - - if (!destDirectory.Exists) - { - destDirectory.Create(); - } - - FileSystemHelper.CopyDirectory(sourceDirectory, destDirectory, overwrite); - } - - public bool CheckUniqueFileName(string path, out string newPath) - { - Guard.NotEmpty(path, nameof(path)); - - newPath = null; - - var file = GetFile(path); - if (!file.Exists) - { - return false; - } - - var pattern = string.Concat(file.Title, "-*", file.Extension); - var dir = file.Directory; - var files = new HashSet(SearchFiles(dir, pattern, false).Select(x => Path.GetFileName(x)), StringComparer.OrdinalIgnoreCase); - - int i = 1; - while (true) - { - var fileName = string.Concat(file.Title, "-", i, file.Extension); - if (!files.Contains(fileName)) - { - // Found our gap - newPath = Combine(dir, fileName); - return true; - } - - i++; - } - } - - public IFile CreateFile(string path) - { - var fileInfo = new FileInfo(MapStorage(path)); - - if (fileInfo.Exists) - { - throw new ArgumentException("File " + path + " already exists"); - } - - // Ensure the directory exists - var dirName = Path.GetDirectoryName(fileInfo.FullName); - if (!Directory.Exists(dirName)) - { - Directory.CreateDirectory(dirName); - } - - File.WriteAllBytes(fileInfo.FullName, new byte[0]); + } + } + } + + var appVirtualPath = HostingEnvironment.IsHosted + ? HostingEnvironment.ApplicationVirtualPath.TrimEnd('/') + : string.Empty; + + if (publicPath.StartsWith("~/")) + { + // prepend application virtual path + return appVirtualPath + publicPath.Substring(1); + } + + if (publicPath.IsEmpty() && !basePathIsAbsolute) + { + // > /MyAppRoot/Media + return appVirtualPath + basePath; + } + + _isCloudStorage = true; + + return publicPath; + } + + public string Root => _root; + + /// + /// Maps a relative path into the storage path. + /// + /// The relative path to be mapped. + /// The relative path combined with the storage path. + protected virtual string MapStorage(string path) + { + var mappedPath = string.IsNullOrEmpty(path) ? _storagePath : Path.Combine(_storagePath, Fix(path)); + return ValidatePath(_storagePath, mappedPath); + } + + /// + /// Maps a relative path into the public path. + /// + /// The relative path to be mapped. + /// The relative path combined with the public path in an URL friendly format ('/' character for directory separator). + protected virtual string MapPublic(string path) + { + return string.IsNullOrEmpty(path) ? _publicPath : HttpUtility.UrlDecode(Path.Combine(_publicPath, path).Replace(Path.DirectorySeparatorChar, '/')); + } + + static string Fix(string path) + { + return string.IsNullOrEmpty(path) + ? "" + : Path.DirectorySeparatorChar != '/' + ? path.Replace('/', Path.DirectorySeparatorChar).TrimStart('/', '\\') + : path.TrimStart('/', '\\'); + } + + public string GetPublicUrl(IFile file, bool forCloud = false) + { + return MapPublic(file.Path); + } + + public string GetPublicUrl(string path, bool forCloud = false) + { + return MapPublic(path); + } + + public virtual string GetStoragePath(string url) + { + if (url.HasValue()) + { + if (url.StartsWith("~/")) + { + url = VirtualPathUtility.ToAbsolute(url); + } + + if (url.StartsWith(_publicPath)) + { + return HttpUtility.UrlDecode(url.Substring(_publicPath.Length).Replace('/', Path.DirectorySeparatorChar)); + } + } + + return null; + } + + public bool FileExists(string path) + { + return File.Exists(MapStorage(path)); + } + + public bool FolderExists(string path) + { + return Directory.Exists(MapStorage(path)); + } + + public IFile GetFile(string path) + { + return new LocalFile(path, MapStorage(path)); + } + + public IFolder GetFolder(string path) + { + return new LocalFolder(path, MapStorage(path)); + } + + public IFolder GetFolderForFile(string path) + { + var fileInfo = new FileInfo(MapStorage(path)); + if (!fileInfo.Exists) + { + throw new FileNotFoundException("File " + path + " does not exist"); + } + + if (!fileInfo.Directory.Exists) + { + throw new DirectoryNotFoundException("Folder " + path + " does not exist"); + } + + // Get relative path of the folder + var folderPath = Path.GetDirectoryName(path); + + return new LocalFolder(folderPath, fileInfo.Directory); + } + + public long CountFiles(string path, string pattern, Func predicate, bool deep = true) + { + 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, deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .Select(x => x.Substring(index)); + } + + public IEnumerable ListFiles(string path) + { + var directoryInfo = new DirectoryInfo(MapStorage(path)); + + if (!directoryInfo.Exists) + { + throw new DirectoryNotFoundException("Directory " + path + " does not exist"); + } + + return directoryInfo + .EnumerateFiles() + .Where(fi => !IsHidden(fi)) + .Select(fi => new LocalFile(Path.Combine(path, fi.Name), fi)); + } + + public IEnumerable ListFolders(string path) + { + var directoryInfo = new DirectoryInfo(MapStorage(path)); + + if (!directoryInfo.Exists) + { + try + { + directoryInfo.Create(); + } + catch (Exception ex) + { + if (ex.IsFatal()) + { + throw; + } + throw new ArgumentException(string.Format("The folder could not be created at path: {0}. {1}", path, ex)); + } + } + + return directoryInfo + .EnumerateDirectories() + .Where(di => !IsHidden(di)) + .Select(di => new LocalFolder(Path.Combine(path, di.Name), di)); + } + + private static bool IsHidden(FileSystemInfo di) + { + return (di.Attributes & FileAttributes.Hidden) != 0; + } + + public IFolder CreateFolder(string path) + { + return new LocalFolder(path, Directory.CreateDirectory(MapStorage(path))); + } + + public void DeleteFolder(string path) + { + var directoryInfo = new DirectoryInfo(MapStorage(path)); + + if (!directoryInfo.Exists) + { + throw new DirectoryNotFoundException("Directory " + path + " does not exist."); + } + + directoryInfo.Delete(true); + } + + public void RenameFolder(string path, string newPath) + { + var sourceDirectory = new DirectoryInfo(MapStorage(path)); + if (!sourceDirectory.Exists) + { + throw new DirectoryNotFoundException("Directory " + path + "does not exist."); + } + + var targetDirectory = new DirectoryInfo(MapStorage(newPath)); + if (targetDirectory.Exists) + { + throw new ArgumentException("Directory " + newPath + " already exists."); + } + + Directory.Move(sourceDirectory.FullName, targetDirectory.FullName); + } + + public void CopyFolder(string path, string destinationPath, bool overwrite = true) + { + var sourceDirectory = new DirectoryInfo(MapStorage(path)); + if (!sourceDirectory.Exists) + { + throw new DirectoryNotFoundException("Directory " + path + "does not exist."); + } + + var targetPath = Combine(MapStorage(destinationPath), sourceDirectory.Name); + var destDirectory = new DirectoryInfo(targetPath); + if (!overwrite && destDirectory.Exists) + { + throw new ArgumentException("Directory " + destinationPath + " already exists."); + } + + if (!destDirectory.Exists) + { + destDirectory.Create(); + } + + FileSystemHelper.CopyDirectory(sourceDirectory, destDirectory, overwrite); + } + + public bool CheckUniqueFileName(string path, out string newPath) + { + Guard.NotEmpty(path, nameof(path)); + + newPath = null; + + var file = GetFile(path); + if (!file.Exists) + { + return false; + } + + var pattern = string.Concat(file.Title, "-*", file.Extension); + var dir = file.Directory; + var files = new HashSet(SearchFiles(dir, pattern, false).Select(x => Path.GetFileName(x)), StringComparer.OrdinalIgnoreCase); + + int i = 1; + while (true) + { + var fileName = string.Concat(file.Title, "-", i, file.Extension); + if (!files.Contains(fileName)) + { + // Found our gap + newPath = Combine(dir, fileName); + return true; + } + + i++; + } + } + + public IFile CreateFile(string path) + { + var fileInfo = new FileInfo(MapStorage(path)); + + if (fileInfo.Exists) + { + throw new ArgumentException("File " + path + " already exists"); + } + + // Ensure the directory exists + var dirName = Path.GetDirectoryName(fileInfo.FullName); + if (!Directory.Exists(dirName)) + { + Directory.CreateDirectory(dirName); + } + + File.WriteAllBytes(fileInfo.FullName, new byte[0]); fileInfo.Refresh(); - return new LocalFile(path, fileInfo); - } - - public async Task CreateFileAsync(string path) - { - var fileInfo = new FileInfo(MapStorage(path)); - - if (fileInfo.Exists) - { - throw new ArgumentException("File " + path + " already exists"); - } - - // ensure the directory exists - var dirName = Path.GetDirectoryName(fileInfo.FullName); - if (!Directory.Exists(dirName)) - { - Directory.CreateDirectory(dirName); - } - - using (FileStream stream = new FileStream(fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) - { - await stream.WriteAsync(new byte[0], 0, 0); - } + return new LocalFile(path, fileInfo); + } + + public async Task CreateFileAsync(string path) + { + var fileInfo = new FileInfo(MapStorage(path)); + + if (fileInfo.Exists) + { + throw new ArgumentException("File " + path + " already exists"); + } + + // ensure the directory exists + var dirName = Path.GetDirectoryName(fileInfo.FullName); + if (!Directory.Exists(dirName)) + { + Directory.CreateDirectory(dirName); + } + + using (FileStream stream = new FileStream(fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) + { + await stream.WriteAsync(new byte[0], 0, 0); + } fileInfo.Refresh(); - return new LocalFile(path, fileInfo); - } - - public void DeleteFile(string path) - { - var fileInfo = new FileInfo(MapStorage(path)); - - if (fileInfo.Exists) - { - WaitForUnlockAndExecute(fileInfo, x => x.Delete()); - } - } - - public void RenameFile(string path, string newPath) - { - var sourceFileInfo = new FileInfo(MapStorage(path)); - if (!sourceFileInfo.Exists) - { - throw new FileNotFoundException("File " + path + " does not exist."); - } - - var targetFileInfo = new FileInfo(MapStorage(newPath)); - if (targetFileInfo.Exists) - { - throw new ArgumentException("File " + newPath + " already exists."); - } - - WaitForUnlockAndExecute(sourceFileInfo, x => File.Move(x.FullName, targetFileInfo.FullName)); - } - - public void CopyFile(string path, string newPath, bool overwrite = false) - { - var sourceFileInfo = new FileInfo(MapStorage(path)); - if (!sourceFileInfo.Exists) - { - throw new FileNotFoundException("File " + path + " does not exist"); - } - - var targetPath = MapStorage(newPath); - if (!overwrite) - { - var targetFileInfo = new FileInfo(targetPath); - if (targetFileInfo.Exists) - { - throw new ArgumentException("File " + newPath + " already exists"); - } - } - - WaitForUnlockAndExecute(sourceFileInfo, x => File.Copy(x.FullName, targetPath, overwrite)); - } - - public void SaveStream(string path, Stream inputStream) - { - using (var outputStream = File.OpenWrite(MapStorage(path))) - { - outputStream.SetLength(0); - inputStream.CopyTo(outputStream); - } - } - - public async Task SaveStreamAsync(string path, Stream inputStream) - { - using (var outputStream = File.OpenWrite(MapStorage(path))) - { - outputStream.SetLength(0); - await inputStream.CopyToAsync(outputStream); - } - } - - public string Combine(string path1, string path2) - { - return Path.Combine(path1, path2); - } - - private static void WaitForUnlockAndExecute(FileInfo fi, Action action) - { - try - { - action(fi); - } - catch (IOException) - { - if (!fi.WaitForUnlock(250)) - { - throw; - } - - action(fi); - } - } - - /// - /// Determines if a path lies within the base path boundaries. - /// If not, an exception is thrown. - /// - /// The base path which boundaries are not to be transposed. - /// The path to determine. - /// The mapped path if valid. - /// If the path is invalid. - internal static string ValidatePath(string basePath, string mappedPath) - { - bool valid = false; - string error = null; - - try - { - // Check that we are indeed within the storage directory boundaries - valid = Path.GetFullPath(mappedPath).StartsWith(Path.GetFullPath(basePath), StringComparison.OrdinalIgnoreCase); - } - catch (Exception exception) - { - // Make sure that if invalid for medium trust we give a proper exception - valid = false; - error = exception.Message; - } - - if (!valid) - { - throw new ArgumentException($"{error ?? "Invalid path."} mappedPath: {mappedPath.NaIfEmpty()}"); - } - - return mappedPath; - } + return new LocalFile(path, fileInfo); + } + + public void DeleteFile(string path) + { + var fileInfo = new FileInfo(MapStorage(path)); + + if (fileInfo.Exists) + { + WaitForUnlockAndExecute(fileInfo, x => x.Delete()); + } + } + + public void RenameFile(string path, string newPath) + { + var sourceFileInfo = new FileInfo(MapStorage(path)); + if (!sourceFileInfo.Exists) + { + throw new FileNotFoundException("File " + path + " does not exist."); + } + + var targetFileInfo = new FileInfo(MapStorage(newPath)); + if (targetFileInfo.Exists) + { + throw new ArgumentException("File " + newPath + " already exists."); + } + + WaitForUnlockAndExecute(sourceFileInfo, x => File.Move(x.FullName, targetFileInfo.FullName)); + } + + public void CopyFile(string path, string newPath, bool overwrite = false) + { + var sourceFileInfo = new FileInfo(MapStorage(path)); + if (!sourceFileInfo.Exists) + { + throw new FileNotFoundException("File " + path + " does not exist"); + } + + var targetPath = MapStorage(newPath); + if (!overwrite) + { + var targetFileInfo = new FileInfo(targetPath); + if (targetFileInfo.Exists) + { + throw new ArgumentException("File " + newPath + " already exists"); + } + } + + WaitForUnlockAndExecute(sourceFileInfo, x => File.Copy(x.FullName, targetPath, overwrite)); + } + + public void SaveStream(string path, Stream inputStream) + { + using (var outputStream = File.OpenWrite(MapStorage(path))) + { + outputStream.SetLength(0); + inputStream.CopyTo(outputStream); + } + } + + public async Task SaveStreamAsync(string path, Stream inputStream) + { + using (var outputStream = File.OpenWrite(MapStorage(path))) + { + outputStream.SetLength(0); + await inputStream.CopyToAsync(outputStream); + } + } + + public string Combine(string path1, string path2) + { + return Path.Combine(path1, path2); + } + + private static void WaitForUnlockAndExecute(FileInfo fi, Action action) + { + try + { + action(fi); + } + catch (IOException) + { + if (!fi.WaitForUnlock(250)) + { + throw; + } + + action(fi); + } + } + + /// + /// Determines if a path lies within the base path boundaries. + /// If not, an exception is thrown. + /// + /// The base path which boundaries are not to be transposed. + /// The path to determine. + /// The mapped path if valid. + /// If the path is invalid. + internal static string ValidatePath(string basePath, string mappedPath) + { + bool valid = false; + string error = null; + + try + { + // Check that we are indeed within the storage directory boundaries + valid = Path.GetFullPath(mappedPath).StartsWith(Path.GetFullPath(basePath), StringComparison.OrdinalIgnoreCase); + } + catch (Exception exception) + { + // Make sure that if invalid for medium trust we give a proper exception + valid = false; + error = exception.Message; + } + + if (!valid) + { + throw new ArgumentException($"{error ?? "Invalid path."} mappedPath: {mappedPath.NaIfEmpty()}"); + } + + return mappedPath; + } public class LocalFile : IFile { @@ -557,40 +551,19 @@ private FileInfo GetFileInfo() return _fileInfo ?? (_fileInfo = new FileInfo(_localPath)); } - public string Path - { - get { return _relativePath; } - } + public string Path => _relativePath; - public string Directory - { - get { return _relativePath.Substring(0, _relativePath.Length - Name.Length); } - } + public string Directory => _relativePath.Substring(0, _relativePath.Length - Name.Length); - public string Name - { - get { return _fileInfo?.Name ?? _name ?? (_name = System.IO.Path.GetFileName(_localPath)); } - } + public string Name => _fileInfo?.Name ?? _name ?? (_name = System.IO.Path.GetFileName(_localPath)); - public string Title - { - get { return System.IO.Path.GetFileNameWithoutExtension(_localPath); } - } + public string Title => System.IO.Path.GetFileNameWithoutExtension(_localPath); - public long Size - { - get { return GetFileInfo().Length; } - } + public long Size => GetFileInfo().Length; - public DateTime LastUpdated - { - get { return GetFileInfo().LastWriteTimeUtc; } - } + public DateTime LastUpdated => GetFileInfo().LastWriteTimeUtc; - public string Extension - { - get { return _fileInfo?.Extension ?? System.IO.Path.GetExtension(_localPath); } - } + public string Extension => _fileInfo?.Extension ?? System.IO.Path.GetExtension(_localPath); public Size Dimensions { @@ -601,7 +574,7 @@ public Size Dimensions try { var mime = MimeTypes.MapNameToMimeType(Name); - _dimensions = ImageHeader.GetDimensions(OpenRead(), mime, false); + _dimensions = ImageHeader.GetDimensions(OpenRead(), mime, false); } catch { @@ -611,16 +584,17 @@ public Size Dimensions return _dimensions.Value; } + internal set => _dimensions = value; } public bool Exists { - get - { + get + { if (_fileInfo != null) return _fileInfo.Exists; - return System.IO.File.Exists(_localPath); + return System.IO.File.Exists(_localPath); } } @@ -631,7 +605,13 @@ public Stream OpenRead() public Stream OpenWrite() { - return new FileStream(_localPath, FileMode.Open, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); + var fi = GetFileInfo(); + if (!fi.Directory.Exists) + { + System.IO.Directory.CreateDirectory(fi.Directory.FullName); + } + + return new FileStream(_localPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); } public Stream CreateFile() @@ -645,8 +625,8 @@ public Task CreateFileAsync() } } - public class LocalFolder : IFolder - { + public class LocalFolder : IFolder + { private readonly string _localPath; private readonly string _relativePath; private string _name; @@ -670,25 +650,13 @@ private DirectoryInfo GetDirInfo() return _dirInfo ?? (_dirInfo = new DirectoryInfo(_localPath)); } - public string Path - { - get { return _relativePath; } - } + public string Path => _relativePath; - public string Name - { - get { return _dirInfo?.Name ?? _name ?? (_name = GetDirName(_relativePath)); } - } + public string Name => _dirInfo?.Name ?? _name ?? (_name = GetDirName(_relativePath)); - public DateTime LastUpdated - { - get { return GetDirInfo().LastWriteTimeUtc; } - } + public DateTime LastUpdated => GetDirInfo().LastWriteTimeUtc; - public long Size - { - get { return GetDirectorySize(GetDirInfo()); } - } + public long Size => GetDirectorySize(GetDirInfo()); public bool Exists { @@ -702,18 +670,19 @@ public bool Exists } public IFolder Parent - { - get - { + { + get + { var parent = GetDirInfo().Parent; - + if (parent != null) - { - return new LocalFolder(System.IO.Path.GetDirectoryName(_relativePath), parent); - } - throw new ArgumentException("Directory " + _dirInfo.Name + " does not have a parent directory"); - } - } + { + return new LocalFolder(System.IO.Path.GetDirectoryName(_relativePath), parent); + } + + throw new InvalidOperationException("Directory " + _dirInfo.Name + " does not have a parent directory"); + } + } private static string GetDirName(string fullPath) { @@ -729,29 +698,29 @@ private static string GetDirName(string fullPath) private static long GetDirectorySize(DirectoryInfo directoryInfo) - { - long size = 0; - - FileInfo[] fileInfos = directoryInfo.GetFiles(); - foreach (FileInfo fileInfo in fileInfos) - { - if (!IsHidden(fileInfo)) - { - size += fileInfo.Length; - } - } - DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories(); - foreach (DirectoryInfo dInfo in directoryInfos) - { - if (!IsHidden(dInfo)) - { - size += GetDirectorySize(dInfo); - } - } - - return size; - } - } - - } + { + long size = 0; + + FileInfo[] fileInfos = directoryInfo.GetFiles(); + foreach (FileInfo fileInfo in fileInfos) + { + if (!IsHidden(fileInfo)) + { + size += fileInfo.Length; + } + } + DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories(); + foreach (DirectoryInfo dInfo in directoryInfos) + { + if (!IsHidden(dInfo)) + { + size += GetDirectorySize(dInfo); + } + } + + return size; + } + } + + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/IO/LockFile/ILockFile.cs b/src/Libraries/SmartStore.Core/IO/LockFile/ILockFile.cs index 1856377808..32b901619a 100644 --- a/src/Libraries/SmartStore.Core/IO/LockFile/ILockFile.cs +++ b/src/Libraries/SmartStore.Core/IO/LockFile/ILockFile.cs @@ -1,19 +1,15 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Core.IO { - /// - /// Represents a Lock File acquired on the file system - /// - /// - /// The instance needs to be disposed in order to release the lock explicitly - /// - public interface ILockFile : IDisposable - { - void Release(); - } + /// + /// Represents a Lock File acquired on the file system + /// + /// + /// The instance needs to be disposed in order to release the lock explicitly + /// + public interface ILockFile : IDisposable + { + void Release(); + } } diff --git a/src/Libraries/SmartStore.Core/IO/LockFile/ILockFileManager.cs b/src/Libraries/SmartStore.Core/IO/LockFile/ILockFileManager.cs index 9cd529bf1a..f3f04237cb 100644 --- a/src/Libraries/SmartStore.Core/IO/LockFile/ILockFileManager.cs +++ b/src/Libraries/SmartStore.Core/IO/LockFile/ILockFileManager.cs @@ -1,28 +1,26 @@ -using System; - -namespace SmartStore.Core.IO +namespace SmartStore.Core.IO { - /// - /// Abstraction for application-wide lock files creation. - /// - /// - /// All virtual paths passed in or returned are relative to "~/App_Data". - /// - public interface ILockFileManager - { - /// - /// Attempts to acquire an exclusive lock file. - /// - /// The filename of the lock file to create relative to ~/App_Data. - /// A reference to the lock file object if the lock is granted. - /// true if the lock is granted; otherwise, false. - bool TryAcquireLock(string path, out ILockFile lockFile); + /// + /// Abstraction for application-wide lock files creation. + /// + /// + /// All virtual paths passed in or returned are relative to "~/App_Data". + /// + public interface ILockFileManager + { + /// + /// Attempts to acquire an exclusive lock file. + /// + /// The filename of the lock file to create relative to ~/App_Data. + /// A reference to the lock file object if the lock is granted. + /// true if the lock is granted; otherwise, false. + bool TryAcquireLock(string path, out ILockFile lockFile); - /// - /// Wether a lock file is already existing. - /// - /// The filename of the lock file to test. - /// true if the lock file exists; otherwise, false. - bool IsLocked(string path); - } + /// + /// Wether a lock file is already existing. + /// + /// The filename of the lock file to test. + /// true if the lock file exists; otherwise, false. + bool IsLocked(string path); + } } diff --git a/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs b/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs index 1693635837..f4277071bc 100644 --- a/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs +++ b/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs @@ -1,54 +1,53 @@ -using System; +using System.IO; using System.Threading; -using System.IO; using SmartStore.Utilities.Threading; namespace SmartStore.Core.IO { - public class LockFile : ILockFile - { - private readonly string _path; - private readonly string _content; - private readonly IVirtualFolder _folder; - private readonly ReaderWriterLockSlim _rwLock; - private bool _released; + public class LockFile : ILockFile + { + private readonly string _path; + private readonly string _content; + private readonly IVirtualFolder _folder; + private readonly ReaderWriterLockSlim _rwLock; + private bool _released; - public LockFile(IVirtualFolder folder, string path, string content, ReaderWriterLockSlim rwLock) - { - _folder = folder; - _content = content; - _rwLock = rwLock; - _path = path; + public LockFile(IVirtualFolder folder, string path, string content, ReaderWriterLockSlim rwLock) + { + _folder = folder; + _content = content; + _rwLock = rwLock; + _path = path; - // create the physical lock file - _folder.CreateTextFile(_path, content); - } + // create the physical lock file + _folder.CreateTextFile(_path, content); + } - public void Dispose() - { - Release(); - } + public void Dispose() + { + Release(); + } - public void Release() - { - using (_rwLock.GetWriteLock()) - { - 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; - } + public void Release() + { + using (_rwLock.GetWriteLock()) + { + 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; + } - _released = true; + _released = true; - // check it has not been granted in the meantime - var current = _folder.ReadFile(_path); - if (current == _content) - { - _folder.DeleteFile(_path); - } - } - } - } + // check it has not been granted in the meantime + var current = _folder.ReadFile(_path); + if (current == _content) + { + _folder.DeleteFile(_path); + } + } + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs b/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs index 20d00e3441..0965d810f2 100644 --- a/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs +++ b/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs @@ -1,95 +1,95 @@ using System; using System.Globalization; -using System.Threading; using System.IO; +using System.Threading; using SmartStore.Utilities.Threading; namespace SmartStore.Core.IO { - public class LockFileManager : ILockFileManager - { - private readonly IApplicationEnvironment _env; - private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + public class LockFileManager : ILockFileManager + { + private readonly IApplicationEnvironment _env; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + static LockFileManager() + { + Expiration = TimeSpan.FromMinutes(10); + } - static LockFileManager() - { - Expiration = TimeSpan.FromMinutes(10); - } + public LockFileManager(IApplicationEnvironment env) + { + _env = env; + } - public LockFileManager(IApplicationEnvironment env) - { - _env = env; - } + public static TimeSpan Expiration + { + get; + private set; + } - public static TimeSpan Expiration - { - get; - private set; - } + public bool TryAcquireLock(string path, out ILockFile lockFile) + { + lockFile = null; - public bool TryAcquireLock(string path, out ILockFile lockFile) - { - lockFile = null; + if (!_rwLock.TryEnterWriteLock(0)) + { + return false; + } - if (!_rwLock.TryEnterWriteLock(0)) - { - return false; - } + try + { + if (IsLockedInternal(path)) + { + return false; + } - try - { - if (IsLockedInternal(path)) - { - return false; - } - - lockFile = new LockFile(_env.TenantFolder, path, DateTime.UtcNow.ToString("u"), _rwLock); - return true; - } - catch - { - // an error occured while reading/creating the lock file - return false; - } - finally - { - _rwLock.ExitWriteLock(); - } - } + lockFile = new LockFile(_env.TenantFolder, path, DateTime.UtcNow.ToString("u"), _rwLock); + return true; + } + catch + { + // an error occured while reading/creating the lock file + return false; + } + finally + { + _rwLock.ExitWriteLock(); + } + } - public bool IsLocked(string path) - { - using (_rwLock.GetWriteLock()) - { - try - { - return IsLockedInternal(path); - } - catch - { - // an error occured while reading the file - return true; - } - } - } + public bool IsLocked(string path) + { + using (_rwLock.GetWriteLock()) + { + try + { + return IsLockedInternal(path); + } + catch + { + // an error occured while reading the file + return true; + } + } + } - private bool IsLockedInternal(string 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); + private bool IsLockedInternal(string 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); - if (DateTime.TryParse(content, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var creationUtc)) - { - // if expired the file is not removed - // it should be automatically as there is a finalizer in LockFile - // or the next taker can do it, unless it also fails, again - return creationUtc.ToUniversalTime().Add(Expiration) > DateTime.UtcNow; - } - } + if (DateTime.TryParse(content, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var creationUtc)) + { + // if expired the file is not removed + // it should be automatically as there is a finalizer in LockFile + // or the next taker can do it, unless it also fails, again + return creationUtc.ToUniversalTime().Add(Expiration) > DateTime.UtcNow; + } + } - return false; - } - } + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/MimeTypes.cs b/src/Libraries/SmartStore.Core/IO/MimeTypes.cs index a0ec5b68bb..50c8e79c3c 100644 --- a/src/Libraries/SmartStore.Core/IO/MimeTypes.cs +++ b/src/Libraries/SmartStore.Core/IO/MimeTypes.cs @@ -1,766 +1,766 @@ using System; -using System.IO; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using Microsoft.Win32; namespace SmartStore.Core.IO -{ +{ public static class MimeTypes { - private static readonly char[] _pathSeparatorChars = new [] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, Path.VolumeSeparatorChar }; - private static readonly Dictionary _mimeMap; - private static readonly Dictionary _extMap; + private static readonly char[] _pathSeparatorChars = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, Path.VolumeSeparatorChar }; + private static readonly Dictionary _mimeMap; + private static readonly Dictionary _extMap; - const string DefaultMimeType = "application/octet-stream"; + 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 - // + 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) - { + _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 + {"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] + {"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"}, + {"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) - { + _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"}, - {".pjp", "image/jpeg"}, - {".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 */ + {".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"}, + {".pjp", "image/jpeg"}, + {".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"}, + {".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); + // 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); + // 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 { } - } + // Vice versa, Extension > MimeType + if (!_extMap.ContainsKey(ext)) + { + _extMap.Add(ext, contentType); + } + } + } + } + } + } + } + } + catch { } + } - public static string MapNameToMimeType(string fileNameOrExtension) + public static string MapNameToMimeType(string fileNameOrExtension) { - var fileName = GetFileName(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; - } - } + for (int i = 0; i < fileName.Length; i++) + { + if ((fileName[i] == '.') && _extMap.TryGetValue(fileName.Substring(i), out var str)) + { + return str; + } + } - return DefaultMimeType; + return DefaultMimeType; } /// @@ -774,34 +774,34 @@ public static string MapMimeTypeToExtension(string mimeType) if (mimeType.IsEmpty()) return null; - if (_mimeMap.TryGetValue(mimeType, out var extension)) - { - return extension.TrimStart('.'); - } + if (_mimeMap.TryGetValue(mimeType, out var extension)) + { + return extension.TrimStart('.'); + } - return null; + return null; } - private static void ShuffleMap(Dictionary source, Dictionary target) - { - foreach (var kvp in source) - { - if (!target.ContainsKey(kvp.Value)) - { - target.Add(kvp.Value, kvp.Key); - } - } - } + private static void ShuffleMap(Dictionary source, Dictionary target) + { + foreach (var kvp in source) + { + if (!target.ContainsKey(kvp.Value)) + { + target.Add(kvp.Value, kvp.Key); + } + } + } - private static string GetFileName(string path) - { - var startIndex = path.LastIndexOfAny(_pathSeparatorChars); - if (startIndex < 0) - { - return path; - } + private static string GetFileName(string path) + { + var startIndex = path.LastIndexOfAny(_pathSeparatorChars); + if (startIndex < 0) + { + return path; + } - return path.Substring(startIndex); - } - } + return path.Substring(startIndex); + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/SeekableReadOnlyStream.cs b/src/Libraries/SmartStore.Core/IO/SeekableReadOnlyStream.cs index ff741cd45a..041c4ce451 100644 --- a/src/Libraries/SmartStore.Core/IO/SeekableReadOnlyStream.cs +++ b/src/Libraries/SmartStore.Core/IO/SeekableReadOnlyStream.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; namespace SmartStore.Core.IO @@ -20,8 +19,8 @@ public SeekableReadOnlyStream(Stream underlyingStream, int seekBackBufferSize) _seekBackBuffer = new byte[seekBackBufferSize]; } - public override bool CanRead { get { return true; } } - public override bool CanSeek { get { return true; } } + public override bool CanRead => true; + public override bool CanSeek => true; public override int Read(byte[] buffer, int offset, int count) { @@ -208,8 +207,8 @@ private long SeekFromEnd(long offset) public override long Position { - get { return _underlyingPosition - (_seekBackBufferCount - _seekBackBufferIndex); } - set { Seek(value, SeekOrigin.Begin); } + get => _underlyingPosition - (_seekBackBufferCount - _seekBackBufferIndex); + set => Seek(value, SeekOrigin.Begin); } protected override void Dispose(bool disposing) @@ -219,9 +218,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - public override bool CanTimeout { get { return _underlyingStream.CanTimeout; } } - public override bool CanWrite { get { return _underlyingStream.CanWrite; } } - public override long Length { get { return _underlyingStream.Length; } } + public override bool CanTimeout => _underlyingStream.CanTimeout; + public override bool CanWrite => _underlyingStream.CanWrite; + public override long Length => _underlyingStream.Length; public override void SetLength(long value) { _underlyingStream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) { _underlyingStream.Write(buffer, offset, count); } public override void Flush() { _underlyingStream.Flush(); } diff --git a/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs b/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs index e3c55571d4..0c5032b1d4 100644 --- a/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs +++ b/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs @@ -1,20 +1,19 @@ -using System; -using System.IO; +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, out _); - } + 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, out _); + } /// /// Determines whether this file system entry is a symbolic link. @@ -33,8 +32,8 @@ public static bool IsSymbolicLink(this FileSystemInfo fsi, out string finalPathN /// The symbolic link in question. /// The path to the target. public static string GetFinalPathName(this FileSystemInfo fsi) - { - return SymbolicLink.GetFinalPathName(fsi.FullName); - } - } + { + 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 index fe61575ad9..77e25d52df 100644 --- a/src/Libraries/SmartStore.Core/IO/SymLink/SymbolicLink.cs +++ b/src/Libraries/SmartStore.Core/IO/SymLink/SymbolicLink.cs @@ -1,45 +1,44 @@ using System; using System.ComponentModel; using System.IO; -using System.Runtime.CompilerServices; 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); + 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, out string finalPathName) { @@ -48,8 +47,8 @@ public static bool IsSymbolicLink(FileSystemInfo fsi, out string finalPathName) finalPathName = null; // FileSystemInfo.Exists has some problems with symbolic links - var exists = (fsi is DirectoryInfo) - ? Directory.Exists(fsi.FullName) + var exists = (fsi is DirectoryInfo) + ? Directory.Exists(fsi.FullName) : File.Exists(fsi.FullName); if (!exists) @@ -74,44 +73,44 @@ public static bool IsSymbolicLink(FileSystemInfo fsi, out string finalPathName) } 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); - } - } - } + { + 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 2d78ea0447..a7eea24f4e 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs @@ -2,160 +2,160 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Web.Hosting; -using System.Web.Caching; -using SmartStore.Utilities; using System.Web; +using System.Web.Caching; +using System.Web.Hosting; using SmartStore.Core.Logging; +using SmartStore.Utilities; namespace SmartStore.Core.IO { - public class DefaultVirtualPathProvider : IVirtualPathProvider - { - private readonly ILogger _logger; - - public DefaultVirtualPathProvider(ILogger logger) - { - _logger = logger; - } - - public virtual string Combine(params string[] paths) - { - return Path.Combine(paths).Replace(Path.DirectorySeparatorChar, '/'); - } - - public virtual bool DirectoryExists(string virtualPath) - { - return HostingEnvironment.VirtualPathProvider.DirectoryExists(virtualPath); - } - - public virtual bool FileExists(string virtualPath) - { - return HostingEnvironment.VirtualPathProvider.FileExists(virtualPath); - } - - public virtual CacheDependency GetCacheDependency(string virtualPath, IEnumerable dependencies, DateTime utcStart) - { - return HostingEnvironment.VirtualPathProvider.GetCacheDependency(virtualPath, dependencies, utcStart); - } - - public virtual string GetCacheKey(string virtualPath) - { - return HostingEnvironment.VirtualPathProvider.GetCacheKey(virtualPath); - } - - public virtual IEnumerable ListDirectories(string virtualPath) - { - return HostingEnvironment - .VirtualPathProvider - .GetDirectory(virtualPath) - .Directories - .OfType() - .Select(d => VirtualPathUtility.ToAppRelative(d.VirtualPath)); - } - - public virtual IEnumerable ListFiles(string virtualPath) - { - return HostingEnvironment - .VirtualPathProvider - .GetDirectory(virtualPath) - .Files - .OfType() - .Select(f => VirtualPathUtility.ToAppRelative(f.VirtualPath)); - } - - public virtual string GetFileHash(string virtualPath, IEnumerable dependencies) - { - return HostingEnvironment.VirtualPathProvider.GetFileHash(virtualPath, dependencies); - } - - public virtual string MapPath(string virtualPath) - { - return CommonHelper.MapPath(virtualPath); - } - - public virtual string Normalize(string virtualPath) - { - return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).VirtualPath; - } - - public virtual Stream OpenFile(string virtualPath) - { - return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).Open(); - } - - public virtual string ToAppRelative(string virtualPath) - { - if (IsMalformedVirtualPath(virtualPath)) - return null; - - try - { - string result = VirtualPathUtility.ToAppRelative(virtualPath); - - // In some cases, ToAppRelative doesn't normalize the path. In those cases, - // the path is invalid. - // Example: - // ApplicationPath: /Foo - // VirtualPath : ~/Bar/../Blah/Blah2 - // Result : /Blah/Blah2 <= that is not an app relative path! - if (!result.StartsWith("~/")) - { - _logger.Info("Path '{0}' cannot be made app relative: Path returned ('{1}') is not app relative.".FormatCurrent(virtualPath, result)); - return null; - } - return result; - } - catch (Exception e) - { - // The initial path might have been invalid (e.g. path indicates a path outside the application root) - _logger.Info(e, "Path '{0}' cannot be made app relative".FormatCurrent(virtualPath)); - return null; - } - } - - /// - /// We want to reject path that contains ".." going outside of the application root. - /// ToAppRelative does that already, but we want to do the same while avoiding exceptions. - /// - /// Note: This method doesn't detect all cases of malformed paths, it merely checks - /// for *some* cases of malformed paths, so this is not a replacement for full virtual path - /// verification through VirtualPathUtilty methods. - /// In other words, !IsMalformed does *not* imply "IsWellformed". - /// - public bool IsMalformedVirtualPath(string virtualPath) - { - if (string.IsNullOrEmpty(virtualPath)) - return true; - - if (virtualPath.IndexOf("..", StringComparison.OrdinalIgnoreCase) >= 0) - { - virtualPath = virtualPath.Replace(Path.DirectorySeparatorChar, '/'); - string rootPrefix = virtualPath.StartsWith("~/") ? "~/" : virtualPath.StartsWith("/") ? "/" : ""; - if (!string.IsNullOrEmpty(rootPrefix)) - { - string[] terms = virtualPath.Substring(rootPrefix.Length).Split('/'); - int depth = 0; - foreach (var term in terms) - { - if (term == "..") - { - if (depth == 0) - { - _logger.Info("Path '{0}' cannot be made app relative: Too many '..'".FormatCurrent(virtualPath)); - return true; - } - depth--; - } - else - { - depth++; - } - } - } - } - - return false; - } - } + public class DefaultVirtualPathProvider : IVirtualPathProvider + { + private readonly ILogger _logger; + + public DefaultVirtualPathProvider(ILogger logger) + { + _logger = logger; + } + + public virtual string Combine(params string[] paths) + { + return Path.Combine(paths).Replace(Path.DirectorySeparatorChar, '/'); + } + + public virtual bool DirectoryExists(string virtualPath) + { + return HostingEnvironment.VirtualPathProvider.DirectoryExists(virtualPath); + } + + public virtual bool FileExists(string virtualPath) + { + return HostingEnvironment.VirtualPathProvider.FileExists(virtualPath); + } + + public virtual CacheDependency GetCacheDependency(string virtualPath, IEnumerable dependencies, DateTime utcStart) + { + return HostingEnvironment.VirtualPathProvider.GetCacheDependency(virtualPath, dependencies, utcStart); + } + + public virtual string GetCacheKey(string virtualPath) + { + return HostingEnvironment.VirtualPathProvider.GetCacheKey(virtualPath); + } + + public virtual IEnumerable ListDirectories(string virtualPath) + { + return HostingEnvironment + .VirtualPathProvider + .GetDirectory(virtualPath) + .Directories + .OfType() + .Select(d => VirtualPathUtility.ToAppRelative(d.VirtualPath)); + } + + public virtual IEnumerable ListFiles(string virtualPath) + { + return HostingEnvironment + .VirtualPathProvider + .GetDirectory(virtualPath) + .Files + .OfType() + .Select(f => VirtualPathUtility.ToAppRelative(f.VirtualPath)); + } + + public virtual string GetFileHash(string virtualPath, IEnumerable dependencies) + { + return HostingEnvironment.VirtualPathProvider.GetFileHash(virtualPath, dependencies); + } + + public virtual string MapPath(string virtualPath) + { + return CommonHelper.MapPath(virtualPath); + } + + public virtual string Normalize(string virtualPath) + { + return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).VirtualPath; + } + + public virtual Stream OpenFile(string virtualPath) + { + return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).Open(); + } + + public virtual string ToAppRelative(string virtualPath) + { + if (IsMalformedVirtualPath(virtualPath)) + return null; + + try + { + string result = VirtualPathUtility.ToAppRelative(virtualPath); + + // In some cases, ToAppRelative doesn't normalize the path. In those cases, + // the path is invalid. + // Example: + // ApplicationPath: /Foo + // VirtualPath : ~/Bar/../Blah/Blah2 + // Result : /Blah/Blah2 <= that is not an app relative path! + if (!result.StartsWith("~/")) + { + _logger.Info("Path '{0}' cannot be made app relative: Path returned ('{1}') is not app relative.".FormatCurrent(virtualPath, result)); + return null; + } + return result; + } + catch (Exception e) + { + // The initial path might have been invalid (e.g. path indicates a path outside the application root) + _logger.Info(e, "Path '{0}' cannot be made app relative".FormatCurrent(virtualPath)); + return null; + } + } + + /// + /// We want to reject path that contains ".." going outside of the application root. + /// ToAppRelative does that already, but we want to do the same while avoiding exceptions. + /// + /// Note: This method doesn't detect all cases of malformed paths, it merely checks + /// for *some* cases of malformed paths, so this is not a replacement for full virtual path + /// verification through VirtualPathUtilty methods. + /// In other words, !IsMalformed does *not* imply "IsWellformed". + /// + public bool IsMalformedVirtualPath(string virtualPath) + { + if (string.IsNullOrEmpty(virtualPath)) + return true; + + if (virtualPath.IndexOf("..", StringComparison.OrdinalIgnoreCase) >= 0) + { + virtualPath = virtualPath.Replace(Path.DirectorySeparatorChar, '/'); + string rootPrefix = virtualPath.StartsWith("~/") ? "~/" : virtualPath.StartsWith("/") ? "/" : ""; + if (!string.IsNullOrEmpty(rootPrefix)) + { + string[] terms = virtualPath.Substring(rootPrefix.Length).Split('/'); + int depth = 0; + foreach (var term in terms) + { + if (term == "..") + { + if (depth == 0) + { + _logger.Info("Path '{0}' cannot be made app relative: Too many '..'".FormatCurrent(virtualPath)); + return true; + } + depth--; + } + else + { + depth++; + } + } + } + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs index e4177560ad..b363e0f4bd 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs @@ -1,65 +1,63 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace SmartStore.Core.IO { - /// - /// Abstraction over the virtual files/directories of a web site. - /// - public interface IVirtualFolder - { - IVirtualPathProvider VirtualPathProvider { get; } - - /// - /// Virtual root path, e.g. ~/App_Data or ~/Themes - /// - string RootPath { get; } - - string GetVirtualPath(string relativePath); - - string MapPath(string relativePath); - string Combine(params string[] paths); - - bool DirectoryExists(string relativePath); - bool FileExists(string relativePath); - - IEnumerable ListDirectories(string relativePath); - IEnumerable ListFiles(string relativePath, bool deep = false); - string GetDirectoryName(string relativePath); - - Stream OpenFile(string relativePath); - void CreateTextFile(string relativePath, string content); - Task CreateTextFileAsync(string relativePath, string content); - Stream CreateFile(string relativePath); - void CreateDirectory(string relativePath); - - void DeleteFile(string relativePath); - void DeleteDirectory(string relativePath); - - string ReadFile(string relativePath); - Task ReadFileAsync(string relativePath); - void CopyFile(string relativePath, Stream destination); - - DateTime GetFileLastWriteTimeUtc(string relativePath); - } - - public static class IVirtualFolderExtensions - { - public static bool TryDeleteDirectory(this IVirtualFolder folder, string relativePath) - { - try - { - folder.DeleteDirectory(relativePath); - return true; - } - catch - { - return false; - } - } - } + /// + /// Abstraction over the virtual files/directories of a web site. + /// + public interface IVirtualFolder + { + IVirtualPathProvider VirtualPathProvider { get; } + + /// + /// Virtual root path, e.g. ~/App_Data or ~/Themes + /// + string RootPath { get; } + + string GetVirtualPath(string relativePath); + + string MapPath(string relativePath); + string Combine(params string[] paths); + + bool DirectoryExists(string relativePath); + bool FileExists(string relativePath); + + IEnumerable ListDirectories(string relativePath); + IEnumerable ListFiles(string relativePath, bool deep = false); + string GetDirectoryName(string relativePath); + + Stream OpenFile(string relativePath); + void CreateTextFile(string relativePath, string content); + Task CreateTextFileAsync(string relativePath, string content); + Stream CreateFile(string relativePath); + void CreateDirectory(string relativePath); + + void DeleteFile(string relativePath); + void DeleteDirectory(string relativePath); + + string ReadFile(string relativePath); + Task ReadFileAsync(string relativePath); + void CopyFile(string relativePath, Stream destination); + + DateTime GetFileLastWriteTimeUtc(string relativePath); + } + + public static class IVirtualFolderExtensions + { + public static bool TryDeleteDirectory(this IVirtualFolder folder, string relativePath) + { + try + { + folder.DeleteDirectory(relativePath); + return true; + } + catch + { + return false; + } + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualPathProvider.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualPathProvider.cs index 8fd1c36c90..2a7d3717f0 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualPathProvider.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualPathProvider.cs @@ -1,41 +1,40 @@ using System; using System.Collections.Generic; -using System.Linq; using System.IO; using System.Web.Caching; namespace SmartStore.Core.IO -{ +{ public interface IVirtualPathProvider { - string MapPath(string virtualPath); - string Combine(params string[] paths); - string Normalize(string virtualPath); - string ToAppRelative(string virtualPath); + string MapPath(string virtualPath); + string Combine(params string[] paths); + string Normalize(string virtualPath); + string ToAppRelative(string virtualPath); - bool DirectoryExists(string virtualPath); - bool FileExists(string virtualPath); + bool DirectoryExists(string virtualPath); + bool FileExists(string virtualPath); - CacheDependency GetCacheDependency(string virtualPath, IEnumerable dependencies, DateTime utcStart); - string GetCacheKey(string virtualPath); - string GetFileHash(string virtualPath, IEnumerable dependencies); + CacheDependency GetCacheDependency(string virtualPath, IEnumerable dependencies, DateTime utcStart); + string GetCacheKey(string virtualPath); + string GetFileHash(string virtualPath, IEnumerable dependencies); - IEnumerable ListDirectories(string virtualPath); - IEnumerable ListFiles(string virtualPath); + IEnumerable ListDirectories(string virtualPath); + IEnumerable ListFiles(string virtualPath); - Stream OpenFile(string virtualPath); + Stream OpenFile(string virtualPath); } - public static class IVirtualPathProviderExtensions - { - public static string GetFileHash(this IVirtualPathProvider vpp, string virtualPath) - { - return vpp.GetFileHash(virtualPath, new[] { virtualPath }); - } + public static class IVirtualPathProviderExtensions + { + public static string GetFileHash(this IVirtualPathProvider vpp, string virtualPath) + { + return vpp.GetFileHash(virtualPath, new[] { virtualPath }); + } - public static CacheDependency GetCacheDependency(this IVirtualPathProvider vpp, string virtualPath, DateTime utcStart) - { - return vpp.GetCacheDependency(virtualPath, new[] { virtualPath }, utcStart); - } - } + public static CacheDependency GetCacheDependency(this IVirtualPathProvider vpp, string virtualPath, DateTime utcStart) + { + return vpp.GetCacheDependency(virtualPath, new[] { virtualPath }, utcStart); + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs index 6355143bc6..0798c23a4d 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs @@ -7,231 +7,225 @@ namespace SmartStore.Core.IO { - public class VirtualFolder : IVirtualFolder - { - private readonly IVirtualPathProvider _vpp; - private readonly ILogger _logger; - private readonly string _root; - - public VirtualFolder(string root, IVirtualPathProvider vpp) - : this(root, vpp, NullLogger.Instance) - { - } - - public VirtualFolder(string root, IVirtualPathProvider vpp, ILogger logger) - { - Guard.NotEmpty(root, nameof(root)); - Guard.NotNull(vpp, nameof(vpp)); - Guard.NotNull(logger, nameof(logger)); - - if (!root.StartsWith("~/")) - { - throw new ArgumentException("Root path must be a valid application relative path starting with '~/'", nameof(root)); - } - - _root = root.Replace(Path.DirectorySeparatorChar, '/').EnsureEndsWith("/"); - _vpp = vpp; - _logger = logger; - } - - public IVirtualPathProvider VirtualPathProvider - { - get { return _vpp; } - } - - public string RootPath - { - get { return _root; } - } - - public virtual string MapPath(string relativePath) - { - return _vpp.MapPath(GetVirtualPath(relativePath)); - } - - public virtual string Combine(params string[] paths) - { - return _vpp.Combine(paths); - } - - public virtual bool DirectoryExists(string relativePath) - { - return _vpp.DirectoryExists(GetVirtualPath(relativePath)); - } - - public virtual bool FileExists(string relativePath) - { - return _vpp.FileExists(GetVirtualPath(relativePath)); - } - - public virtual IEnumerable ListDirectories(string relativePath) - { - var path = GetVirtualPath(relativePath); - - if (!_vpp.DirectoryExists(path)) - { - return Enumerable.Empty(); - } - - return _vpp.ListDirectories(path).Select(x => x.Substring(_root.Length)); - } - - public virtual IEnumerable ListFiles(string relativePath, bool deep = false) - { - var path = GetVirtualPath(relativePath); - - if (!_vpp.DirectoryExists(path)) - { - return Enumerable.Empty(); - } - - var files = _vpp.ListFiles(path).Select(x => x.Substring(_root.Length)); - - if (deep) - { - return files.Concat(ListDirectories(path).SelectMany(d => ListFiles(d, true))); - } - - return files; - } - - public virtual string GetDirectoryName(string relativePath) - { - return Path.GetDirectoryName(relativePath).Replace(Path.DirectorySeparatorChar, '/'); - } - - public virtual Stream OpenFile(string relativePath) - { - return _vpp.OpenFile(GetVirtualPath(relativePath)); - } - - public void CreateTextFile(string relativePath, string content) - { - Guard.NotEmpty(relativePath, nameof(relativePath)); - - using (var stream = CreateFile(relativePath)) - { - using (var tw = new StreamWriter(stream)) - { - tw.Write(content); - } - } - } - - public async Task CreateTextFileAsync(string relativePath, string content) - { - Guard.NotEmpty(relativePath, nameof(relativePath)); - - using (var stream = CreateFile(relativePath)) - { - using (var tw = new StreamWriter(stream)) - { - await tw.WriteAsync(content); - } - } - } - - public virtual Stream CreateFile(string relativePath) - { - var filePath = MapPath(relativePath); - var folderPath = Path.GetDirectoryName(filePath); - - if (!Directory.Exists(folderPath)) - { - Directory.CreateDirectory(folderPath); - } - - return File.Create(filePath); - } - - public virtual DateTime GetFileLastWriteTimeUtc(string relativePath) - { - return File.GetLastWriteTimeUtc(MapPath(relativePath)); - } - - public virtual void DeleteFile(string relativePath) - { - File.Delete(MapPath(relativePath)); - } - - public virtual void CreateDirectory(string relativePath) - { - Directory.CreateDirectory(MapPath(relativePath)); - } - - public virtual void DeleteDirectory(string relativePath) - { - Directory.Delete(MapPath(relativePath), true); - } - - public virtual string ReadFile(string relativePath) - { - Guard.NotEmpty(relativePath, nameof(relativePath)); - - var path = GetVirtualPath(relativePath); - - if (!_vpp.FileExists(path)) - { - return null; - } - - using (var stream = _vpp.OpenFile(path)) - { - using (var reader = new StreamReader(stream)) - { - return reader.ReadToEnd(); - } - } - } - - public virtual async Task ReadFileAsync(string relativePath) - { - Guard.NotEmpty(relativePath, nameof(relativePath)); - - var path = GetVirtualPath(relativePath); - - if (!_vpp.FileExists(path)) - { - return null; - } - - using (var stream = _vpp.OpenFile(path)) - { - using (var reader = new StreamReader(stream)) - { - return await reader.ReadToEndAsync(); - } - } - } - - public virtual void CopyFile(string relativePath, Stream destination) - { - Guard.NotEmpty(relativePath, nameof(relativePath)); - Guard.NotNull(destination, nameof(destination)); - - var path = GetVirtualPath(relativePath); - - if (!_vpp.FileExists(path)) - { - throw Error.InvalidOperation("File '{0}' does not exist".FormatInvariant(path)); - } - - using (var stream = _vpp.OpenFile(path)) - { - stream.CopyTo(destination); - } - } - - public string GetVirtualPath(string relativePath) - { - Guard.NotNull(relativePath, nameof(relativePath)); - - if (relativePath.StartsWith("~/")) - { - return relativePath; - } - - return _root + relativePath.EmptyNull().Replace(Path.DirectorySeparatorChar, '/').TrimStart('/'); - } - } + public class VirtualFolder : IVirtualFolder + { + private readonly IVirtualPathProvider _vpp; + private readonly ILogger _logger; + private readonly string _root; + + public VirtualFolder(string root, IVirtualPathProvider vpp) + : this(root, vpp, NullLogger.Instance) + { + } + + public VirtualFolder(string root, IVirtualPathProvider vpp, ILogger logger) + { + Guard.NotEmpty(root, nameof(root)); + Guard.NotNull(vpp, nameof(vpp)); + Guard.NotNull(logger, nameof(logger)); + + if (!root.StartsWith("~/")) + { + throw new ArgumentException("Root path must be a valid application relative path starting with '~/'", nameof(root)); + } + + _root = root.Replace(Path.DirectorySeparatorChar, '/').EnsureEndsWith("/"); + _vpp = vpp; + _logger = logger; + } + + public IVirtualPathProvider VirtualPathProvider => _vpp; + + public string RootPath => _root; + + public virtual string MapPath(string relativePath) + { + return _vpp.MapPath(GetVirtualPath(relativePath)); + } + + public virtual string Combine(params string[] paths) + { + return _vpp.Combine(paths); + } + + public virtual bool DirectoryExists(string relativePath) + { + return _vpp.DirectoryExists(GetVirtualPath(relativePath)); + } + + public virtual bool FileExists(string relativePath) + { + return _vpp.FileExists(GetVirtualPath(relativePath)); + } + + public virtual IEnumerable ListDirectories(string relativePath) + { + var path = GetVirtualPath(relativePath); + + if (!_vpp.DirectoryExists(path)) + { + return Enumerable.Empty(); + } + + return _vpp.ListDirectories(path).Select(x => x.Substring(_root.Length)); + } + + public virtual IEnumerable ListFiles(string relativePath, bool deep = false) + { + var path = GetVirtualPath(relativePath); + + if (!_vpp.DirectoryExists(path)) + { + return Enumerable.Empty(); + } + + var files = _vpp.ListFiles(path).Select(x => x.Substring(_root.Length)); + + if (deep) + { + return files.Concat(ListDirectories(path).SelectMany(d => ListFiles(d, true))); + } + + return files; + } + + public virtual string GetDirectoryName(string relativePath) + { + return Path.GetDirectoryName(relativePath).Replace(Path.DirectorySeparatorChar, '/'); + } + + public virtual Stream OpenFile(string relativePath) + { + return _vpp.OpenFile(GetVirtualPath(relativePath)); + } + + public void CreateTextFile(string relativePath, string content) + { + Guard.NotEmpty(relativePath, nameof(relativePath)); + + using (var stream = CreateFile(relativePath)) + { + using (var tw = new StreamWriter(stream)) + { + tw.Write(content); + } + } + } + + public async Task CreateTextFileAsync(string relativePath, string content) + { + Guard.NotEmpty(relativePath, nameof(relativePath)); + + using (var stream = CreateFile(relativePath)) + { + using (var tw = new StreamWriter(stream)) + { + await tw.WriteAsync(content); + } + } + } + + public virtual Stream CreateFile(string relativePath) + { + var filePath = MapPath(relativePath); + var folderPath = Path.GetDirectoryName(filePath); + + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + return File.Create(filePath); + } + + public virtual DateTime GetFileLastWriteTimeUtc(string relativePath) + { + return File.GetLastWriteTimeUtc(MapPath(relativePath)); + } + + public virtual void DeleteFile(string relativePath) + { + File.Delete(MapPath(relativePath)); + } + + public virtual void CreateDirectory(string relativePath) + { + Directory.CreateDirectory(MapPath(relativePath)); + } + + public virtual void DeleteDirectory(string relativePath) + { + Directory.Delete(MapPath(relativePath), true); + } + + public virtual string ReadFile(string relativePath) + { + Guard.NotEmpty(relativePath, nameof(relativePath)); + + var path = GetVirtualPath(relativePath); + + if (!_vpp.FileExists(path)) + { + return null; + } + + using (var stream = _vpp.OpenFile(path)) + { + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + + public virtual async Task ReadFileAsync(string relativePath) + { + Guard.NotEmpty(relativePath, nameof(relativePath)); + + var path = GetVirtualPath(relativePath); + + if (!_vpp.FileExists(path)) + { + return null; + } + + using (var stream = _vpp.OpenFile(path)) + { + using (var reader = new StreamReader(stream)) + { + return await reader.ReadToEndAsync(); + } + } + } + + public virtual void CopyFile(string relativePath, Stream destination) + { + Guard.NotEmpty(relativePath, nameof(relativePath)); + Guard.NotNull(destination, nameof(destination)); + + var path = GetVirtualPath(relativePath); + + if (!_vpp.FileExists(path)) + { + throw Error.InvalidOperation("File '{0}' does not exist".FormatInvariant(path)); + } + + using (var stream = _vpp.OpenFile(path)) + { + stream.CopyTo(destination); + } + } + + public string GetVirtualPath(string relativePath) + { + Guard.NotNull(relativePath, nameof(relativePath)); + + if (relativePath.StartsWith("~/")) + { + return relativePath; + } + + return _root + relativePath.EmptyNull().Replace(Path.DirectorySeparatorChar, '/').TrimStart('/'); + } + } } diff --git a/src/Libraries/SmartStore.Core/IPageable.cs b/src/Libraries/SmartStore.Core/IPageable.cs index 9131f20d99..d0d046c4c0 100644 --- a/src/Libraries/SmartStore.Core/IPageable.cs +++ b/src/Libraries/SmartStore.Core/IPageable.cs @@ -1,12 +1,12 @@ using System; -using System.Runtime.Serialization; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using System.Threading.Tasks; namespace SmartStore.Core -{ +{ /// /// A collection of objects that has been split into pages. /// @@ -77,43 +77,43 @@ public interface IPageable : IEnumerable } - /// - /// Paged list interface - /// - public interface IPagedList : IPageable, IList - { - /// - /// 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 - /// - /// The query - /// A query with applied paging args - IQueryable ApplyPaging(IQueryable query); - - /// - /// Loads the data synchronously. - /// - /// When true, always reloads data. When false, first checks to see whether data has been loaded already and skips if so. - /// Returns itself for chaining. - IPagedList Load(bool force = false); - - /// - /// Loads the data asynchronously. - /// - /// When true, always reloads data. When false, first checks to see whether data has been loaded already and skips if so. - /// Returns itself for chaining. - Task> LoadAsync(bool force = false); + /// + /// Paged list interface + /// + public interface IPagedList : IPageable, IList + { + /// + /// 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 + /// + /// The query + /// A query with applied paging args + IQueryable ApplyPaging(IQueryable query); + + /// + /// Loads the data synchronously. + /// + /// When true, always reloads data. When false, first checks to see whether data has been loaded already and skips if so. + /// Returns itself for chaining. + IPagedList Load(bool force = false); + + /// + /// Loads the data asynchronously. + /// + /// When true, always reloads data. When false, first checks to see whether data has been loaded already and skips if so. + /// Returns itself for chaining. + Task> LoadAsync(bool force = false); /// /// The total number of items asynchronously. diff --git a/src/Libraries/SmartStore.Core/ISoftDeletable.cs b/src/Libraries/SmartStore.Core/ISoftDeletable.cs index 88fe0be01b..2dd6ec0c6a 100644 --- a/src/Libraries/SmartStore.Core/ISoftDeletable.cs +++ b/src/Libraries/SmartStore.Core/ISoftDeletable.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; - -namespace SmartStore.Core +namespace SmartStore.Core { - public interface ISoftDeletable - { - bool Deleted { get; } - } + public interface ISoftDeletable + { + bool Deleted { get; } + } } diff --git a/src/Libraries/SmartStore.Core/IStoreContext.cs b/src/Libraries/SmartStore.Core/IStoreContext.cs index 1802ddbf23..6ff76ed3c7 100644 --- a/src/Libraries/SmartStore.Core/IStoreContext.cs +++ b/src/Libraries/SmartStore.Core/IStoreContext.cs @@ -2,44 +2,44 @@ namespace SmartStore.Core { - /// - /// Store context - /// - public interface IStoreContext - { - /// - /// Sets a store override to be used for the current request - /// - /// The store override or null to remove the override - void SetRequestStore(int? storeId); + /// + /// Store context + /// + public interface IStoreContext + { + /// + /// Sets a store override to be used for the current request + /// + /// The store override or null to remove the override + void SetRequestStore(int? storeId); - /// - /// Sets a store override to be used for the current user's session (e.g. for preview mode) - /// - /// The store override or null to remove the override - void SetPreviewStore(int? storeId); + /// + /// Sets a store override to be used for the current user's session (e.g. for preview mode) + /// + /// The store override or null to remove the override + void SetPreviewStore(int? storeId); - /// - /// Gets the store override for the current request - /// - /// The store override or null - int? GetRequestStore(); + /// + /// Gets the store override for the current request + /// + /// The store override or null + int? GetRequestStore(); - /// - /// Gets the store override for the current session - /// - /// The store override or null - int? GetPreviewStore(); - - /// - /// Gets or sets the current store - /// - /// Setter is for virtualization and testing purposes - Store CurrentStore { get; set; } + /// + /// Gets the store override for the current session + /// + /// The store override or null + int? GetPreviewStore(); - /// - /// IsSingleStoreMode ? 0 : CurrentStore.Id - /// - int CurrentStoreIdIfMultiStoreMode { get; } - } -} + /// + /// Gets or sets the current store + /// + /// Setter is for virtualization and testing purposes + Store CurrentStore { get; set; } + + /// + /// IsSingleStoreMode ? 0 : CurrentStore.Id + /// + int CurrentStoreIdIfMultiStoreMode { get; } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/ITransient.cs b/src/Libraries/SmartStore.Core/ITransient.cs index 761dcee833..d07cf7c224 100644 --- a/src/Libraries/SmartStore.Core/ITransient.cs +++ b/src/Libraries/SmartStore.Core/ITransient.cs @@ -1,9 +1,7 @@ -using System; - -namespace SmartStore +namespace SmartStore { - public interface ITransient - { - bool IsTransient { get; set; } - } + public interface ITransient + { + bool IsTransient { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/IWebHelper.cs b/src/Libraries/SmartStore.Core/IWebHelper.cs index 2d089bd64d..31cd154105 100644 --- a/src/Libraries/SmartStore.Core/IWebHelper.cs +++ b/src/Libraries/SmartStore.Core/IWebHelper.cs @@ -2,9 +2,6 @@ namespace SmartStore.Core { - /// - /// Represents a common helper - /// public partial interface IWebHelper { /// @@ -13,21 +10,21 @@ public partial interface IWebHelper /// URL referrer string GetUrlReferrer(); - /// - /// Gets a unique client identifier - /// - /// A unique identifier - /// - /// The client identifier is a hashed combination of client ip address and user agent. - /// This method returns null if IP or user agent (or both) cannot be determined. - /// - string GetClientIdent(); - - /// - /// Get context IP address - /// - /// URL referrer - string GetCurrentIpAddress(); + /// + /// Gets a unique client identifier + /// + /// A unique identifier + /// + /// The client identifier is a hashed combination of client ip address and user agent. + /// This method returns null if IP or user agent (or both) cannot be determined. + /// + string GetClientIdent(); + + /// + /// Get context IP address + /// + /// URL referrer + string GetCurrentIpAddress(); /// /// Gets this page name @@ -49,7 +46,7 @@ public partial interface IWebHelper /// /// true - secured, false - not secured bool IsCurrentConnectionSecured(); - + /// /// Gets server variable by name /// @@ -87,7 +84,7 @@ public partial interface IWebHelper /// .ashx /// bool IsStaticResource(HttpRequest request); - + /// /// Maps a virtual path to a physical disk path. /// @@ -112,7 +109,7 @@ public partial interface IWebHelper /// Query string to remove /// New url string RemoveQueryString(string url, string queryString); - + /// /// Gets query string value by name /// diff --git a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs index 4ed72318a1..6c785c9785 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; @@ -18,37 +16,34 @@ namespace SmartStore.Core.Infrastructure /// public class AppDomainTypeFinder : ITypeFinder { - private static object s_lock = new object(); + private static readonly object s_lock = new object(); - private readonly Regex _systemAssembliesPattern = new Regex(@"^System|^mscorlib|^Microsoft", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + private readonly Regex _systemAssembliesPattern = new Regex(@"^System|^mscorlib|^Microsoft", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - private bool _ignoreReflectionErrors = true; - private bool _loadAppDomainAssemblies = true; + private readonly bool _ignoreReflectionErrors = true; + private bool _loadAppDomainAssemblies = true; private IList _customAssemblyNames = new List(); - public AppDomainTypeFinder() - { - Logger = NullLogger.Instance; - } - - public ILogger Logger - { - get; - set; - } + public AppDomainTypeFinder() + { + Logger = NullLogger.Instance; + } - public virtual AppDomain App + public ILogger Logger { - get { return AppDomain.CurrentDomain; } + get; + set; } + public virtual AppDomain App => AppDomain.CurrentDomain; + /// - /// Gets or sets wether SmartStore should iterate assemblies in the app domain when loading SmartStore types. Loading patterns are applied when loading these assemblies. - /// + /// Gets or sets wether SmartStore should iterate assemblies in the app domain when loading SmartStore types. Loading patterns are applied when loading these assemblies. + /// public bool LoadAppDomainAssemblies { - get { return _loadAppDomainAssemblies; } - set { _loadAppDomainAssemblies = value; } + get => _loadAppDomainAssemblies; + set => _loadAppDomainAssemblies = value; } /// @@ -56,8 +51,8 @@ public bool LoadAppDomainAssemblies /// public IList CustomAssemblyNames { - get { return _customAssemblyNames; } - set { _customAssemblyNames = value; } + get => _customAssemblyNames; + set => _customAssemblyNames = value; } public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses = true) @@ -66,94 +61,94 @@ public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable GetAssemblies(bool ignoreInactivePlugins = false) - { - var assemblies = new HashSet(); + public virtual IEnumerable GetAssemblies(bool ignoreInactivePlugins = false) + { + var assemblies = new HashSet(); - var addedAssemblyNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var addedAssemblyNames = new HashSet(StringComparer.OrdinalIgnoreCase); - if (_loadAppDomainAssemblies) - { - AddAppDomainAssemblies(addedAssemblyNames, assemblies); - } + if (_loadAppDomainAssemblies) + { + AddAppDomainAssemblies(addedAssemblyNames, assemblies); + } - AddCustomAssemblies(addedAssemblyNames, assemblies); + AddCustomAssemblies(addedAssemblyNames, assemblies); - if (ignoreInactivePlugins) - { - return assemblies.Where(x => PluginManager.IsActivePluginAssembly(x)).AsReadOnly(); - } + if (ignoreInactivePlugins) + { + return assemblies.Where(x => PluginManager.IsActivePluginAssembly(x)).AsReadOnly(); + } - return assemblies.AsReadOnly(); - } + return assemblies.AsReadOnly(); + } - /// - /// Iterates all assemblies in the AppDomain and if its name matches the configured patterns adds it to our list. - /// - /// - /// - private void AddAppDomainAssemblies(HashSet addedAssemblyNames, HashSet assemblies) + /// + /// Iterates all assemblies in the AppDomain and if its name matches the configured patterns adds it to our list. + /// + /// + /// + private void AddAppDomainAssemblies(HashSet addedAssemblyNames, HashSet assemblies) { - var domainAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var domainAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var assembly in domainAssemblies) + foreach (var assembly in domainAssemblies) { if (Matches(assembly)) { @@ -175,50 +170,50 @@ protected virtual void AddCustomAssemblies(HashSet addedAssemblyNames, H { if (!addedAssemblyNames.Contains(assemblyName)) { - try - { - var assembly = Assembly.Load(assemblyName); - assemblies.Add(assembly); - Logger.DebugFormat("Loaded custom assembly '{0}'", assemblyName); - } - finally - { - addedAssemblyNames.Add(assemblyName); - } + try + { + var assembly = Assembly.Load(assemblyName); + assemblies.Add(assembly); + Logger.DebugFormat("Loaded custom assembly '{0}'", assemblyName); + } + finally + { + addedAssemblyNames.Add(assemblyName); + } } } } - /// - /// Checks whether an assembly is one of the shipped system or 3rd party assemblies that don't need to be investigated. - /// - /// The assembly to check. - /// True if the assembly should be loaded into SmartStore. - public virtual bool Matches(Assembly assembly) - { + /// + /// Checks whether an assembly is one of the shipped system or 3rd party assemblies that don't need to be investigated. + /// + /// The assembly to check. + /// True if the assembly should be loaded into SmartStore. + public virtual bool Matches(Assembly assembly) + { var name = assembly.FullName; - // Check if passed assembly is a system assembly - if (_systemAssembliesPattern.IsMatch(name)) - { - return false; - } - - // Check if the passed assembly starts with "SmartStore." - if (name.StartsWith("SmartStore.", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Check if any of the referenced assemblies of the passed assembly start with "SmartStore." - // In this case it's obviously a SmartStore plugin. - if (assembly.GetReferencedAssemblies().Any(x => x.FullName.StartsWith("SmartStore.", StringComparison.OrdinalIgnoreCase))) - { - return true; - } - - return false; - } + // Check if passed assembly is a system assembly + if (_systemAssembliesPattern.IsMatch(name)) + { + return false; + } + + // Check if the passed assembly starts with "SmartStore." + if (name.StartsWith("SmartStore.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check if any of the referenced assemblies of the passed assembly start with "SmartStore." + // In this case it's obviously a SmartStore plugin. + if (assembly.GetReferencedAssemblies().Any(x => x.FullName.StartsWith("SmartStore.", StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + return false; + } protected virtual bool DoesTypeImplementOpenGeneric(Type type, Type openGeneric) { @@ -236,11 +231,11 @@ protected virtual bool DoesTypeImplementOpenGeneric(Type type, Type openGeneric) return false; } - catch + catch { return false; } } - + } -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ApplicationEnvironment.cs b/src/Libraries/SmartStore.Core/Infrastructure/ApplicationEnvironment.cs index f2582a6096..15ba56ebc8 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ApplicationEnvironment.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ApplicationEnvironment.cs @@ -6,42 +6,31 @@ namespace SmartStore.Core { - public class ApplicationEnvironment : IApplicationEnvironment - { - public ApplicationEnvironment(IVirtualPathProvider vpp, ILogger logger) - { - WebRootFolder = new VirtualFolder("~/", vpp, logger); - AppDataFolder = new VirtualFolder("~/App_Data/", vpp, logger); - ThemesFolder = new VirtualFolder(CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/"), vpp, logger); - PluginsFolder = new VirtualFolder("~/Plugins/", vpp, logger); + public class ApplicationEnvironment : IApplicationEnvironment + { + public ApplicationEnvironment(IVirtualPathProvider vpp, ILogger logger) + { + WebRootFolder = new VirtualFolder("~/", vpp, logger); + AppDataFolder = new VirtualFolder("~/App_Data/", vpp, logger); + ThemesFolder = new VirtualFolder(CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/"), vpp, logger); + PluginsFolder = new VirtualFolder("~/Plugins/", vpp, logger); - if (DataSettings.Current.IsValid()) - { - TenantFolder = new VirtualFolder(DataSettings.Current.TenantPath, vpp, logger); - } - } + if (DataSettings.Current.IsValid()) + { + TenantFolder = new VirtualFolder(DataSettings.Current.TenantPath, vpp, logger); + } + } - public virtual string MachineName - { - get - { - return Environment.MachineName; - } - } + public virtual string MachineName => Environment.MachineName; - public virtual string EnvironmentIdentifier - { - get - { - // use the current host and the process id as two servers could run on the same machine - return Environment.MachineName + "-" + System.Diagnostics.Process.GetCurrentProcess().Id; - } - } + public virtual string EnvironmentIdentifier => + // use the current host and the process id as two servers could run on the same machine + Environment.MachineName + "-" + System.Diagnostics.Process.GetCurrentProcess().Id; - public virtual IVirtualFolder WebRootFolder { get; private set; } - public virtual IVirtualFolder AppDataFolder { get; private set; } - public virtual IVirtualFolder ThemesFolder { get; private set; } - public virtual IVirtualFolder PluginsFolder { get; private set; } - public virtual IVirtualFolder TenantFolder { get; private set; } - } + public virtual IVirtualFolder WebRootFolder { get; private set; } + public virtual IVirtualFolder AppDataFolder { get; private set; } + public virtual IVirtualFolder ThemesFolder { get; private set; } + public virtual IVirtualFolder PluginsFolder { get; private set; } + public virtual IVirtualFolder TenantFolder { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ApplicationStart.cs b/src/Libraries/SmartStore.Core/Infrastructure/ApplicationStart.cs index 766adcf12d..14b51e277c 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ApplicationStart.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ApplicationStart.cs @@ -1,9 +1,9 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using System.Web; -using System.Web.Mvc.Filters; using System.Web.Mvc; +using System.Web.Mvc.Filters; using SmartStore.Core.Logging; namespace SmartStore.Core.Infrastructure @@ -156,7 +156,7 @@ private static List GetStarterModuleInfos() .Select(x => new StarterModuleInfo { ModuleType = x }) .ToList(); } - + return _starterModuleInfos; } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs b/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs index 153405382b..9463d4a094 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs @@ -1,15 +1,20 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using SmartStore.Utilities; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using SmartStore.ComponentModel; +using SmartStore.Utilities; namespace SmartStore { + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class ObjectSignatureAttribute : Attribute + { + } + /// /// Provides a standard base class for facilitating sophisticated comparison of objects. /// @@ -18,7 +23,7 @@ public abstract class ComparableObject { private readonly HashSet _extraSignatureProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - private static readonly ConcurrentDictionary _signaturePropertyNames = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _signaturePropertyNames = new ConcurrentDictionary(); public override bool Equals(object obj) { @@ -46,19 +51,19 @@ public override int GetHashCode() var signatureProperties = GetSignatureProperties().ToArray(); Type t = this.GetType(); - var combiner = HashCodeCombiner.Start(); + var combiner = HashCodeCombiner.Start(); - // It's possible for two objects to return the same hash code based on - // identically valued properties, even if they're of two different types, - // so we include the object's type in the hash calculation - combiner.Add(t.GetHashCode()); + // It's possible for two objects to return the same hash code based on + // identically valued properties, even if they're of two different types, + // so we include the object's type in the hash calculation + combiner.Add(t.GetHashCode()); foreach (var prop in signatureProperties) { var value = prop.GetValue(this); if (value != null) - combiner.Add(value.GetHashCode()); + combiner.Add(value.GetHashCode()); } if (signatureProperties.Length > 0) @@ -113,17 +118,17 @@ protected virtual bool HasSameSignatureAs(ComparableObject compareTo) /// public IEnumerable GetSignatureProperties() { - var type = GetType(); - var propertyNames = GetSignaturePropertyNamesCore(); - - foreach (var name in propertyNames) - { - var fastProperty = FastProperty.GetProperty(type, name); - if (fastProperty != null) - { - yield return fastProperty; - } - } + var type = GetType(); + var propertyNames = GetSignaturePropertyNamesCore(); + + foreach (var name in propertyNames) + { + var fastProperty = FastProperty.GetProperty(type, name); + if (fastProperty != null) + { + yield return fastProperty; + } + } } /// @@ -133,22 +138,22 @@ public IEnumerable GetSignatureProperties() protected virtual string[] GetSignaturePropertyNamesCore() { Type type = this.GetType(); - string[] names; + string[] names; - if (!_signaturePropertyNames.TryGetValue(type, out names)) - { - names = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => Attribute.IsDefined(p, typeof(ObjectSignatureAttribute), true)) - .Select(p => p.Name) - .ToArray(); + if (!_signaturePropertyNames.TryGetValue(type, out names)) + { + names = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => Attribute.IsDefined(p, typeof(ObjectSignatureAttribute), true)) + .Select(p => p.Name) + .ToArray(); - _signaturePropertyNames.TryAdd(type, names); - } + _signaturePropertyNames.TryAdd(type, names); + } - if (_extraSignatureProperties.Count == 0) - { - return names; - } + if (_extraSignatureProperties.Count == 0) + { + return names; + } return names.Union(_extraSignatureProperties).ToArray(); } @@ -163,12 +168,11 @@ protected void RegisterSignatureProperty(string propertyName) { Guard.NotEmpty(propertyName, nameof(propertyName)); - _extraSignatureProperties.Add(propertyName); + _extraSignatureProperties.Add(propertyName); } } - /// /// Generic version of . /// @@ -198,5 +202,4 @@ public virtual bool Equals(T other) return base.Equals(other); } } - } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ContextState.cs b/src/Libraries/SmartStore.Core/Infrastructure/ContextState.cs index e59bd5f44a..ae9af5f59e 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ContextState.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ContextState.cs @@ -4,86 +4,86 @@ namespace SmartStore.Core.Infrastructure { - /// - /// Holds some state for the current HttpContext or thread - /// - /// The type of data to store - public class ContextState where T : class - { - private readonly string _name; - private readonly Func _defaultValue; + /// + /// Holds some state for the current HttpContext or thread + /// + /// The type of data to store + public class ContextState where T : class + { + private readonly string _name; + private readonly Func _defaultValue; - public ContextState(string name) - { - _name = name; - } + public ContextState(string name) + { + _name = name; + } - public ContextState(string name, Func defaultValue) - { - _name = name; - _defaultValue = defaultValue; - } + public ContextState(string name, Func defaultValue) + { + _name = name; + _defaultValue = defaultValue; + } - public T GetState() - { - var key = BuildKey(); + public T GetState() + { + var key = BuildKey(); - if (HttpContext.Current == null) - { - var data = CallContext.GetData(key); - - if (data == null) - { - if (_defaultValue != null) - { - CallContext.SetData(key, data = _defaultValue()); - return data as T; - } - } - - return data as T; - } + if (HttpContext.Current == null) + { + var data = CallContext.GetData(key); - if (HttpContext.Current.Items[key] == null) - { - HttpContext.Current.Items[key] = _defaultValue?.Invoke(); - } + if (data == null) + { + if (_defaultValue != null) + { + CallContext.SetData(key, data = _defaultValue()); + return data as T; + } + } - return HttpContext.Current.Items[key] as T; - } + return data as T; + } - public void SetState(T state) - { - if (HttpContext.Current == null) - { - CallContext.SetData(BuildKey(), state); - } - else - { - HttpContext.Current.Items[BuildKey()] = state; - } - } + if (HttpContext.Current.Items[key] == null) + { + HttpContext.Current.Items[key] = _defaultValue?.Invoke(); + } - public void RemoveState() - { - var key = BuildKey(); + return HttpContext.Current.Items[key] as T; + } - if (HttpContext.Current == null) - { - CallContext.FreeNamedDataSlot(key); - } - else - { - if (HttpContext.Current.Items.Contains(key)) - { - HttpContext.Current.Items.Remove(key); - } - } - } + public void SetState(T state) + { + if (HttpContext.Current == null) + { + CallContext.SetData(BuildKey(), state); + } + else + { + HttpContext.Current.Items[BuildKey()] = state; + } + } - private string BuildKey() - { - return "__ContextState." + _name; - } - } + public void RemoveState() + { + var key = BuildKey(); + + if (HttpContext.Current == null) + { + CallContext.FreeNamedDataSlot(key); + } + else + { + if (HttpContext.Current.Items.Contains(key)) + { + HttpContext.Current.Items.Remove(key); + } + } + } + + private string BuildKey() + { + return "__ContextState." + _name; + } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/AutofacRequestLifetimeHttpModule.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/AutofacRequestLifetimeHttpModule.cs index f101841492..d32030bcbd 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/AutofacRequestLifetimeHttpModule.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/AutofacRequestLifetimeHttpModule.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Web; using Autofac.Integration.Mvc; @@ -10,68 +9,68 @@ namespace SmartStore.Core.Infrastructure.DependencyManagement /// that creates a nested lifetime scope for each HTTP request. /// public class AutofacRequestLifetimeHttpModule : IHttpModule - { - public void Init(HttpApplication context) - { - Guard.NotNull(context, nameof(context)); + { + public void Init(HttpApplication context) + { + Guard.NotNull(context, nameof(context)); - context.EndRequest += OnEndRequest; - } + context.EndRequest += OnEndRequest; + } - public static void OnEndRequest(object sender, EventArgs e) - { - if (LifetimeScopeProvider != null) - { - LifetimeScopeProvider.EndLifetimeScope(); - } + public static void OnEndRequest(object sender, EventArgs e) + { + if (LifetimeScopeProvider != null) + { + LifetimeScopeProvider.EndLifetimeScope(); + } - // Dispose all other disposable object in HttpContext.Items - PurgeContextItems(sender as HttpApplication); - } + // Dispose all other disposable object in HttpContext.Items + PurgeContextItems(sender as HttpApplication); + } - private static void PurgeContextItems(HttpApplication app) - { - var items = app?.Context?.Items; + private static void PurgeContextItems(HttpApplication app) + { + var items = app?.Context?.Items; - if (items != null) - { - int size = items.Count; - if (size > 0) - { - var keys = new object[size]; - items.Keys.CopyTo(keys, 0); + if (items != null) + { + int size = items.Count; + if (size > 0) + { + var keys = new object[size]; + items.Keys.CopyTo(keys, 0); - for (int i = 0; i < size; i++) - { - var obj = items[keys[i]] as IDisposable; - if (obj != null) - { - try - { - obj.Dispose(); - } - catch { } - } - } - } - } - } + for (int i = 0; i < size; i++) + { + var obj = items[keys[i]] as IDisposable; + if (obj != null) + { + try + { + obj.Dispose(); + } + catch { } + } + } + } + } + } - public static void SetLifetimeScopeProvider(ILifetimeScopeProvider lifetimeScopeProvider) - { - LifetimeScopeProvider = lifetimeScopeProvider ?? throw new ArgumentNullException("lifetimeScopeProvider"); - } + public static void SetLifetimeScopeProvider(ILifetimeScopeProvider lifetimeScopeProvider) + { + LifetimeScopeProvider = lifetimeScopeProvider ?? throw new ArgumentNullException("lifetimeScopeProvider"); + } - internal static ILifetimeScopeProvider LifetimeScopeProvider - { - get; - private set; - } + internal static ILifetimeScopeProvider LifetimeScopeProvider + { + get; + private set; + } - public void Dispose() - { - } + public void Dispose() + { + } - } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs index a839f412c1..dbbef7fe1a 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs @@ -16,48 +16,45 @@ namespace SmartStore.Core.Infrastructure.DependencyManagement public class ContainerManager { private readonly IContainer _container; - private readonly ConcurrentDictionary _cachedActivators = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _cachedActivators = new ConcurrentDictionary(); public ContainerManager(IContainer container) { _container = container; } - public IContainer Container - { - get { return _container; } - } + public IContainer Container => _container; [DebuggerStepThrough] public T Resolve(object key = null, ILifetimeScope scope = null) where T : class { if (key == null) { - return (scope ?? Scope()).Resolve(); + return (scope ?? Scope()).Resolve(); } - return (scope ?? Scope()).ResolveKeyed(key); + return (scope ?? Scope()).ResolveKeyed(key); } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class { - return (scope ?? Scope()).ResolveNamed(name); + return (scope ?? Scope()).ResolveNamed(name); } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public object Resolve(Type type, ILifetimeScope scope = null) { - return (scope ?? Scope()).Resolve(type); + return (scope ?? Scope()).Resolve(type); } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) { - return (scope ?? Scope()).ResolveNamed(name, type); + return (scope ?? Scope()).ResolveNamed(name, type); } [DebuggerStepThrough] @@ -65,10 +62,10 @@ public T[] ResolveAll(object key = null, ILifetimeScope scope = null) { if (key == null) { - return (scope ?? Scope()).Resolve>().ToArray(); + return (scope ?? Scope()).Resolve>().ToArray(); } - return (scope ?? Scope()).ResolveKeyed>(key).ToArray(); + return (scope ?? Scope()).ResolveKeyed>(key).ToArray(); } public T ResolveUnregistered(ILifetimeScope scope = null) where T : class @@ -76,143 +73,138 @@ public T ResolveUnregistered(ILifetimeScope scope = null) where T : class return ResolveUnregistered(typeof(T), scope) as T; } - public object ResolveUnregistered(Type type, ILifetimeScope scope = null) - { - object[] parameterInstances = null; - - if (!_cachedActivators.TryGetValue(type, out FastActivator activator)) - { - var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (var constructor in constructors) - { - var parameterTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray(); - if (TryResolveAll(parameterTypes, out parameterInstances, scope)) - { - activator = new FastActivator(constructor); - _cachedActivators.TryAdd(type, activator); - break; - } - } - } - - if (activator != null) - { - if (parameterInstances == null) - { - TryResolveAll(activator.ParameterTypes, out parameterInstances, scope); + public object ResolveUnregistered(Type type, ILifetimeScope scope = null) + { + object[] parameterInstances = null; + + if (!_cachedActivators.TryGetValue(type, out FastActivator activator)) + { + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (var constructor in constructors) + { + var parameterTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray(); + if (TryResolveAll(parameterTypes, out parameterInstances, scope)) + { + activator = new FastActivator(constructor); + _cachedActivators.TryAdd(type, activator); + break; + } } + } - if (parameterInstances != null) - { - return activator.Activate(parameterInstances); - } - } + if (activator != null) + { + if (parameterInstances == null) + { + TryResolveAll(activator.ParameterTypes, out parameterInstances, scope); + } + + if (parameterInstances != null) + { + return activator.Activate(parameterInstances); + } + } throw new SmartException("No constructor for {0} was found that had all the dependencies satisfied.".FormatInvariant(type.Name.NaIfEmpty())); } - private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope scope = null) - { - instances = null; + private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope scope = null) + { + instances = null; - try - { - var instances2 = new object[types.Length]; + try + { + var instances2 = new object[types.Length]; - for (int i = 0; i < types.Length; i++) - { - var service = Resolve(types[i], scope); - if (service == null) - { - return false; - } + for (int i = 0; i < types.Length; i++) + { + var service = Resolve(types[i], scope); + if (service == null) + { + return false; + } - instances2[i] = service; - } + instances2[i] = service; + } - instances = instances2; - return true; - } - catch (Exception ex) - { - _container.Resolve().GetLogger(this.GetType()).Error(ex); - return false; - } - } + instances = instances2; + return true; + } + catch (Exception ex) + { + _container.Resolve().GetLogger(this.GetType()).Error(ex); + return false; + } + } [DebuggerStepThrough] public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) { - instance = null; + instance = null; - try - { - return (scope ?? Scope()).TryResolve(serviceType, out instance); - } - catch - { - return false; - } + try + { + return (scope ?? Scope()).TryResolve(serviceType, out instance); + } + catch + { + return false; + } } [DebuggerStepThrough] public bool TryResolve(ILifetimeScope scope, out T instance) - { - instance = default(T); + where T : class + { + instance = default(T); - try - { - return (scope ?? Scope()).TryResolve(out instance); - } - catch - { - return false; - } - } + try + { + return (scope ?? Scope()).TryResolve(out instance); + } + catch + { + return false; + } + } - public bool IsRegistered(Type serviceType, ILifetimeScope scope = null) + public bool IsRegistered(Type serviceType, ILifetimeScope scope = null) { - return (scope ?? Scope()).IsRegistered(serviceType); + return (scope ?? Scope()).IsRegistered(serviceType); } - public object ResolveOptional(Type serviceType, ILifetimeScope scope = null) + public object ResolveOptional(Type serviceType, ILifetimeScope scope = null) { - return (scope ?? Scope()).ResolveOptional(serviceType); + return (scope ?? Scope()).ResolveOptional(serviceType); } - public T InjectProperties(T instance, ILifetimeScope scope = null) - { - return (scope ?? Scope()).InjectProperties(instance); - } + public T InjectProperties(T instance, ILifetimeScope scope = null) + { + return (scope ?? Scope()).InjectProperties(instance); + } - public T InjectUnsetProperties(T instance, ILifetimeScope scope = null) - { - return (scope ?? Scope()).InjectUnsetProperties(instance); - } + public T InjectUnsetProperties(T instance, ILifetimeScope scope = null) + { + return (scope ?? Scope()).InjectUnsetProperties(instance); + } [DebuggerStepThrough] public ILifetimeScope Scope() { - var scope = _container.Resolve().GetLifetimeScope(null); - return scope ?? _container; + var scope = _container.Resolve().GetLifetimeScope(null); + return scope ?? _container; } - public ILifetimeScopeAccessor ScopeAccessor - { - get - { - return _container.Resolve(); - } - } + public ILifetimeScopeAccessor ScopeAccessor => _container.Resolve(); } public static class ContainerManagerExtensions { - public static IRegistrationBuilder WithNullCache(this IRegistrationBuilder registration) where TReflectionActivatorData : ReflectionActivatorData - { - return registration.WithParameter(Autofac.Core.ResolvedParameter.ForNamed("null")); - } + public static IRegistrationBuilder WithNullCache(this IRegistrationBuilder registration) where TReflectionActivatorData : ReflectionActivatorData + { + return registration.WithParameter(Autofac.Core.ResolvedParameter.ForNamed("null")); + } } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs index 1c7fed864c..b4b0886ac5 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs @@ -1,88 +1,99 @@ using System; -using System.Diagnostics; using System.Web; using Autofac; -using Autofac.Core.Lifetime; -using SmartStore.Core.Data; -using SmartStore.Core.Logging; using SmartStore.Utilities; namespace SmartStore.Core.Infrastructure.DependencyManagement { - public class DefaultLifetimeScopeAccessor : ILifetimeScopeAccessor - { - private ContextState _state; - private readonly ILifetimeScope _rootContainer; - internal static readonly object ScopeTag = "AutofacWebRequest"; - - public DefaultLifetimeScopeAccessor(ILifetimeScope rootContainer) - { - Guard.NotNull(rootContainer, nameof(rootContainer)); - - //rootContainer.ChildLifetimeScopeBeginning += OnScopeBeginning; - - this._rootContainer = rootContainer; - this._state = new ContextState("CustomLifetimeScopeProvider.WorkScope"); - } - - public ILifetimeScope ApplicationContainer - { - get { return _rootContainer; } - } - - public IDisposable BeginContextAwareScope() - { - var disposer = HttpContext.Current != null - ? ActionDisposable.Empty - : new ActionDisposable(() => this.EndLifetimeScope()); - - return disposer; - } - - public void EndLifetimeScope() - { - try - { - var scope = _state.GetState(); - if (scope != null) - { - scope.Dispose(); - _state.RemoveState(); - } - } - catch { } - } - - public ILifetimeScope GetLifetimeScope(Action configurationAction) - { - var scope = _state.GetState(); - if (scope == null) - { - _state.SetState((scope = BeginLifetimeScope(configurationAction))); - //scope.CurrentScopeEnding += OnScopeEnding; - } - - return scope; - } - - //private void OnScopeBeginning(object sender, LifetimeScopeBeginningEventArgs args) - //{ - // bool isWeb = System.Web.HttpContext.Current != null; - // Debug.WriteLine("Scope Begin, Web: " + isWeb); - //} - - //private void OnScopeEnding(object sender, LifetimeScopeEndingEventArgs args) - //{ - // bool isWeb = System.Web.HttpContext.Current != null; - // Debug.WriteLine("Scope END, Web: " + isWeb); - //} - - public ILifetimeScope BeginLifetimeScope(Action configurationAction) - { - return (configurationAction == null) - ? _rootContainer.BeginLifetimeScope(ScopeTag) - : _rootContainer.BeginLifetimeScope(ScopeTag, configurationAction); - } - - } + public class DefaultLifetimeScopeAccessor : ILifetimeScopeAccessor + { + class ContextAwareScope : IDisposable + { + private readonly Action _disposer; + + public ContextAwareScope(Action disposer) + { + _disposer = disposer; + } + + public void Dispose() + { + _disposer?.Invoke(); + } + } + + private readonly ContextState _state; + private readonly ILifetimeScope _rootContainer; + internal static readonly object ScopeTag = "AutofacWebRequest"; + + public DefaultLifetimeScopeAccessor(ILifetimeScope rootContainer) + { + Guard.NotNull(rootContainer, nameof(rootContainer)); + + //rootContainer.ChildLifetimeScopeBeginning += OnScopeBeginning; + + _rootContainer = rootContainer; + _state = new ContextState("CustomLifetimeScopeProvider.WorkScope"); + } + + public ILifetimeScope ApplicationContainer => _rootContainer; + + public IDisposable BeginContextAwareScope() + { + // Stack-like behaviour for Non-HttpContext thread: + // Only the first call returns a disposer, all nested calls to this method are void. + return HttpContext.Current != null + ? (IDisposable)ActionDisposable.Empty + : new ContextAwareScope( + _state.GetState() == null + ? this.EndLifetimeScope + : (Action)null); + } + + public void EndLifetimeScope() + { + try + { + var scope = _state.GetState(); + if (scope != null) + { + scope.Dispose(); + _state.RemoveState(); + } + } + catch { } + } + + public ILifetimeScope GetLifetimeScope(Action configurationAction) + { + var scope = _state.GetState(); + if (scope == null) + { + _state.SetState((scope = BeginLifetimeScope(configurationAction))); + //scope.CurrentScopeEnding += OnScopeEnding; + } + + return scope; + } + + //private void OnScopeBeginning(object sender, LifetimeScopeBeginningEventArgs args) + //{ + // bool isWeb = System.Web.HttpContext.Current != null; + // Debug.WriteLine("Scope Begin, Web: " + isWeb); + //} + + //private void OnScopeEnding(object sender, LifetimeScopeEndingEventArgs args) + //{ + // bool isWeb = System.Web.HttpContext.Current != null; + // Debug.WriteLine("Scope END, Web: " + isWeb); + //} + + public ILifetimeScope BeginLifetimeScope(Action configurationAction) + { + return (configurationAction == null) + ? _rootContainer.BeginLifetimeScope(ScopeTag) + : _rootContainer.BeginLifetimeScope(ScopeTag, configurationAction); + } + + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeProvider.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeProvider.cs index 78421250f6..eab1548dc0 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeProvider.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeProvider.cs @@ -4,32 +4,29 @@ namespace SmartStore.Core.Infrastructure.DependencyManagement { - public class DefaultLifetimeScopeProvider : ILifetimeScopeProvider - { - private readonly ILifetimeScopeAccessor _accessor; + public class DefaultLifetimeScopeProvider : ILifetimeScopeProvider + { + private readonly ILifetimeScopeAccessor _accessor; - public DefaultLifetimeScopeProvider(ILifetimeScopeAccessor accessor) - { - Guard.NotNull(accessor, nameof(accessor)); + public DefaultLifetimeScopeProvider(ILifetimeScopeAccessor accessor) + { + Guard.NotNull(accessor, nameof(accessor)); - this._accessor = accessor; - AutofacRequestLifetimeHttpModule.SetLifetimeScopeProvider(this); - } + this._accessor = accessor; + AutofacRequestLifetimeHttpModule.SetLifetimeScopeProvider(this); + } - public ILifetimeScope ApplicationContainer - { - get { return _accessor.ApplicationContainer; } - } + public ILifetimeScope ApplicationContainer => _accessor.ApplicationContainer; - public void EndLifetimeScope() - { - _accessor.EndLifetimeScope(); - } + public void EndLifetimeScope() + { + _accessor.EndLifetimeScope(); + } - public ILifetimeScope GetLifetimeScope(Action configurationAction) - { - return _accessor.GetLifetimeScope(configurationAction); - } + public ILifetimeScope GetLifetimeScope(Action configurationAction) + { + return _accessor.GetLifetimeScope(configurationAction); + } - } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs index ee09073226..da03f8fba5 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs @@ -1,52 +1,51 @@ using System; -using System.Collections.Generic; using Autofac; namespace SmartStore.Core.Infrastructure.DependencyManagement { - public interface ILifetimeScopeAccessor - { - /// - /// Gets the global, application-wide container. - /// - ILifetimeScope ApplicationContainer { get; } + public interface ILifetimeScopeAccessor + { + /// + /// Gets the global, application-wide container. + /// + ILifetimeScope ApplicationContainer { get; } - /// - /// Ends the current lifetime scope. - /// - void EndLifetimeScope(); + /// + /// Ends the current lifetime scope. + /// + void EndLifetimeScope(); - /// - /// Gets a nested lifetime scope that services can be resolved from - /// - /// A configuration action that will execute during lifetime scope creation. - /// - /// A new or existing nested lifetime scope. - /// - ILifetimeScope GetLifetimeScope(Action configurationAction); + /// + /// Gets a nested lifetime scope that services can be resolved from + /// + /// A configuration action that will execute during lifetime scope creation. + /// + /// A new or existing nested lifetime scope. + /// + ILifetimeScope GetLifetimeScope(Action configurationAction); - /// - /// Either creates a new lifetime scope when HttpContext.Current is null, - /// OR returns the current http context scoped lifetime. - /// - /// - /// A disposable object which does nothing when internal lifetime scope is bound to the http context, - /// OR ends the lifetime scope otherwise. - /// - /// - /// This method is intended for usage in background threads or tasks. There may be situations where HttpContext is present, - /// especially when a task was started with TaskScheduler.FromCurrentSynchronizationContext(). In this case it may not be - /// desirable to create a new scope, but use the existing, http context bound scope instead. - /// - IDisposable BeginContextAwareScope(); + /// + /// Either creates a new lifetime scope when HttpContext.Current is null, + /// OR returns the current http context scoped lifetime. + /// + /// + /// A disposable object which does nothing when internal lifetime scope is bound to the http context, + /// OR ends the lifetime scope otherwise. + /// + /// + /// This method is intended for usage in background threads or tasks. There may be situations where HttpContext is present, + /// especially when a task was started with TaskScheduler.FromCurrentSynchronizationContext(). In this case it may not be + /// desirable to create a new scope, but use the existing, http context bound scope instead. + /// + IDisposable BeginContextAwareScope(); - /// - /// Creates a nested lifetime scope that services can be resolved from - /// - /// A configuration action that will execute during lifetime scope creation. - /// - /// A new nested lifetime scope. - /// - ILifetimeScope BeginLifetimeScope(Action configurationAction); - } + /// + /// Creates a nested lifetime scope that services can be resolved from + /// + /// A configuration action that will execute during lifetime scope creation. + /// + /// A new nested lifetime scope. + /// + ILifetimeScope BeginLifetimeScope(Action configurationAction); + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/Work.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/Work.cs index afcba925eb..7c2bf558aa 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/Work.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/Work.cs @@ -2,21 +2,15 @@ namespace SmartStore.Core.Infrastructure.DependencyManagement { - public class Work where T : class - { - private readonly Func, T> _resolve; + public class Work where T : class + { + private readonly Func, T> _resolve; - public Work(Func, T> resolve) - { - _resolve = resolve; - } + public Work(Func, T> resolve) + { + _resolve = resolve; + } - public T Value - { - get - { - return _resolve(this); - } - } - } + public T Value => _resolve(this); + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs b/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs index e3b01f2e42..34dc6784b2 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs @@ -5,7 +5,6 @@ namespace SmartStore { - public abstract class DisposableObject : IDisposable { private bool _isDisposed; @@ -13,7 +12,7 @@ public abstract class DisposableObject : IDisposable public virtual bool IsDisposed { [DebuggerStepThrough] - get { return _isDisposed; } + get => _isDisposed; } [DebuggerStepThrough] @@ -37,11 +36,11 @@ protected void CheckDisposed(string errorMessage) [DebuggerStepThrough] public void Dispose() { - if (!_isDisposed) - { - Dispose(true); - GC.SuppressFinalize(this); - } + if (!_isDisposed) + { + Dispose(true); + GC.SuppressFinalize(this); + } } protected void Dispose(bool disposing) @@ -104,7 +103,5 @@ protected static void DisposeMember(object member) { Dispose(false); } - } - } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/EngineContext.cs b/src/Libraries/SmartStore.Core/Infrastructure/EngineContext.cs index e610c7b134..2a40869ee4 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/EngineContext.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/EngineContext.cs @@ -1,8 +1,6 @@ using System; using System.Configuration; -using System.Diagnostics; using System.Runtime.CompilerServices; -using SmartStore.Core.Configuration; using SmartStore.Utilities; namespace SmartStore.Core.Infrastructure @@ -35,29 +33,29 @@ public static void Replace(IEngine engine) { Singleton.Instance = engine; } - + /// /// Creates a factory instance and adds a http application injecting facility. /// /// A new factory public static IEngine CreateEngineInstance() { - var engineTypeSetting = CommonHelper.GetAppSetting("sm:EngineType"); - if (engineTypeSetting.HasValue()) + var engineTypeSetting = CommonHelper.GetAppSetting("sm:EngineType"); + if (engineTypeSetting.HasValue()) { - var engineType = Type.GetType(engineTypeSetting); + var engineType = Type.GetType(engineTypeSetting); if (engineType == null) - { - throw new ConfigurationErrorsException("The type '" + engineType + "' could not be found. Please check the configuration at /configuration/appSettings/add[@key=sm:EngineType] or check for missing assemblies."); - } - + { + throw new ConfigurationErrorsException("The type '" + engineType + "' could not be found. Please check the configuration at /configuration/appSettings/add[@key=sm:EngineType] or check for missing assemblies."); + } + if (!typeof(IEngine).IsAssignableFrom(engineType)) - { - throw new ConfigurationErrorsException("The type '" + engineType + "' doesn't implement 'SmartStore.Core.Infrastructure.IEngine' and cannot be configured in /configuration/appSettings/add[@key=sm:EngineType] for that purpose."); - } + { + throw new ConfigurationErrorsException("The type '" + engineType + "' doesn't implement 'SmartStore.Core.Infrastructure.IEngine' and cannot be configured in /configuration/appSettings/add[@key=sm:EngineType] for that purpose."); + } - return Activator.CreateInstance(engineType) as IEngine; + return Activator.CreateInstance(engineType) as IEngine; } return new SmartStoreEngine(); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Error.cs b/src/Libraries/SmartStore.Core/Infrastructure/Error.cs index 1b73c2f89c..bb09490f09 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/Error.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/Error.cs @@ -1,6 +1,6 @@ using System; -using System.Globalization; using System.Diagnostics; +using System.Globalization; using System.Runtime.CompilerServices; namespace SmartStore diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs b/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs index b21c366e30..15aaef296c 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -10,13 +9,13 @@ namespace SmartStore public class Guard { const string AgainstMessage = "Assertion evaluation failed with 'false'."; - const string ImplementsMessage = "Type '{0}' must implement type '{1}'."; - const string InheritsFromMessage = "Type '{0}' must inherit from type '{1}'."; - const string IsTypeOfMessage = "Type '{0}' must be of type '{1}'."; - const string IsEqualMessage = "Compared objects must be equal."; - const string IsPositiveMessage = "Argument '{0}' must be a positive value. Value: '{1}'."; - const string IsTrueMessage = "True expected for '{0}' but the condition was False."; - const string NotNegativeMessage = "Argument '{0}' cannot be a negative value. Value: '{1}'."; + const string ImplementsMessage = "Type '{0}' must implement type '{1}'."; + const string InheritsFromMessage = "Type '{0}' must inherit from type '{1}'."; + const string IsTypeOfMessage = "Type '{0}' must be of type '{1}'."; + const string IsEqualMessage = "Compared objects must be equal."; + const string IsPositiveMessage = "Argument '{0}' must be a positive value. Value: '{1}'."; + const string IsTrueMessage = "True expected for '{0}' but the condition was False."; + const string NotNegativeMessage = "Argument '{0}' cannot be a negative value. Value: '{1}'."; const string NotEmptyStringMessage = "String parameter '{0}' cannot be null or all whitespace."; const string NotEmptyColMessage = "Collection cannot be null and must contain at least one item."; const string NotEmptyGuidMessage = "Argument '{0}' cannot be an empty guid."; @@ -32,160 +31,160 @@ private Guard() { } - #region 3.0 + #region 3.0 - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotNull(object arg, string argName) - { - if (arg == null) - throw new ArgumentNullException(argName); - } + { + if (arg == null) + throw new ArgumentNullException(argName); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotEmpty(string arg, string argName) - { - if (string.IsNullOrWhiteSpace(arg)) - throw Error.Argument(argName, NotEmptyStringMessage, argName); - } + { + if (string.IsNullOrWhiteSpace(arg)) + throw Error.Argument(argName, NotEmptyStringMessage, argName); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotEmpty(ICollection arg, string argName) - { - if (arg == null || !arg.Any()) - throw Error.Argument(argName, NotEmptyColMessage); - } + { + if (arg == null || !arg.Any()) + throw Error.Argument(argName, NotEmptyColMessage); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotEmpty(Guid arg, string argName) - { - if (arg == Guid.Empty) - throw Error.Argument(argName, NotEmptyGuidMessage, argName); - } + { + if (arg == Guid.Empty) + throw Error.Argument(argName, NotEmptyGuidMessage, argName); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InRange(T arg, T min, T max, string argName) where T : struct, IComparable - { - if (arg.CompareTo(min) < 0 || arg.CompareTo(max) > 0) - throw Error.ArgumentOutOfRange(argName, InRangeMessage, argName, min, max); - } + { + if (arg.CompareTo(min) < 0 || arg.CompareTo(max) > 0) + throw Error.ArgumentOutOfRange(argName, InRangeMessage, argName, min, max); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotOutOfLength(string arg, int maxLength, string argName) - { - if (arg.Trim().Length > maxLength) - { - throw Error.Argument(argName, NotOutOfLengthMessage, argName, maxLength); - } - } - - [DebuggerStepThrough] + { + if (arg.Trim().Length > maxLength) + { + throw Error.Argument(argName, NotOutOfLengthMessage, argName, maxLength); + } + } + + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotNegative(T arg, string argName, string message = NotNegativeMessage) where T : struct, IComparable - { - if (arg.CompareTo(default(T)) < 0) - throw Error.ArgumentOutOfRange(argName, message.FormatInvariant(argName, arg)); - } + { + if (arg.CompareTo(default(T)) < 0) + throw Error.ArgumentOutOfRange(argName, message.FormatInvariant(argName, arg)); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotZero(T arg, string argName) where T : struct, IComparable - { - if (arg.CompareTo(default(T)) == 0) - throw Error.ArgumentOutOfRange(argName, NotZeroMessage, argName, arg); - } + { + if (arg.CompareTo(default(T)) == 0) + throw Error.ArgumentOutOfRange(argName, NotZeroMessage, argName, arg); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Against(bool assertion, string message = AgainstMessage) where TException : Exception - { - if (assertion) - throw (TException)Activator.CreateInstance(typeof(TException), message); - } + { + if (assertion) + throw (TException)Activator.CreateInstance(typeof(TException), message); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Against(Func assertion, string message = AgainstMessage) where TException : Exception - { - //Execute the lambda and if it evaluates to true then throw the exception. - if (assertion()) - throw (TException)Activator.CreateInstance(typeof(TException), message); - } + { + //Execute the lambda and if it evaluates to true then throw the exception. + if (assertion()) + throw (TException)Activator.CreateInstance(typeof(TException), message); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IsPositive(T arg, string argName, string message = IsPositiveMessage) where T : struct, IComparable - { - if (arg.CompareTo(default(T)) < 1) - throw Error.ArgumentOutOfRange(argName, message.FormatInvariant(argName)); - } + { + if (arg.CompareTo(default(T)) < 1) + throw Error.ArgumentOutOfRange(argName, message.FormatInvariant(argName)); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IsTrue(bool arg, string argName, string message = IsTrueMessage) - { - if (!arg) - throw Error.Argument(argName, message.FormatInvariant(argName)); - } + { + if (!arg) + throw Error.Argument(argName, message.FormatInvariant(argName)); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IsEnumType(Type arg, string argName) - { - NotNull(arg, argName); + { + NotNull(arg, argName); - if (!arg.IsEnum) - throw Error.Argument(argName, IsEnumTypeMessage, arg.FullName); - } + if (!arg.IsEnum) + throw Error.Argument(argName, IsEnumTypeMessage, arg.FullName); + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IsEnumType(Type enumType, object arg, string argName) - { - NotNull(arg, argName); + { + NotNull(arg, argName); - if (!Enum.IsDefined(enumType, arg)) - { - throw Error.ArgumentOutOfRange(argName, IsEnumTypeMessage2, argName, enumType.FullName); - } - } + if (!Enum.IsDefined(enumType, arg)) + { + throw Error.ArgumentOutOfRange(argName, IsEnumTypeMessage2, argName, enumType.FullName); + } + } - [DebuggerStepThrough] + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NotDisposed(DisposableObject arg, string argName) - { - NotNull(arg, argName); - - if (arg.IsDisposed) - throw Error.ObjectDisposed(argName); - } - - [DebuggerStepThrough] - public static void PagingArgsValid(int indexArg, int sizeArg, string indexArgName, string sizeArgName) - { - NotNegative(indexArg, indexArgName, "PageIndex cannot be below 0"); - - if (indexArg > 0) - { - // if pageIndex is specified (> 0), PageSize CANNOT be 0 - IsPositive(sizeArg, sizeArgName, "PageSize cannot be below 1 if a PageIndex greater 0 was provided."); - } - else - { - // pageIndex 0 actually means: take all! - NotNegative(sizeArg, sizeArgName); - } - } - - #endregion + { + NotNull(arg, argName); + + if (arg.IsDisposed) + throw Error.ObjectDisposed(argName); + } [DebuggerStepThrough] - [Obsolete("Use NotNull() with nameof operator instead")] + public static void PagingArgsValid(int indexArg, int sizeArg, string indexArgName, string sizeArgName) + { + NotNegative(indexArg, indexArgName, "PageIndex cannot be below 0"); + + if (indexArg > 0) + { + // if pageIndex is specified (> 0), PageSize CANNOT be 0 + IsPositive(sizeArg, sizeArgName, "PageSize cannot be below 1 if a PageIndex greater 0 was provided."); + } + else + { + // pageIndex 0 actually means: take all! + NotNegative(sizeArg, sizeArgName); + } + } + + #endregion + + [DebuggerStepThrough] + [Obsolete("Use NotNull() with nameof operator instead")] public static void ArgumentNotNull(object arg, string argName) { if (arg == null) @@ -193,27 +192,27 @@ public static void ArgumentNotNull(object arg, string argName) } [DebuggerStepThrough] - [Obsolete("Use NotNull() with nameof operator instead")] - public static void ArgumentNotNull(Func arg) + [Obsolete("Use NotNull() with nameof operator instead")] + public static void ArgumentNotNull(Func arg) { if (arg() == null) throw new ArgumentNullException(GetParamName(arg)); } [DebuggerStepThrough] - [Obsolete("Use NotEmpty() with nameof operator instead")] - public static void ArgumentNotEmpty(Func arg) + [Obsolete("Use NotEmpty() with nameof operator instead")] + public static void ArgumentNotEmpty(Func arg) { - if (arg().IsEmpty()) - { - var argName = GetParamName(arg); - throw Error.Argument(argName, "String parameter '{0}' cannot be null or all whitespace.", argName); - } + if (arg().IsEmpty()) + { + var argName = GetParamName(arg); + throw Error.Argument(argName, "String parameter '{0}' cannot be null or all whitespace.", argName); + } } [DebuggerStepThrough] - [Obsolete("Use NotEmpty() with nameof operator instead")] - public static void ArgumentNotEmpty(string arg, string argName) + [Obsolete("Use NotEmpty() with nameof operator instead")] + public static void ArgumentNotEmpty(string arg, string argName) { if (arg.IsEmpty()) throw Error.Argument(argName, "String parameter '{0}' cannot be null or all whitespace.", argName); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/IApplicationEnvironment.cs b/src/Libraries/SmartStore.Core/Infrastructure/IApplicationEnvironment.cs index c105628a46..4dbf9dc23a 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/IApplicationEnvironment.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/IApplicationEnvironment.cs @@ -1,17 +1,16 @@ -using System; -using SmartStore.Core.IO; +using SmartStore.Core.IO; namespace SmartStore.Core { - public interface IApplicationEnvironment - { - string MachineName { get; } - string EnvironmentIdentifier { get; } + public interface IApplicationEnvironment + { + string MachineName { get; } + string EnvironmentIdentifier { get; } - IVirtualFolder WebRootFolder { get; } - IVirtualFolder AppDataFolder { get; } - IVirtualFolder ThemesFolder { get; } - IVirtualFolder PluginsFolder { get; } - IVirtualFolder TenantFolder { get; } - } + IVirtualFolder WebRootFolder { get; } + IVirtualFolder AppDataFolder { get; } + IVirtualFolder ThemesFolder { get; } + IVirtualFolder PluginsFolder { get; } + IVirtualFolder TenantFolder { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ICloneable.cs b/src/Libraries/SmartStore.Core/Infrastructure/ICloneable.cs index 6b82c27d85..5c23d126c2 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ICloneable.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ICloneable.cs @@ -2,7 +2,6 @@ namespace SmartStore { - /// /// Generic variant of . /// @@ -15,5 +14,4 @@ public interface ICloneable : ICloneable /// The cloned instance new T Clone(); } - } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs index 4454dfc343..cbe93c2a1d 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using SmartStore.Core.Configuration; using SmartStore.Core.Infrastructure.DependencyManagement; namespace SmartStore.Core.Infrastructure @@ -14,17 +12,17 @@ namespace SmartStore.Core.Infrastructure public interface IEngine { ContainerManager ContainerManager { get; } - + /// /// Initialize components and plugins in the SmartStore environment. /// void Initialize(); - /// - /// Determines whether the app has been installed, successfully bootstrapped - /// AND the very first HTTP request has been issued. - /// - bool IsFullyInitialized { get; set; } + /// + /// Determines whether the app has been installed, successfully bootstrapped + /// AND the very first HTTP request has been issued. + /// + bool IsFullyInitialized { get; set; } T Resolve(string name = null) where T : class; diff --git a/src/Libraries/SmartStore.Core/Infrastructure/IOrdered.cs b/src/Libraries/SmartStore.Core/Infrastructure/IOrdered.cs index 087ee4505d..ca93fcbd3d 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/IOrdered.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/IOrdered.cs @@ -1,6 +1,4 @@ -using System; - -namespace SmartStore +namespace SmartStore { public interface IOrdered { diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ITypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/ITypeFinder.cs index a198846e58..e9f44832e4 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ITypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ITypeFinder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; @@ -12,51 +11,47 @@ namespace SmartStore.Core.Infrastructure /// public interface ITypeFinder { - ///// - ///// Gets all SmartStore specific assemblies (core & all plugins) - ///// - ///// Indicates whether uninstalled plugin's assemblies should be ignored - ///// A list of assemblies that should be loaded by the SmartStore factory. - IEnumerable GetAssemblies(bool ignoreInactivePlugins = false); - IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses = true); + ///// + ///// Gets all SmartStore specific assemblies (core & all plugins) + ///// + ///// Indicates whether uninstalled plugin's assemblies should be ignored + ///// A list of assemblies that should be loaded by the SmartStore factory. + IEnumerable GetAssemblies(bool ignoreInactivePlugins = false); + IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses = true); } - public class NullTypeFinder : ITypeFinder - { - private static readonly ITypeFinder s_instance = new NullTypeFinder(); - - public static ITypeFinder Instance - { - get { return s_instance; } - } - - public IEnumerable GetAssemblies(bool ignoreInactivePlugins = false) - { - return Enumerable.Empty(); - } - - public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses = true) - { - return Enumerable.Empty(); - } - } - - public static class ITypeFinderExtensions - { - public static IEnumerable FindClassesOfType(this ITypeFinder finder, bool onlyConcreteClasses = true, bool ignoreInactivePlugins = false) - { - return finder.FindClassesOfType(typeof(T), finder.GetAssemblies(ignoreInactivePlugins), onlyConcreteClasses); - } - - public static IEnumerable FindClassesOfType(this ITypeFinder finder, Type assignTypeFrom, bool onlyConcreteClasses = true, bool ignoreInactivePlugins = false) - { - return finder.FindClassesOfType(assignTypeFrom, finder.GetAssemblies(ignoreInactivePlugins), onlyConcreteClasses); - } - - public static IEnumerable FindClassesOfType(this ITypeFinder finder, IEnumerable assemblies, bool onlyConcreteClasses = true) - { - return finder.FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses); - } - - } + public class NullTypeFinder : ITypeFinder + { + private static readonly ITypeFinder s_instance = new NullTypeFinder(); + + public static ITypeFinder Instance => s_instance; + + public IEnumerable GetAssemblies(bool ignoreInactivePlugins = false) + { + return Enumerable.Empty(); + } + + public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses = true) + { + return Enumerable.Empty(); + } + } + + public static class ITypeFinderExtensions + { + public static IEnumerable FindClassesOfType(this ITypeFinder finder, bool onlyConcreteClasses = true, bool ignoreInactivePlugins = false) + { + return finder.FindClassesOfType(typeof(T), finder.GetAssemblies(ignoreInactivePlugins), onlyConcreteClasses); + } + + public static IEnumerable FindClassesOfType(this ITypeFinder finder, Type assignTypeFrom, bool onlyConcreteClasses = true, bool ignoreInactivePlugins = false) + { + return finder.FindClassesOfType(assignTypeFrom, finder.GetAssemblies(ignoreInactivePlugins), onlyConcreteClasses); + } + + public static IEnumerable FindClassesOfType(this ITypeFinder finder, IEnumerable assemblies, bool onlyConcreteClasses = true) + { + return finder.FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses); + } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs b/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs deleted file mode 100644 index 42b53f80c4..0000000000 --- a/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace SmartStore -{ - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public sealed class ObjectSignatureAttribute : Attribute - { - } -} diff --git a/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs b/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs index fafd0508bd..f28d4bbab9 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs @@ -1,12 +1,9 @@ -using System; -using System.Text; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace SmartStore { public static class RegularExpressions { - internal static readonly string ValidRealPattern = "^([-]|[.]|[-.]|[0-9])[0-9]*[.]*[0-9]+$"; internal static readonly string ValidIntegerPattern = "^([-]|[0-9])[0-9]*$"; @@ -19,21 +16,20 @@ public static class RegularExpressions public static readonly Regex IsPositiveInteger = new Regex(@"^\d{1,10}", RegexOptions.Compiled); public static readonly Regex IsNumeric = new Regex("(" + ValidRealPattern + ")|(" + ValidIntegerPattern + ")", RegexOptions.Compiled); - // //public static readonly Regex IsWebUrl = new Regex(@"(http|https)://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?", RegexOptions.Singleline | RegexOptions.Compiled); - ///// see https://msdn.microsoft.com/en-us/library/ms998267.aspx - //public static readonly Regex IsWebUrl = new Regex(@"^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_\=~]*)?$", RegexOptions.Singleline | RegexOptions.Compiled); + // //public static readonly Regex IsWebUrl = new Regex(@"(http|https)://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?", RegexOptions.Singleline | RegexOptions.Compiled); + ///// see https://msdn.microsoft.com/en-us/library/ms998267.aspx + //public static readonly Regex IsWebUrl = new Regex(@"^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_\=~]*)?$", RegexOptions.Singleline | RegexOptions.Compiled); - public static readonly Regex IsEmail = new Regex("^(?:[\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\`\\{\\|\\}\\~]+\\.)*[\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\`\\{\\|\\}\\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-](?!\\.)){0,61}[a-zA-Z0-9]?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\\[(?:(?:[01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.){3}(?:[01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\]))$", RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static readonly Regex IsEmail = new Regex("^(?:[\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\`\\{\\|\\}\\~]+\\.)*[\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\`\\{\\|\\}\\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-](?!\\.)){0,61}[a-zA-Z0-9]?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\\[(?:(?:[01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.){3}(?:[01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\]))$", RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase); public static readonly Regex IsGuid = new Regex(@"\{?[a-fA-F0-9]{8}(?:-(?:[a-fA-F0-9]){4}){3}-[a-fA-F0-9]{12}\}?", RegexOptions.Compiled); public static readonly Regex IsBase64Guid = new Regex(@"[a-zA-Z0-9+/=]{22,24}", RegexOptions.Compiled); public static readonly Regex IsCultureCode = new Regex(@"^[a-z]{2}(-[A-Z]{2})?$", RegexOptions.Singleline | RegexOptions.Compiled); - public static readonly Regex IsYearRange = new Regex(@"^(\d{4})-(\d{4})$", RegexOptions.Compiled); + public static readonly Regex IsYearRange = new Regex(@"^(\d{4})-(\d{4})$", RegexOptions.Compiled); - public static readonly Regex IsIban = new Regex(@"[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}", RegexOptions.Singleline | RegexOptions.Compiled); - public static readonly Regex IsBic = new Regex(@"([a-zA-Z]{4}[a-zA-Z]{2}[a-zA-Z0-9]{2}([a-zA-Z0-9]{3})?)", RegexOptions.Singleline | RegexOptions.Compiled); + public static readonly Regex IsIban = new Regex(@"[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}", RegexOptions.Singleline | RegexOptions.Compiled); + public static readonly Regex IsBic = new Regex(@"([a-zA-Z]{4}[a-zA-Z]{2}[a-zA-Z0-9]{2}([a-zA-Z0-9]{3})?)", RegexOptions.Singleline | RegexOptions.Compiled); } - } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Singleton.cs b/src/Libraries/SmartStore.Core/Infrastructure/Singleton.cs index 66c50bd5fa..a28b7c56f8 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/Singleton.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/Singleton.cs @@ -14,10 +14,12 @@ public class Singleton : Singleton { static T instance; - /// The singleton instance for the specified type T. Only one instance (at the time) of this object for each type of T. + /// + /// The singleton instance for the specified type T. Only one instance (at the time) of this object for each type of T. + /// public static T Instance { - get { return instance; } + get => instance; set { instance = value; @@ -38,10 +40,7 @@ static SingletonList() } /// The singleton instance for the specified type T. Only one instance (at the time) of this list for each type of T. - public new static IList Instance - { - get { return Singleton>.Instance; } - } + public new static IList Instance => Singleton>.Instance; } /// @@ -57,10 +56,7 @@ static SingletonDictionary() } /// The singleton instance for the specified type T. Only one instance (at the time) of this dictionary for each type of T. - public new static IDictionary Instance - { - get { return Singleton>.Instance; } - } + public new static IDictionary Instance => Singleton>.Instance; } /// @@ -76,9 +72,6 @@ static Singleton() static readonly IDictionary allSingletons; /// Dictionary of type to singleton instances. - public static IDictionary AllSingletons - { - get { return allSingletons; } - } + public static IDictionary AllSingletons => allSingletons; } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs index 48979a36b0..5fabdf7d55 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs @@ -17,147 +17,134 @@ public class SmartStoreEngine : IEngine { private ContainerManager _containerManager; - public SmartStoreEngine() - { - Logger = NullLogger.Instance; - } - - protected ILogger Logger - { - get; - private set; - } - - protected virtual void RunStartupTasks() + public SmartStoreEngine() { - var typeFinder = _containerManager.Resolve(); + Logger = NullLogger.Instance; + } + + protected ILogger Logger + { + get; + private set; + } + + protected virtual void RunStartupTasks() + { + var typeFinder = _containerManager.Resolve(); var startUpTaskTypes = typeFinder.FindClassesOfType(ignoreInactivePlugins: true); var startUpTasks = new List(); foreach (var startUpTaskType in startUpTaskTypes) { - startUpTasks.Add((IApplicationStart)Activator.CreateInstance(startUpTaskType)); + startUpTasks.Add((IApplicationStart)Activator.CreateInstance(startUpTaskType)); + } + + // execute tasks async grouped by order + var groupedTasks = startUpTasks.OrderBy(st => st.Order).ToLookup(x => x.Order); + foreach (var tasks in groupedTasks) + { + Parallel.ForEach(tasks, task => + { + try + { + Logger.DebugFormat("Executing startup task '{0}'", task.GetType().FullName); + task.Start(); + } + catch (Exception ex) + { + Logger.ErrorFormat(ex, "Error while executing startup task '{0}'", task.GetType().FullName); + } + }); + } + } + + protected virtual ITypeFinder CreateTypeFinder() + { + return new WebAppTypeFinder(); + } + + protected virtual object CreateDependencyResolver(IContainer container) + { + var scopeProvider = container.Resolve(); + var dependencyResolver = new AutofacDependencyResolver(container, scopeProvider); + return dependencyResolver; + } + + protected virtual ContainerManager RegisterDependencies() + { + var typeFinder = CreateTypeFinder(); + + // core dependencies + var builder = new ContainerBuilder(); + builder.RegisterInstance(this).As(); + builder.RegisterInstance(typeFinder).As(); + + // Autofac + builder.Register(x => new DefaultLifetimeScopeAccessor(x.Resolve())).As().SingleInstance(); + builder.Register(x => new DefaultLifetimeScopeProvider(x.Resolve())).As().SingleInstance(); + builder.Register(x => new AutofacDependencyResolver(x.Resolve(), x.Resolve())).As().SingleInstance(); + + // Logging dependencies should be available very early + builder.RegisterModule(new LoggingModule()); + + // Register dependencies provided by other assemblies + var registrarTypes = typeFinder.FindClassesOfType(); + var registrarInstances = new List(); + foreach (var type in registrarTypes) + { + registrarInstances.Add((IDependencyRegistrar)Activator.CreateInstance(type)); + } + + // Sort + registrarInstances = registrarInstances.OrderBy(t => t.Order).ToList(); + foreach (var registrar in registrarInstances) + { + var type = registrar.GetType(); + Debug.WriteLine("Executing dependency registrar '{0}'.".FormatInvariant(type.FullName)); + registrar.Register(builder, typeFinder, PluginManager.IsActivePluginAssembly(type.Assembly)); } - // execute tasks async grouped by order - var groupedTasks = startUpTasks.OrderBy(st => st.Order).ToLookup(x => x.Order); - foreach (var tasks in groupedTasks) - { - Parallel.ForEach(tasks, task => - { - try - { - Logger.DebugFormat("Executing startup task '{0}'", task.GetType().FullName); - task.Start(); - } - catch (Exception ex) - { - Logger.ErrorFormat(ex, "Error while executing startup task '{0}'", task.GetType().FullName); - } - }); - } + var container = builder.Build(); + _containerManager = new ContainerManager(container); + + // MVC dependency resolver + DependencyResolver.SetResolver(container.Resolve()); + + // Logger + this.Logger = container.Resolve().GetLogger("SmartStore.Bootstrapper"); + ((AppDomainTypeFinder)typeFinder).Logger = this.Logger; + + return _containerManager; } - protected virtual ITypeFinder CreateTypeFinder() - { - return new WebAppTypeFinder(); - } - - protected virtual object CreateDependencyResolver(IContainer container) - { - var scopeProvider = container.Resolve(); - var dependencyResolver = new AutofacDependencyResolver(container, scopeProvider); - return dependencyResolver; - } - - protected virtual ContainerManager RegisterDependencies() - { - var builder = new ContainerBuilder(); - var container = builder.Build(); - var typeFinder = CreateTypeFinder(); - - _containerManager = new ContainerManager(container); - - // core dependencies - builder = new ContainerBuilder(); - builder.RegisterInstance(this).As(); - builder.RegisterInstance(typeFinder).As(); - - // Autofac - var lifetimeScopeAccessor = new DefaultLifetimeScopeAccessor(container); - var lifetimeScopeProvider = new DefaultLifetimeScopeProvider(lifetimeScopeAccessor); - builder.RegisterInstance(lifetimeScopeAccessor).As(); - builder.RegisterInstance(lifetimeScopeProvider).As(); - - var dependencyResolver = new AutofacDependencyResolver(container, lifetimeScopeProvider); - builder.RegisterInstance(dependencyResolver); - DependencyResolver.SetResolver(dependencyResolver); - - // Logging dependencies should be available very early - builder.RegisterModule(new LoggingModule()); - -#pragma warning disable 612, 618 - builder.Update(container); -#pragma warning restore 612, 618 - - // Propagate logger - var logger = container.Resolve().GetLogger("SmartStore.Bootstrapper"); - this.Logger = logger; - ((AppDomainTypeFinder)typeFinder).Logger = logger; - - // Register dependencies provided by other assemblies - builder = new ContainerBuilder(); - var registrarTypes = typeFinder.FindClassesOfType(); - var registrarInstances = new List(); - foreach (var type in registrarTypes) - { - registrarInstances.Add((IDependencyRegistrar)Activator.CreateInstance(type)); - } - - // Sort - registrarInstances = registrarInstances.OrderBy(t => t.Order).ToList(); - foreach (var registrar in registrarInstances) - { - var type = registrar.GetType(); - logger.DebugFormat("Executing dependency registrar '{0}'", type.FullName); - registrar.Register(builder, typeFinder, PluginManager.IsActivePluginAssembly(type.Assembly)); - } - -#pragma warning disable 612, 618 - builder.Update(container); -#pragma warning restore 612, 618 - - return _containerManager; - } - /// /// Initialize components and plugins /// public void Initialize() { - RegisterDependencies(); + RegisterDependencies(); - if (DataSettings.DatabaseIsInstalled()) - { - RunStartupTasks(); - } + if (DataSettings.DatabaseIsInstalled()) + { + RunStartupTasks(); + } } - public bool IsFullyInitialized - { - get; - set; - } + public bool IsFullyInitialized + { + get; + set; + } [DebuggerStepThrough] - public T Resolve(string name = null) where T : class - { + public T Resolve(string name = null) where T : class + { if (name.HasValue()) { return ContainerManager.ResolveNamed(name); } return ContainerManager.Resolve(); - } + } [DebuggerStepThrough] public object Resolve(Type type, string name = null) @@ -175,14 +162,8 @@ public T[] ResolveAll() return ContainerManager.ResolveAll(); } - public IContainer Container - { - get { return _containerManager.Container; } - } + public IContainer Container => _containerManager.Container; - public ContainerManager ContainerManager - { - get { return _containerManager; } - } - } + public ContainerManager ContainerManager => _containerManager; + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs index a437eebd7d..4b6981b2ef 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; using System.Web; @@ -18,27 +16,21 @@ namespace SmartStore.Core.Infrastructure /// public class WebAppTypeFinder : AppDomainTypeFinder { - private bool _ensureBinFolderAssembliesLoaded; + private readonly Lazy> _allAssembliesResolver; + private readonly Lazy> _activeAssembliesResolver; - private readonly Lazy> _allAssembliesResolver; - private readonly Lazy> _activeAssembliesResolver; - - public WebAppTypeFinder() + public WebAppTypeFinder() { - _ensureBinFolderAssembliesLoaded = CommonHelper.GetAppSetting("sm:EnableDynamicDiscovery", true); - _allAssembliesResolver = new Lazy>(() => this.GetAssembliesInternal(false)); - _activeAssembliesResolver = new Lazy>(() => this.GetAssembliesInternal(true)); - } + EnsureBinFolderAssembliesLoaded = CommonHelper.GetAppSetting("sm:EnableDynamicDiscovery", true); + _allAssembliesResolver = new Lazy>(() => this.GetAssembliesInternal(false)); + _activeAssembliesResolver = new Lazy>(() => this.GetAssembliesInternal(true)); + } /// /// Gets or sets wether assemblies in the bin folder of the web application should be explicitly checked for being loaded on application load. /// This is needed in situations where plugins need to be loaded into the AppDomain after the application has been reloaded. /// - public bool EnsureBinFolderAssembliesLoaded - { - get { return _ensureBinFolderAssembliesLoaded; } - set { _ensureBinFolderAssembliesLoaded = value; } - } + public bool EnsureBinFolderAssembliesLoaded { get; set; } /// /// Gets a physical disk path of \Bin directory @@ -58,102 +50,105 @@ public virtual string GetBinDirectory() } } - public override IEnumerable GetAssemblies(bool ignoreInactivePlugins = false) - { - if (ignoreInactivePlugins) - { - return _activeAssembliesResolver.Value; - } - else - { - return _allAssembliesResolver.Value; - } - } - - private IEnumerable GetAssembliesInternal(bool ignoreInactivePlugins) - { - if (ignoreInactivePlugins) - { - var allAssemblies = _allAssembliesResolver.Value; - return allAssemblies.Where(x => PluginManager.IsActivePluginAssembly(x)).AsReadOnly(); - } - - if (this.EnsureBinFolderAssembliesLoaded) - { - LoadPluginAssemblies(); - } - - return base.GetAssemblies(false); - } - - /// - /// Makes sure matching plugin assemblies are loaded into the app domain. - /// - protected virtual void LoadPluginAssemblies() - { - var alreadyLoadedAssemblyNames = new HashSet( - AppDomain.CurrentDomain.GetAssemblies().Select(x => x.FullName), - StringComparer.OrdinalIgnoreCase); - - var pluginAssemblies = new HashSet(PluginManager.ReferencedPlugins.Select(x => x.Assembly.Assembly)); - - foreach (var assembly in pluginAssemblies) - { - try - { - if (!alreadyLoadedAssemblyNames.Contains(assembly.FullName)) - { - App.Load(assembly.FullName); - Logger.DebugFormat("Loaded plugin assembly '{0}'", assembly.FullName); - } - } - catch (Exception ex) - { - Logger.Error(ex); - } - } - } - - #region Obsolete - - ///// - ///// Makes sure matching assemblies in the supplied folder are loaded into the app domain. - ///// - ///// The physical path to a directory containing dlls to load into the app domain. - //protected virtual void LoadCandidateAssembliesFromFolder(string path) - //{ - // var alreadyLoadedAssemblyNames = new HashSet(); - // var appDomainAssemblies = base.GetAssemblies(); - - // foreach (var a in appDomainAssemblies) - // { - // alreadyLoadedAssemblyNames.Add(a.FullName); - // } - - // if (!Directory.Exists(path)) - // { - // return; - // } - - // var dlls = Directory.GetFiles(path, "*.dll"); - - // foreach (string dllPath in dlls) - // { - // try - // { - // var an = AssemblyName.GetAssemblyName(dllPath); - // if (!alreadyLoadedAssemblyNames.Contains(an.FullName) && Matches(an.FullName)) - // { - // App.Load(an); - // } - // } - // catch (BadImageFormatException ex) - // { - // Trace.TraceError(ex.ToString()); - // } - // } - //} - - #endregion - } + public override IEnumerable GetAssemblies(bool ignoreInactivePlugins = false) + { + if (ignoreInactivePlugins) + { + return _activeAssembliesResolver.Value; + } + else + { + return _allAssembliesResolver.Value; + } + } + + private IEnumerable GetAssembliesInternal(bool ignoreInactivePlugins) + { + if (ignoreInactivePlugins) + { + var allAssemblies = _allAssembliesResolver.Value; + return allAssemblies.Where(x => PluginManager.IsActivePluginAssembly(x)).AsReadOnly(); + } + + if (this.EnsureBinFolderAssembliesLoaded) + { + LoadPluginAssemblies(); + } + + return base.GetAssemblies(false); + } + + /// + /// Makes sure matching plugin assemblies are loaded into the app domain. + /// + protected virtual void LoadPluginAssemblies() + { + var alreadyLoadedAssemblyNames = new HashSet( + AppDomain.CurrentDomain.GetAssemblies().Select(x => x.FullName), + StringComparer.OrdinalIgnoreCase); + + var pluginAssemblies = new HashSet(PluginManager.ReferencedPlugins.Select(x => x.Assembly.Assembly)); + + foreach (var assembly in pluginAssemblies) + { + try + { + if (assembly == null) + continue; + + if (!alreadyLoadedAssemblyNames.Contains(assembly.FullName)) + { + App.Load(assembly.FullName); + Logger.DebugFormat("Loaded plugin assembly '{0}'", assembly.FullName); + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + } + + #region Obsolete + + ///// + ///// Makes sure matching assemblies in the supplied folder are loaded into the app domain. + ///// + ///// The physical path to a directory containing dlls to load into the app domain. + //protected virtual void LoadCandidateAssembliesFromFolder(string path) + //{ + // var alreadyLoadedAssemblyNames = new HashSet(); + // var appDomainAssemblies = base.GetAssemblies(); + + // foreach (var a in appDomainAssemblies) + // { + // alreadyLoadedAssemblyNames.Add(a.FullName); + // } + + // if (!Directory.Exists(path)) + // { + // return; + // } + + // var dlls = Directory.GetFiles(path, "*.dll"); + + // foreach (string dllPath in dlls) + // { + // try + // { + // var an = AssemblyName.GetAssemblyName(dllPath); + // if (!alreadyLoadedAssemblyNames.Contains(an.FullName) && Matches(an.FullName)) + // { + // App.Load(an); + // } + // } + // catch (BadImageFormatException ex) + // { + // Trace.TraceError(ex.ToString()); + // } + // } + //} + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Linq/Expanders/IPathExpander.cs b/src/Libraries/SmartStore.Core/Linq/Expanders/IPathExpander.cs index aaf66dc839..6c29db695c 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expanders/IPathExpander.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expanders/IPathExpander.cs @@ -1,10 +1,9 @@ using System; -using System.Linq; using System.Linq.Expressions; namespace SmartStore.Linq { - + /// /// Instructs the unit of work to eager load entities. /// diff --git a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs index ce8c87c8b1..85df064f60 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using SmartStore.Utilities; @@ -16,13 +15,7 @@ public LambdaPathExpander() _expands = new List(); } - public IList Expands - { - get - { - return _expands; - } - } + public IList Expands => _expands; public virtual void Expand(Expression> path) { @@ -61,7 +54,7 @@ public virtual void Expand(Type type, string path) if (prop == null) throw new ArgumentException("The property or member '{0}' does not exist in type '{1}'.".FormatInvariant(member, t.FullName)); - Type memberType = prop.PropertyType; + Type memberType = prop.PropertyType; DoExpand(t, member); @@ -104,7 +97,7 @@ private void DoExpand(Type type, string path) var entityParam = Expression.Parameter(type, "x"); // {x} path = String.Concat("x.", path.Trim('.')); - var expression = System.Linq.Dynamic.DynamicExpression.ParseLambda( + var expression = System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda( new ParameterExpression[] { entityParam }, typeof(object), path.Trim('.')); diff --git a/src/Libraries/SmartStore.Core/Linq/Expanders/StringPathExpander.cs b/src/Libraries/SmartStore.Core/Linq/Expanders/StringPathExpander.cs index ca3f29bc24..77c816ad54 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expanders/StringPathExpander.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expanders/StringPathExpander.cs @@ -7,7 +7,7 @@ namespace SmartStore.Linq { - + public class StringPathExpander : IPathExpander { private readonly IList _expands; @@ -17,13 +17,7 @@ public StringPathExpander() _expands = new List(); } - public IList Expands - { - get - { - return _expands; - } - } + public IList Expands => _expands; public virtual void Expand(Expression> path) { diff --git a/src/Libraries/SmartStore.Core/Linq/ExpressionStarter.cs b/src/Libraries/SmartStore.Core/Linq/ExpressionStarter.cs index 2dc8902748..da2f6bc694 100644 --- a/src/Libraries/SmartStore.Core/Linq/ExpressionStarter.cs +++ b/src/Libraries/SmartStore.Core/Linq/ExpressionStarter.cs @@ -15,9 +15,9 @@ public class ExpressionStarter { private Expression> _predicate; - internal ExpressionStarter() - : this(false) - { + internal ExpressionStarter() + : this(false) + { } internal ExpressionStarter(bool defaultExpression) @@ -107,19 +107,19 @@ public static implicit operator ExpressionStarter(Expression> r #region Implement Expression methods and properties - public Func Compile() - { - return Predicate.Compile(); + public Func Compile() + { + return Predicate.Compile(); } - public Func Compile(DebugInfoGenerator debugInfoGenerator) - { - return Predicate.Compile(debugInfoGenerator); + public Func Compile(DebugInfoGenerator debugInfoGenerator) + { + return Predicate.Compile(debugInfoGenerator); } - public Expression> Update(Expression body, IEnumerable parameters) - { - return Predicate.Update(body, parameters); + public Expression> Update(Expression body, IEnumerable parameters) + { + return Predicate.Update(body, parameters); } #endregion @@ -140,14 +140,14 @@ public Expression> Update(Expression body, IEnumerable Predicate.TailCall; - public void CompileToMethod(MethodBuilder method) - { - Predicate.CompileToMethod(method); + public void CompileToMethod(MethodBuilder method) + { + Predicate.CompileToMethod(method); } - public void CompileToMethod(MethodBuilder method, DebugInfoGenerator debugInfoGenerator) - { - Predicate.CompileToMethod(method, debugInfoGenerator); + public void CompileToMethod(MethodBuilder method, DebugInfoGenerator debugInfoGenerator) + { + Predicate.CompileToMethod(method, debugInfoGenerator); } #endregion diff --git a/src/Libraries/SmartStore.Core/Linq/Expressions/MemberAccessPathVisitor.cs b/src/Libraries/SmartStore.Core/Linq/Expressions/MemberAccessPathVisitor.cs index e2369449a4..2378763d0d 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expressions/MemberAccessPathVisitor.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expressions/MemberAccessPathVisitor.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; diff --git a/src/Libraries/SmartStore.Core/Linq/LinqContainsPredicateBuilder.cs b/src/Libraries/SmartStore.Core/Linq/LinqContainsPredicateBuilder.cs index 909318e2c3..dc6879aac8 100644 --- a/src/Libraries/SmartStore.Core/Linq/LinqContainsPredicateBuilder.cs +++ b/src/Libraries/SmartStore.Core/Linq/LinqContainsPredicateBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; namespace SmartStore.Linq diff --git a/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs b/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs index 0115da0eb6..3041ea8726 100644 --- a/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs +++ b/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Linq.Expressions; namespace SmartStore.Linq @@ -37,15 +36,15 @@ protected override Expression VisitParameter(ParameterExpression node) } /// Start an expression - public static ExpressionStarter New(Expression> expr = null) - { - return new ExpressionStarter(expr); + public static ExpressionStarter New(Expression> expr = null) + { + return new ExpressionStarter(expr); } /// Create an expression with a stub expression true or false to use when the expression is not yet started. - public static ExpressionStarter New(bool defaultExpression) - { - return new ExpressionStarter(defaultExpression); + public static ExpressionStarter New(bool defaultExpression) + { + return new ExpressionStarter(defaultExpression); } [Obsolete("Use PredicateBuilder.New() instead.")] diff --git a/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs b/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs index e734c92653..905030ede6 100644 --- a/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs +++ b/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs @@ -1,44 +1,42 @@ -using System; - -namespace SmartStore.Core.Localization +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"); - } + /// + /// 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; } - } + 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 index fc02435c1e..7a97e459b3 100644 --- a/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs +++ b/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs @@ -1,152 +1,148 @@ 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) - }; - } - } + public class LocalizationFileResolver : ILocalizationFileResolver + { + private readonly ICacheManager _cache; + + public LocalizationFileResolver(ICacheManager cache) + { + _cache = cache; + } + + public LocalizationFileResolveResult Resolve( + string culture, + string virtualPath, + string pattern, + bool cache = true, + string fallbackCulture = "en") + { + Guard.NotEmpty(culture, nameof(culture)); + Guard.NotEmpty(virtualPath, nameof(virtualPath)); + Guard.NotEmpty(pattern, nameof(pattern)); + + if (pattern.IndexOf('*') < 0) + { + throw new ArgumentException("The pattern must contain a wildcard char for substitution, e.g. 'lang-*.js'.", nameof(pattern)); + } + + virtualPath = FixPath(virtualPath); + var cacheKey = "core:locfile:" + virtualPath.ToLower() + pattern + "/" + culture; + string result = null; + + if (cache && _cache.Contains(cacheKey)) + { + result = _cache.Get(cacheKey); + return result != null ? CreateResult(result, virtualPath, pattern) : null; + } + + if (!LocalizationHelper.IsValidCultureCode(culture)) + { + throw new ArgumentException($"'{culture}' is not a valid culture code.", nameof(culture)); + } + + var ci = CultureInfo.GetCultureInfo(culture); + var directory = new DirectoryInfo(CommonHelper.MapPath(virtualPath, false)); + + if (!directory.Exists) + { + throw new DirectoryNotFoundException($"Path '{virtualPath}' does not exist."); + } + + // 1: Match passed culture + result = ResolveMatchingFile(ci, directory, pattern); + + if (result == null && fallbackCulture.HasValue() && culture != fallbackCulture) + { + if (!LocalizationHelper.IsValidCultureCode(fallbackCulture)) + { + throw new ArgumentException($"'{culture}' is not a valid culture code.", nameof(fallbackCulture)); + } + + // 2: Match fallback culture + ci = CultureInfo.GetCultureInfo(fallbackCulture); + result = ResolveMatchingFile(ci, directory, pattern); + } + + if (cache) + { + _cache.Put(cacheKey, result, TimeSpan.FromHours(24)); + } + + if (result.HasValue()) + { + return CreateResult(result, virtualPath, pattern); + } + + return null; + } + + private string ResolveMatchingFile(CultureInfo ci, DirectoryInfo directory, string pattern) + { + string result = null; + + // 1: Exact match + // ----------------------------------------------------- + var fileName = pattern.Replace("*", ci.Name); + if (File.Exists(Path.Combine(directory.FullName, fileName))) + { + result = ci.Name; + } + + // 2: Match neutral culture, e.g. de-DE > de + // ----------------------------------------------------- + if (result == null && !ci.IsNeutralCulture && ci.Parent != null) + { + ci = ci.Parent; + fileName = pattern.Replace("*", ci.Name); + if (File.Exists(Path.Combine(directory.FullName, fileName))) + { + result = ci.Name; + } + } + + // 2: Match any region, e.g. de-DE > de-CH + // ----------------------------------------------------- + if (result == null && ci.IsNeutralCulture) + { + // Convert pattern to Regex: "lang-*.js" > "^lang.(.+?).js$" + var rgPattern = "^" + pattern.Replace("*", @"(.+?)") + "$"; + var rgFileName = new Regex(rgPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + + foreach (var fi in directory.EnumerateFiles(pattern.Replace("*", ci.Name + "-*"), SearchOption.TopDirectoryOnly)) + { + var culture = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(culture)) + { + result = culture; + break; + } + } + } + + return result; + } + + private string FixPath(string virtualPath) + { + return VirtualPathUtility.ToAppRelative(virtualPath).EnsureEndsWith("/"); + } + + private LocalizationFileResolveResult CreateResult(string culture, string virtualPath, string pattern) + { + var fileName = pattern.Replace("*", culture); + return new LocalizationFileResolveResult + { + Culture = culture, + VirtualPath = VirtualPathUtility.ToAbsolute(virtualPath + fileName) + }; + } + } } diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs index 324083d31d..1449395774 100644 --- a/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs +++ b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs @@ -6,7 +6,7 @@ namespace SmartStore.Core.Localization { - public static class LocalizationHelper + public static class LocalizationHelper { private readonly static HashSet _cultureCodes = new HashSet( @@ -20,70 +20,155 @@ public static bool IsValidCultureCode(string locale) return locale.HasValue() && _cultureCodes.Contains(locale); } - /// - /// Enumerates all parent cultures, excluding the top-most invariant culture - /// - /// The ISO culture code, e.g. de-DE, en-US or just en - /// Parent cultures - public static IEnumerable EnumerateParentCultures(string locale) - { - if (locale.IsEmpty() || !_cultureCodes.Contains(locale)) - { - return Enumerable.Empty(); - } - - return EnumerateParentCultures(CultureInfo.GetCultureInfo(locale)); - } - - /// - /// Enumerates all parent cultures, excluding the top-most invariant culture - /// - /// The culture info to enumerate parents for - /// Parent cultures - public static IEnumerable EnumerateParentCultures(CultureInfo culture) - { - if (culture == null) - { - yield break; - } - - while (culture.Parent.TwoLetterISOLanguageName != "iv") - { - yield return culture.Parent; - culture = culture.Parent; - } - } - - public static string GetLanguageNativeName(string locale) - { - try - { - if (locale.HasValue()) - { - var info = CultureInfo.GetCultureInfoByIetfLanguageTag(locale); - if (info != null) - return info.NativeName; - } - } - catch { } - - return null; - } - - public static string GetCurrencySymbol(string locale) - { - try - { - if (locale.HasValue()) - { - var info = new RegionInfo(locale); - if (info != null) - return info.CurrencySymbol; - } - } - catch { } - - return null; - } - } + /// + /// Enumerates all parent cultures, excluding the top-most invariant culture + /// + /// The ISO culture code, e.g. de-DE, en-US or just en + /// Parent cultures + public static IEnumerable EnumerateParentCultures(string locale) + { + if (locale.IsEmpty() || !_cultureCodes.Contains(locale)) + { + return Enumerable.Empty(); + } + + return EnumerateParentCultures(CultureInfo.GetCultureInfo(locale)); + } + + /// + /// Enumerates all parent cultures, excluding the top-most invariant culture + /// + /// The culture info to enumerate parents for + /// Parent cultures + public static IEnumerable EnumerateParentCultures(CultureInfo culture) + { + if (culture == null) + { + yield break; + } + + while (culture.Parent.TwoLetterISOLanguageName != "iv") + { + yield return culture.Parent; + culture = culture.Parent; + } + } + + public static bool TryGetCultureInfoForLocale(string locale, out CultureInfo culture) + { + culture = null; + + try + { + culture = CultureInfo.GetCultureInfoByIetfLanguageTag(locale); + return culture != null; + } + catch + { + return false; + } + } + + public static string GetLanguageNativeName(string locale) + { + if (TryGetCultureInfoForLocale(locale, out var culture)) + { + return culture.NativeName; + } + + return null; + } + + public static string GetLanguageNativeName(string locale, bool englishName, bool shortName = false) + { + try + { + if (locale.HasValue()) + { + var info = CultureInfo.GetCultureInfoByIetfLanguageTag(locale); + if (info != null) + { + if (shortName) + { + info = info.Parent; + } + + if (englishName) + { + return info.EnglishName; + } + else + { + return info.NativeName; + } + + } + } + } + catch + { + } + + return null; + } + + public static string NormalizeLanguageDisplayName(string languageName, bool stripRegion = false, CultureInfo culture = null) + { + if (string.IsNullOrEmpty(languageName) || languageName.Length == 0) + { + return languageName; + } + + // First char to upper. + if (char.IsLower(languageName[0])) + { + languageName = (culture ?? CultureInfo.InvariantCulture).TextInfo.ToTitleCase(languageName); + } + + var bracketIndex = languageName.IndexOfAny(new[] { '(', '[' }); + var hasRegion = bracketIndex > -1; + var endBracket = ')'; + + if (hasRegion) + { + if (languageName[bracketIndex] == '[') + { + endBracket = ']'; + } + + if (stripRegion) + { + languageName = languageName.Substring(0, bracketIndex).TrimEnd(); + } + } + + // Remove everything after ',' within Region part + if (hasRegion && !stripRegion) + { + var commaIndex = languageName.IndexOf(','); + if (commaIndex > -1) + { + languageName = languageName.Substring(0, commaIndex) + endBracket; + } + } + + return languageName; + } + + public static string GetCurrencySymbol(string locale) + { + try + { + if (locale.HasValue()) + { + var info = new RegionInfo(locale); + if (info != null) + return info.CurrencySymbol; + } + } + catch { } + + return null; + } + } } diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs b/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs index 5965d20b51..82dc95d9ee 100644 --- a/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs +++ b/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs @@ -3,8 +3,8 @@ namespace SmartStore.Core.Localization { - [Serializable] - public class LocalizedString : IHtmlString + [Serializable] + public class LocalizedString : IHtmlString { private readonly string _localized; private readonly string _textHint; @@ -30,48 +30,33 @@ public static LocalizedString TextOrDefault(string text, LocalizedString default return new LocalizedString(text); } - public string TextHint - { - get { return _textHint; } - } + public string TextHint => _textHint; - public object[] Args - { - get { return _args; } - } + public object[] Args => _args; - public string Text - { - get { return _localized; } - } + public string Text => _localized; + + /// + /// Returns a js encoded string which already contains delimiters. + /// + public IHtmlString JsText => System.Web.Mvc.MvcHtmlString.Create(_localized.EncodeJsString()); - /// - /// Returns a js encoded string which already contains delimiters. - /// - public IHtmlString JsText - { - get - { - return System.Web.Mvc.MvcHtmlString.Create(_localized.EncodeJsString()); - } - } - - public static implicit operator string(LocalizedString obj) + public static implicit operator string(LocalizedString obj) { return obj.Text; } - public static implicit operator LocalizedString(string obj) - { - return new LocalizedString(obj); - } + public static implicit operator LocalizedString(string obj) + { + return new LocalizedString(obj); + } public override string ToString() { return _localized; } - public string ToHtmlString() + public string ToHtmlString() { return _localized; } diff --git a/src/Libraries/SmartStore.Core/Localization/Localizer.cs b/src/Libraries/SmartStore.Core/Localization/Localizer.cs index bb96ea085b..9b173a53b1 100644 --- a/src/Libraries/SmartStore.Core/Localization/Localizer.cs +++ b/src/Libraries/SmartStore.Core/Localization/Localizer.cs @@ -1,5 +1,5 @@ namespace SmartStore.Core.Localization { public delegate LocalizedString Localizer(string key, params object[] args); - public delegate LocalizedString LocalizerEx(string key, int languageId, params object[] args); + public delegate LocalizedString LocalizerEx(string key, int languageId, params object[] args); } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs b/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs index 0b43a01c1d..fb77afe3ec 100644 --- a/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs +++ b/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs @@ -1,26 +1,18 @@ -using System; - -namespace SmartStore.Core.Localization +namespace SmartStore.Core.Localization { - public static class NullLocalizer - { - private static readonly Localizer _instance; - private static readonly LocalizerEx _instanceEx; + public static class NullLocalizer + { + private static readonly Localizer _instance; + private static readonly LocalizerEx _instanceEx; - static NullLocalizer() - { - _instance = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); - _instanceEx = (format, languageId, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); - } + static NullLocalizer() + { + _instance = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); + _instanceEx = (format, languageId, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); + } - public static Localizer Instance - { - get { return _instance; } - } + public static Localizer Instance => _instance; - public static LocalizerEx InstanceEx - { - get { return _instanceEx; } - } - } + public static LocalizerEx InstanceEx => _instanceEx; + } } diff --git a/src/Libraries/SmartStore.Core/Logging/IChronometer.cs b/src/Libraries/SmartStore.Core/Logging/IChronometer.cs index fb02512a32..ce8e9064d2 100644 --- a/src/Libraries/SmartStore.Core/Logging/IChronometer.cs +++ b/src/Libraries/SmartStore.Core/Logging/IChronometer.cs @@ -3,44 +3,41 @@ namespace SmartStore.Core.Logging { - public interface IChronometer : IDisposable - { - void StepStart(string key, string message); - void StepStop(string key); - } - - public static class IChronometerExtensions - { - public static IDisposable Step(this IChronometer chronometer, string message) - { - Guard.NotEmpty(message, nameof(message)); - - var key = "step" + CommonHelper.GenerateRandomDigitCode(10); - - chronometer.StepStart(key, message); - return new ActionDisposable(() => chronometer.StepStop(key)); - } - } - - public class NullChronometer : IChronometer - { - private static readonly IChronometer _instance = new NullChronometer(); - - public static IChronometer Instance - { - get { return _instance; } - } - - public void StepStart(string key, string message) - { - } - - public void StepStop(string key) - { - } - - public void Dispose() - { - } - } + public interface IChronometer : IDisposable + { + void StepStart(string key, string message); + void StepStop(string key); + } + + public static class IChronometerExtensions + { + public static IDisposable Step(this IChronometer chronometer, string message) + { + Guard.NotEmpty(message, nameof(message)); + + var key = "step" + CommonHelper.GenerateRandomDigitCode(10); + + chronometer.StepStart(key, message); + return new ActionDisposable(() => chronometer.StepStop(key)); + } + } + + public class NullChronometer : IChronometer + { + private static readonly IChronometer _instance = new NullChronometer(); + + public static IChronometer Instance => _instance; + + public void StepStart(string key, string message) + { + } + + public void StepStop(string key) + { + } + + public void Dispose() + { + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs b/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs index 3ff8526a96..8ae000f45b 100644 --- a/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs +++ b/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs @@ -5,10 +5,10 @@ namespace SmartStore.Core.Logging { - /// - /// Customer activity service interface - /// - public partial interface ICustomerActivityService + /// + /// Customer activity service interface + /// + public partial interface ICustomerActivityService { /// /// Inserts an activity log type item @@ -74,27 +74,27 @@ ActivityLog InsertActivity(string systemKeyword, /// Activity log void DeleteActivity(ActivityLog activityLog); - /// - /// Gets all activity log items - /// - /// Log item creation from; null to load all customers - /// Log item creation to; null to load all customers - /// Customer identifier; null to load all customers - /// Activity log type identifier - /// Page index - /// Page size - /// Customer email - /// Customer system name - /// Activity log collection - IPagedList GetAllActivities( - DateTime? createdOnFrom, + /// + /// Gets all activity log items + /// + /// Log item creation from; null to load all customers + /// Log item creation to; null to load all customers + /// Customer identifier; null to load all customers + /// Activity log type identifier + /// Page index + /// Page size + /// Customer email + /// Customer system name + /// Activity log collection + IPagedList GetAllActivities( + DateTime? createdOnFrom, DateTime? createdOnTo, - int? customerId, + int? customerId, int activityLogTypeId, - int pageIndex, - int pageSize, - string email = null, - bool? customerSystemAccount = null); + int pageIndex, + int pageSize, + string email = null, + bool? customerSystemAccount = null); /// /// Gets an activity log item @@ -103,16 +103,16 @@ IPagedList GetAllActivities( /// Activity log item ActivityLog GetActivityById(int activityLogId); - /// - /// Gets activity logs be identifier - /// - /// Activity log identifiers - /// List of activity logs - IList GetActivityByIds(int[] activityLogIds); + /// + /// Gets activity logs be identifier + /// + /// Activity log identifiers + /// List of activity logs + IList GetActivityByIds(int[] activityLogIds); - /// - /// Clears activity log - /// - void ClearAllActivities(); + /// + /// Clears activity log + /// + void ClearAllActivities(); } } diff --git a/src/Libraries/SmartStore.Core/Logging/ILogger.cs b/src/Libraries/SmartStore.Core/Logging/ILogger.cs index edec0f8dd0..3d985cb8bb 100644 --- a/src/Libraries/SmartStore.Core/Logging/ILogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/ILogger.cs @@ -2,25 +2,25 @@ namespace SmartStore.Core.Logging { - /// - /// Interface that all loggers implement - /// - public partial interface ILogger + /// + /// Interface that all loggers implement + /// + public partial interface ILogger { - /// - /// Checks if this logger is enabled for a given passed as parameter. - /// - /// true if this logger is enabled for level, otherwise false - /// Result - bool IsEnabledFor(LogLevel level); + /// + /// Checks if this logger is enabled for a given passed as parameter. + /// + /// true if this logger is enabled for level, otherwise false + /// Result + bool IsEnabledFor(LogLevel level); - /// - /// Generates a logging event for the specified level using the message and exception - /// - /// The level of the message to be logged - /// The exception to log, including its stack trace. Pass null to not log an exception - /// The message object to log - /// An Object array containing zero or more objects to format. Can be null. - void Log(LogLevel level, Exception exception, string message, object[] args); + /// + /// Generates a logging event for the specified level using the message and exception + /// + /// The level of the message to be logged + /// The exception to log, including its stack trace. Pass null to not log an exception + /// The message object to log + /// An Object array containing zero or more objects to format. Can be null. + void Log(LogLevel level, Exception exception, string message, object[] args); } } diff --git a/src/Libraries/SmartStore.Core/Logging/ILoggerFactory.cs b/src/Libraries/SmartStore.Core/Logging/ILoggerFactory.cs index 89f159f187..31ed223fec 100644 --- a/src/Libraries/SmartStore.Core/Logging/ILoggerFactory.cs +++ b/src/Libraries/SmartStore.Core/Logging/ILoggerFactory.cs @@ -2,18 +2,18 @@ namespace SmartStore.Core.Logging { - public interface ILoggerFactory - { - ILogger GetLogger(Type type); - ILogger GetLogger(string name); - void FlushAll(); - } + public interface ILoggerFactory + { + ILogger GetLogger(Type type); + ILogger GetLogger(string name); + void FlushAll(); + } - public static class ILoggerFactoryExtensions - { - public static ILogger CreateLogger(this ILoggerFactory factory) where T : class - { - return factory.GetLogger(typeof(T)); - } - } + public static class ILoggerFactoryExtensions + { + public static ILogger CreateLogger(this ILoggerFactory factory) where T : class + { + return factory.GetLogger(typeof(T)); + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/LogLevel.cs b/src/Libraries/SmartStore.Core/Logging/LogLevel.cs index 8792dbee72..d370399f51 100644 --- a/src/Libraries/SmartStore.Core/Logging/LogLevel.cs +++ b/src/Libraries/SmartStore.Core/Logging/LogLevel.cs @@ -8,7 +8,7 @@ public enum LogLevel Debug = 10, Information = 20, Warning = 30, - Error = 40 , + Error = 40, Fatal = 50 } } diff --git a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs index cd9e45dc31..40081ea027 100644 --- a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs +++ b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs @@ -5,71 +5,191 @@ namespace SmartStore.Core.Logging { public static class LoggingExtensions { - public static bool IsDebugEnabled(this ILogger l) => l.IsEnabledFor(LogLevel.Debug); - public static bool IsInfoEnabled(this ILogger l) => l.IsEnabledFor(LogLevel.Information); - public static bool IsWarnEnabled(this ILogger l) => l.IsEnabledFor(LogLevel.Warning); - public static bool IsErrorEnabled(this ILogger l) => l.IsEnabledFor(LogLevel.Error); - public static bool IsFatalEnabled(this ILogger l) => l.IsEnabledFor(LogLevel.Fatal); - - - - public static void Debug(this ILogger l, string msg) => FilteredLog(l, LogLevel.Debug, null, msg, null); - public static void Debug(this ILogger l, Func msgFactory) => FilteredLog(l, LogLevel.Debug, null, msgFactory); - public static void Debug(this ILogger l, Exception ex, string msg) => FilteredLog(l, LogLevel.Debug, ex, msg, null); - public static void DebugFormat(this ILogger l, string msg, params object[] args) => FilteredLog(l, LogLevel.Debug, null, msg, args); - public static void DebugFormat(this ILogger l, Exception ex, string msg, params object[] args) => FilteredLog(l, LogLevel.Debug, ex, msg, args); - - public static void Info(this ILogger l, string msg) => FilteredLog(l, LogLevel.Information, null, msg, null); - public static void Info(this ILogger l, Func msgFactory) => FilteredLog(l, LogLevel.Information, null, msgFactory); - public static void Info(this ILogger l, Exception ex, string msg) => FilteredLog(l, LogLevel.Information, ex, msg, null); - public static void InfoFormat(this ILogger l, string msg, params object[] args) => FilteredLog(l, LogLevel.Information, null, msg, args); - public static void InfoFormat(this ILogger l, Exception ex, string msg, params object[] args) => FilteredLog(l, LogLevel.Information, ex, msg, args); - - public static void Warn(this ILogger l, string msg) => FilteredLog(l, LogLevel.Warning, null, msg, null); - public static void Warn(this ILogger l, Func msgFactory) => FilteredLog(l, LogLevel.Warning, null, msgFactory); - public static void Warn(this ILogger l, Exception ex, string msg) => FilteredLog(l, LogLevel.Warning, ex, msg, null); - public static void WarnFormat(this ILogger l, string msg, params object[] args) => FilteredLog(l, LogLevel.Warning, null, msg, args); - public static void WarnFormat(this ILogger l, Exception ex, string msg, params object[] args) => FilteredLog(l, LogLevel.Warning, ex, msg, args); - - public static void Error(this ILogger l, string msg) => FilteredLog(l, LogLevel.Error, null, msg, null); - public static void Error(this ILogger l, Func msgFactory) => FilteredLog(l, LogLevel.Error, null, msgFactory); - public static void Error(this ILogger l, Exception ex) => FilteredLog(l, LogLevel.Error, ex, null, null); - public static void Error(this ILogger l, Exception ex, string msg) => FilteredLog(l, LogLevel.Error, ex, msg, null); - public static void ErrorFormat(this ILogger l, string msg, params object[] args) => FilteredLog(l, LogLevel.Error, null, msg, args); - public static void ErrorFormat(this ILogger l, Exception ex, string msg, params object[] args) => FilteredLog(l, LogLevel.Error, ex, msg, args); - - public static void Fatal(this ILogger l, string msg) => FilteredLog(l, LogLevel.Fatal, null, msg, null); - public static void Fatal(this ILogger l, Func msgFactory) => FilteredLog(l, LogLevel.Fatal, null, msgFactory); - public static void Fatal(this ILogger l, Exception ex) => FilteredLog(l, LogLevel.Fatal, ex, null, null); - public static void Fatal(this ILogger l, Exception ex, string msg) => FilteredLog(l, LogLevel.Fatal, ex, msg, null); - public static void FatalFormat(this ILogger l, string msg, params object[] args) => FilteredLog(l, LogLevel.Fatal, null, msg, args); - public static void FatalFormat(this ILogger l, Exception ex, string msg, params object[] args) => FilteredLog(l, LogLevel.Fatal, ex, msg, args); - - public static void ErrorsAll(this ILogger logger, Exception exception) - { - while (exception != null) - { - FilteredLog(logger, LogLevel.Error, exception, exception.Message, null); - exception = exception.InnerException; - } - } + public static bool IsDebugEnabled(this ILogger l) + { + return l.IsEnabledFor(LogLevel.Debug); + } + + public static bool IsInfoEnabled(this ILogger l) + { + return l.IsEnabledFor(LogLevel.Information); + } + + public static bool IsWarnEnabled(this ILogger l) + { + return l.IsEnabledFor(LogLevel.Warning); + } + + public static bool IsErrorEnabled(this ILogger l) + { + return l.IsEnabledFor(LogLevel.Error); + } + + public static bool IsFatalEnabled(this ILogger l) + { + return l.IsEnabledFor(LogLevel.Fatal); + } + + public static void Debug(this ILogger l, string msg) + { + FilteredLog(l, LogLevel.Debug, null, msg, null); + } + + public static void Debug(this ILogger l, Func msgFactory) + { + FilteredLog(l, LogLevel.Debug, null, msgFactory); + } + + public static void Debug(this ILogger l, Exception ex, string msg) + { + FilteredLog(l, LogLevel.Debug, ex, msg, null); + } + + public static void DebugFormat(this ILogger l, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Debug, null, msg, args); + } + + public static void DebugFormat(this ILogger l, Exception ex, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Debug, ex, msg, args); + } + + public static void Info(this ILogger l, string msg) + { + FilteredLog(l, LogLevel.Information, null, msg, null); + } + + public static void Info(this ILogger l, Func msgFactory) + { + FilteredLog(l, LogLevel.Information, null, msgFactory); + } + + public static void Info(this ILogger l, Exception ex, string msg) + { + FilteredLog(l, LogLevel.Information, ex, msg, null); + } + + public static void InfoFormat(this ILogger l, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Information, null, msg, args); + } + + public static void InfoFormat(this ILogger l, Exception ex, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Information, ex, msg, args); + } + + public static void Warn(this ILogger l, string msg) + { + FilteredLog(l, LogLevel.Warning, null, msg, null); + } + + public static void Warn(this ILogger l, Func msgFactory) + { + FilteredLog(l, LogLevel.Warning, null, msgFactory); + } + + public static void Warn(this ILogger l, Exception ex, string msg) + { + FilteredLog(l, LogLevel.Warning, ex, msg, null); + } + + public static void WarnFormat(this ILogger l, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Warning, null, msg, args); + } + + public static void WarnFormat(this ILogger l, Exception ex, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Warning, ex, msg, args); + } + + public static void Error(this ILogger l, string msg) + { + FilteredLog(l, LogLevel.Error, null, msg, null); + } + + public static void Error(this ILogger l, Func msgFactory) + { + FilteredLog(l, LogLevel.Error, null, msgFactory); + } + + public static void Error(this ILogger l, Exception ex) + { + FilteredLog(l, LogLevel.Error, ex, null, null); + } + + public static void Error(this ILogger l, Exception ex, string msg) + { + FilteredLog(l, LogLevel.Error, ex, msg, null); + } + + public static void ErrorFormat(this ILogger l, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Error, null, msg, args); + } + + public static void ErrorFormat(this ILogger l, Exception ex, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Error, ex, msg, args); + } + + public static void Fatal(this ILogger l, string msg) + { + FilteredLog(l, LogLevel.Fatal, null, msg, null); + } + + public static void Fatal(this ILogger l, Func msgFactory) + { + FilteredLog(l, LogLevel.Fatal, null, msgFactory); + } + + public static void Fatal(this ILogger l, Exception ex) + { + FilteredLog(l, LogLevel.Fatal, ex, null, null); + } + + public static void Fatal(this ILogger l, Exception ex, string msg) + { + FilteredLog(l, LogLevel.Fatal, ex, msg, null); + } + + public static void FatalFormat(this ILogger l, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Fatal, null, msg, args); + } + + public static void FatalFormat(this ILogger l, Exception ex, string msg, params object[] args) + { + FilteredLog(l, LogLevel.Fatal, ex, msg, args); + } + + public static void ErrorsAll(this ILogger logger, Exception exception) + { + while (exception != null) + { + FilteredLog(logger, LogLevel.Error, exception, exception.Message, null); + exception = exception.InnerException; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void FilteredLog(ILogger logger, LogLevel level, Exception exception, string message, object[] objects) - { - if (logger.IsEnabledFor(level)) - { - logger.Log(level, exception, message, objects); - } - } + { + if (logger.IsEnabledFor(level)) + { + logger.Log(level, exception, message, objects); + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void FilteredLog(ILogger logger, LogLevel level, Exception exception, Func messageFactory) - { - if (logger.IsEnabledFor(level)) - { - logger.Log(level, exception, messageFactory(), null); - } - } - } + { + if (logger.IsEnabledFor(level)) + { + logger.Log(level, exception, messageFactory(), null); + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/LoggingModule.cs b/src/Libraries/SmartStore.Core/Logging/LoggingModule.cs index 31f73d2807..34ca8cea69 100644 --- a/src/Libraries/SmartStore.Core/Logging/LoggingModule.cs +++ b/src/Libraries/SmartStore.Core/Logging/LoggingModule.cs @@ -1,143 +1,143 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using Autofac; using Autofac.Core; using Autofac.Core.Activators.Reflection; +using Autofac.Core.Registration; using SmartStore.ComponentModel; using SmartStore.Core.Data; namespace SmartStore.Core.Logging { - public class LoggingModule : Autofac.Module - { - protected override void Load(ContainerBuilder builder) - { - builder.RegisterType().As().SingleInstance(); - - builder.RegisterType().As().SingleInstance(); - - // call GetLogger in response to the request for an ILogger implementation - if (DataSettings.DatabaseIsInstalled()) - { - builder.Register(GetContextualLogger).As().ExternallyOwned(); - } - else - { - // the install logger should append to a rolling text file only. - builder.Register(GetInstallLogger).As().ExternallyOwned(); - } - } - - protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration) - { - bool hasCtorLogger = false; - bool hasPropertyLogger = false; - - FastProperty[] loggerProperties = null; - - var ra = registration.Activator as ReflectionActivator; - if (ra != null) - { - // // Look for ctor parameters of type "ILogger" - var ctors = ra.ConstructorFinder.FindConstructors(ra.LimitType); - var loggerParameters = ctors.SelectMany(ctor => ctor.GetParameters()).Where(pi => pi.ParameterType == typeof(ILogger)); - hasCtorLogger = loggerParameters.Any(); - - // Autowire properties - // Look for settable properties of type "ILogger" - loggerProperties = ra.LimitType - .GetProperties(BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance) - .Select(p => new - { - PropertyInfo = p, - p.PropertyType, - IndexParameters = p.GetIndexParameters(), - Accessors = p.GetAccessors(false) - }) - .Where(x => x.PropertyType == typeof(ILogger)) // must be a logger - .Where(x => x.IndexParameters.Count() == 0) // must not be an indexer - .Where(x => x.Accessors.Length != 1 || x.Accessors[0].ReturnType == typeof(void)) //must have get/set, or only set - .Select(x => FastProperty.Create(x.PropertyInfo)) - .ToArray(); - - hasPropertyLogger = loggerProperties.Length > 0; - - // Ignore components known to be without logger dependencies - if (!hasCtorLogger && !hasPropertyLogger) - return; - - if (hasPropertyLogger) - { - registration.Metadata.Add("LoggerProperties", loggerProperties); - } - } - - if (hasCtorLogger) - { - registration.Preparing += (sender, args) => - { - var logger = GetLoggerFor(args.Component.Activator.LimitType, args.Context); - args.Parameters = new[] { TypedParameter.From(logger) }.Concat(args.Parameters); - }; - } - - if (hasPropertyLogger) - { - registration.Activating += (sender, args) => - { - var logger = GetLoggerFor(args.Component.Activator.LimitType, args.Context); - var loggerProps = args.Component.Metadata.Get("LoggerProperties") as FastProperty[]; - if (loggerProps != null) - { - foreach (var prop in loggerProps) - { - prop.SetValue(args.Instance, logger); - } - } - }; - } - } - - private ILogger GetLoggerFor(Type componentType, IComponentContext ctx) - { - return ctx.Resolve(new TypedParameter(typeof(Type), componentType)); - } - - private static ILogger GetContextualLogger(IComponentContext context, IEnumerable parameters) - { - // return an ILogger in response to Resolve(componentTypeParameter) - var loggerFactory = context.Resolve(); - - Type containingType = null; - - if (parameters != null && parameters.Any()) - { - if (parameters.Any(x => x is TypedParameter)) - { - containingType = parameters.TypedAs(); - } - else if (parameters.Any(x => x is NamedParameter)) - { - containingType = parameters.Named("Autofac.AutowiringPropertyInjector.InstanceType"); - } - } - - if (containingType != null) - { - return loggerFactory.GetLogger(containingType); - } - else - { - return loggerFactory.GetLogger("SmartStore"); - } - } - - private static ILogger GetInstallLogger(IComponentContext context) - { - return context.Resolve().GetLogger("Install"); - } - } + public class LoggingModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As().SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + + // call GetLogger in response to the request for an ILogger implementation + if (DataSettings.DatabaseIsInstalled()) + { + builder.Register(GetContextualLogger).As().ExternallyOwned(); + } + else + { + // the install logger should append to a rolling text file only. + builder.Register(GetInstallLogger).As().ExternallyOwned(); + } + } + + protected override void AttachToComponentRegistration(IComponentRegistryBuilder componentRegistry, IComponentRegistration registration) + { + bool hasCtorLogger = false; + bool hasPropertyLogger = false; + + FastProperty[] loggerProperties = null; + + var ra = registration.Activator as ReflectionActivator; + if (ra != null) + { + // // Look for ctor parameters of type "ILogger" + var ctors = ra.ConstructorFinder.FindConstructors(ra.LimitType); + var loggerParameters = ctors.SelectMany(ctor => ctor.GetParameters()).Where(pi => pi.ParameterType == typeof(ILogger)); + hasCtorLogger = loggerParameters.Any(); + + // Autowire properties + // Look for settable properties of type "ILogger" + loggerProperties = ra.LimitType + .GetProperties(BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance) + .Select(p => new + { + PropertyInfo = p, + p.PropertyType, + IndexParameters = p.GetIndexParameters(), + Accessors = p.GetAccessors(false) + }) + .Where(x => x.PropertyType == typeof(ILogger)) // must be a logger + .Where(x => x.IndexParameters.Count() == 0) // must not be an indexer + .Where(x => x.Accessors.Length != 1 || x.Accessors[0].ReturnType == typeof(void)) //must have get/set, or only set + .Select(x => FastProperty.Create(x.PropertyInfo)) + .ToArray(); + + hasPropertyLogger = loggerProperties.Length > 0; + + // Ignore components known to be without logger dependencies + if (!hasCtorLogger && !hasPropertyLogger) + return; + + if (hasPropertyLogger) + { + registration.Metadata.Add("LoggerProperties", loggerProperties); + } + } + + if (hasCtorLogger) + { + registration.Preparing += (sender, args) => + { + var logger = GetLoggerFor(args.Component.Activator.LimitType, args.Context); + args.Parameters = new[] { TypedParameter.From(logger) }.Concat(args.Parameters); + }; + } + + if (hasPropertyLogger) + { + registration.Activating += (sender, args) => + { + var logger = GetLoggerFor(args.Component.Activator.LimitType, args.Context); + var loggerProps = args.Component.Metadata.Get("LoggerProperties") as FastProperty[]; + if (loggerProps != null) + { + foreach (var prop in loggerProps) + { + prop.SetValue(args.Instance, logger); + } + } + }; + } + } + + private ILogger GetLoggerFor(Type componentType, IComponentContext ctx) + { + return ctx.Resolve(new TypedParameter(typeof(Type), componentType)); + } + + private static ILogger GetContextualLogger(IComponentContext context, IEnumerable parameters) + { + // return an ILogger in response to Resolve(componentTypeParameter) + var loggerFactory = context.Resolve(); + + Type containingType = null; + + if (parameters != null && parameters.Any()) + { + if (parameters.Any(x => x is TypedParameter)) + { + containingType = parameters.TypedAs(); + } + else if (parameters.Any(x => x is NamedParameter)) + { + containingType = parameters.Named("Autofac.AutowiringPropertyInjector.InstanceType"); + } + } + + if (containingType != null) + { + return loggerFactory.GetLogger(containingType); + } + else + { + return loggerFactory.GetLogger("SmartStore"); + } + } + + private static ILogger GetInstallLogger(IComponentContext context) + { + return context.Resolve().GetLogger("Install"); + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/Notifier.cs b/src/Libraries/SmartStore.Core/Logging/Notifier.cs index 7f033bdd1b..04afde9843 100644 --- a/src/Libraries/SmartStore.Core/Logging/Notifier.cs +++ b/src/Libraries/SmartStore.Core/Logging/Notifier.cs @@ -1,57 +1,65 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using SmartStore.Core.Localization; namespace SmartStore.Core.Logging { - public interface INotifier - { - void Add(NotifyType type, LocalizedString message, bool durable = true); - ICollection Entries { get; } - } - - public class Notifier : INotifier - { - private readonly HashSet _entries = new HashSet(); - - public void Add(NotifyType type, LocalizedString message, bool durable = true) - { - _entries.Add(new NotifyEntry { Type = type, Message = message, Durable = durable }); - } - - public ICollection Entries - { - get { return _entries; } - } - } - - public static class INotifierExtension - { + public interface INotifier + { + void Add(NotifyType type, LocalizedString message, bool durable = true); + ICollection Entries { get; } + } + + public class Notifier : INotifier + { + private readonly HashSet _entries = new HashSet(); + + public void Add(NotifyType type, LocalizedString message, bool durable = true) + { + _entries.Add(new NotifyEntry { Type = type, Message = message, Durable = durable }); + } + + public ICollection Entries => _entries; + } + + public static class INotifierExtension + { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Information(this INotifier notifier, LocalizedString message, bool durable = true) - { - notifier.Add(NotifyType.Info, message, durable); - } + { + notifier.Add(NotifyType.Info, message, durable); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Success(this INotifier notifier, LocalizedString message, bool durable = true) - { - notifier.Add(NotifyType.Success, message, durable); - } + { + notifier.Add(NotifyType.Success, message, durable); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Warning(this INotifier notifier, LocalizedString message, bool durable = true) - { - notifier.Add(NotifyType.Warning, message, durable); - } + { + notifier.Add(NotifyType.Warning, message, durable); + } + + public static void Error(this INotifier notifier, Exception exception, bool durable = true) + { + if (exception == null) + return; + + while (exception.InnerException != null) + { + exception = exception.InnerException; + } + + notifier.Add(NotifyType.Error, exception.Message, durable); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Error(this INotifier notifier, LocalizedString message, bool durable = true) - { - notifier.Add(NotifyType.Error, message, durable); - } - } + { + notifier.Add(NotifyType.Error, message, durable); + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/NotifyEntry.cs b/src/Libraries/SmartStore.Core/Logging/NotifyEntry.cs index 1196fc4790..a1bf825772 100644 --- a/src/Libraries/SmartStore.Core/Logging/NotifyEntry.cs +++ b/src/Libraries/SmartStore.Core/Logging/NotifyEntry.cs @@ -1,28 +1,27 @@ using System; -using System.Collections.Generic; using SmartStore.Core.Localization; namespace SmartStore.Core.Logging { - public enum NotifyType - { - Info, - Success, - Warning, - Error - } + public enum NotifyType + { + Info, + Success, + Warning, + Error + } - [Serializable] - public class NotifyEntry : ComparableObject - { - [ObjectSignature] - public NotifyType Type { get; set; } + [Serializable] + public class NotifyEntry : ComparableObject + { + [ObjectSignature] + public NotifyType Type { get; set; } - [ObjectSignature] - public LocalizedString Message { get; set; } + [ObjectSignature] + public LocalizedString Message { get; set; } - public bool Durable { get; set; } - } + public bool Durable { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/NullLogger.cs b/src/Libraries/SmartStore.Core/Logging/NullLogger.cs index ffdb5139a5..1ca0bf46cb 100644 --- a/src/Libraries/SmartStore.Core/Logging/NullLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/NullLogger.cs @@ -1,35 +1,35 @@ using System; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Logging; namespace SmartStore.Core.Logging { public partial class NullLogger : ILogger { - private static readonly ILogger s_instance = new NullLogger(); + private static readonly ILogger s_instance = new NullLogger(); + + public static ILogger Instance => s_instance; - public static ILogger Instance - { - get - { - return s_instance; - } - } - public bool IsEnabledFor(LogLevel level) { return false; } - public void Log(LogLevel level, Exception exception, string message, object[] args) - { - } - } + public void Log(LogLevel level, Exception exception, string message, object[] args) + { + } + } + + public class NullLoggerFactory : ILoggerFactory + { + public ILogger GetLogger(Type type) + { + return NullLogger.Instance; + } + + public ILogger GetLogger(string name) + { + return NullLogger.Instance; + } - public class NullLoggerFactory : ILoggerFactory - { - public ILogger GetLogger(Type type) => NullLogger.Instance; - public ILogger GetLogger(string name) => NullLogger.Instance; - public void FlushAll() { } - } + public void FlushAll() { } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs index 71c5916162..073e3d5d3d 100644 --- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs @@ -7,14 +7,14 @@ namespace SmartStore.Core.Logging { - public class TraceLogger : DisposableObject, ILogger - { - private readonly TraceSource _traceSource; - private readonly StreamWriter _streamWriter; + public class TraceLogger : DisposableObject, ILogger + { + private readonly TraceSource _traceSource; + private readonly StreamWriter _streamWriter; - public TraceLogger() : this(GetDefaultFileName()) - { - } + public TraceLogger() : this(GetDefaultFileName()) + { + } private static string GetDefaultFileName() { @@ -31,12 +31,12 @@ private static string GetDefaultFileName() } public TraceLogger(string fileName) - { - Guard.NotEmpty(fileName, nameof(fileName)); + { + Guard.NotEmpty(fileName, nameof(fileName)); - _traceSource = new TraceSource("SmartStore"); - _traceSource.Switch = new SourceSwitch("LogSwitch", "Error"); - _traceSource.Listeners.Remove("Default"); + _traceSource = new TraceSource("SmartStore"); + _traceSource.Switch = new SourceSwitch("LogSwitch", "Error"); + _traceSource.Listeners.Remove("Default"); if (CommonHelper.IsDevEnvironment) { @@ -57,102 +57,102 @@ public TraceLogger(string fileName) }; try - { - // force UTF-8 encoding (even if the text just contains ANSI characters) - var append = File.Exists(fileName); - _streamWriter = new StreamWriter(fileName, append, Encoding.UTF8); - - textListener.Writer = _streamWriter; - - _traceSource.Listeners.Add(textListener); - } - catch (IOException) - { - // file is locked by another process - } - - // Allow the trace source to send messages to - // listeners for all event types. Currently only - // error messages or higher go to the listeners. - // Messages must get past the source switch to - // get to the listeners, regardless of the settings - // for the listeners. - _traceSource.Switch.Level = SourceLevels.All; - } - - public bool IsEnabledFor(LogLevel level) - { + { + // force UTF-8 encoding (even if the text just contains ANSI characters) + var append = File.Exists(fileName); + _streamWriter = new StreamWriter(fileName, append, Encoding.UTF8); + + textListener.Writer = _streamWriter; + + _traceSource.Listeners.Add(textListener); + } + catch (IOException) + { + // file is locked by another process + } + + // Allow the trace source to send messages to + // listeners for all event types. Currently only + // error messages or higher go to the listeners. + // Messages must get past the source switch to + // get to the listeners, regardless of the settings + // for the listeners. + _traceSource.Switch.Level = SourceLevels.All; + } + + public bool IsEnabledFor(LogLevel level) + { return _traceSource.Switch.ShouldTrace(LogLevelToEventType(level)); - } + } - public void Log(LogLevel level, Exception exception, string message, object[] args) - { - var type = LogLevelToEventType(level); + public void Log(LogLevel level, Exception exception, string message, object[] args) + { + var type = LogLevelToEventType(level); - if (exception != null && !exception.IsFatal()) - { - message = message.Grow(exception.ToString(), Environment.NewLine); - } + if (exception != null && !exception.IsFatal()) + { + message = message.Grow(exception.ToString(), Environment.NewLine); + } - if (message.HasValue()) - { - var msg = args != null && args.Any() - ? message.FormatCurrent(args) - : message; + if (message.HasValue()) + { + var msg = args != null && args.Any() + ? message.FormatCurrent(args) + : message; - _traceSource.TraceEvent(type, (int)type, msg); + _traceSource.TraceEvent(type, (int)type, msg); } - } - - private TraceEventType LogLevelToEventType(LogLevel level) - { - switch (level) - { - case LogLevel.Debug: - return TraceEventType.Verbose; - case LogLevel.Error: - return TraceEventType.Error; - case LogLevel.Fatal: - return TraceEventType.Critical; - case LogLevel.Warning: - return TraceEventType.Warning; - default: - return TraceEventType.Information; - } - } - - private LogLevel EventTypeToLogLevel(TraceEventType eventType) + } + + private TraceEventType LogLevelToEventType(LogLevel level) { - switch (eventType) + switch (level) { - case TraceEventType.Verbose: - return LogLevel.Debug; - case TraceEventType.Error: - return LogLevel.Error; - case TraceEventType.Critical: - return LogLevel.Fatal; - case TraceEventType.Warning: - return LogLevel.Warning; + case LogLevel.Debug: + return TraceEventType.Verbose; + case LogLevel.Error: + return TraceEventType.Error; + case LogLevel.Fatal: + return TraceEventType.Critical; + case LogLevel.Warning: + return TraceEventType.Warning; default: - return LogLevel.Information; + return TraceEventType.Information; } } + //private LogLevel EventTypeToLogLevel(TraceEventType eventType) + //{ + // switch (eventType) + // { + // case TraceEventType.Verbose: + // return LogLevel.Debug; + // case TraceEventType.Error: + // return LogLevel.Error; + // case TraceEventType.Critical: + // return LogLevel.Fatal; + // case TraceEventType.Warning: + // return LogLevel.Warning; + // default: + // return LogLevel.Information; + // } + //} + public void Flush() - { - _traceSource.Flush(); - } - - protected override void OnDispose(bool disposing) - { - _traceSource.Flush(); - _traceSource.Close(); - - if (_streamWriter != null) - { - _streamWriter.Close(); - _streamWriter.Dispose(); - } - } - } + { + _traceSource.Flush(); + } + + protected override void OnDispose(bool disposing) + { + _traceSource.Flush(); + _traceSource.Close(); + + if (_streamWriter != null) + { + _streamWriter.Close(); + _streamWriter.Dispose(); + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs index 6bd1f49e02..be5b37a6b1 100644 --- a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Globalization; using System.Web; -using log4net; using log4net.Core; using log4net.Util; using SmartStore.Core.Data; @@ -12,149 +11,146 @@ namespace SmartStore.Core.Logging { - [DebuggerDisplay("Logger: {Name}")] - public class Log4netLogger : ILogger - { - private static readonly Type _declaringType = typeof(Log4netLogger); - private static readonly ContextState> _state; - - private readonly log4net.Core.ILogger _logger; - - static Log4netLogger() - { - _state = new ContextState>("Log4netLogger.LogicalThreadContext", () => new Dictionary(StringComparer.OrdinalIgnoreCase)); - } - - public Log4netLogger(log4net.Core.ILogger logger) - { - Guard.NotNull(logger, nameof(logger)); - - _logger = logger; - } - - public string Name - { - get { return _logger.Name; } - } - - public bool IsEnabledFor(LogLevel level) - { - return _logger.IsEnabledFor(ConvertLevel(level)); - } - - public void Log(LogLevel level, Exception exception, string message, object[] args) - { - var logLevel = ConvertLevel(level); - - if (message == null && exception != null) - { - message = exception.Message; - } - - object messageObj = message; - - if (args != null && args.Length > 0) - { - messageObj = new SystemStringFormat(CultureInfo.CurrentUICulture, message, args); - } - - var loggingEvent = new LoggingEvent( - _declaringType, - _logger.Repository, - _logger.Name, - logLevel, - messageObj, - exception); - - loggingEvent.Properties["LevelId"] = (int)level; - TryAddExtendedThreadInfo(loggingEvent); - - _logger.Log(loggingEvent); - } - - protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) - { - var isAppInitialized = EngineContext.Current.IsFullyInitialized; - - // Don't knowingly run into exception - var httpRequest = isAppInitialized ? HttpContext.Current.SafeGetHttpRequest() : null; - - if (httpRequest == null) - { - loggingEvent.Properties["CustomerId"] = DBNull.Value; - loggingEvent.Properties["Url"] = DBNull.Value; - loggingEvent.Properties["Referrer"] = DBNull.Value; - loggingEvent.Properties["HttpMethod"] = DBNull.Value; - loggingEvent.Properties["Ip"] = DBNull.Value; - - return; - } - - var props = _state.GetState(); - - // Load the log4net thread with additional properties if they are available - var threadInfoMissing = !props.ContainsKey("sm:ThreadInfoAdded"); - - if (threadInfoMissing) - { - using (new ActionDisposable(() => props["sm:ThreadInfoAdded"] = true)) - { - if (DataSettings.DatabaseIsInstalled() && isAppInitialized) - { - var container = EngineContext.Current.ContainerManager; - - // CustomerId - if (container.TryResolve(null, out IWorkContext workContext)) - { - try - { - props["CustomerId"] = workContext.CurrentCustomer.Id; - } - catch - { - props["CustomerId"] = DBNull.Value; - } - } - - - // Url & stuff - if (container.TryResolve(null, out IWebHelper webHelper)) - { - try - { - props["Url"] = webHelper.GetThisPageUrl(true); - props["Referrer"] = webHelper.GetUrlReferrer(); - props["HttpMethod"] = httpRequest?.HttpMethod; - props["Ip"] = webHelper.GetCurrentIpAddress(); - } - catch { } - } - } - } - } - - loggingEvent.Properties["CustomerId"] = props.Get("CustomerId") ?? DBNull.Value; - loggingEvent.Properties["Url"] = props.Get("Url") ?? DBNull.Value; - loggingEvent.Properties["Referrer"] = props.Get("Referrer") ?? DBNull.Value; - loggingEvent.Properties["HttpMethod"] = props.Get("HttpMethod") ?? DBNull.Value; - loggingEvent.Properties["Ip"] = props.Get("Ip") ?? DBNull.Value; - } - - private static Level ConvertLevel(LogLevel level) - { - switch (level) - { - case LogLevel.Information: - return Level.Info; - case LogLevel.Warning: - return Level.Warn; - case LogLevel.Error: - return Level.Error; - case LogLevel.Fatal: - return Level.Fatal; - default: - return Level.Debug; - } - } - } + [DebuggerDisplay("Logger: {Name}")] + public class Log4netLogger : ILogger + { + private static readonly Type _declaringType = typeof(Log4netLogger); + private static readonly ContextState> _state; + + private readonly log4net.Core.ILogger _logger; + + static Log4netLogger() + { + _state = new ContextState>("Log4netLogger.LogicalThreadContext", () => new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + public Log4netLogger(log4net.Core.ILogger logger) + { + Guard.NotNull(logger, nameof(logger)); + + _logger = logger; + } + + public string Name => _logger.Name; + + public bool IsEnabledFor(LogLevel level) + { + return _logger.IsEnabledFor(ConvertLevel(level)); + } + + public void Log(LogLevel level, Exception exception, string message, object[] args) + { + var logLevel = ConvertLevel(level); + + if (message == null && exception != null) + { + message = exception.Message; + } + + object messageObj = message; + + if (args != null && args.Length > 0) + { + messageObj = new SystemStringFormat(CultureInfo.CurrentUICulture, message, args); + } + + var loggingEvent = new LoggingEvent( + _declaringType, + _logger.Repository, + _logger.Name, + logLevel, + messageObj, + exception); + + loggingEvent.Properties["LevelId"] = (int)level; + TryAddExtendedThreadInfo(loggingEvent); + + _logger.Log(loggingEvent); + } + + protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) + { + var isAppInitialized = EngineContext.Current.IsFullyInitialized; + + // Don't knowingly run into exception + var httpRequest = isAppInitialized ? HttpContext.Current.SafeGetHttpRequest() : null; + + if (httpRequest == null) + { + loggingEvent.Properties["CustomerId"] = DBNull.Value; + loggingEvent.Properties["Url"] = DBNull.Value; + loggingEvent.Properties["Referrer"] = DBNull.Value; + loggingEvent.Properties["HttpMethod"] = DBNull.Value; + loggingEvent.Properties["Ip"] = DBNull.Value; + + return; + } + + var props = _state.GetState(); + + // Load the log4net thread with additional properties if they are available + var threadInfoMissing = !props.ContainsKey("sm:ThreadInfoAdded"); + + if (threadInfoMissing) + { + using (new ActionDisposable(() => props["sm:ThreadInfoAdded"] = true)) + { + if (DataSettings.DatabaseIsInstalled() && isAppInitialized) + { + var container = EngineContext.Current.ContainerManager; + + // CustomerId + if (container.TryResolve(null, out IWorkContext workContext)) + { + try + { + props["CustomerId"] = workContext.CurrentCustomer.Id; + } + catch + { + props["CustomerId"] = DBNull.Value; + } + } + + + // Url & stuff + if (container.TryResolve(null, out IWebHelper webHelper)) + { + try + { + props["Url"] = webHelper.GetThisPageUrl(true); + props["Referrer"] = webHelper.GetUrlReferrer(); + props["HttpMethod"] = httpRequest?.HttpMethod; + props["Ip"] = webHelper.GetCurrentIpAddress(); + } + catch { } + } + } + } + } + + loggingEvent.Properties["CustomerId"] = props.Get("CustomerId") ?? DBNull.Value; + loggingEvent.Properties["Url"] = props.Get("Url") ?? DBNull.Value; + loggingEvent.Properties["Referrer"] = props.Get("Referrer") ?? DBNull.Value; + loggingEvent.Properties["HttpMethod"] = props.Get("HttpMethod") ?? DBNull.Value; + loggingEvent.Properties["Ip"] = props.Get("Ip") ?? DBNull.Value; + } + + private static Level ConvertLevel(LogLevel level) + { + switch (level) + { + case LogLevel.Information: + return Level.Info; + case LogLevel.Warning: + return Level.Warn; + case LogLevel.Error: + return Level.Error; + case LogLevel.Fatal: + return Level.Fatal; + default: + return Level.Debug; + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLoggerFactory.cs b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLoggerFactory.cs index 6b6f26299d..b949429081 100644 --- a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLoggerFactory.cs +++ b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLoggerFactory.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Concurrent; using System.Data; -using System.Diagnostics; using System.IO; using System.Linq; using System.Web.Hosting; using log4net; using log4net.Appender; using log4net.Config; -using log4net.Core; using log4net.Repository; -using log4net.Util; using SmartStore.Core.Data; using SmartStore.Utilities; @@ -22,21 +19,21 @@ public class Log4netLoggerFactory : ILoggerFactory, IRegisteredObject public Log4netLoggerFactory() { - if (HostingEnvironment.IsHosted) - { - var configFile = GetConfigFile(CommonHelper.GetAppSetting("log4net.Config", @"Config\log4net.config")); + if (HostingEnvironment.IsHosted) + { + var configFile = GetConfigFile(CommonHelper.GetAppSetting("log4net.Config", @"Config\log4net.config")); - XmlConfigurator.ConfigureAndWatch(configFile); + XmlConfigurator.ConfigureAndWatch(configFile); - var repository = LogManager.GetRepository(); - repository.ConfigurationChanged += OnConfigurationChanged; - TryConfigureDbAppender(repository); + var repository = LogManager.GetRepository(); + repository.ConfigurationChanged += OnConfigurationChanged; + TryConfigureDbAppender(repository); - HostingEnvironment.RegisterObject(this); - } + HostingEnvironment.RegisterObject(this); + } } - private void OnConfigurationChanged(object sender, EventArgs e) + private void OnConfigurationChanged(object sender, EventArgs e) { _loggerCache.Clear(); TryConfigureDbAppender(sender as ILoggerRepository); @@ -98,33 +95,33 @@ public void FlushAll() } - #region IRegisteredObject - - public void Stop(bool immediate) - { - RemoveEmptyLogFiles(); - HostingEnvironment.UnregisterObject(this); - } - - internal static void RemoveEmptyLogFiles() - { - var fileAppenders = LogManager.GetRepository()?.GetAppenders()?.OfType(); - if (fileAppenders != null) - { - foreach (var appender in fileAppenders) - { - // Delete log file if it's empty - var logFile = new FileInfo(appender.File); - if (logFile.Exists && logFile.Length <= 0) - { - logFile.Delete(); - } - } - } - } - - #endregion - } + #region IRegisteredObject + + public void Stop(bool immediate) + { + RemoveEmptyLogFiles(); + HostingEnvironment.UnregisterObject(this); + } + + internal static void RemoveEmptyLogFiles() + { + var fileAppenders = LogManager.GetRepository()?.GetAppenders()?.OfType(); + if (fileAppenders != null) + { + foreach (var appender in fileAppenders) + { + // Delete log file if it's empty + var logFile = new FileInfo(appender.File); + if (logFile.Exists && logFile.Length <= 0) + { + logFile.Delete(); + } + } + } + } + + #endregion + } //public class DbAppender : AdoNetAppender //{ diff --git a/src/Libraries/SmartStore.Core/Logging/log4net/SmartFileAppender.cs b/src/Libraries/SmartStore.Core/Logging/log4net/SmartFileAppender.cs index 2894412966..c9f11411f2 100644 --- a/src/Libraries/SmartStore.Core/Logging/log4net/SmartFileAppender.cs +++ b/src/Libraries/SmartStore.Core/Logging/log4net/SmartFileAppender.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using log4net.Appender; using log4net.Util; namespace SmartStore.Core.Logging { - public class SmartFileAppender : RollingFileAppender + public class SmartFileAppender : RollingFileAppender { /// /// Dictionary of already known suffixes (based on previous attempts) for a given filename. diff --git a/src/Libraries/SmartStore.Core/Money.cs b/src/Libraries/SmartStore.Core/Money.cs index 613c4fc05b..88fe208a27 100644 --- a/src/Libraries/SmartStore.Core/Money.cs +++ b/src/Libraries/SmartStore.Core/Money.cs @@ -1,140 +1,124 @@ 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); - } - } - - /// - /// 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) - { + 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 => 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 => decimal.Round(Amount, DecimalDigits); + + /// + /// Truncates the amount to the number of significant decimal digits + /// of the associated currency. + /// + public decimal TruncatedAmount => (decimal)((long)Math.Truncate(Amount * DecimalDigits)) / DecimalDigits; + + /// + /// The formatted amount + /// + public string Formatted => 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) + { // Prevent stack overflow. if (obj != null) { @@ -144,340 +128,613 @@ public override bool Equals(object obj) return false; } - 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 - } + 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) + { + return a.Equals(b); + } + + public static bool operator !=(Money a, Money b) + { + return !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) + { + return a.Amount == b; + } + + public static bool operator !=(Money a, int b) + { + return a.Amount != b; + } + + public static bool operator >(Money a, int b) + { + return a.Amount > b; + } + + public static bool operator <(Money a, int b) + { + return a.Amount < b; + } + + public static bool operator <=(Money a, int b) + { + return a.Amount <= b; + } + + public static bool operator >=(Money a, int b) + { + return a.Amount >= b; + } + + public static bool operator ==(Money a, float b) + { + return a.Amount == (decimal)b; + } + + public static bool operator !=(Money a, float b) + { + return a.Amount != (decimal)b; + } + + public static bool operator >(Money a, float b) + { + return a.Amount > (decimal)b; + } + + public static bool operator <(Money a, float b) + { + return a.Amount < (decimal)b; + } + + public static bool operator <=(Money a, float b) + { + return a.Amount <= (decimal)b; + } + + public static bool operator >=(Money a, float b) + { + return a.Amount >= (decimal)b; + } + + public static bool operator ==(Money a, double b) + { + return a.Amount == (decimal)b; + } + + public static bool operator !=(Money a, double b) + { + return a.Amount != (decimal)b; + } + + public static bool operator >(Money a, double b) + { + return a.Amount > (decimal)b; + } + + public static bool operator <(Money a, double b) + { + return a.Amount < (decimal)b; + } + + public static bool operator <=(Money a, double b) + { + return a.Amount <= (decimal)b; + } + + public static bool operator >=(Money a, double b) + { + return a.Amount >= (decimal)b; + } + + public static bool operator ==(Money a, decimal b) + { + return a.Amount == b; + } + + public static bool operator !=(Money a, decimal b) + { + return a.Amount != b; + } + + public static bool operator >(Money a, decimal b) + { + return a.Amount > b; + } + + public static bool operator <(Money a, decimal b) + { + return a.Amount < b; + } + + public static bool operator <=(Money a, decimal b) + { + return a.Amount <= b; + } + + public static bool operator >=(Money a, decimal b) + { + return 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) + { + return money.Amount != 0; + } + + public static explicit operator string(Money money) + { + return money.ToString(true, false); + } + + public static explicit operator byte(Money money) + { + return System.Convert.ToByte(money.Amount); + } + + public static explicit operator decimal(Money money) + { + return money.Amount; + } + + public static explicit operator double(Money money) + { + return System.Convert.ToDouble(money.Amount); + } + + public static explicit operator float(Money money) + { + return System.Convert.ToSingle(money.Amount); + } + + public static explicit operator int(Money money) + { + return System.Convert.ToInt32(money.Amount); + } + + public static explicit operator long(Money money) + { + return System.Convert.ToInt64(money.Amount); + } + + public static explicit operator sbyte(Money money) + { + return System.Convert.ToSByte(money.Amount); + } + + public static explicit operator short(Money money) + { + return System.Convert.ToInt16(money.Amount); + } + + public static explicit operator ushort(Money money) + { + return System.Convert.ToUInt16(money.Amount); + } + + public static explicit operator uint(Money money) + { + return System.Convert.ToUInt32(money.Amount); + } + + public static explicit operator ulong(Money money) + { + return System.Convert.ToUInt64(money.Amount); + } + + TypeCode IConvertible.GetTypeCode() + { + return TypeCode.Decimal; + } + + object IConvertible.ToType(Type conversionType, IFormatProvider provider) + { + return System.Convert.ChangeType(this.Amount, conversionType, provider); + } + + bool IConvertible.ToBoolean(IFormatProvider provider) + { + return 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) + { + return (byte)this.Amount; + } + + decimal IConvertible.ToDecimal(IFormatProvider provider) + { + return this.Amount; + } + + double IConvertible.ToDouble(IFormatProvider provider) + { + return (double)this.Amount; + } + + short IConvertible.ToInt16(IFormatProvider provider) + { + return (short)this.Amount; + } + + int IConvertible.ToInt32(IFormatProvider provider) + { + return (int)this.Amount; + } + + long IConvertible.ToInt64(IFormatProvider provider) + { + return (long)this.Amount; + } + + sbyte IConvertible.ToSByte(IFormatProvider provider) + { + return (sbyte)this.Amount; + } + + float IConvertible.ToSingle(IFormatProvider provider) + { + return (float)this.Amount; + } + + ushort IConvertible.ToUInt16(IFormatProvider provider) + { + return (ushort)this.Amount; + } + + uint IConvertible.ToUInt32(IFormatProvider provider) + { + return (uint)this.Amount; + } + + ulong IConvertible.ToUInt64(IFormatProvider provider) + { + return (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) + { + return a + (decimal)b; + } + + public static Money operator +(Money a, float b) + { + return a + (decimal)b; + } + + public static Money operator +(Money a, double b) + { + return a + (decimal)b; + } + + public static Money operator +(Money a, decimal b) + { + return 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) + { + return a + (decimal)b; + } + + public static Money operator -(Money a, float b) + { + return a + (decimal)b; + } + + public static Money operator -(Money a, double b) + { + return a + (decimal)b; + } + + public static Money operator -(Money a, decimal b) + { + return 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) + { + return a * (decimal)b; + } + + public static Money operator *(Money a, float b) + { + return a * (decimal)b; + } + + public static Money operator *(Money a, double b) + { + return a * (decimal)b; + } + + public static Money operator *(Money a, decimal b) + { + return 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) + { + return a / (decimal)b; + } + + public static Money operator /(Money a, float b) + { + return a / (decimal)b; + } + + public static Money operator /(Money a, double b) + { + return a / (decimal)b; + } + + public static Money operator /(Money a, decimal b) + { + return 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/ExtensionDescriptor.cs b/src/Libraries/SmartStore.Core/Packaging/ExtensionDescriptor.cs index 2d014ace46..f9c6c83a14 100644 --- a/src/Libraries/SmartStore.Core/Packaging/ExtensionDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Packaging/ExtensionDescriptor.cs @@ -1,36 +1,34 @@ using System; -using System.Collections.Generic; -using System.Linq; namespace SmartStore.Core.Packaging { - public class ExtensionDescriptor - { - /// - /// Virtual path base, "~/Themes" or "~/Plugins" - /// - public string Location { get; set; } + public class ExtensionDescriptor + { + /// + /// Virtual path base, "~/Themes" or "~/Plugins" + /// + public string Location { get; set; } - /// - /// Folder name under virtual path base - /// - public string Id { get; set; } + /// + /// Folder name under virtual path base + /// + public string Id { get; set; } - /// - /// The extension type: Plugin | Theme - /// - public string ExtensionType { get; set; } + /// + /// The extension type: Plugin | Theme + /// + public string ExtensionType { get; set; } - // extension metadata - public string Name { get; set; } - public string Path { get; set; } - public string Description { get; set; } - public Version Version { get; set; } - public Version MinAppVersion { get; set; } - public string Author { get; set; } - public string WebSite { get; set; } - public string Tags { get; set; } - } + // extension metadata + public string Name { get; set; } + public string Path { get; set; } + public string Description { get; set; } + public Version Version { get; set; } + public Version MinAppVersion { get; set; } + public string Author { get; set; } + public string WebSite { get; set; } + public string Tags { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs b/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs index d02cc71cce..988efe48c4 100644 --- a/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs +++ b/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs @@ -1,115 +1,114 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.IO; +using System.Linq; using SmartStore.Core.Logging; using SmartStore.Utilities; namespace SmartStore.Core.Packaging { - public interface IFolderUpdater - { - void Backup(DirectoryInfo existingFolder, DirectoryInfo backupfolder, params string[] ignorePatterns); - void Restore(DirectoryInfo backupfolder, DirectoryInfo existingFolder); - } - - [SuppressMessage("ReSharper", "NotAccessedField.Local")] - public class FolderUpdater : IFolderUpdater - { - public class FolderContent - { - public DirectoryInfo Folder { get; set; } - public IEnumerable Files { get; set; } - } - - private readonly ILogger _logger; - - public FolderUpdater(ILogger logger) - { - _logger = logger; - } - - public void Backup(DirectoryInfo existingFolder, DirectoryInfo backupfolder, params string[] ignorePatterns) - { - var ignores = ignorePatterns.Select(x => new Wildcard(x)); - CopyFolder(GetFolderContent(existingFolder, ignores), backupfolder); - } - - public void Restore(DirectoryInfo backupfolder, DirectoryInfo existingFolder) - { - CopyFolder(GetFolderContent(backupfolder, Enumerable.Empty()), existingFolder); - } - - private void CopyFolder(FolderContent source, DirectoryInfo dest) - { - foreach (var file in source.Files) - { - CopyFile(source.Folder, file, dest); - } - } - - private void CopyFile(DirectoryInfo sourceFolder, string fileName, DirectoryInfo destinationFolder) - { - var sourceFile = new FileInfo(Path.Combine(sourceFolder.FullName, fileName)); - var destFile = new FileInfo(Path.Combine(destinationFolder.FullName, fileName)); - - // If destination file exist, overwrite only if changed - if (destFile.Exists) - { - if (sourceFile.Length == destFile.Length) - { - var source = File.ReadAllBytes(sourceFile.FullName); - var dest = File.ReadAllBytes(destFile.FullName); - if (source.SequenceEqual(dest)) - { - return; - } - } - } - - // Create destination directory - if (!destFile.Directory.Exists) - { - destFile.Directory.Create(); - } - - File.Copy(sourceFile.FullName, destFile.FullName, true); - } - - private FolderContent GetFolderContent(DirectoryInfo folder, IEnumerable ignores) - { - var files = new List(); - GetFolderContent(folder, "", files, ignores); - return new FolderContent { Folder = folder, Files = files }; - } - - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private void GetFolderContent(DirectoryInfo folder, string prefix, List files, IEnumerable ignores) - { - if (!folder.Exists) - return; - - if (ignores.Any(w => w.IsMatch(prefix))) - return; - - foreach (var file in folder.GetFiles()) - { - var path = Path.Combine(prefix, file.Name); - var ignore = ignores.Any(w => w.IsMatch(path)); - if (!ignore) - { - files.Add(path); - } - } - - foreach (var child in folder.GetDirectories()) - { - GetFolderContent(child, Path.Combine(prefix, child.Name), files, ignores); - } - } - - } + public interface IFolderUpdater + { + void Backup(DirectoryInfo existingFolder, DirectoryInfo backupfolder, params string[] ignorePatterns); + void Restore(DirectoryInfo backupfolder, DirectoryInfo existingFolder); + } + + [SuppressMessage("ReSharper", "NotAccessedField.Local")] + public class FolderUpdater : IFolderUpdater + { + public class FolderContent + { + public DirectoryInfo Folder { get; set; } + public IEnumerable Files { get; set; } + } + + private readonly ILogger _logger; + + public FolderUpdater(ILogger logger) + { + _logger = logger; + } + + public void Backup(DirectoryInfo existingFolder, DirectoryInfo backupfolder, params string[] ignorePatterns) + { + var ignores = ignorePatterns.Select(x => new Wildcard(x)); + CopyFolder(GetFolderContent(existingFolder, ignores), backupfolder); + } + + public void Restore(DirectoryInfo backupfolder, DirectoryInfo existingFolder) + { + CopyFolder(GetFolderContent(backupfolder, Enumerable.Empty()), existingFolder); + } + + private void CopyFolder(FolderContent source, DirectoryInfo dest) + { + foreach (var file in source.Files) + { + CopyFile(source.Folder, file, dest); + } + } + + private void CopyFile(DirectoryInfo sourceFolder, string fileName, DirectoryInfo destinationFolder) + { + var sourceFile = new FileInfo(Path.Combine(sourceFolder.FullName, fileName)); + var destFile = new FileInfo(Path.Combine(destinationFolder.FullName, fileName)); + + // If destination file exist, overwrite only if changed + if (destFile.Exists) + { + if (sourceFile.Length == destFile.Length) + { + var source = File.ReadAllBytes(sourceFile.FullName); + var dest = File.ReadAllBytes(destFile.FullName); + if (source.SequenceEqual(dest)) + { + return; + } + } + } + + // Create destination directory + if (!destFile.Directory.Exists) + { + destFile.Directory.Create(); + } + + File.Copy(sourceFile.FullName, destFile.FullName, true); + } + + private FolderContent GetFolderContent(DirectoryInfo folder, IEnumerable ignores) + { + var files = new List(); + GetFolderContent(folder, "", files, ignores); + return new FolderContent { Folder = folder, Files = files }; + } + + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + private void GetFolderContent(DirectoryInfo folder, string prefix, List files, IEnumerable ignores) + { + if (!folder.Exists) + return; + + if (ignores.Any(w => w.IsMatch(prefix))) + return; + + foreach (var file in folder.GetFiles()) + { + var path = Path.Combine(prefix, file.Name); + var ignore = ignores.Any(w => w.IsMatch(path)); + if (!ignore) + { + files.Add(path); + } + } + + foreach (var child in folder.GetDirectories()) + { + GetFolderContent(child, Path.Combine(prefix, child.Name), files, ignores); + } + } + + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/IPackageBuilder.cs b/src/Libraries/SmartStore.Core/Packaging/IPackageBuilder.cs index ed9f9fcde6..259ca2e6ae 100644 --- a/src/Libraries/SmartStore.Core/Packaging/IPackageBuilder.cs +++ b/src/Libraries/SmartStore.Core/Packaging/IPackageBuilder.cs @@ -1,13 +1,12 @@ -using System; -using System.IO; -using SmartStore.Core.Themes; +using System.IO; using SmartStore.Core.Plugins; +using SmartStore.Core.Themes; namespace SmartStore.Core.Packaging { - public interface IPackageBuilder - { - Stream BuildPackage(PluginDescriptor pluginDescriptor); - Stream BuildPackage(ThemeManifest themeManifest); - } + public interface IPackageBuilder + { + Stream BuildPackage(PluginDescriptor pluginDescriptor); + Stream BuildPackage(ThemeManifest themeManifest); + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/IPackageInstaller.cs b/src/Libraries/SmartStore.Core/Packaging/IPackageInstaller.cs index 44005d3956..187858ab75 100644 --- a/src/Libraries/SmartStore.Core/Packaging/IPackageInstaller.cs +++ b/src/Libraries/SmartStore.Core/Packaging/IPackageInstaller.cs @@ -1,13 +1,11 @@ -using System; -using System.IO; -using NuGet; +using System.IO; namespace SmartStore.Core.Packaging { - public interface IPackageInstaller - { - //PackageInfo Install(string packageId, string version, string location, string applicationFolder); - PackageInfo Install(Stream packageStream, string location, string applicationPath); - void Uninstall(string packageId, string applicationFolder); - } + public interface IPackageInstaller + { + //PackageInfo Install(string packageId, string version, string location, string applicationFolder); + PackageInfo Install(Stream packageStream, string location, string applicationPath); + void Uninstall(string packageId, string applicationFolder); + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/IPackageManager.cs b/src/Libraries/SmartStore.Core/Packaging/IPackageManager.cs index 2478640a74..c497d2be95 100644 --- a/src/Libraries/SmartStore.Core/Packaging/IPackageManager.cs +++ b/src/Libraries/SmartStore.Core/Packaging/IPackageManager.cs @@ -1,15 +1,13 @@ -using System; -using System.IO; -using NuGet; +using System.IO; namespace SmartStore.Core.Packaging { - public interface IPackageManager - { - PackageInfo Install(Stream packageStream, string location, string applicationPath); - void Uninstall(string packageId, string applicationPath); + public interface IPackageManager + { + PackageInfo Install(Stream packageStream, string location, string applicationPath); + void Uninstall(string packageId, string applicationPath); - PackagingResult BuildPluginPackage(string pluginName); - PackagingResult BuildThemePackage(string themeName); - } -} + PackagingResult BuildPluginPackage(string pluginName); + PackagingResult BuildThemePackage(string themeName); + } +} diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs index 8a06699b59..644b6b15a7 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs @@ -1,107 +1,100 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using NuGet; using SmartStore.Core.Plugins; using SmartStore.Core.Themes; -using NuGet; namespace SmartStore.Core.Packaging { - internal abstract class ExtensionReferenceRepository : PackageRepositoryBase - { - - public ExtensionReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository) - { - Guard.NotNull(project, nameof(project)); - Guard.NotNull(sourceRepository, nameof(sourceRepository)); - - Project = project; - SourceRepository = sourceRepository; - } - - public IProjectSystem Project - { - get; - set; - } - - public IPackageRepository SourceRepository - { - get; - set; - } - - public override void AddPackage(IPackage package) { } - - public override void RemovePackage(IPackage package) { } - - - public override string Source - { - get { return Project.Root; } - } - - public override bool SupportsPrereleasePackages - { - get { return true; } - } - } - - /// - /// This repository implementation informs about what plugin packages are already installed. - /// - internal class PluginReferenceRepository : ExtensionReferenceRepository - { - private readonly IList _descriptors; - - public PluginReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository, IPluginFinder pluginFinder) - : base(project, sourceRepository) - { - _descriptors = pluginFinder.GetPluginDescriptors().ToList(); - } - - public override IQueryable GetPackages() - { - IEnumerable repositoryPackages = SourceRepository.GetPackages().ToList(); - IEnumerable packages = from plugin in _descriptors - let id = PackagingUtils.BuildPackageId(plugin.SystemName, "Plugin") - let version = plugin.Version != null ? new SemanticVersion(plugin.Version) : null - let package = repositoryPackages.FirstOrDefault(p => p.Id == id && (version == null || p.Version == version)) - where package != null - select package; - - return packages.AsQueryable(); - } - - } - - /// - /// This repository implementation informs about what theme packages are already installed. - /// - internal class ThemeReferenceRepository : ExtensionReferenceRepository - { - private readonly ICollection _themeManifests; - - public ThemeReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository, IThemeRegistry themeRegistry) - : base(project, sourceRepository) - { - _themeManifests = themeRegistry.GetThemeManifests(true); - } - - public override IQueryable GetPackages() - { - IEnumerable repositoryPackages = SourceRepository.GetPackages().ToList(); - IEnumerable packages = from theme in _themeManifests - let id = PackagingUtils.BuildPackageId(theme.ThemeName, "Theme") - let version = theme.Version != null ? new SemanticVersion(theme.Version) : null - let package = repositoryPackages.FirstOrDefault(p => p.Id == id && (version == null || p.Version == version)) - where package != null - select package; - - return packages.AsQueryable(); - } - - } + internal abstract class ExtensionReferenceRepository : PackageRepositoryBase + { + + public ExtensionReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository) + { + Guard.NotNull(project, nameof(project)); + Guard.NotNull(sourceRepository, nameof(sourceRepository)); + + Project = project; + SourceRepository = sourceRepository; + } + + public IProjectSystem Project + { + get; + set; + } + + public IPackageRepository SourceRepository + { + get; + set; + } + + public override void AddPackage(IPackage package) { } + + public override void RemovePackage(IPackage package) { } + + + public override string Source => Project.Root; + + public override bool SupportsPrereleasePackages => true; + } + + /// + /// This repository implementation informs about what plugin packages are already installed. + /// + internal class PluginReferenceRepository : ExtensionReferenceRepository + { + private readonly IList _descriptors; + + public PluginReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository, IPluginFinder pluginFinder) + : base(project, sourceRepository) + { + _descriptors = pluginFinder.GetPluginDescriptors().ToList(); + } + + public override IQueryable GetPackages() + { + IEnumerable repositoryPackages = SourceRepository.GetPackages().ToList(); + IEnumerable packages = from plugin in _descriptors + let id = PackagingUtils.BuildPackageId(plugin.SystemName, "Plugin") + let version = plugin.Version != null ? new SemanticVersion(plugin.Version) : null + let package = repositoryPackages.FirstOrDefault(p => p.Id == id && (version == null || p.Version == version)) + where package != null + select package; + + return packages.AsQueryable(); + } + + } + + /// + /// This repository implementation informs about what theme packages are already installed. + /// + internal class ThemeReferenceRepository : ExtensionReferenceRepository + { + private readonly ICollection _themeManifests; + + public ThemeReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository, IThemeRegistry themeRegistry) + : base(project, sourceRepository) + { + _themeManifests = themeRegistry.GetThemeManifests(true); + } + + public override IQueryable GetPackages() + { + IEnumerable repositoryPackages = SourceRepository.GetPackages().ToList(); + IEnumerable packages = from theme in _themeManifests + let id = PackagingUtils.BuildPackageId(theme.ThemeName, "Theme") + let version = theme.Version != null ? new SemanticVersion(theme.Version) : null + let package = repositoryPackages.FirstOrDefault(p => p.Id == id && (version == null || p.Version == version)) + where package != null + select package; + + return packages.AsQueryable(); + } + + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs index a32cae4e91..22e5a3c580 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs @@ -1,97 +1,85 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Reflection; using System.Runtime.Versioning; using NuGet; namespace SmartStore.Core.Packaging { - internal class FileBasedProjectSystem : PhysicalFileSystem, IProjectSystem - { - - public FileBasedProjectSystem(string root) - : base(root) - { - } - - public void AddFrameworkReference(string name) - { - throw new NotSupportedException(); - } - - public void AddImport(string targetPath, ProjectImportLocation location) - { - throw new NotSupportedException(); - } - - public void AddReference(string referencePath, Stream stream) - { - throw new NotSupportedException(); - } - - public bool FileExistsInProject(string path) - { - return FileExists(path); - } - - public bool IsBindingRedirectSupported - { - get { return false; } - } - - public bool IsSupportedFile(string path) - { - return true; - } - - public string ProjectName - { - get { return Root; } - } - - protected virtual string GetReferencePath(string name) - { - return Path.Combine("bin", name); - } - - public bool ReferenceExists(string name) - { - string path = GetReferencePath(name); - return FileExists(path); - } - - public void RemoveImport(string targetPath) - { - throw new NotSupportedException(); - } - - public void RemoveReference(string name) - { - throw new NotSupportedException(); - } - - public string ResolvePath(string path) - { - return GetFullPath(path); - } - - public FrameworkName TargetFramework - { - get { return VersionUtility.DefaultTargetFramework; } - } - - public dynamic GetPropertyValue(string propertyName) - { - if (propertyName == null) - { - return null; - } - - // Return empty string for the root namespace of this project. - return propertyName.Equals("RootNamespace", StringComparison.OrdinalIgnoreCase) ? String.Empty : null; - } - } + internal class FileBasedProjectSystem : PhysicalFileSystem, IProjectSystem + { + + public FileBasedProjectSystem(string root) + : base(root) + { + } + + public void AddFrameworkReference(string name) + { + throw new NotSupportedException(); + } + + public void AddImport(string targetPath, ProjectImportLocation location) + { + throw new NotSupportedException(); + } + + public void AddReference(string referencePath, Stream stream) + { + throw new NotSupportedException(); + } + + public bool FileExistsInProject(string path) + { + return FileExists(path); + } + + public bool IsBindingRedirectSupported => false; + + public bool IsSupportedFile(string path) + { + return true; + } + + public string ProjectName => Root; + + protected virtual string GetReferencePath(string name) + { + return Path.Combine("bin", name); + } + + public bool ReferenceExists(string name) + { + string path = GetReferencePath(name); + return FileExists(path); + } + + public void RemoveImport(string targetPath) + { + throw new NotSupportedException(); + } + + public void RemoveReference(string name) + { + throw new NotSupportedException(); + } + + public string ResolvePath(string path) + { + return GetFullPath(path); + } + + public FrameworkName TargetFramework => VersionUtility.DefaultTargetFramework; + + public dynamic GetPropertyValue(string propertyName) + { + if (propertyName == null) + { + return null; + } + + // Return empty string for the root namespace of this project. + return propertyName.Equals("RootNamespace", StringComparison.OrdinalIgnoreCase) ? String.Empty : null; + } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/NugetLogger.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/NugetLogger.cs index f2c4c8a65f..8a2942f601 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/NugetLogger.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/NugetLogger.cs @@ -5,37 +5,37 @@ namespace SmartStore.Core.Packaging { - internal class NugetLogger : NuGet.ILogger - { - private readonly Log.ILogger _logger; + internal class NugetLogger : NuGet.ILogger + { + private readonly Log.ILogger _logger; - public NugetLogger(Log.ILogger logger) - { - _logger = logger; - } + public NugetLogger(Log.ILogger logger) + { + _logger = logger; + } - public void Log(MessageLevel level, string message, params object[] args) - { - switch (level) - { - case MessageLevel.Debug: - _logger.Debug(String.Format(message, args)); - break; - case MessageLevel.Error: - _logger.Error(String.Format(message, args)); - break; - case MessageLevel.Info: - _logger.Info(String.Format(message, args)); - break; - case MessageLevel.Warning: - _logger.Warn(String.Format(message, args)); - break; - } - } + public void Log(MessageLevel level, string message, params object[] args) + { + switch (level) + { + case MessageLevel.Debug: + _logger.Debug(String.Format(message, args)); + break; + case MessageLevel.Error: + _logger.Error(String.Format(message, args)); + break; + case MessageLevel.Info: + _logger.Info(String.Format(message, args)); + break; + case MessageLevel.Warning: + _logger.Warn(String.Format(message, args)); + break; + } + } - public FileConflictResolution ResolveFileConflict(string message) - { - return FileConflictResolution.OverwriteAll; - } - } + public FileConflictResolution ResolveFileConflict(string message) + { + return FileConflictResolution.OverwriteAll; + } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs index 5d98420378..24d9271ae6 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs @@ -1,34 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Linq; using NuGet; -using SmartStore.Utilities; namespace SmartStore.Core.Packaging { - /// - /// This repository implementation fakes a source (remote) repository - /// - internal class NullSourceRepository : PackageRepositoryBase - { - public override IQueryable GetPackages() - { - return Enumerable.Empty().AsQueryable(); - } + /// + /// This repository implementation fakes a source (remote) repository + /// + internal class NullSourceRepository : PackageRepositoryBase + { + public override IQueryable GetPackages() + { + return Enumerable.Empty().AsQueryable(); + } - public override string Source - { - get { return string.Empty; } - } + public override string Source => string.Empty; - public override bool SupportsPrereleasePackages - { - get { return true; } - } + public override bool SupportsPrereleasePackages => true; - public override void AddPackage(IPackage package) { } + public override void AddPackage(IPackage package) { } - public override void RemovePackage(IPackage package) { } - } + public override void RemovePackage(IPackage package) { } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs b/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs index c156852da3..d3d23fda59 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs @@ -1,201 +1,189 @@ using System; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using NuGet; +using SmartStore.Core.IO; using SmartStore.Core.Plugins; using SmartStore.Core.Themes; -using NuGet; using NuGetPackageBuilder = NuGet.PackageBuilder; -using SmartStore.Core.IO; -using System.Runtime.Versioning; namespace SmartStore.Core.Packaging { - public class PackageBuilder : IPackageBuilder - { - private readonly IApplicationEnvironment _env; + public class PackageBuilder : IPackageBuilder + { + private readonly IApplicationEnvironment _env; - public PackageBuilder(IApplicationEnvironment env) - { - this._env = env; - } + public PackageBuilder(IApplicationEnvironment env) + { + this._env = env; + } - private static readonly string[] _ignoredThemeExtensions = new[] { + private static readonly string[] _ignoredThemeExtensions = new[] { "obj", "pdb", "exclude" }; - private static readonly string[] _ignoredThemePaths = new[] { + private static readonly string[] _ignoredThemePaths = new[] { "/obj/" }; - private static bool IgnoreFile(string filePath) - { - return String.IsNullOrEmpty(filePath) || - _ignoredThemePaths.Any(filePath.Contains) || - _ignoredThemeExtensions.Contains(Path.GetExtension(filePath).NullEmpty() ?? ""); - } - - - public Stream BuildPackage(PluginDescriptor pluginDescriptor) - { - return BuildPackage(pluginDescriptor.ConvertToExtensionDescriptor()); - } - - public Stream BuildPackage(ThemeManifest themeManifest) - { - return BuildPackage(themeManifest.ConvertToExtensionDescriptor()); - } - - private Stream BuildPackage(ExtensionDescriptor extensionDescriptor) - { - var context = new BuildContext(); - BeginPackage(context); - try - { - EstablishPaths(context, _env.WebRootFolder, extensionDescriptor.Id, extensionDescriptor.ExtensionType); - SetCoreProperties(context, extensionDescriptor); - EmbedFiles(context); - } - finally - { - EndPackage(context); - } - - if (context.Stream.CanSeek) - { - context.Stream.Seek(0, SeekOrigin.Begin); - } - - return context.Stream; - } - - private static void BeginPackage(BuildContext context) - { - context.Stream = new MemoryStream(); - context.Builder = new NuGetPackageBuilder(); - } - - private static void EndPackage(BuildContext context) - { - context.Builder.Save(context.Stream); - } - - private static void SetCoreProperties(BuildContext context, ExtensionDescriptor extensionDescriptor) - { - context.Builder.Id = PackagingUtils.BuildPackageId(extensionDescriptor.Id, extensionDescriptor.ExtensionType); - context.Builder.Version = new SemanticVersion(extensionDescriptor.Version); - context.Builder.Title = extensionDescriptor.Name ?? extensionDescriptor.Id; - context.Builder.Description = extensionDescriptor.Description.NullEmpty() ?? "No Description"; - context.Builder.Authors.Add(extensionDescriptor.Author ?? "Unknown"); - - if (Uri.IsWellFormedUriString(extensionDescriptor.WebSite, UriKind.Absolute)) - { - context.Builder.ProjectUrl = new Uri(extensionDescriptor.WebSite); - } - } - - private static void EstablishPaths(BuildContext context, IVirtualFolder webRootFolder, string extensionName, string extensionType = "Plugin") - { - context.SourceFolder = webRootFolder; - if (extensionType.IsCaseInsensitiveEqual("theme")) - { - context.SourcePath = "Themes/" + extensionName + "/"; - context.TargetPath = "\\Content\\Themes\\" + extensionName + "\\"; - } - else - { - context.SourcePath = "Plugins/" + extensionName + "/"; - context.TargetPath = "\\Content\\Plugins\\" + extensionName + "\\"; - } - } - - private static void EmbedFiles(BuildContext context) - { - var basePath = context.SourcePath; - foreach (var path in context.SourceFolder.ListFiles(context.SourcePath, true)) - { - // skip ignores files - if (IgnoreFile(path)) - { - continue; - } - // full virtual path given but we need the relative path so it can be put into - // the package that way (the package itself is the logical base path). - // Get it by stripping the basePath off including the slash. - var relativePath = path.Replace(basePath, ""); - EmbedVirtualFile(context, relativePath); - } - } - - private static void EmbedVirtualFile(BuildContext context, string relativePath) - { - var file = new VirtualPackageFile( - context.SourceFolder, - context.SourcePath + relativePath, - context.TargetPath + relativePath); - context.Builder.Files.Add(file); - } - - - #region Nested type: BuildContext - - private class BuildContext - { - public Stream Stream { get; set; } - public NuGetPackageBuilder Builder { get; set; } - - public IVirtualFolder SourceFolder { get; set; } - public string SourcePath { get; set; } - public string TargetPath { get; set; } - } - - #endregion - - #region Nested type: VirtualPackageFile - - private class VirtualPackageFile : IPackageFile - { - private readonly IVirtualFolder _webRootFolder; - private readonly string _relativePath; - private readonly string _packagePath; - - public VirtualPackageFile(IVirtualFolder webRootFolder, string relativePath, string packagePath) - { - _webRootFolder = webRootFolder; - _relativePath = relativePath; - _packagePath = packagePath; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Supposed to return an open stream.")] - public Stream GetStream() - { - var stream = new MemoryStream(); - _webRootFolder.CopyFile(_relativePath, stream); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } - - public string Path - { - get { return _packagePath; } - } - - public string EffectivePath - { - get { return _packagePath; } - } - - public FrameworkName TargetFramework - { - get { return null; } - } - - public IEnumerable SupportedFrameworks - { - get { return Enumerable.Empty(); } - } - } - - #endregion - } + private static bool IgnoreFile(string filePath) + { + return String.IsNullOrEmpty(filePath) || + _ignoredThemePaths.Any(filePath.Contains) || + _ignoredThemeExtensions.Contains(Path.GetExtension(filePath).NullEmpty() ?? ""); + } + + + public Stream BuildPackage(PluginDescriptor pluginDescriptor) + { + return BuildPackage(pluginDescriptor.ConvertToExtensionDescriptor()); + } + + public Stream BuildPackage(ThemeManifest themeManifest) + { + return BuildPackage(themeManifest.ConvertToExtensionDescriptor()); + } + + private Stream BuildPackage(ExtensionDescriptor extensionDescriptor) + { + var context = new BuildContext(); + BeginPackage(context); + try + { + EstablishPaths(context, _env.WebRootFolder, extensionDescriptor.Id, extensionDescriptor.ExtensionType); + SetCoreProperties(context, extensionDescriptor); + EmbedFiles(context); + } + finally + { + EndPackage(context); + } + + if (context.Stream.CanSeek) + { + context.Stream.Seek(0, SeekOrigin.Begin); + } + + return context.Stream; + } + + private static void BeginPackage(BuildContext context) + { + context.Stream = new MemoryStream(); + context.Builder = new NuGetPackageBuilder(); + } + + private static void EndPackage(BuildContext context) + { + context.Builder.Save(context.Stream); + } + + private static void SetCoreProperties(BuildContext context, ExtensionDescriptor extensionDescriptor) + { + context.Builder.Id = PackagingUtils.BuildPackageId(extensionDescriptor.Id, extensionDescriptor.ExtensionType); + context.Builder.Version = new SemanticVersion(extensionDescriptor.Version); + context.Builder.Title = extensionDescriptor.Name ?? extensionDescriptor.Id; + context.Builder.Description = extensionDescriptor.Description.NullEmpty() ?? "No Description"; + context.Builder.Authors.Add(extensionDescriptor.Author ?? "Unknown"); + + if (Uri.IsWellFormedUriString(extensionDescriptor.WebSite, UriKind.Absolute)) + { + context.Builder.ProjectUrl = new Uri(extensionDescriptor.WebSite); + } + } + + private static void EstablishPaths(BuildContext context, IVirtualFolder webRootFolder, string extensionName, string extensionType = "Plugin") + { + context.SourceFolder = webRootFolder; + if (extensionType.IsCaseInsensitiveEqual("theme")) + { + context.SourcePath = "Themes/" + extensionName + "/"; + context.TargetPath = "\\Content\\Themes\\" + extensionName + "\\"; + } + else + { + context.SourcePath = "Plugins/" + extensionName + "/"; + context.TargetPath = "\\Content\\Plugins\\" + extensionName + "\\"; + } + } + + private static void EmbedFiles(BuildContext context) + { + var basePath = context.SourcePath; + foreach (var path in context.SourceFolder.ListFiles(context.SourcePath, true)) + { + // skip ignores files + if (IgnoreFile(path)) + { + continue; + } + // full virtual path given but we need the relative path so it can be put into + // the package that way (the package itself is the logical base path). + // Get it by stripping the basePath off including the slash. + var relativePath = path.Replace(basePath, ""); + EmbedVirtualFile(context, relativePath); + } + } + + private static void EmbedVirtualFile(BuildContext context, string relativePath) + { + var file = new VirtualPackageFile( + context.SourceFolder, + context.SourcePath + relativePath, + context.TargetPath + relativePath); + context.Builder.Files.Add(file); + } + + + #region Nested type: BuildContext + + private class BuildContext + { + public Stream Stream { get; set; } + public NuGetPackageBuilder Builder { get; set; } + + public IVirtualFolder SourceFolder { get; set; } + public string SourcePath { get; set; } + public string TargetPath { get; set; } + } + + #endregion + + #region Nested type: VirtualPackageFile + + private class VirtualPackageFile : IPackageFile + { + private readonly IVirtualFolder _webRootFolder; + private readonly string _relativePath; + private readonly string _packagePath; + + public VirtualPackageFile(IVirtualFolder webRootFolder, string relativePath, string packagePath) + { + _webRootFolder = webRootFolder; + _relativePath = relativePath; + _packagePath = packagePath; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Supposed to return an open stream.")] + public Stream GetStream() + { + var stream = new MemoryStream(); + _webRootFolder.CopyFile(_relativePath, stream); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + + public string Path => _packagePath; + + public string EffectivePath => _packagePath; + + public FrameworkName TargetFramework => null; + + public IEnumerable SupportedFrameworks => Enumerable.Empty(); + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/PackageInfo.cs b/src/Libraries/SmartStore.Core/Packaging/PackageInfo.cs index 2de699d27a..410c992ec2 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackageInfo.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackageInfo.cs @@ -1,17 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace SmartStore.Core.Packaging +namespace SmartStore.Core.Packaging { - public class PackageInfo - { - public string Id { get; set; } - public string Name { get; set; } - public string Version { get; set; } - public string Type { get; set; } - public string Path { get; set; } - public ExtensionDescriptor ExtensionDescriptor { get; set; } - } + public class PackageInfo + { + public string Id { get; set; } + public string Name { get; set; } + public string Version { get; set; } + public string Type { get; set; } + public string Path { get; set; } + public ExtensionDescriptor ExtensionDescriptor { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs b/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs index 935b00e542..98e98970dd 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs @@ -1,299 +1,296 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using NuGet; -using NuGetPackageManager = NuGet.PackageManager; +using SmartStore.Core.IO; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; -using Log = SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Core.Themes; -using SmartStore.Core.IO; -using SmartStore.Core; -using SmartStore.Core.Localization; +using Log = SmartStore.Core.Logging; namespace SmartStore.Core.Packaging { - public class PackageInstaller : IPackageInstaller - { - private readonly IVirtualPathProvider _virtualPathProvider; - private readonly IPluginFinder _pluginFinder; - private readonly IThemeRegistry _themeRegistry; - private readonly IFolderUpdater _folderUpdater; - private readonly INotifier _notifier; - private readonly Log.ILogger _logger; - - public PackageInstaller( - IVirtualPathProvider virtualPathProvider, - IPluginFinder pluginFinder, - IThemeRegistry themeRegistry, - IFolderUpdater folderUpdater, - INotifier notifier, - Log.ILogger logger) - { - _virtualPathProvider = virtualPathProvider; - _pluginFinder = pluginFinder; - _themeRegistry = themeRegistry; - _folderUpdater = folderUpdater; - _notifier = notifier; - _logger = logger; - } - - public Localizer T { get; set; } - - //public PackageInfo Install(string packageId, string version, string location, string applicationFolder) - //{ - // // instantiates the appropriate package repository - // IPackageRepository packageRepository = PackageRepositoryFactory.Default.CreateRepository(location); - - // // gets an IPackage instance from the repository - // var packageVersion = String.IsNullOrEmpty(version) ? null : new SemanticVersion(version); - // var package = packageRepository.FindPackage(packageId, packageVersion); - // if (package == null) - // { - // throw new ArgumentException("The specified package could not be found, id:{0} version:{1}".FormatCurrent(packageId, version.IsEmpty() ? "No version" : version)); - // } - - // return InstallPackage(package, packageRepository, location, applicationFolder); - //} - - public PackageInfo Install(Stream packageStream, string location, string applicationPath) - { - Guard.NotNull(packageStream, nameof(packageStream)); - - IPackage package; - try - { - package = new ZipPackage(packageStream); - - } - catch (Exception ex) - { - throw new SmartException(T("Admin.Packaging.StreamError"), ex); - } - - // instantiates the appropriate package repository - var packageRepository = new NullSourceRepository(); - return InstallPackage(package, packageRepository, location, applicationPath); - } - - /// - /// Tries to install the package - /// - /// The package to install - /// The repository - /// The virtual location of the package file, usually ~/App_Data - /// The virtual app root path, usually ~/ - /// An instance of type - protected PackageInfo InstallPackage(IPackage package, IPackageRepository packageRepository, string location, string applicationPath) - { - bool previousInstalled; - - // 1. See if extension was previous installed and backup its folder if so - try - { - previousInstalled = BackupExtensionFolder(package.ExtensionFolder(), package.ExtensionId()); - } - catch (Exception exception) - { - throw new SmartException(T("Admin.Packaging.BackupError"), exception); - } - - if (previousInstalled) - { - // 2. If extension is installed, need to un-install first - try - { - UninstallExtensionIfNeeded(package); - } - catch (Exception exception) - { - throw new SmartException(T("Admin.Packaging.UninstallError"), exception); - } - } - - var packageInfo = ExecuteInstall(package, packageRepository, location, applicationPath); - - // check if the new package is compatible with current SmartStore version - var descriptor = package.GetExtensionDescriptor(packageInfo.Type); - - if (descriptor != null) - { - packageInfo.ExtensionDescriptor = descriptor; - - if (!PluginManager.IsAssumedCompatible(descriptor.MinAppVersion)) - { - if (previousInstalled) - { - // restore the previous version - RestoreExtensionFolder(package.ExtensionFolder(), package.ExtensionId()); - } - else - { - // just uninstall the new package - Uninstall(package.Id, _virtualPathProvider.MapPath("~\\")); - } - - var msg = T("Admin.Packaging.IsIncompatible", SmartStoreVersion.CurrentFullVersion); - _logger.Error(msg); - throw new SmartException(msg); - } - } - - return packageInfo; - } - - /// - /// Executes a package installation. - /// - /// The package to install. - /// The repository for the package. - /// The source location. - /// The path where to install the package. - /// The package information. - protected PackageInfo ExecuteInstall(IPackage package, IPackageRepository packageRepository, string sourceLocation, string targetPath) - { - var logger = new NugetLogger(_logger); - - var project = new FileBasedProjectSystem(targetPath) { Logger = logger }; - - IPackageRepository referenceRepository; - if (package.IsTheme()) - { - referenceRepository = new ThemeReferenceRepository(project, packageRepository, _themeRegistry); - } - else - { - referenceRepository = new PluginReferenceRepository(project, packageRepository, _pluginFinder); - } - - var projectManager = new ProjectManager( - packageRepository, - new DefaultPackagePathResolver(targetPath), - project, - referenceRepository - ) { Logger = logger }; - - // add the package to the project - projectManager.AddPackageReference(package, true, false); - - return new PackageInfo - { - Id = package.Id, - Name = package.Title ?? package.Id, - Version = package.Version.ToString(), - Type = package.IsTheme() ? "Theme" : "Plugin", - Path = targetPath - }; - } - - public void Uninstall(string packageId, string applicationFolder) - { - string extensionFullPath = string.Empty; - - if (packageId.StartsWith(PackagingUtils.GetExtensionPrefix("Theme"))) - { - extensionFullPath = _virtualPathProvider.MapPath("~/Themes/" + packageId.Substring(PackagingUtils.GetExtensionPrefix("Theme").Length)); - } - else if (packageId.StartsWith(PackagingUtils.GetExtensionPrefix("Plugin"))) - { - extensionFullPath = _virtualPathProvider.MapPath("~/Plugins/" + packageId.Substring(PackagingUtils.GetExtensionPrefix("Plugin").Length)); - } - - if (string.IsNullOrEmpty(extensionFullPath) || !System.IO.Directory.Exists(extensionFullPath)) - { - throw new SmartException(T("Admin.Packaging.NotFound", packageId)); - } - - // If the package was not installed through nuget we still need to try to uninstall it by removing its directory - if (Directory.Exists(extensionFullPath)) - { - Directory.Delete(extensionFullPath, true); - } - } - - [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Local")] - private bool RestoreExtensionFolder(string extensionFolder, string extensionId) - { - var virtualSource = _virtualPathProvider.Combine("~", extensionFolder, extensionId); - var source = new DirectoryInfo(_virtualPathProvider.MapPath(virtualSource)); - - if (source.Exists) - { - var tempPath = _virtualPathProvider.Combine("~/App_Data", "_Backup", extensionFolder, extensionId); - string localTempPath = null; - for (int i = 0; i < 1000; i++) - { - localTempPath = _virtualPathProvider.MapPath(tempPath) + (i == 0 ? "" : "." + i.ToString()); - if (!System.IO.Directory.Exists(localTempPath)) - { - System.IO.Directory.CreateDirectory(localTempPath); - break; - } - localTempPath = null; - } - - if (localTempPath == null) - { - throw new SmartException(T("Admin.Packaging.TooManyBackups", tempPath)); - } - - var backupFolder = new DirectoryInfo(localTempPath); - _folderUpdater.Restore(backupFolder, source); - _notifier.Information(T("Admin.Packaging.RestoreSuccess", virtualSource)); - - return true; - } - - return false; - } - - private bool BackupExtensionFolder(string extensionFolder, string extensionId) - { - var source = new DirectoryInfo(_virtualPathProvider.MapPath(_virtualPathProvider.Combine("~", extensionFolder, extensionId))); - - if (source.Exists) - { - var tempPath = _virtualPathProvider.Combine("~/App_Data", "_Backup", extensionFolder, extensionId); - string localTempPath = null; - for (int i = 0; i < 1000; i++) - { - localTempPath = _virtualPathProvider.MapPath(tempPath) + (i == 0 ? "" : "." + i.ToString()); - if (!System.IO.Directory.Exists(localTempPath)) - { - System.IO.Directory.CreateDirectory(localTempPath); - break; - } - localTempPath = null; - } - - if (localTempPath == null) - { - throw new SmartException(T("Admin.Packaging.TooManyBackups", tempPath)); - } - - var backupFolder = new DirectoryInfo(localTempPath); - _folderUpdater.Backup(source, backupFolder); - _notifier.Information(T("Admin.Packaging.BackupSuccess", backupFolder.Name)); - - return true; - } - - return false; - } - - private void UninstallExtensionIfNeeded(IPackage package) - { - // Nuget requires to un-install the currently installed packages if the new - // package is the same version or an older version - try - { - Uninstall(package.Id, _virtualPathProvider.MapPath("~\\")); - //_notifier.Information("Successfully un-installed local package {0}".FormatInvariant(package.ExtensionId())); - } - catch { } - } - } + public class PackageInstaller : IPackageInstaller + { + private readonly IVirtualPathProvider _virtualPathProvider; + private readonly IPluginFinder _pluginFinder; + private readonly IThemeRegistry _themeRegistry; + private readonly IFolderUpdater _folderUpdater; + private readonly INotifier _notifier; + private readonly Log.ILogger _logger; + + public PackageInstaller( + IVirtualPathProvider virtualPathProvider, + IPluginFinder pluginFinder, + IThemeRegistry themeRegistry, + IFolderUpdater folderUpdater, + INotifier notifier, + Log.ILogger logger) + { + _virtualPathProvider = virtualPathProvider; + _pluginFinder = pluginFinder; + _themeRegistry = themeRegistry; + _folderUpdater = folderUpdater; + _notifier = notifier; + _logger = logger; + } + + public Localizer T { get; set; } + + //public PackageInfo Install(string packageId, string version, string location, string applicationFolder) + //{ + // // instantiates the appropriate package repository + // IPackageRepository packageRepository = PackageRepositoryFactory.Default.CreateRepository(location); + + // // gets an IPackage instance from the repository + // var packageVersion = String.IsNullOrEmpty(version) ? null : new SemanticVersion(version); + // var package = packageRepository.FindPackage(packageId, packageVersion); + // if (package == null) + // { + // throw new ArgumentException("The specified package could not be found, id:{0} version:{1}".FormatCurrent(packageId, version.IsEmpty() ? "No version" : version)); + // } + + // return InstallPackage(package, packageRepository, location, applicationFolder); + //} + + public PackageInfo Install(Stream packageStream, string location, string applicationPath) + { + Guard.NotNull(packageStream, nameof(packageStream)); + + IPackage package; + try + { + package = new ZipPackage(packageStream); + + } + catch (Exception ex) + { + throw new SmartException(T("Admin.Packaging.StreamError"), ex); + } + + // instantiates the appropriate package repository + var packageRepository = new NullSourceRepository(); + return InstallPackage(package, packageRepository, location, applicationPath); + } + + /// + /// Tries to install the package + /// + /// The package to install + /// The repository + /// The virtual location of the package file, usually ~/App_Data + /// The virtual app root path, usually ~/ + /// An instance of type + protected PackageInfo InstallPackage(IPackage package, IPackageRepository packageRepository, string location, string applicationPath) + { + bool previousInstalled; + + // 1. See if extension was previous installed and backup its folder if so + try + { + previousInstalled = BackupExtensionFolder(package.ExtensionFolder(), package.ExtensionId()); + } + catch (Exception exception) + { + throw new SmartException(T("Admin.Packaging.BackupError"), exception); + } + + if (previousInstalled) + { + // 2. If extension is installed, need to un-install first + try + { + UninstallExtensionIfNeeded(package); + } + catch (Exception exception) + { + throw new SmartException(T("Admin.Packaging.UninstallError"), exception); + } + } + + var packageInfo = ExecuteInstall(package, packageRepository, location, applicationPath); + + // check if the new package is compatible with current SmartStore version + var descriptor = package.GetExtensionDescriptor(packageInfo.Type); + + if (descriptor != null) + { + packageInfo.ExtensionDescriptor = descriptor; + + if (!PluginManager.IsAssumedCompatible(descriptor.MinAppVersion)) + { + if (previousInstalled) + { + // restore the previous version + RestoreExtensionFolder(package.ExtensionFolder(), package.ExtensionId()); + } + else + { + // just uninstall the new package + Uninstall(package.Id, _virtualPathProvider.MapPath("~\\")); + } + + var msg = T("Admin.Packaging.IsIncompatible", SmartStoreVersion.CurrentFullVersion); + _logger.Error(msg); + throw new SmartException(msg); + } + } + + return packageInfo; + } + + /// + /// Executes a package installation. + /// + /// The package to install. + /// The repository for the package. + /// The source location. + /// The path where to install the package. + /// The package information. + protected PackageInfo ExecuteInstall(IPackage package, IPackageRepository packageRepository, string sourceLocation, string targetPath) + { + var logger = new NugetLogger(_logger); + + var project = new FileBasedProjectSystem(targetPath) { Logger = logger }; + + IPackageRepository referenceRepository; + if (package.IsTheme()) + { + referenceRepository = new ThemeReferenceRepository(project, packageRepository, _themeRegistry); + } + else + { + referenceRepository = new PluginReferenceRepository(project, packageRepository, _pluginFinder); + } + + var projectManager = new ProjectManager( + packageRepository, + new DefaultPackagePathResolver(targetPath), + project, + referenceRepository + ) + { Logger = logger }; + + // add the package to the project + projectManager.AddPackageReference(package, true, false); + + return new PackageInfo + { + Id = package.Id, + Name = package.Title ?? package.Id, + Version = package.Version.ToString(), + Type = package.IsTheme() ? "Theme" : "Plugin", + Path = targetPath + }; + } + + public void Uninstall(string packageId, string applicationFolder) + { + string extensionFullPath = string.Empty; + + if (packageId.StartsWith(PackagingUtils.GetExtensionPrefix("Theme"))) + { + extensionFullPath = _virtualPathProvider.MapPath("~/Themes/" + packageId.Substring(PackagingUtils.GetExtensionPrefix("Theme").Length)); + } + else if (packageId.StartsWith(PackagingUtils.GetExtensionPrefix("Plugin"))) + { + extensionFullPath = _virtualPathProvider.MapPath("~/Plugins/" + packageId.Substring(PackagingUtils.GetExtensionPrefix("Plugin").Length)); + } + + if (string.IsNullOrEmpty(extensionFullPath) || !System.IO.Directory.Exists(extensionFullPath)) + { + throw new SmartException(T("Admin.Packaging.NotFound", packageId)); + } + + // If the package was not installed through nuget we still need to try to uninstall it by removing its directory + if (Directory.Exists(extensionFullPath)) + { + Directory.Delete(extensionFullPath, true); + } + } + + [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Local")] + private bool RestoreExtensionFolder(string extensionFolder, string extensionId) + { + var virtualSource = _virtualPathProvider.Combine("~", extensionFolder, extensionId); + var source = new DirectoryInfo(_virtualPathProvider.MapPath(virtualSource)); + + if (source.Exists) + { + var tempPath = _virtualPathProvider.Combine("~/App_Data", "_Backup", extensionFolder, extensionId); + string localTempPath = null; + for (int i = 0; i < 1000; i++) + { + localTempPath = _virtualPathProvider.MapPath(tempPath) + (i == 0 ? "" : "." + i.ToString()); + if (!System.IO.Directory.Exists(localTempPath)) + { + System.IO.Directory.CreateDirectory(localTempPath); + break; + } + localTempPath = null; + } + + if (localTempPath == null) + { + throw new SmartException(T("Admin.Packaging.TooManyBackups", tempPath)); + } + + var backupFolder = new DirectoryInfo(localTempPath); + _folderUpdater.Restore(backupFolder, source); + _notifier.Information(T("Admin.Packaging.RestoreSuccess", virtualSource)); + + return true; + } + + return false; + } + + private bool BackupExtensionFolder(string extensionFolder, string extensionId) + { + var source = new DirectoryInfo(_virtualPathProvider.MapPath(_virtualPathProvider.Combine("~", extensionFolder, extensionId))); + + if (source.Exists) + { + var tempPath = _virtualPathProvider.Combine("~/App_Data", "_Backup", extensionFolder, extensionId); + string localTempPath = null; + for (int i = 0; i < 1000; i++) + { + localTempPath = _virtualPathProvider.MapPath(tempPath) + (i == 0 ? "" : "." + i.ToString()); + if (!System.IO.Directory.Exists(localTempPath)) + { + System.IO.Directory.CreateDirectory(localTempPath); + break; + } + localTempPath = null; + } + + if (localTempPath == null) + { + throw new SmartException(T("Admin.Packaging.TooManyBackups", tempPath)); + } + + var backupFolder = new DirectoryInfo(localTempPath); + _folderUpdater.Backup(source, backupFolder); + _notifier.Information(T("Admin.Packaging.BackupSuccess", backupFolder.Name)); + + return true; + } + + return false; + } + + private void UninstallExtensionIfNeeded(IPackage package) + { + // Nuget requires to un-install the currently installed packages if the new + // package is the same version or an older version + try + { + Uninstall(package.Id, _virtualPathProvider.MapPath("~\\")); + //_notifier.Information("Successfully un-installed local package {0}".FormatInvariant(package.ExtensionId())); + } + catch { } + } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/PackageManager.cs b/src/Libraries/SmartStore.Core/Packaging/PackageManager.cs index 2d1285007f..0e399621c1 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackageManager.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackageManager.cs @@ -1,98 +1,95 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.IO; -using NuGet; -using Log = SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Core.Themes; +using Log = SmartStore.Core.Logging; namespace SmartStore.Core.Packaging { - public class PackageManager : IPackageManager - { - private readonly IPluginFinder _pluginFinder; - private readonly IThemeRegistry _themeRegistry; - private readonly IPackageBuilder _packageBuilder; - private readonly IPackageInstaller _packageInstaller; - private readonly Log.ILogger _logger; + public class PackageManager : IPackageManager + { + private readonly IPluginFinder _pluginFinder; + private readonly IThemeRegistry _themeRegistry; + private readonly IPackageBuilder _packageBuilder; + private readonly IPackageInstaller _packageInstaller; + private readonly Log.ILogger _logger; - public PackageManager( - IPluginFinder pluginFinder, - IThemeRegistry themeRegistry, - IPackageBuilder packageBuilder, - IPackageInstaller packageInstaller, - Log.ILogger logger) - { - _pluginFinder = pluginFinder; - _themeRegistry = themeRegistry; - _packageBuilder = packageBuilder; - _packageInstaller = packageInstaller; - _logger = logger; - } + public PackageManager( + IPluginFinder pluginFinder, + IThemeRegistry themeRegistry, + IPackageBuilder packageBuilder, + IPackageInstaller packageInstaller, + Log.ILogger logger) + { + _pluginFinder = pluginFinder; + _themeRegistry = themeRegistry; + _packageBuilder = packageBuilder; + _packageInstaller = packageInstaller; + _logger = logger; + } - private PackageInfo DoInstall(Func installer) - { - try - { - return installer(); - } - catch (SmartException) - { - throw; - } - catch (Exception exception) - { - var message = "There was an error installing the requested package. " + - "This can happen if the server does not have write access to the '~/Plugins' or '~/Themes' folder of the web site. " + - "If the site is running in shared hosted environement, adding write access to these folders sometimes needs to be done manually through the Hoster control panel. " + - "Once Themes and Plugins have been installed, it is recommended to remove write access to these folders."; - throw new SmartException(message, exception); - } - } + private PackageInfo DoInstall(Func installer) + { + try + { + return installer(); + } + catch (SmartException) + { + throw; + } + catch (Exception exception) + { + var message = "There was an error installing the requested package. " + + "This can happen if the server does not have write access to the '~/Plugins' or '~/Themes' folder of the web site. " + + "If the site is running in shared hosted environement, adding write access to these folders sometimes needs to be done manually through the Hoster control panel. " + + "Once Themes and Plugins have been installed, it is recommended to remove write access to these folders."; + throw new SmartException(message, exception); + } + } - public PackageInfo Install(Stream packageStream, string location, string applicationPath) - { - return DoInstall(() => _packageInstaller.Install(packageStream, location, applicationPath)); - } + public PackageInfo Install(Stream packageStream, string location, string applicationPath) + { + return DoInstall(() => _packageInstaller.Install(packageStream, location, applicationPath)); + } - public void Uninstall(string packageId, string applicationPath) - { - _packageInstaller.Uninstall(packageId, applicationPath); - } + public void Uninstall(string packageId, string applicationPath) + { + _packageInstaller.Uninstall(packageId, applicationPath); + } - public PackagingResult BuildPluginPackage(string pluginName) - { - var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName(pluginName); - if (pluginDescriptor == null) - { - return null; - } - return new PackagingResult - { - ExtensionType = "Plugin", - PackageName = pluginDescriptor.FolderName, - PackageVersion = pluginDescriptor.Version.ToString(), - PackageStream = _packageBuilder.BuildPackage(pluginDescriptor) - }; - } + public PackagingResult BuildPluginPackage(string pluginName) + { + var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName(pluginName); + if (pluginDescriptor == null) + { + return null; + } + return new PackagingResult + { + ExtensionType = "Plugin", + PackageName = pluginDescriptor.FolderName, + PackageVersion = pluginDescriptor.Version.ToString(), + PackageStream = _packageBuilder.BuildPackage(pluginDescriptor) + }; + } - public PackagingResult BuildThemePackage(string themeName) - { - var themeManifest = _themeRegistry.GetThemeManifest(themeName); - if (themeName == null) - { - return null; - } - return new PackagingResult - { - ExtensionType = "Theme", - PackageName = themeManifest.ThemeName, - PackageVersion = themeManifest.Version, - PackageStream = _packageBuilder.BuildPackage(themeManifest) - }; - } - } + public PackagingResult BuildThemePackage(string themeName) + { + var themeManifest = _themeRegistry.GetThemeManifest(themeName); + if (themeName == null) + { + return null; + } + return new PackagingResult + { + ExtensionType = "Theme", + PackageName = themeManifest.ThemeName, + PackageVersion = themeManifest.Version, + PackageStream = _packageBuilder.BuildPackage(themeManifest) + }; + } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/PackagingResult.cs b/src/Libraries/SmartStore.Core/Packaging/PackagingResult.cs index 69e054e009..312e737439 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackagingResult.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackagingResult.cs @@ -2,12 +2,12 @@ namespace SmartStore.Core.Packaging { - public class PackagingResult - { - public string ExtensionType { get; set; } + public class PackagingResult + { + public string ExtensionType { get; set; } - public string PackageName { get; set; } - public string PackageVersion { get; set; } - public Stream PackageStream { get; set; } - } + public string PackageName { get; set; } + public string PackageVersion { get; set; } + public Stream PackageStream { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs b/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs index 0d50f65851..cbaa6e0cd0 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Hosting; @@ -9,140 +8,140 @@ namespace SmartStore.Core.Packaging { - public static class PackagingUtils - { - public static string GetExtensionPrefix(string extensionType) - { - return string.Format("SmartStore.{0}.", extensionType); - } - - public static string BuildPackageId(string extensionName, string extensionType) - { - return GetExtensionPrefix(extensionType) + extensionName; - } - - internal static bool IsTheme(this IPackage package) - { - return IsTheme(package.Id); - } - - public static bool IsTheme(this PackageInfo info) - { - return IsTheme(info.Id); - } - - internal static string ExtensionFolder(this IPackage package) - { - return ExtensionFolder(package.IsTheme()); - } - - internal static string ExtensionId(this IPackage package) - { - return ExtensionId(package.IsTheme(), package.Id); - } - - public static string ExtensionId(this PackageInfo info) - { - return ExtensionId(info.IsTheme(), info.Id); - } - - public static bool IsTheme(string packageId) - { - return packageId.StartsWith(GetExtensionPrefix("Theme")); - } - - private static string ExtensionFolder(bool isTheme) - { - return isTheme ? "Themes" : "Plugins"; - } - - private static string ExtensionId(bool isTheme, string packageId) - { - return isTheme ? - packageId.Substring(GetExtensionPrefix("Theme").Length) : - packageId.Substring(GetExtensionPrefix("Plugin").Length); - } - - internal static ExtensionDescriptor ConvertToExtensionDescriptor(this PluginDescriptor pluginDescriptor) - { - // TODO: (pkg) Add Icons to extension manifests - var descriptor = new ExtensionDescriptor - { - ExtensionType = "Plugin", - Location = "~/Plugins", - Path = pluginDescriptor.PhysicalPath, - Id = pluginDescriptor.FolderName, - Author = pluginDescriptor.Author, - MinAppVersion = pluginDescriptor.MinAppVersion, - Version = pluginDescriptor.Version, - Name = pluginDescriptor.FriendlyName, - Description = pluginDescriptor.Description, - WebSite = pluginDescriptor.Url, // TODO: (pkg) Add author url to plugin manifests, - Tags = string.Empty // TODO: (pkg) Add tags to plugin manifests, - }; - - return descriptor; - } - - internal static ExtensionDescriptor ConvertToExtensionDescriptor(this ThemeManifest themeManifest) - { - var descriptor = new ExtensionDescriptor - { - ExtensionType = "Theme", - Location = "~/Themes", - Path = themeManifest.Path, - Id = themeManifest.ThemeName, - Author = themeManifest.Author.HasValue() ? themeManifest.Author : "[Unknown]", - MinAppVersion = SmartStoreVersion.Version, // TODO: (pkg) Add SupportedVersion to theme manifests - Version = new Version(themeManifest.Version), - Name = themeManifest.ThemeTitle, - Description = string.Empty, // TODO: (pkg) Add description to theme manifests - WebSite = themeManifest.Url, - Tags = string.Empty // TODO: (pkg) Add tags to theme manifests, - }; - - return descriptor; - } - - internal static ExtensionDescriptor GetExtensionDescriptor(this IPackage package, string extensionType) - { - bool isTheme = extensionType.IsCaseInsensitiveEqual("Theme"); - - IPackageFile packageFile = package.GetFiles().FirstOrDefault(file => - { - var fileName = Path.GetFileName(file.Path); - return fileName != null && fileName.Equals(isTheme ? "theme.config" : "Description.txt", StringComparison.OrdinalIgnoreCase); - }); - - ExtensionDescriptor descriptor = null; - - if (packageFile != null) - { - var filePath = packageFile.EffectivePath; - if (filePath.HasValue()) - { - filePath = Path.Combine(HostingEnvironment.MapPath("~/"), filePath); - if (isTheme) - { - var themeManifest = ThemeManifest.Create(Path.GetDirectoryName(filePath)); - if (themeManifest != null) - { - descriptor = themeManifest.ConvertToExtensionDescriptor(); - } - } - else // is a Plugin - { - var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(filePath); - if (pluginDescriptor != null) - { - descriptor = pluginDescriptor.ConvertToExtensionDescriptor(); - } - } - } - } - - return descriptor; - } - } + public static class PackagingUtils + { + public static string GetExtensionPrefix(string extensionType) + { + return string.Format("SmartStore.{0}.", extensionType); + } + + public static string BuildPackageId(string extensionName, string extensionType) + { + return GetExtensionPrefix(extensionType) + extensionName; + } + + internal static bool IsTheme(this IPackage package) + { + return IsTheme(package.Id); + } + + public static bool IsTheme(this PackageInfo info) + { + return IsTheme(info.Id); + } + + internal static string ExtensionFolder(this IPackage package) + { + return ExtensionFolder(package.IsTheme()); + } + + internal static string ExtensionId(this IPackage package) + { + return ExtensionId(package.IsTheme(), package.Id); + } + + public static string ExtensionId(this PackageInfo info) + { + return ExtensionId(info.IsTheme(), info.Id); + } + + public static bool IsTheme(string packageId) + { + return packageId.StartsWith(GetExtensionPrefix("Theme")); + } + + private static string ExtensionFolder(bool isTheme) + { + return isTheme ? "Themes" : "Plugins"; + } + + private static string ExtensionId(bool isTheme, string packageId) + { + return isTheme ? + packageId.Substring(GetExtensionPrefix("Theme").Length) : + packageId.Substring(GetExtensionPrefix("Plugin").Length); + } + + internal static ExtensionDescriptor ConvertToExtensionDescriptor(this PluginDescriptor pluginDescriptor) + { + // TODO: (pkg) Add Icons to extension manifests + var descriptor = new ExtensionDescriptor + { + ExtensionType = "Plugin", + Location = "~/Plugins", + Path = pluginDescriptor.PhysicalPath, + Id = pluginDescriptor.FolderName, + Author = pluginDescriptor.Author, + MinAppVersion = pluginDescriptor.MinAppVersion, + Version = pluginDescriptor.Version, + Name = pluginDescriptor.FriendlyName, + Description = pluginDescriptor.Description, + WebSite = pluginDescriptor.Url, // TODO: (pkg) Add author url to plugin manifests, + Tags = string.Empty // TODO: (pkg) Add tags to plugin manifests, + }; + + return descriptor; + } + + internal static ExtensionDescriptor ConvertToExtensionDescriptor(this ThemeManifest themeManifest) + { + var descriptor = new ExtensionDescriptor + { + ExtensionType = "Theme", + Location = "~/Themes", + Path = themeManifest.Path, + Id = themeManifest.ThemeName, + Author = themeManifest.Author.HasValue() ? themeManifest.Author : "[Unknown]", + MinAppVersion = SmartStoreVersion.Version, // TODO: (pkg) Add SupportedVersion to theme manifests + Version = new Version(themeManifest.Version), + Name = themeManifest.ThemeTitle, + Description = string.Empty, // TODO: (pkg) Add description to theme manifests + WebSite = themeManifest.Url, + Tags = string.Empty // TODO: (pkg) Add tags to theme manifests, + }; + + return descriptor; + } + + internal static ExtensionDescriptor GetExtensionDescriptor(this IPackage package, string extensionType) + { + bool isTheme = extensionType.IsCaseInsensitiveEqual("Theme"); + + IPackageFile packageFile = package.GetFiles().FirstOrDefault(file => + { + var fileName = Path.GetFileName(file.Path); + return fileName != null && fileName.Equals(isTheme ? "theme.config" : "Description.txt", StringComparison.OrdinalIgnoreCase); + }); + + ExtensionDescriptor descriptor = null; + + if (packageFile != null) + { + var filePath = packageFile.EffectivePath; + if (filePath.HasValue()) + { + filePath = Path.Combine(HostingEnvironment.MapPath("~/"), filePath); + if (isTheme) + { + var themeManifest = ThemeManifest.Create(Path.GetDirectoryName(filePath)); + if (themeManifest != null) + { + descriptor = themeManifest.ConvertToExtensionDescriptor(); + } + } + else // is a Plugin + { + var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(filePath); + if (pluginDescriptor != null) + { + descriptor = pluginDescriptor.ConvertToExtensionDescriptor(); + } + } + } + } + + return descriptor; + } + } } diff --git a/src/Libraries/SmartStore.Core/Packaging/Readme.txt b/src/Libraries/SmartStore.Core/Packaging/Readme.txt index 58566c0667..2052796b87 100644 --- a/src/Libraries/SmartStore.Core/Packaging/Readme.txt +++ b/src/Libraries/SmartStore.Core/Packaging/Readme.txt @@ -1,4 +1,4 @@  -Inspiration for the SmartStore.NET packaging infrastructure comes from Orchard (http://orchardproject.net/), +Inspiration for the Smartstore packaging infrastructure comes from Orchard (http://orchardproject.net/), a fantastic ASP.NET MVC based open source CMS from the Outercurve Foundation. Most of the types here were copied over and refactored accordingly. \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs b/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs index 8d4fb58dab..0e5cae0836 100644 --- a/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs +++ b/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs @@ -14,221 +14,222 @@ namespace SmartStore.Core.Packaging { public sealed class AppUpdater : DisposableObject - { - public const string UpdatePackagePath = "~/App_Data/Update"; - - private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); - private TraceLogger _logger; - - #region Package update - - [SuppressMessage("ReSharper", "RedundantAssignment")] - public bool InstallablePackageExists() - { - string packagePath = null; - var package = FindPackage(false, out packagePath); - - if (package == null) - return false; - - if (!ValidatePackage(package)) - return false; - - if (!CheckEnvironment()) - return false; - - return true; - } - - internal bool TryUpdateFromPackage() - { - // NEVER EVER (!!!) make an attempt to auto-update in a dev environment!!!!!!! - if (CommonHelper.IsDevEnvironment) - return false; - - using (_rwLock.GetUpgradeableReadLock()) - { - try - { - string packagePath = null; - var package = FindPackage(true, out packagePath); - - if (package == null) - return false; - - if (!ValidatePackage(package)) - return false; - - if (!CheckEnvironment()) - return false; - - using (_rwLock.GetWriteLock()) - { - Backup(); - - var info = ExecuteUpdate(package); - - if (info != null) - { - var newPath = packagePath + ".applied"; - if (File.Exists(newPath)) - { - File.Delete(packagePath); - } - else - { - File.Move(packagePath, newPath); - } - } - - return info != null; - } - } - catch (Exception ex) - { - _logger.Error(ex, "An error occured while updating the application"); - return false; - } - } - } - - private TraceLogger CreateLogger(IPackage package) - { - var logFile = Path.Combine(CommonHelper.MapPath(UpdatePackagePath, false), "Updater.{0}.log".FormatInvariant(package.Version.ToString())); - return new TraceLogger(logFile); - } - - private IPackage FindPackage(bool createLogger, out string path) - { - path = null; - var dir = CommonHelper.MapPath(UpdatePackagePath, false); - - if (!Directory.Exists(dir)) - return null; - - var files = Directory.GetFiles(dir, "SmartStore.*.nupkg", SearchOption.TopDirectoryOnly); - - // TODO: allow more than one package in folder and return newest - if (files.Length == 0 || files.Length > 1) - return null; - - try - { - path = files[0]; - IPackage package = new ZipPackage(files[0]); - if (createLogger) - { - _logger = CreateLogger(package); - _logger.Info("Found update package '{0}'".FormatInvariant(package.GetFullName())); - } - return package; - } - catch { } - - return null; - } - - private bool ValidatePackage(IPackage package) - { - if (package.Id != "SmartStore") - return false; - - var currentVersion = new SemanticVersion(SmartStoreVersion.Version); - return package.Version > currentVersion; - } - - private bool CheckEnvironment() - { - // TODO: Check it :-) - return true; - } - - private void Backup() - { - var source = new DirectoryInfo(CommonHelper.MapPath("~/")); - - var tempPath = CommonHelper.MapPath("~/App_Data/_Backup/App/SmartStore"); - string localTempPath = null; - for (int i = 0; i < 50; i++) - { - localTempPath = tempPath + (i == 0 ? "" : "." + i.ToString()); - if (!Directory.Exists(localTempPath)) - { - Directory.CreateDirectory(localTempPath); - break; - } - localTempPath = null; - } - - if (localTempPath == null) - { - var exception = new SmartException("Too many backups in '{0}'.".FormatInvariant(tempPath)); - _logger.Error(exception); - throw exception; - } - - var backupFolder = new DirectoryInfo(localTempPath); - var folderUpdater = new FolderUpdater(_logger); - folderUpdater.Backup(source, backupFolder, "App_Data", "Media"); - - _logger.Info("Backup successfully created in folder '{0}'.".FormatInvariant(localTempPath)); - } - - private PackageInfo ExecuteUpdate(IPackage package) - { - var appPath = CommonHelper.MapPath("~/"); - - var logger = new NugetLogger(_logger); - - var project = new FileBasedProjectSystem(appPath) { Logger = logger }; - - var nullRepository = new NullSourceRepository(); - - var projectManager = new ProjectManager( - nullRepository, - new DefaultPackagePathResolver(appPath), - project, - nullRepository - ) { Logger = logger }; - - // Perform the update - projectManager.AddPackageReference(package, true, false); - - var info = new PackageInfo - { - Id = package.Id, - Name = package.Title ?? package.Id, - Version = package.Version.ToString(), - Type = "App", - Path = appPath - }; - - _logger.Info("Update '{0}' successfully executed.".FormatInvariant(info.Name)); - - return info; - } - - #endregion - - #region Migrations - - internal void ExecuteMigrations() - { - if (!DataSettings.DatabaseIsInstalled()) - return; - - var currentVersion = SmartStoreVersion.Version; - var prevVersion = DataSettings.Current.AppVersion ?? new Version(1, 0); - - if (prevVersion < new Version(3, 0, 0)) - { - throw new ApplicationException($"Smartstore {currentVersion} does not support automatic upgrade from version {prevVersion}. Please upgrade to version 3.x first."); - } - - TryMigrateDefaultTenant(); - - if (prevVersion >= currentVersion) - return; + { + public const string UpdatePackagePath = "~/App_Data/Update"; + + private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + private TraceLogger _logger; + + #region Package update + + [SuppressMessage("ReSharper", "RedundantAssignment")] + public bool InstallablePackageExists() + { + string packagePath = null; + var package = FindPackage(false, out packagePath); + + if (package == null) + return false; + + if (!ValidatePackage(package)) + return false; + + if (!CheckEnvironment()) + return false; + + return true; + } + + internal bool TryUpdateFromPackage() + { + // NEVER EVER (!!!) make an attempt to auto-update in a dev environment!!!!!!! + if (CommonHelper.IsDevEnvironment) + return false; + + using (_rwLock.GetUpgradeableReadLock()) + { + try + { + string packagePath = null; + var package = FindPackage(true, out packagePath); + + if (package == null) + return false; + + if (!ValidatePackage(package)) + return false; + + if (!CheckEnvironment()) + return false; + + using (_rwLock.GetWriteLock()) + { + Backup(); + + var info = ExecuteUpdate(package); + + if (info != null) + { + var newPath = packagePath + ".applied"; + if (File.Exists(newPath)) + { + File.Delete(packagePath); + } + else + { + File.Move(packagePath, newPath); + } + } + + return info != null; + } + } + catch (Exception ex) + { + _logger.Error(ex, "An error occured while updating the application"); + return false; + } + } + } + + private TraceLogger CreateLogger(IPackage package) + { + var logFile = Path.Combine(CommonHelper.MapPath(UpdatePackagePath, false), "Updater.{0}.log".FormatInvariant(package.Version.ToString())); + return new TraceLogger(logFile); + } + + private IPackage FindPackage(bool createLogger, out string path) + { + path = null; + var dir = CommonHelper.MapPath(UpdatePackagePath, false); + + if (!Directory.Exists(dir)) + return null; + + var files = Directory.GetFiles(dir, "SmartStore.*.nupkg", SearchOption.TopDirectoryOnly); + + // TODO: allow more than one package in folder and return newest + if (files.Length == 0 || files.Length > 1) + return null; + + try + { + path = files[0]; + IPackage package = new ZipPackage(files[0]); + if (createLogger) + { + _logger = CreateLogger(package); + _logger.Info("Found update package '{0}'".FormatInvariant(package.GetFullName())); + } + return package; + } + catch { } + + return null; + } + + private bool ValidatePackage(IPackage package) + { + if (package.Id != "SmartStore") + return false; + + var currentVersion = new SemanticVersion(SmartStoreVersion.Version); + return package.Version > currentVersion; + } + + private bool CheckEnvironment() + { + // TODO: Check it :-) + return true; + } + + private void Backup() + { + var source = new DirectoryInfo(CommonHelper.MapPath("~/")); + + var tempPath = CommonHelper.MapPath("~/App_Data/_Backup/App/SmartStore"); + string localTempPath = null; + for (int i = 0; i < 50; i++) + { + localTempPath = tempPath + (i == 0 ? "" : "." + i.ToString()); + if (!Directory.Exists(localTempPath)) + { + Directory.CreateDirectory(localTempPath); + break; + } + localTempPath = null; + } + + if (localTempPath == null) + { + var exception = new SmartException("Too many backups in '{0}'.".FormatInvariant(tempPath)); + _logger.Error(exception); + throw exception; + } + + var backupFolder = new DirectoryInfo(localTempPath); + var folderUpdater = new FolderUpdater(_logger); + folderUpdater.Backup(source, backupFolder, "App_Data", "Media"); + + _logger.Info("Backup successfully created in folder '{0}'.".FormatInvariant(localTempPath)); + } + + private PackageInfo ExecuteUpdate(IPackage package) + { + var appPath = CommonHelper.MapPath("~/"); + + var logger = new NugetLogger(_logger); + + var project = new FileBasedProjectSystem(appPath) { Logger = logger }; + + var nullRepository = new NullSourceRepository(); + + var projectManager = new ProjectManager( + nullRepository, + new DefaultPackagePathResolver(appPath), + project, + nullRepository + ) + { Logger = logger }; + + // Perform the update + projectManager.AddPackageReference(package, true, false); + + var info = new PackageInfo + { + Id = package.Id, + Name = package.Title ?? package.Id, + Version = package.Version.ToString(), + Type = "App", + Path = appPath + }; + + _logger.Info("Update '{0}' successfully executed.".FormatInvariant(info.Name)); + + return info; + } + + #endregion + + #region Migrations + + internal void ExecuteMigrations() + { + if (!DataSettings.DatabaseIsInstalled()) + return; + + var currentVersion = SmartStoreVersion.Version; + var prevVersion = DataSettings.Current.AppVersion ?? new Version(1, 0); + + if (prevVersion < new Version(3, 0, 0)) + { + throw new ApplicationException($"Smartstore {currentVersion} does not support automatic upgrade from version {prevVersion}. Please upgrade to version 3.x first."); + } + + TryMigrateDefaultTenant(); + + if (prevVersion >= currentVersion) + return; if (prevVersion < new Version(2, 1)) { @@ -244,144 +245,144 @@ internal void ExecuteMigrations() } DataSettings.Current.AppVersion = currentVersion; - DataSettings.Current.Save(); - } - - private bool TryMigrateDefaultTenant() - { - // We introduced basic multi-tenancy in V3 [...] - - if (!IsPreTenancyVersion()) - { - return false; - } - - var tenantDir = Directory.CreateDirectory(CommonHelper.MapPath("~/App_Data/Tenants/Default")); - var tenantTempDir = tenantDir.CreateSubdirectory("_temp"); - - var appDataDir = CommonHelper.MapPath("~/App_Data"); - - // Move Settings.txt - File.Move(Path.Combine(appDataDir, "Settings.txt"), Path.Combine(tenantDir.FullName, "Settings.txt")); - - // Move InstalledPlugins.txt - File.Move(Path.Combine(appDataDir, "InstalledPlugins.txt"), Path.Combine(tenantDir.FullName, "InstalledPlugins.txt")); - - // Move SmartStore.db.sdf - var path = Path.Combine(appDataDir, "SmartStore.db.sdf"); - if (File.Exists(path)) - { - File.Move(path, Path.Combine(tenantDir.FullName, "SmartStore.db.sdf")); - } - - Func moveTenantFolder = (sourceFolder, targetFolder) => - { - var sourcePath = Path.Combine(appDataDir, sourceFolder); - - if (Directory.Exists(sourcePath)) - { - Directory.Move(sourcePath, Path.Combine(tenantDir.FullName, targetFolder ?? sourceFolder)); - return true; - } - - return false; - }; - - // Move tenant specific Folders - moveTenantFolder("ImportProfiles", null); - moveTenantFolder("ExportProfiles", null); - moveTenantFolder("Indexing", null); - moveTenantFolder("Lucene", null); - moveTenantFolder("_temp\\BizBackups", null); - moveTenantFolder("_temp\\ShopConnector", null); - - // Move all media files and folders to new subfolder "Default" - var mediaInfos = (new DirectoryInfo(CommonHelper.MapPath("~/Media"))).EnumerateFileSystemInfos().Where(x => !x.Name.IsCaseInsensitiveEqual("Default")); - var mediaFiles = mediaInfos.OfType(); - var mediaDirs = mediaInfos.OfType().ToArray(); - var tenantMediaDir = new DirectoryInfo(CommonHelper.MapPath("~/Media/Default")); - if (!tenantMediaDir.Exists) - { - tenantMediaDir.Create(); - } - - foreach (var dir in mediaDirs) - { - dir.MoveTo(Path.Combine(tenantMediaDir.FullName, dir.Name)); - } - - foreach (var file in mediaFiles) - { - file.MoveTo(Path.Combine(tenantMediaDir.FullName, file.Name)); - } - - return true; - } - - private bool IsPreTenancyVersion() - { - var appDataDir = CommonHelper.MapPath("~/App_Data"); - - return File.Exists(Path.Combine(appDataDir, "Settings.txt")) - && File.Exists(Path.Combine(appDataDir, "InstalledPlugins.txt")) - && !Directory.Exists(Path.Combine(appDataDir, "Tenants\\Default")); - } - - private void MigrateInitial() - { - var installedPlugins = PluginFileParser.ParseInstalledPluginsFile(); - if (installedPlugins.Count == 0) - return; - - var renamedPlugins = new List(); - - var pluginRenameMap = new Dictionary - { - { "CurrencyExchange.ECB", null /* null means: remove it */ }, - { "CurrencyExchange.MoneyConverter", null }, - { "ExternalAuth.OpenId", null }, - { "Tax.Free", null }, - { "Api.WebApi", "SmartStore.WebApi" }, - { "DiscountRequirement.MustBeAssignedToCustomerRole", "SmartStore.DiscountRules" }, - { "DiscountRequirement.HadSpentAmount", "SmartStore.DiscountRules" }, - { "DiscountRequirement.HasAllProducts", "SmartStore.DiscountRules" }, - { "DiscountRequirement.HasOneProduct", "SmartStore.DiscountRules" }, - { "DiscountRequirement.Store", "SmartStore.DiscountRules" }, - { "DiscountRequirement.BillingCountryIs", "SmartStore.DiscountRules" }, - { "DiscountRequirement.ShippingCountryIs", "SmartStore.DiscountRules" }, - { "DiscountRequirement.HasPaymentMethod", "SmartStore.DiscountRules.HasPaymentMethod" }, - { "DiscountRequirement.HasShippingOption", "SmartStore.DiscountRules.HasShippingOption" }, - { "DiscountRequirement.PurchasedAllProducts", "SmartStore.DiscountRules.PurchasedProducts" }, - { "DiscountRequirement.PurchasedOneProduct", "SmartStore.DiscountRules.PurchasedProducts" }, - { "PromotionFeed.Froogle", "SmartStore.GoogleMerchantCenter" }, - { "PromotionFeed.Billiger", "SmartStore.Billiger" }, - { "PromotionFeed.ElmarShopinfo", "SmartStore.ElmarShopinfo" }, - { "PromotionFeed.Guenstiger", "SmartStore.Guenstiger" }, - { "Payments.AccardaKar", "SmartStore.AccardaKar" }, - { "Payments.AmazonPay", "SmartStore.AmazonPay" }, - { "Developer.DevTools", "SmartStore.DevTools" }, - { "ExternalAuth.Facebook", "SmartStore.FacebookAuth" }, - { "ExternalAuth.Twitter", "SmartStore.TwitterAuth" }, - { "SMS.Clickatell", "SmartStore.Clickatell" }, - { "Widgets.GoogleAnalytics", "SmartStore.GoogleAnalytics" }, - { "Misc.DemoShop", "SmartStore.DemoShopControlling" }, - { "Admin.OrderNumberFormatter", "SmartStore.OrderNumberFormatter" }, - { "Admin.Debitoor", "SmartStore.Debitoor" }, + DataSettings.Current.Save(); + } + + private bool TryMigrateDefaultTenant() + { + // We introduced basic multi-tenancy in V3 [...] + + if (!IsPreTenancyVersion()) + { + return false; + } + + var tenantDir = Directory.CreateDirectory(CommonHelper.MapPath("~/App_Data/Tenants/Default")); + var tenantTempDir = tenantDir.CreateSubdirectory("_temp"); + + var appDataDir = CommonHelper.MapPath("~/App_Data"); + + // Move Settings.txt + File.Move(Path.Combine(appDataDir, "Settings.txt"), Path.Combine(tenantDir.FullName, "Settings.txt")); + + // Move InstalledPlugins.txt + File.Move(Path.Combine(appDataDir, "InstalledPlugins.txt"), Path.Combine(tenantDir.FullName, "InstalledPlugins.txt")); + + // Move SmartStore.db.sdf + var path = Path.Combine(appDataDir, "SmartStore.db.sdf"); + if (File.Exists(path)) + { + File.Move(path, Path.Combine(tenantDir.FullName, "SmartStore.db.sdf")); + } + + Func moveTenantFolder = (sourceFolder, targetFolder) => + { + var sourcePath = Path.Combine(appDataDir, sourceFolder); + + if (Directory.Exists(sourcePath)) + { + Directory.Move(sourcePath, Path.Combine(tenantDir.FullName, targetFolder ?? sourceFolder)); + return true; + } + + return false; + }; + + // Move tenant specific Folders + moveTenantFolder("ImportProfiles", null); + moveTenantFolder("ExportProfiles", null); + moveTenantFolder("Indexing", null); + moveTenantFolder("Lucene", null); + moveTenantFolder("_temp\\BizBackups", null); + moveTenantFolder("_temp\\ShopConnector", null); + + // Move all media files and folders to new subfolder "Default" + var mediaInfos = (new DirectoryInfo(CommonHelper.MapPath("~/Media"))).EnumerateFileSystemInfos().Where(x => !x.Name.IsCaseInsensitiveEqual("Default")); + var mediaFiles = mediaInfos.OfType(); + var mediaDirs = mediaInfos.OfType().ToArray(); + var tenantMediaDir = new DirectoryInfo(CommonHelper.MapPath("~/Media/Default")); + if (!tenantMediaDir.Exists) + { + tenantMediaDir.Create(); + } + + foreach (var dir in mediaDirs) + { + dir.MoveTo(Path.Combine(tenantMediaDir.FullName, dir.Name)); + } + + foreach (var file in mediaFiles) + { + file.MoveTo(Path.Combine(tenantMediaDir.FullName, file.Name)); + } + + return true; + } + + private bool IsPreTenancyVersion() + { + var appDataDir = CommonHelper.MapPath("~/App_Data"); + + return File.Exists(Path.Combine(appDataDir, "Settings.txt")) + && File.Exists(Path.Combine(appDataDir, "InstalledPlugins.txt")) + && !Directory.Exists(Path.Combine(appDataDir, "Tenants\\Default")); + } + + private void MigrateInitial() + { + var installedPlugins = PluginFileParser.ParseInstalledPluginsFile(); + if (installedPlugins.Count == 0) + return; + + var renamedPlugins = new List(); + + var pluginRenameMap = new Dictionary + { + { "CurrencyExchange.ECB", null /* null means: remove it */ }, + { "CurrencyExchange.MoneyConverter", null }, + { "ExternalAuth.OpenId", null }, + { "Tax.Free", null }, + { "Api.WebApi", "SmartStore.WebApi" }, + { "DiscountRequirement.MustBeAssignedToCustomerRole", "SmartStore.DiscountRules" }, + { "DiscountRequirement.HadSpentAmount", "SmartStore.DiscountRules" }, + { "DiscountRequirement.HasAllProducts", "SmartStore.DiscountRules" }, + { "DiscountRequirement.HasOneProduct", "SmartStore.DiscountRules" }, + { "DiscountRequirement.Store", "SmartStore.DiscountRules" }, + { "DiscountRequirement.BillingCountryIs", "SmartStore.DiscountRules" }, + { "DiscountRequirement.ShippingCountryIs", "SmartStore.DiscountRules" }, + { "DiscountRequirement.HasPaymentMethod", "SmartStore.DiscountRules.HasPaymentMethod" }, + { "DiscountRequirement.HasShippingOption", "SmartStore.DiscountRules.HasShippingOption" }, + { "DiscountRequirement.PurchasedAllProducts", "SmartStore.DiscountRules.PurchasedProducts" }, + { "DiscountRequirement.PurchasedOneProduct", "SmartStore.DiscountRules.PurchasedProducts" }, + { "PromotionFeed.Froogle", "SmartStore.GoogleMerchantCenter" }, + { "PromotionFeed.Billiger", "SmartStore.Billiger" }, + { "PromotionFeed.ElmarShopinfo", "SmartStore.ElmarShopinfo" }, + { "PromotionFeed.Guenstiger", "SmartStore.Guenstiger" }, + { "Payments.AccardaKar", "SmartStore.AccardaKar" }, + { "Payments.AmazonPay", "SmartStore.AmazonPay" }, + { "Developer.DevTools", "SmartStore.DevTools" }, + { "ExternalAuth.Facebook", "SmartStore.FacebookAuth" }, + { "ExternalAuth.Twitter", "SmartStore.TwitterAuth" }, + { "SMS.Clickatell", "SmartStore.Clickatell" }, + { "Widgets.GoogleAnalytics", "SmartStore.GoogleAnalytics" }, + { "Misc.DemoShop", "SmartStore.DemoShopControlling" }, + { "Admin.OrderNumberFormatter", "SmartStore.OrderNumberFormatter" }, + { "Admin.Debitoor", "SmartStore.Debitoor" }, { "Widgets.ETracker", "SmartStore.ETracker" }, { "Payments.PayPalDirect", "SmartStore.PayPal" }, { "Payments.PayPalStandard", "SmartStore.PayPal" }, { "Payments.PayPalExpress", "SmartStore.PayPal" }, - { "Developer.Glimpse", "SmartStore.Glimpse" }, - { "Import.Biz", "SmartStore.BizImporter" }, - { "Payments.Sofortueberweisung", "SmartStore.Sofortueberweisung" }, - { "Payments.PostFinanceECommerce", "SmartStore.PostFinanceECommerce" }, - { "Misc.MailChimp", "SmartStore.MailChimp" }, - { "Mobile.SMS.Verizon", "SmartStore.Verizon" }, - { "Widgets.LivePersonChat", "SmartStore.LivePersonChat" }, - { "Payments.CashOnDelivery", "SmartStore.OfflinePayment" }, - { "Payments.Invoice", "SmartStore.OfflinePayment" }, - { "Payments.PayInStore", "SmartStore.OfflinePayment" }, - { "Payments.Prepayment", "SmartStore.OfflinePayment" }, + { "Developer.Glimpse", "SmartStore.Glimpse" }, + { "Import.Biz", "SmartStore.BizImporter" }, + { "Payments.Sofortueberweisung", "SmartStore.Sofortueberweisung" }, + { "Payments.PostFinanceECommerce", "SmartStore.PostFinanceECommerce" }, + { "Misc.MailChimp", "SmartStore.MailChimp" }, + { "Mobile.SMS.Verizon", "SmartStore.Verizon" }, + { "Widgets.LivePersonChat", "SmartStore.LivePersonChat" }, + { "Payments.CashOnDelivery", "SmartStore.OfflinePayment" }, + { "Payments.Invoice", "SmartStore.OfflinePayment" }, + { "Payments.PayInStore", "SmartStore.OfflinePayment" }, + { "Payments.Prepayment", "SmartStore.OfflinePayment" }, { "Payments.IPaymentCreditCard", "SmartStore.IPayment" }, { "Payments.IPaymentDirectDebit", "SmartStore.IPayment" }, { "Payments.AuthorizeNet", "SmartStore.AuthorizeNet" }, @@ -389,43 +390,43 @@ private void MigrateInitial() { "Shipping.CanadaPost", "SmartStore.CanadaPost" }, { "Shipping.Fedex", "SmartStore.Fedex" }, { "Shipping.UPS", "SmartStore.UPS" }, - { "Payments.Manual", "SmartStore.OfflinePayment" }, + { "Payments.Manual", "SmartStore.OfflinePayment" }, { "Shipping.USPS", "SmartStore.USPS" }, { "Widgets.TrustedShopsSeal", "SmartStore.TrustedShops" }, { "Widgets.TrustedShopsCustomerReviews", "SmartStore.TrustedShops" }, { "Widgets.TrustedShopsCustomerProtection", "SmartStore.TrustedShops" }, { "Shipping.ByWeight", "SmartStore.ShippingByWeight" }, - { "Payments.DirectDebit", "SmartStore.OfflinePayment" }, - { "Tax.FixedRate", "SmartStore.Tax" }, - { "Tax.CountryStateZip", "SmartStore.Tax" }, + { "Payments.DirectDebit", "SmartStore.OfflinePayment" }, + { "Tax.FixedRate", "SmartStore.Tax" }, + { "Tax.CountryStateZip", "SmartStore.Tax" }, { "Shipping.ByTotal", "SmartStore.Shipping" }, { "Shipping.FixedRate", "SmartStore.Shipping" } - }; - - foreach (var name in installedPlugins) - { - if (pluginRenameMap.ContainsKey(name)) - { - string newName = pluginRenameMap[name]; - if (newName != null && !renamedPlugins.Contains(newName)) - { - renamedPlugins.Add(newName); - } - } - else - { - renamedPlugins.Add(name); - } - } - - PluginFileParser.SaveInstalledPluginsFile(renamedPlugins); - } + }; + + foreach (var name in installedPlugins) + { + if (pluginRenameMap.ContainsKey(name)) + { + string newName = pluginRenameMap[name]; + if (newName != null && !renamedPlugins.Contains(newName)) + { + renamedPlugins.Add(newName); + } + } + else + { + renamedPlugins.Add(name); + } + } + + PluginFileParser.SaveInstalledPluginsFile(renamedPlugins); + } private void DeleteSearchIndex() { - var tenantPath = CommonHelper.MapPath(DataSettings.Current.TenantPath); + var tenantPath = CommonHelper.MapPath(DataSettings.Current.TenantPath); - try + try { var indexingDir = new DirectoryInfo(Path.Combine(tenantPath, "Indexing")); if (indexingDir.Exists) @@ -446,19 +447,19 @@ private void DeleteSearchIndex() catch { } } - #endregion - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - if (_logger != null) - { - _logger.Dispose(); - _logger = null; - } - } - } - } + #endregion + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + if (_logger != null) + { + _logger.Dispose(); + _logger = null; + } + } + } + } } diff --git a/src/Libraries/SmartStore.Core/PagedList.cs b/src/Libraries/SmartStore.Core/PagedList.cs index 731374737a..96539bf667 100644 --- a/src/Libraries/SmartStore.Core/PagedList.cs +++ b/src/Libraries/SmartStore.Core/PagedList.cs @@ -62,24 +62,18 @@ public int TotalCount public int PageNumber { - get - { - return this.PageIndex + 1; - } - set - { - this.PageIndex = value - 1; - } + get => this.PageIndex + 1; + set => this.PageIndex = value - 1; } public int TotalPages { - get + get { - if (this.PageSize == 0) - return 0; + if (this.PageSize == 0) + return 0; - var total = this.TotalCount / this.PageSize; + var total = this.TotalCount / this.PageSize; if (this.TotalCount % this.PageSize > 0) total++; @@ -88,53 +82,17 @@ public int TotalPages } } - public bool HasPreviousPage - { - get - { - return this.PageIndex > 0; - } - } + public bool HasPreviousPage => this.PageIndex > 0; - public bool HasNextPage - { - get - { - return (this.PageIndex < (this.TotalPages - 1)); - } - } + public bool HasNextPage => (this.PageIndex < (this.TotalPages - 1)); - public int FirstItemIndex - { - get - { - return (this.PageIndex * this.PageSize) + 1; - } - } + public int FirstItemIndex => (this.PageIndex * this.PageSize) + 1; - public int LastItemIndex - { - get - { - return Math.Min(this.TotalCount, ((this.PageIndex * this.PageSize) + this.PageSize)); - } - } + public int LastItemIndex => Math.Min(this.TotalCount, ((this.PageIndex * this.PageSize) + this.PageSize)); - public bool IsFirstPage - { - get - { - return (this.PageIndex <= 0); - } - } + public bool IsFirstPage => (this.PageIndex <= 0); - public bool IsLastPage - { - get - { - return (this.PageIndex >= (this.TotalPages - 1)); - } - } + public bool IsLastPage => (this.PageIndex >= (this.TotalPages - 1)); public virtual IEnumerator GetEnumerator() { @@ -145,19 +103,19 @@ public virtual IEnumerator GetEnumerator() public class PagedList : PagedListBase { public PagedList(int pageIndex, int pageSize, int totalItemsCount) - : base(pageIndex, pageSize, totalItemsCount) + : base(pageIndex, pageSize, totalItemsCount) { } - public static PagedList Create(IEnumerable source, int pageIndex, int pageSize) - { - return new PagedList(source, pageIndex, pageSize); - } + public static PagedList Create(IEnumerable source, int pageIndex, int pageSize) + { + return new PagedList(source, pageIndex, pageSize); + } - public static PagedList Create(IEnumerable source, int pageIndex, int pageSize, int totalCount) - { - return new PagedList(source, pageIndex, pageSize, totalCount); - } - } + public static PagedList Create(IEnumerable source, int pageIndex, int pageSize, int totalCount) + { + return new PagedList(source, pageIndex, pageSize, totalCount); + } + } } diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs index 299e219785..3796a1350a 100644 --- a/src/Libraries/SmartStore.Core/PagedList`T.cs +++ b/src/Libraries/SmartStore.Core/PagedList`T.cs @@ -1,194 +1,189 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; +using System.Collections.ObjectModel; using System.Data.Entity; using System.Data.Entity.Infrastructure; -using System.Collections; -using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; namespace SmartStore.Core { public class PagedList : IPagedList { - private bool _isEfQuery; - private bool _queryIsPagedAlready; - private int? _totalCount; + private bool _isEfQuery; + private bool _queryIsPagedAlready; + private int? _totalCount; - private List _list; + private List _list; - /// The 0-based page index - public PagedList(IEnumerable source, int pageIndex, int pageSize) - { - Guard.NotNull(source, "source"); + /// The 0-based page index + public PagedList(IEnumerable source, int pageIndex, int pageSize) + { + Guard.NotNull(source, "source"); - Init(source.AsQueryable(), pageIndex, pageSize, null); - } + Init(source.AsQueryable(), pageIndex, pageSize, null); + } - /// The 0-based page index - public PagedList(IEnumerable source, int pageIndex, int pageSize, int totalCount) + /// The 0-based page index + public PagedList(IEnumerable source, int pageIndex, int pageSize, int totalCount) { Guard.NotNull(source, "source"); Init(source.AsQueryable(), pageIndex, pageSize, totalCount); } - private void Init(IQueryable source, int pageIndex, int pageSize, int? totalCount) - { - Guard.NotNull(source, nameof(source)); - Guard.PagingArgsValid(pageIndex, pageSize, "pageIndex", "pageSize"); - - SourceQuery = source; - PageIndex = pageIndex; - PageSize = pageSize; - - _totalCount = totalCount; - _queryIsPagedAlready = totalCount.HasValue; - _isEfQuery = source.Provider is IDbAsyncQueryProvider; - } - - private void EnsureIsLoaded() - { - if (_list == null) - { - if (_totalCount == null) - { - _totalCount = SourceQuery.Count(); - } - - if (_queryIsPagedAlready) - { - _list = SourceQuery.ToList(); - } - else - { - _list = ApplyPaging(SourceQuery).ToList(); - } - } - } - - private async Task EnsureIsLoadedAsync() - { - if (_list == null) - { - if (_totalCount == null) - { - _totalCount = await SourceQuery.CountAsync(); - } - - if (_queryIsPagedAlready) - { - _list = await SourceQuery.ToListAsync(); - } - else - { - _list = await ApplyPaging(SourceQuery).ToListAsync(); - } - } - } - - #region IPageable Members - - public IQueryable SourceQuery { get; private set; } - - public IPagedList AlterQuery(Func, IQueryable> alterer) - { - var result = alterer?.Invoke(SourceQuery); - SourceQuery = 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) - { - // Paging unnecessary - return query; - } - else - { - var skip = PageIndex * PageSize; - if (_isEfQuery) - { - return query.Skip(() => skip).Take(() => PageSize); - } - else - { - return query.Skip(skip).Take(PageSize); - } - } - } - - public IPagedList Load(bool force = false) - { - // Returns instance for chaining. - if (force && _list != null) - { - _list.Clear(); - _list = null; - } - - EnsureIsLoaded(); - - return this; - } - - public async Task> LoadAsync(bool force = false) - { - // Returns instance for chaining. - if (force && _list != null) - { - _list.Clear(); - _list = null; - } - - await EnsureIsLoadedAsync(); - - return this; - } - - public int PageIndex { get; set; } - - public int PageSize { get; set; } - - public async Task GetTotalCountAsync() - { - if (!_totalCount.HasValue) - { - _totalCount = await SourceQuery.CountAsync(); - } - - return _totalCount.Value; - } - - public int TotalCount + private void Init(IQueryable source, int pageIndex, int pageSize, int? totalCount) { - get - { - if (!_totalCount.HasValue) - { - _totalCount = SourceQuery.Count(); - } - - return _totalCount.Value; - } - set - { - _totalCount = value; - } + Guard.NotNull(source, nameof(source)); + Guard.PagingArgsValid(pageIndex, pageSize, "pageIndex", "pageSize"); + + SourceQuery = source; + PageIndex = pageIndex; + PageSize = pageSize; + + _totalCount = totalCount; + _queryIsPagedAlready = totalCount.HasValue; + _isEfQuery = source.Provider is IDbAsyncQueryProvider; } - public int PageNumber + private void EnsureIsLoaded() { - get + if (_list == null) { - return this.PageIndex + 1; + if (_totalCount == null) + { + _totalCount = SourceQuery.Count(); + } + + if (_queryIsPagedAlready) + { + _list = SourceQuery.ToList(); + } + else + { + _list = ApplyPaging(SourceQuery).ToList(); + } } - set + } + + private async Task EnsureIsLoadedAsync() + { + if (_list == null) + { + if (_totalCount == null) + { + _totalCount = await SourceQuery.CountAsync(); + } + + if (_queryIsPagedAlready) + { + _list = await SourceQuery.ToListAsync(); + } + else + { + _list = await ApplyPaging(SourceQuery).ToListAsync(); + } + } + } + + #region IPageable Members + + public IQueryable SourceQuery { get; private set; } + + public IPagedList AlterQuery(Func, IQueryable> alterer) + { + var result = alterer?.Invoke(SourceQuery); + SourceQuery = 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) + { + // Paging unnecessary + return query; + } + else + { + var skip = PageIndex * PageSize; + if (_isEfQuery) + { + return skip == 0 + ? query.Take(() => PageSize) + : query.Skip(() => skip).Take(() => PageSize); + } + else + { + return skip == 0 + ? query.Take(PageSize) + : query.Skip(skip).Take(PageSize); + } + } + } + + public IPagedList Load(bool force = false) + { + // Returns instance for chaining. + if (force && _list != null) + { + _list.Clear(); + _list = null; + } + + EnsureIsLoaded(); + + return this; + } + + public async Task> LoadAsync(bool force = false) + { + // Returns instance for chaining. + if (force && _list != null) + { + _list.Clear(); + _list = null; + } + + await EnsureIsLoadedAsync(); + + return this; + } + + public int PageIndex { get; set; } + + public int PageSize { get; set; } + + public async Task GetTotalCountAsync() + { + if (!_totalCount.HasValue) + { + _totalCount = await SourceQuery.CountAsync(); + } + + return _totalCount.Value; + } + + public int TotalCount + { + get { - this.PageIndex = value - 1; + if (!_totalCount.HasValue) + { + _totalCount = SourceQuery.Count(); + } + + return _totalCount.Value; } + set => _totalCount = value; + } + + public int PageNumber + { + get => this.PageIndex + 1; + set => this.PageIndex = value - 1; } public int TotalPages @@ -204,170 +199,131 @@ public int TotalPages } } - public bool HasPreviousPage + public bool HasPreviousPage => this.PageIndex > 0; + + public bool HasNextPage => (this.PageIndex < (this.TotalPages - 1)); + + public int FirstItemIndex => (this.PageIndex * this.PageSize) + 1; + + public int LastItemIndex => Math.Min(this.TotalCount, ((this.PageIndex * this.PageSize) + this.PageSize)); + + public bool IsFirstPage => (this.PageIndex <= 0); + + public bool IsLastPage => (this.PageIndex >= (this.TotalPages - 1)); + + #endregion + + #region IList Members + + public void Add(T item) + { + EnsureIsLoaded(); + _list.Add(item); + } + + public void Clear() { - get + if (_list != null) { - return this.PageIndex > 0; + _list.Clear(); + _list = null; } } - public bool HasNextPage + public bool Contains(T item) { - get - { - return (this.PageIndex < (this.TotalPages - 1)); - } + EnsureIsLoaded(); + return _list.Contains(item); } - public int FirstItemIndex + public void CopyTo(T[] array, int arrayIndex) { - get + EnsureIsLoaded(); + _list.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + if (_list != null) { - return (this.PageIndex * this.PageSize) + 1; + return _list.Remove(item); } + + return false; } - public int LastItemIndex + public int Count { get { - return Math.Min(this.TotalCount, ((this.PageIndex * this.PageSize) + this.PageSize)); + EnsureIsLoaded(); + return _list.Count; } } - public bool IsFirstPage + public bool IsReadOnly => false; + + public int IndexOf(T item) { - get + EnsureIsLoaded(); + return _list.IndexOf(item); + } + + public void Insert(int index, T item) + { + EnsureIsLoaded(); + _list.Insert(index, item); + } + + public void RemoveAt(int index) + { + if (_list != null) { - return (this.PageIndex <= 0); + _list.RemoveAt(index); } } - public bool IsLastPage + public T this[int index] { get { - return (this.PageIndex >= (this.TotalPages - 1)); + EnsureIsLoaded(); + return _list[index]; } + set + { + EnsureIsLoaded(); + _list[index] = value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + EnsureIsLoaded(); + return _list.GetEnumerator(); + } + + #endregion + + #region Utils + + public void AddRange(IEnumerable collection) + { + EnsureIsLoaded(); + _list.AddRange(collection); + } + + public ReadOnlyCollection AsReadOnly() + { + EnsureIsLoaded(); + return _list.AsReadOnly(); } - #endregion - - #region IList Members - - public void Add(T item) - { - EnsureIsLoaded(); - _list.Add(item); - } - - public void Clear() - { - if (_list != null) - { - _list.Clear(); - _list = null; - } - } - - public bool Contains(T item) - { - EnsureIsLoaded(); - return _list.Contains(item); - } - - public void CopyTo(T[] array, int arrayIndex) - { - EnsureIsLoaded(); - _list.CopyTo(array, arrayIndex); - } - - public bool Remove(T item) - { - if (_list != null) - { - return _list.Remove(item); - } - - return false; - } - - public int Count - { - get - { - EnsureIsLoaded(); - return _list.Count; - } - } - - public bool IsReadOnly - { - get { return false; } - } - - public int IndexOf(T item) - { - EnsureIsLoaded(); - return _list.IndexOf(item); - } - - public void Insert(int index, T item) - { - EnsureIsLoaded(); - _list.Insert(index, item); - } - - public void RemoveAt(int index) - { - if (_list != null) - { - _list.RemoveAt(index); - } - } - - public T this[int index] - { - get - { - EnsureIsLoaded(); - return _list[index]; - } - set - { - EnsureIsLoaded(); - _list[index] = value; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - public IEnumerator GetEnumerator() - { - EnsureIsLoaded(); - return _list.GetEnumerator(); - } - - #endregion - - #region Utils - - public void AddRange(IEnumerable collection) - { - EnsureIsLoaded(); - _list.AddRange(collection); - } - - public ReadOnlyCollection AsReadOnly() - { - EnsureIsLoaded(); - return _list.AsReadOnly(); - } - - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs b/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs index 80ae85fd5a..560532bcd6 100644 --- a/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs +++ b/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs @@ -10,7 +10,7 @@ public abstract class BasePlugin : IPlugin /// /// Install plugin /// - public virtual void Install() + public virtual void Install() { PluginManager.MarkPluginAsInstalled(this.PluginDescriptor.SystemName); } @@ -18,7 +18,7 @@ public virtual void Install() /// /// Uninstall plugin /// - public virtual void Uninstall() + public virtual void Uninstall() { PluginManager.MarkPluginAsUninstalled(this.PluginDescriptor.SystemName); } diff --git a/src/Libraries/SmartStore.Core/Plugins/IConfigurable.cs b/src/Libraries/SmartStore.Core/Plugins/IConfigurable.cs index 5a0308eb79..aaee54810c 100644 --- a/src/Libraries/SmartStore.Core/Plugins/IConfigurable.cs +++ b/src/Libraries/SmartStore.Core/Plugins/IConfigurable.cs @@ -1,19 +1,18 @@ -using System; -using System.Web.Routing; +using System.Web.Routing; namespace SmartStore.Core.Plugins { - /// - /// Marks a concrete provider or plugin implementation as configurable through the backend - /// - public interface IConfigurable - { - /// - /// Gets a route for provider or plugin configuration - /// - /// Action name - /// Controller name - /// Route values - void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues); - } + /// + /// Marks a concrete provider or plugin implementation as configurable through the backend + /// + public interface IConfigurable + { + /// + /// Gets a route for provider or plugin configuration + /// + /// Action name + /// Controller name + /// Route values + void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues); + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/ICookiePublisher.cs b/src/Libraries/SmartStore.Core/Plugins/ICookiePublisher.cs index aea384dafd..c069257aed 100644 --- a/src/Libraries/SmartStore.Core/Plugins/ICookiePublisher.cs +++ b/src/Libraries/SmartStore.Core/Plugins/ICookiePublisher.cs @@ -1,47 +1,57 @@ -using System; -using System.Web.Routing; +using System.Collections.Generic; +using System.Runtime.Serialization; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Plugins { - /// - /// Marks a module as a cookie publisher. - /// - public interface ICookiePublisher - { - /// - /// Gets the cookie info of the cookie publisher (e.g. plugin or other module). - /// - CookieInfo GetCookieInfo(); - } + /// + /// Marks a module as a cookie publisher. + /// + public interface ICookiePublisher + { + /// + /// Gets the cookie info of the cookie publisher (e.g. plugin or other module). + /// + IEnumerable GetCookieInfo(); + } - /// - /// Plugin cookie infos. - /// - public class CookieInfo - { - /// - /// Name of the plugin. - /// - public string Name { get; set; } + /// + /// Plugin cookie infos. + /// + public class CookieInfo : BaseEntity, ILocalizedEntity + { + /// + /// Name of the plugin. + /// + [DataMember] + public string Name { get; set; } - /// - /// Description of the cookie (e.g. purpose of using the cookie). - /// - public string Description { get; set; } + /// + /// Description of the cookie (e.g. purpose of using the cookie). + /// + [DataMember] + public string Description { get; set; } - /// - /// Type of the cookie. - /// - public CookieType CookieType { get; set; } - } + /// + /// SelectedStoreIds + /// + [DataMember] + public int[] SelectedStoreIds { get; set; } - /// - /// Type of the cookie. - /// - public enum CookieType - { - Required, - Analytics, - ThirdParty - } + /// + /// Type of the cookie. + /// + [DataMember] + public CookieType CookieType { get; set; } + } + + /// + /// Type of the cookie. + /// + public enum CookieType + { + Required, + Analytics, + ThirdParty + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/IPluginFinder.cs b/src/Libraries/SmartStore.Core/Plugins/IPluginFinder.cs index 51afd082ee..8240e3f247 100644 --- a/src/Libraries/SmartStore.Core/Plugins/IPluginFinder.cs +++ b/src/Libraries/SmartStore.Core/Plugins/IPluginFinder.cs @@ -3,51 +3,51 @@ namespace SmartStore.Core.Plugins { - /// - /// Plugin finder - /// + /// + /// Plugin finder + /// public interface IPluginFinder { - /// - /// Gets plugins - /// - /// The type of plugins to get. - /// A value indicating whether to load only installed plugins - /// Plugins - IEnumerable GetPlugins(bool installedOnly = true) where T : class, IPlugin; + /// + /// Gets plugins + /// + /// The type of plugins to get. + /// A value indicating whether to load only installed plugins + /// Plugins + IEnumerable GetPlugins(bool installedOnly = true) where T : class, IPlugin; - /// - /// Get plugin descriptors - /// - /// A value indicating whether to load only installed plugins - /// Plugin descriptors - IEnumerable GetPluginDescriptors(bool installedOnly = true); + /// + /// Get plugin descriptors + /// + /// A value indicating whether to load only installed plugins + /// Plugin descriptors + IEnumerable GetPluginDescriptors(bool installedOnly = true); - /// - /// Get plugin descriptors - /// - /// The type of plugin to get. - /// A value indicating whether to load only installed plugins - /// Plugin descriptors - IEnumerable GetPluginDescriptors(bool installedOnly = true) where T : class, IPlugin; + /// + /// Get plugin descriptors + /// + /// The type of plugin to get. + /// A value indicating whether to load only installed plugins + /// Plugin descriptors + IEnumerable GetPluginDescriptors(bool installedOnly = true) where T : class, IPlugin; PluginDescriptor GetPluginDescriptorByAssembly(Assembly assembly, bool installedOnly = true); - /// - /// Get a plugin descriptor by its system name - /// - /// Plugin system name - /// A value indicating whether to load only installed plugins - /// >Plugin descriptor + /// + /// Get a plugin descriptor by its system name + /// + /// Plugin system name + /// A value indicating whether to load only installed plugins + /// >Plugin descriptor PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true); - /// - /// Get a plugin descriptor by its system name - /// - /// The type of plugin to get. - /// Plugin system name - /// A value indicating whether to load only installed plugins - /// >Plugin descriptor + /// + /// Get a plugin descriptor by its system name + /// + /// The type of plugin to get. + /// Plugin system name + /// A value indicating whether to load only installed plugins + /// >Plugin descriptor PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true) where T : class, IPlugin; } } diff --git a/src/Libraries/SmartStore.Core/Plugins/LoadPluginResult.cs b/src/Libraries/SmartStore.Core/Plugins/LoadPluginResult.cs index 2e51056df1..c153efcdd9 100644 --- a/src/Libraries/SmartStore.Core/Plugins/LoadPluginResult.cs +++ b/src/Libraries/SmartStore.Core/Plugins/LoadPluginResult.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +using System.IO; namespace SmartStore.Core.Plugins { - public class LoadPluginResult - { - public FileInfo DescriptionFile { get; set; } - public PluginDescriptor Descriptor { get; set; } - public bool IsIncompatible { get; set; } - public bool Success { get; set; } - } + public class LoadPluginResult + { + public FileInfo DescriptionFile { get; set; } + public PluginDescriptor Descriptor { get; set; } + public bool IsIncompatible { get; set; } + public bool Success { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs index ab6a32754e..d27c5ad168 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs @@ -1,41 +1,39 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Reflection; +using System.Runtime.Serialization; using System.Xml; using SmartStore.Core.Infrastructure; -using System.Linq; -using SmartStore.Utilities; -using System.Runtime.Serialization; namespace SmartStore.Core.Plugins { - /// - /// Information about an assembly referenced by a plugin. - /// - public sealed class AssemblyReference - { - /// - /// File in ASP.NET temp folder - /// - public FileInfo File { get; internal set; } - /// - /// The original assembly file that a shadow copy was made from it - /// - public FileInfo OriginalFile { get; internal set; } - /// - /// The assembly that has been shadow copied and was loaded into the AppDomain - /// - public Assembly Assembly { get; internal set; } - - /// - /// Exception thrown during activation/probing - /// - internal Exception ActivationException { get; set; } - } - - [DataContract] + /// + /// Information about an assembly referenced by a plugin. + /// + public sealed class AssemblyReference + { + /// + /// File in ASP.NET temp folder + /// + public FileInfo File { get; internal set; } + /// + /// The original assembly file that a shadow copy was made from it + /// + public FileInfo OriginalFile { get; internal set; } + /// + /// The assembly that has been shadow copied and was loaded into the AppDomain + /// + public Assembly Assembly { get; internal set; } + + /// + /// Exception thrown during activation/probing + /// + internal Exception ActivationException { get; set; } + } + + [DataContract] public class PluginDescriptor : IComparable { private string _resourceRootKey; @@ -47,13 +45,13 @@ public PluginDescriptor() MinAppVersion = SmartStoreVersion.Version; } - // Unit tests - public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile, Type pluginClrType) - : this() - { - Assembly = new AssemblyReference { Assembly = referencedAssembly, OriginalFile = originalAssemblyFile, File = originalAssemblyFile }; - PluginClrType = pluginClrType; - } + // Unit tests + public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile, Type pluginClrType) + : this() + { + Assembly = new AssemblyReference { Assembly = referencedAssembly, OriginalFile = originalAssemblyFile, File = originalAssemblyFile }; + PluginClrType = pluginClrType; + } /// /// Plugin file name @@ -65,29 +63,29 @@ public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFi /// public string PhysicalPath { get; set; } - /// - /// Gets the folder name - /// - [DataMember] - public string FolderName { get; internal set; } - - /// - /// 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 + /// + /// Gets the folder name + /// + [DataMember] + public string FolderName { get; internal set; } + + /// + /// 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 { if (_brandImageFileName == null) { // "null" means we haven't checked yet! - var filesToCheck = new [] { "branding.png", "branding.gif", "branding.jpg", "branding.jpeg" }; + var filesToCheck = new[] { "branding.png", "branding.gif", "branding.jpg", "branding.jpeg" }; var dir = this.PhysicalPath; foreach (var file in filesToCheck) { @@ -112,151 +110,143 @@ public string BrandImageFileName /// public Type PluginClrType { get; set; } - /// - /// The plugin assembly. - /// - public AssemblyReference Assembly { get; internal set; } = new AssemblyReference(); + /// + /// The plugin assembly. + /// + public AssemblyReference Assembly { get; internal set; } = new AssemblyReference(); - /// - /// List of assemblies files found in the plugin folder, except the main plugin assembly. - /// - internal AssemblyReference[] ReferencedLocalAssemblies { get; set; } = new AssemblyReference[0]; + /// + /// List of assemblies files found in the plugin folder, except the main plugin assembly. + /// + internal AssemblyReference[] ReferencedLocalAssemblies { get; set; } = new AssemblyReference[0]; - /// - /// Gets or sets the plugin group - /// - [DataMember] - public string Group { get; internal set; } + /// + /// Gets or sets the plugin group + /// + [DataMember] + public string Group { get; internal set; } - public bool IsInKnownGroup - { - get - { - return PluginFileParser.KnownGroups.Contains(this.Group, StringComparer.OrdinalIgnoreCase); - } - } + public bool IsInKnownGroup => PluginFileParser.KnownGroups.Contains(this.Group, StringComparer.OrdinalIgnoreCase); /// /// Gets or sets the friendly name /// [DataMember] - public string FriendlyName { get; set; } + public string FriendlyName { get; set; } /// /// Gets or sets the system name /// [DataMember] - public string SystemName { get; set; } + public string SystemName { get; set; } /// /// Gets the plugin description /// [DataMember] - public string Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the version /// [DataMember] - public Version Version { get; set; } + public Version Version { get; set; } /// /// Gets or sets the minimum supported app version /// [DataMember] - public Version MinAppVersion { get; set; } + public Version MinAppVersion { get; set; } /// /// Gets or sets the author /// [DataMember] - public string Author { get; set; } + public string Author { get; set; } - /// - /// Gets or sets the project/marketplace url - /// - [DataMember] - public string Url { get; set; } + /// + /// Gets or sets the project/marketplace url + /// + [DataMember] + public string Url { get; set; } /// /// Gets or sets the display order /// [DataMember] - public int DisplayOrder { get; set; } + public int DisplayOrder { get; set; } /// /// Gets or sets a value indicating whether the plugin is installed /// [DataMember] - public bool Installed { get; set; } + public bool Installed { get; set; } - /// - /// Gets a value indicating whether the plugin is incompatible with the current application version - /// - [DataMember] - public bool Incompatible { get; set; } - - /// - /// Gets or sets the value indicating whether the plugin is configurable - /// - /// - /// A plugin is configurable when it implements the interface - /// - [DataMember] - public bool IsConfigurable { get; set; } - - /// - /// Gets or sets the root key of string resources. - /// - /// - /// Tries to get it from first entry of resource XML file if not specified. - /// In that case the first resource name should not contain a dot if it's not part of the root key. - /// Otherwise you get the wrong root key. - /// - public string ResourceRootKey - { - get { - if (_resourceRootKey == null) - { - _resourceRootKey = ""; - - try - { - // Try to get root-key from first entry of XML file - var localizationDir = new DirectoryInfo(Path.Combine(PhysicalPath, "Localization")); - - if (localizationDir.Exists) - { - var localizationFile = localizationDir.EnumerateFiles("*.xml").FirstOrDefault(); - if (localizationFile != null) - { - XmlDocument doc = new XmlDocument(); - doc.Load(localizationFile.FullName); - var key = doc.SelectSingleNode(@"//Language/LocaleResource")?.Attributes["Name"]?.InnerText; - if (key.HasValue() && key.Contains('.')) - { - _resourceRootKey = key.Substring(0, key.LastIndexOf('.')); - } - } - } - } - catch (Exception ex) - { - ex.Dump(); - } - } - - return _resourceRootKey; - } - set { - _resourceRootKey = value; - } - } + /// + /// Gets a value indicating whether the plugin is incompatible with the current application version + /// + [DataMember] + public bool Incompatible { get; set; } + + /// + /// Gets or sets the value indicating whether the plugin is configurable + /// + /// + /// A plugin is configurable when it implements the interface + /// + [DataMember] + public bool IsConfigurable { get; set; } + + /// + /// Gets or sets the root key of string resources. + /// + /// + /// Tries to get it from first entry of resource XML file if not specified. + /// In that case the first resource name should not contain a dot if it's not part of the root key. + /// Otherwise you get the wrong root key. + /// + public string ResourceRootKey + { + get + { + if (_resourceRootKey == null) + { + _resourceRootKey = ""; + + try + { + // Try to get root-key from first entry of XML file + var localizationDir = new DirectoryInfo(Path.Combine(PhysicalPath, "Localization")); + + if (localizationDir.Exists) + { + var localizationFile = localizationDir.EnumerateFiles("*.xml").FirstOrDefault(); + if (localizationFile != null) + { + XmlDocument doc = new XmlDocument(); + doc.Load(localizationFile.FullName); + var key = doc.SelectSingleNode(@"//Language/LocaleResource")?.Attributes["Name"]?.InnerText; + if (key.HasValue() && key.Contains('.')) + { + _resourceRootKey = key.Substring(0, key.LastIndexOf('.')); + } + } + } + } + catch (Exception ex) + { + ex.Dump(); + } + } + + return _resourceRootKey; + } + set => _resourceRootKey = value; + } public T Instance() where T : class, IPlugin { - object instance; - if (!EngineContext.Current.ContainerManager.TryResolve(PluginClrType, null, out instance)) + if (!EngineContext.Current.ContainerManager.TryResolve(PluginClrType, null, out var instance)) { // Not registered instance = EngineContext.Current.ContainerManager.ResolveUnregistered(PluginClrType); @@ -274,20 +264,20 @@ public IPlugin Instance() return Instance(); } - [SuppressMessage("ReSharper", "StringCompareToIsCultureSpecific")] - public int CompareTo(PluginDescriptor other) + [SuppressMessage("ReSharper", "StringCompareToIsCultureSpecific")] + public int CompareTo(PluginDescriptor other) { - if (DisplayOrder != other.DisplayOrder) - return DisplayOrder.CompareTo(other.DisplayOrder); - else if (FriendlyName != null) - return FriendlyName.CompareTo(other.FriendlyName); - return 0; + if (DisplayOrder != other.DisplayOrder) + return DisplayOrder.CompareTo(other.DisplayOrder); + else if (FriendlyName != null) + return FriendlyName.CompareTo(other.FriendlyName); + return 0; } - public string GetSettingKey(string name) - { - return "PluginSetting.{0}.{1}".FormatWith(SystemName, name); - } + public string GetSettingKey(string name) + { + return "PluginSetting.{0}.{1}".FormatWith(SystemName, name); + } public override string ToString() { diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs index 14e0f87b26..1ac124939e 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs @@ -17,12 +17,12 @@ internal class GroupComparer : Comparer { public override int Compare(string x, string y) { - return Array.FindIndex( KnownGroups, s => s == x) - Array.FindIndex(KnownGroups, s => s == y); + return Array.FindIndex(KnownGroups, s => s == x) - Array.FindIndex(KnownGroups, s => s == y); } } - - internal readonly static string[] KnownGroups = new string[] - { + + internal readonly static string[] KnownGroups = new string[] + { "Admin", "Marketing", "Payment", @@ -30,47 +30,47 @@ public override int Compare(string x, string y) "Tax", "Analytics", "CMS", - "Media", + "Media", "SEO", "Data", "Globalization", "Api", - "Mobile", + "Mobile", "Social", - "Security", + "Security", "Developer", - "Sales", - "Design", - "Performance", - "B2B", - "Storefront", - "Law" + "Sales", + "Design", + "Performance", + "B2B", + "Storefront", + "Law" }; public readonly static IComparer KnownGroupComparer = new GroupComparer(); - public readonly static string InstalledPluginsFilePath; + public readonly static string InstalledPluginsFilePath; - static PluginFileParser() - { - InstalledPluginsFilePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "InstalledPlugins.txt"); - } + static PluginFileParser() + { + InstalledPluginsFilePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "InstalledPlugins.txt"); + } - public static HashSet ParseInstalledPluginsFile(string filePath = null) + public static HashSet ParseInstalledPluginsFile(string filePath = null) { - filePath = filePath ?? InstalledPluginsFilePath; + filePath = filePath ?? InstalledPluginsFilePath; - var lines = new HashSet(StringComparer.OrdinalIgnoreCase); + var lines = new HashSet(StringComparer.OrdinalIgnoreCase); - // Read and parse the file + // Read and parse the file if (!File.Exists(filePath)) - return lines; + return lines; var text = File.ReadAllText(filePath); if (text.IsEmpty()) - { - return lines; - } - + { + return lines; + } + using (var reader = new StringReader(text)) { string str; @@ -88,10 +88,10 @@ public static HashSet ParseInstalledPluginsFile(string filePath = null) public static void SaveInstalledPluginsFile(ICollection pluginSystemNames, string filePath = null) { - if (pluginSystemNames == null || pluginSystemNames.Count == 0) + if (pluginSystemNames == null) return; - filePath = filePath ?? InstalledPluginsFilePath; + filePath = filePath ?? InstalledPluginsFilePath; string result = ""; foreach (var sn in pluginSystemNames) @@ -108,9 +108,9 @@ public static PluginDescriptor ParsePluginDescriptionFile(string filePath) if (String.IsNullOrEmpty(text)) return descriptor; - string dirName = Path.GetDirectoryName(filePath); - descriptor.PhysicalPath = dirName; - descriptor.FolderName = new DirectoryInfo(dirName).Name; + string dirName = Path.GetDirectoryName(filePath); + descriptor.PhysicalPath = dirName; + descriptor.FolderName = new DirectoryInfo(dirName).Name; var settings = new List(); using (var reader = new StringReader(text)) @@ -171,9 +171,9 @@ public static PluginDescriptor ParsePluginDescriptionFile(string filePath) case "Author": descriptor.Author = value; break; - case "Url": - descriptor.Url = value; - break; + case "Url": + descriptor.Url = value; + break; case "DisplayOrder": { int displayOrder; @@ -184,31 +184,31 @@ public static PluginDescriptor ParsePluginDescriptionFile(string filePath) case "FileName": descriptor.PluginFileName = value; break; - case "ResourceRootKey": - descriptor.ResourceRootKey = value; - break; + case "ResourceRootKey": + descriptor.ResourceRootKey = value; + break; } } - if (IsKnownGroup(group)) - { - descriptor.Group = group; - } - else - { - descriptor.Group = "Misc"; - } + if (IsKnownGroup(group)) + { + descriptor.Group = group; + } + else + { + descriptor.Group = "Misc"; + } return descriptor; } - private static bool IsKnownGroup(string group) - { - if (group.IsEmpty()) - return false; - return KnownGroups.Contains(group, StringComparer.OrdinalIgnoreCase); - } - + private static bool IsKnownGroup(string group) + { + if (group.IsEmpty()) + return false; + return KnownGroups.Contains(group, StringComparer.OrdinalIgnoreCase); + } + public static void SavePluginDescriptionFile(PluginDescriptor plugin) { if (plugin == null) @@ -232,10 +232,10 @@ public static void SavePluginDescriptionFile(PluginDescriptor plugin) keyValues.Add(new KeyValuePair("Version", plugin.Version.ToString())); keyValues.Add(new KeyValuePair("MinAppVersion", string.Join(",", plugin.MinAppVersion))); keyValues.Add(new KeyValuePair("Author", plugin.Author)); - keyValues.Add(new KeyValuePair("Url", plugin.Url)); + keyValues.Add(new KeyValuePair("Url", plugin.Url)); keyValues.Add(new KeyValuePair("DisplayOrder", plugin.DisplayOrder.ToString())); keyValues.Add(new KeyValuePair("FileName", plugin.PluginFileName)); - keyValues.Add(new KeyValuePair("ResourceRootKey", plugin.ResourceRootKey)); + keyValues.Add(new KeyValuePair("ResourceRootKey", plugin.ResourceRootKey)); var sb = new StringBuilder(); for (int i = 0; i < keyValues.Count; i++) @@ -243,7 +243,7 @@ public static void SavePluginDescriptionFile(PluginDescriptor plugin) var key = keyValues[i].Key; var value = keyValues[i].Value; sb.AppendFormat("{0}: {1}", key, value); - if (i != keyValues.Count -1) + if (i != keyValues.Count - 1) sb.Append(Environment.NewLine); } diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs index 4eddd033dd..86b20a756d 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs @@ -7,130 +7,130 @@ namespace SmartStore.Core.Plugins { public class PluginFinder : IPluginFinder { - private IList _plugins; - private readonly IDictionary _nameMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly IDictionary _assemblyMap = new Dictionary(); - - private static readonly object _lock = new object(); - - private PluginFinder() - { - lock (_lock) - { - LoadPlugins(); - } - } - - public static IPluginFinder Current { get; } = new PluginFinder(); - - private void LoadPlugins() - { - if (_plugins != null) - return; - - var plugins = PluginManager.ReferencedPlugins.ToList(); - plugins.Sort(); //sort - _plugins = plugins; - - foreach (var plugin in plugins) - { - _nameMap[plugin.SystemName] = plugin; - if (plugin.Assembly.Assembly != null) - { - _assemblyMap[plugin.Assembly.Assembly] = plugin; - } - } - } - - /// - /// Gets plugins - /// - /// The type of plugins to get. - /// A value indicating whether to load only installed plugins - /// Plugins - public virtual IEnumerable GetPlugins(bool installedOnly = true) where T : class, IPlugin + private IList _plugins; + private readonly IDictionary _nameMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly IDictionary _assemblyMap = new Dictionary(); + + private static readonly object _lock = new object(); + + private PluginFinder() + { + lock (_lock) + { + LoadPlugins(); + } + } + + public static IPluginFinder Current { get; } = new PluginFinder(); + + private void LoadPlugins() + { + if (_plugins != null) + return; + + var plugins = PluginManager.ReferencedPlugins.ToList(); + plugins.Sort(); //sort + _plugins = plugins; + + foreach (var plugin in plugins) + { + _nameMap[plugin.SystemName] = plugin; + if (plugin.Assembly.Assembly != null) + { + _assemblyMap[plugin.Assembly.Assembly] = plugin; + } + } + } + + /// + /// Gets plugins + /// + /// The type of plugins to get. + /// A value indicating whether to load only installed plugins + /// Plugins + public virtual IEnumerable GetPlugins(bool installedOnly = true) where T : class, IPlugin { foreach (var plugin in _plugins) if (typeof(T).IsAssignableFrom(plugin.PluginClrType)) if (!installedOnly || plugin.Installed) - yield return plugin.Instance(); + yield return plugin.Instance(); } - /// - /// Get plugin descriptors - /// - /// A value indicating whether to load only installed plugins - /// Plugin descriptors - public virtual IEnumerable GetPluginDescriptors(bool installedOnly = true) + /// + /// Get plugin descriptors + /// + /// A value indicating whether to load only installed plugins + /// Plugin descriptors + public virtual IEnumerable GetPluginDescriptors(bool installedOnly = true) { foreach (var plugin in _plugins) if (!installedOnly || plugin.Installed) - yield return plugin; + yield return plugin; } - /// - /// Get plugin descriptors - /// - /// The type of plugin to get. - /// A value indicating whether to load only installed plugins - /// Plugin descriptors - public virtual IEnumerable GetPluginDescriptors(bool installedOnly = true) - where T : class, IPlugin + /// + /// Get plugin descriptors + /// + /// The type of plugin to get. + /// A value indicating whether to load only installed plugins + /// Plugin descriptors + public virtual IEnumerable GetPluginDescriptors(bool installedOnly = true) + where T : class, IPlugin { foreach (var plugin in _plugins) if (typeof(T).IsAssignableFrom(plugin.PluginClrType)) if (!installedOnly || plugin.Installed) - yield return plugin; + yield return plugin; } public virtual PluginDescriptor GetPluginDescriptorByAssembly(Assembly assembly, bool installedOnly = true) { - if (assembly != null && _assemblyMap.TryGetValue(assembly, out var descriptor)) - { - if (!installedOnly || descriptor.Installed) - return descriptor; - } + if (assembly != null && _assemblyMap.TryGetValue(assembly, out var descriptor)) + { + if (!installedOnly || descriptor.Installed) + return descriptor; + } - return null; + return null; } - /// - /// Get a plugin descriptor by its system name - /// - /// Plugin system name - /// A value indicating whether to load only installed plugins - /// >Plugin descriptor + /// + /// Get a plugin descriptor by its system name + /// + /// Plugin system name + /// A value indicating whether to load only installed plugins + /// >Plugin descriptor public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true) { - if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor)) - { - if (!installedOnly || descriptor.Installed) - return descriptor; - } + if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor)) + { + if (!installedOnly || descriptor.Installed) + return descriptor; + } - return null; + return null; } - /// - /// Get a plugin descriptor by its system name - /// - /// The type of plugin to get. - /// Plugin system name - /// A value indicating whether to load only installed plugins - /// >Plugin descriptor + /// + /// Get a plugin descriptor by its system name + /// + /// The type of plugin to get. + /// Plugin system name + /// A value indicating whether to load only installed plugins + /// >Plugin descriptor public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true) where T : class, IPlugin { - if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor)) - { - if (!installedOnly || descriptor.Installed) - { - if (typeof(T).IsAssignableFrom(descriptor.PluginClrType)) - return descriptor; - } - } - - return null; + if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor)) + { + if (!installedOnly || descriptor.Installed) + { + if (typeof(T).IsAssignableFrom(descriptor.PluginClrType)) + return descriptor; + } + } + + return null; } - + } -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs index 7d11badfed..b7a84dedc6 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs @@ -1,337 +1,299 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; -using System.Diagnostics; using SmartStore.Core.Data; +using SmartStore.Core.IO; using SmartStore.Core.Logging; using SmartStore.Utilities; namespace SmartStore.Core.Plugins { - public partial class PluginManager - { - private static int? _lastPluginsHash; - private static ICollection _lastPluginAssemblies; + public partial class PluginManager + { + #region PluginsHasher + + class PluginsHasher : DirectoryHasher + { + private readonly IEnumerable _plugins; + + public PluginsHasher(IEnumerable plugins) + : base(CommonHelper.MapPath(PluginsLocation), null, false, CommonHelper.MapPath(DataSettings.Current.TenantPath)) + { + _plugins = plugins; + } + + protected override int ComputeHash() + { + var hashCombiner = new HashCodeCombiner(); + + // Add each *.dll, Description.txt, web.config to the hash + var arrPlugins = _plugins.OrderBy(x => x.FolderName).ToArray(); + foreach (var p in arrPlugins) + { + if (p.Assembly.OriginalFile != null) + { + hashCombiner.Add(p.Assembly.OriginalFile); + } + + p.ReferencedLocalAssemblies.Each(x => hashCombiner.Add(x.OriginalFile)); + + hashCombiner.Add(Path.Combine(p.PhysicalPath, "Description.txt")); + hashCombiner.Add(Path.Combine(p.PhysicalPath, "web.config")); + hashCombiner.Add(p.Installed); + } + + return hashCombiner.CombinedHash; + } + } + + #endregion + + private static ICollection _lastPluginAssemblies; /// /// true when any plugin file has been changed since previous application start. /// public static bool PluginChangeDetected { get; private set; } - /// - /// This checks if any of our plugins have changed, if so it will go - /// - /// - /// Returns true if there were changes to plugins and a cleanup was required, otherwise false is returned. - /// - private static bool DetectAndCleanStalePlugins(IEnumerable plugins) - { - var currentHash = ComputePluginsHash(plugins); - var lastHash = GetLastPluginsHash(); - - // Check if anything has been changed, or if we are in debug mode then always perform cleanup - if (currentHash != lastHash) - { + /// + /// This checks if any of our plugins have changed, if so it will go + /// + /// + /// Returns true if there were changes to plugins and a cleanup was required, otherwise false is returned. + /// + private static bool DetectAndCleanStalePlugins(IEnumerable plugins, PluginsHasher hasher) + { + // Check if anything has been changed, or if we are in debug mode then always perform cleanup + if (hasher.HasChanged) + { PluginChangeDetected = true; Logger.DebugFormat("Plugin changes detected in hash."); var lastAssemblies = GetLastPluginsAssemblies(); - // We need to read the old assembly list and clean them out from the shadow copy folder - var staleFiles = lastAssemblies - .Select(x => new FileInfo(Path.Combine(_shadowCopyDir.FullName, x))) - .ToArray(); - - // If that fails, then we will try to remove it the hard way by renaming it to .delete if it doesn't delete nicely - if (staleFiles.Length > 5) - { - staleFiles.AsParallel().ForAll(f => TryDeleteStaleFile(f)); - } - else - { - staleFiles.Each(f => TryDeleteStaleFile(f)); - } - - return true; - } - - // No plugin changes found - return false; - } - - /// - /// Loads the hash code of the last loaded plugins from disk - /// - /// - private static int? GetLastPluginsHash() - { - if (_lastPluginsHash == null) - { - var filePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "plugins.hash"); - if (!File.Exists(filePath)) - { - _lastPluginsHash = 0; - } - else - { - var hash = File.ReadAllText(filePath, Encoding.UTF8); - _lastPluginsHash = ConvertPluginsHash(hash); - } - } - - if (_lastPluginsHash == 0) - { - return null; - } - - return _lastPluginsHash; - } - - private static void SavePluginsHash(int hashCode) - { - var filePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "plugins.hash"); - File.WriteAllText(filePath, hashCode.ToString(CultureInfo.InvariantCulture), Encoding.UTF8); - _lastPluginsHash = null; - } - - /// - /// Loads the file names of the last shadow copied plugin assemblies from disk - /// - /// - private static ICollection GetLastPluginsAssemblies() - { - if (_lastPluginAssemblies == null) - { - var filePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "plugins.assemblies"); - - if (!File.Exists(filePath)) - { - _lastPluginAssemblies = new List(); - } - else - { - var list = new HashSet(StringComparer.OrdinalIgnoreCase); - - var file = File.ReadAllText(filePath, Encoding.UTF8); - var sr = new StringReader(file); - while (true) - { - var f = sr.ReadLine(); - if (f != null && f.HasValue()) - { - list.Add(f); - } - else - { - break; - } - } - - _lastPluginAssemblies = list; - } - } - - return _lastPluginAssemblies; - } - - private static void SavePluginsAssemblies(IEnumerable plugins) - { - var list = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var p in plugins) - { - p.ReferencedLocalAssemblies.Each(x => list.Add(x.OriginalFile.Name)); - - if (p.Assembly.OriginalFile != null) - { - list.Add(p.Assembly.OriginalFile.Name); - } - } - - var sb = new StringBuilder(); - foreach (var f in list) - { - sb.AppendLine(f); - } - - var filePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "plugins.assemblies"); - File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); - - _lastPluginAssemblies = null; - } - - /// - /// Returns the hash for the passed plugins - /// - /// - private static int ComputePluginsHash(IEnumerable plugins) - { - var hashCombiner = new HashCodeCombiner(); - - // Add each *.dll, Description.txt, web.config to the hash - foreach (var p in plugins) - { - if (p.Assembly.OriginalFile != null) - { - hashCombiner.Add(p.Assembly.OriginalFile); - } - - p.ReferencedLocalAssemblies.Each(x => hashCombiner.Add(x.OriginalFile)); - - hashCombiner.Add(Path.Combine(p.PhysicalPath, "Description.txt")); - hashCombiner.Add(Path.Combine(p.PhysicalPath, "web.config")); - hashCombiner.Add(p.Installed); - } - - return hashCombiner.CombinedHash; - } - - /// - /// Tries to delete the file. If it doesn't go nicely, we'll force rename it to .delete, if that already exists and we cannot - /// remove the .delete file then we rename to Guid + .delete - /// - /// - /// If it deletes nicely return true, otherwise false - private static bool TryDeleteStaleFile(FileInfo f) - { - Debug.WriteLine("Trying to delete shadow copied file " + f.FullName); - - // This is a special case: people may have been usign the CodeGen folder before so we need to cleanup those - // files too, even if we are no longer using it - if (CleanupDeletePluginFiles(new FileInfo(f.FullName + ".delete"))) - return true; - - try - { - f.Delete(); - return true; - } - catch { } - - // If the file doesn't delete, then create the .delete file for it, hopefully the BuildManager will clean up next time - var newName = GetNewDeleteName(f); - - try - { - File.Move(f.FullName, newName); - } - catch (UnauthorizedAccessException) - { - var ex = new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", Path.GetDirectoryName(newName))); + // We need to read the old assembly list and clean them out from the shadow copy folder + var staleFiles = lastAssemblies + .Select(x => new FileInfo(Path.Combine(_shadowCopyDir.FullName, x))) + .ToArray(); + + // If that fails, then we will try to remove it the hard way by renaming it to .delete if it doesn't delete nicely + if (staleFiles.Length > 5) + { + staleFiles.AsParallel().ForAll(f => TryDeleteStaleFile(f)); + } + else + { + staleFiles.Each(f => TryDeleteStaleFile(f)); + } + + return true; + } + + // No plugin changes found + return false; + } + + /// + /// Loads the file names of the last shadow copied plugin assemblies from disk + /// + /// + private static ICollection GetLastPluginsAssemblies() + { + if (_lastPluginAssemblies == null) + { + var filePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "plugins.assemblies"); + + if (!File.Exists(filePath)) + { + _lastPluginAssemblies = new List(); + } + else + { + var list = new HashSet(StringComparer.OrdinalIgnoreCase); + + var file = File.ReadAllText(filePath, Encoding.UTF8); + var sr = new StringReader(file); + while (true) + { + var f = sr.ReadLine(); + if (f != null && f.HasValue()) + { + list.Add(f); + } + else + { + break; + } + } + + _lastPluginAssemblies = list; + } + } + + return _lastPluginAssemblies; + } + + private static void SavePluginsAssemblies(IEnumerable plugins) + { + var list = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var p in plugins) + { + p.ReferencedLocalAssemblies.Each(x => list.Add(x.OriginalFile.Name)); + + if (p.Assembly.OriginalFile != null) + { + list.Add(p.Assembly.OriginalFile.Name); + } + } + + var sb = new StringBuilder(); + foreach (var f in list) + { + sb.AppendLine(f); + } + + var filePath = Path.Combine(CommonHelper.MapPath(DataSettings.Current.TenantPath), "plugins.assemblies"); + File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); + + _lastPluginAssemblies = null; + } + + /// + /// Tries to delete the file. If it doesn't go nicely, we'll force rename it to .delete, if that already exists and we cannot + /// remove the .delete file then we rename to Guid + .delete + /// + /// + /// If it deletes nicely return true, otherwise false + private static bool TryDeleteStaleFile(FileInfo f) + { + Debug.WriteLine("Trying to delete shadow copied file " + f.FullName); + + // This is a special case: people may have been usign the CodeGen folder before so we need to cleanup those + // files too, even if we are no longer using it + if (CleanupDeletePluginFiles(new FileInfo(f.FullName + ".delete"))) + return true; + + try + { + f.Delete(); + return true; + } + catch { } + + // If the file doesn't delete, then create the .delete file for it, hopefully the BuildManager will clean up next time + var newName = GetNewDeleteName(f); + + try + { + File.Move(f.FullName, newName); + } + catch (UnauthorizedAccessException) + { + var ex = new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", Path.GetDirectoryName(newName))); Logger.Error(ex.Message); throw ex; - } - catch (IOException) - { + } + catch (IOException) + { Logger.Error(f.FullName + " rename failed, cannot remove stale plugin"); - throw; - } + throw; + } - // Make it an empty file - try - { - (new StreamWriter(newName)).Close(); - } - catch { } + // Make it an empty file + try + { + (new StreamWriter(newName)).Close(); + } + catch { } Logger.Error("Stale plugin " + f.FullName + " failed to cleanup successfully. A .delete file has been created for it."); - return false; - } - - /// - /// Returns the .delete file name for a particular file. If just the file name + .delete already exists and is locked, - /// then this returns a Guid + .delete format. - /// - /// - /// - private static string GetNewDeleteName(FileInfo pluginFile) - { - var deleteName = pluginFile.FullName + ".delete"; - - try - { - if (File.Exists(deleteName)) - File.Delete(deleteName); - } - catch - { + return false; + } + + /// + /// Returns the .delete file name for a particular file. If just the file name + .delete already exists and is locked, + /// then this returns a Guid + .delete format. + /// + /// + /// + private static string GetNewDeleteName(FileInfo pluginFile) + { + var deleteName = pluginFile.FullName + ".delete"; + + try + { + if (File.Exists(deleteName)) + File.Delete(deleteName); + } + catch + { Logger.Error("Cannot remove file " + deleteName + ".It is locked, renaming to.delete file with GUID"); // Cannot delete, so we will need to use a GUID deleteName = pluginFile.FullName + Guid.NewGuid().ToString("N") + ".delete"; - } - - return deleteName; - } - - /// - /// When given a .delete file, this will attempt to delete the base file that the .delete was created from - /// and then the .delete file itself. if both are removed, then it will return true. - /// - /// - /// - private static bool CleanupDeletePluginFiles(FileInfo f) - { - if (f.Extension != ".delete") - return false; - - var baseFileName = Path.GetDirectoryName(f.FullName) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(f.FullName); - - // NOTE: this is to remove our old GUID named files... we shouldn't have any of these but sometimes things are just locked - var base64Files = f.Directory.GetFiles(Path.GetFileName(baseFileName) + "???????????????????????????????????" + ".delete").ToList(); - Func delFile = x => - { - if (x.IsEmpty()) - return true; - - if (File.Exists(x)) - { - try - { - File.Delete(x); - } - catch - { - return false; - } - } - return true; - }; - - // Always try deleting old guid named files - foreach (var x in base64Files) - { - delFile(x.FullName); - } - - // Try to delete the .dll file - if (!delFile(baseFileName)) - { - return false; - } - - try - { - // Now try to remove the .delete file - f.Delete(); - } - catch { } - - return true; - } - - internal static int ConvertPluginsHash(string val) - { - int outVal; - if (Int32.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out outVal)) - { - return outVal; - } - - return 0; - } - } + } + + return deleteName; + } + + /// + /// When given a .delete file, this will attempt to delete the base file that the .delete was created from + /// and then the .delete file itself. if both are removed, then it will return true. + /// + /// + /// + private static bool CleanupDeletePluginFiles(FileInfo f) + { + if (f.Extension != ".delete") + return false; + + var baseFileName = Path.GetDirectoryName(f.FullName) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(f.FullName); + + // NOTE: this is to remove our old GUID named files... we shouldn't have any of these but sometimes things are just locked + var base64Files = f.Directory.GetFiles(Path.GetFileName(baseFileName) + "???????????????????????????????????" + ".delete").ToList(); + Func delFile = x => + { + if (x.IsEmpty()) + return true; + + if (File.Exists(x)) + { + try + { + File.Delete(x); + } + catch + { + return false; + } + } + return true; + }; + + // Always try deleting old guid named files + foreach (var x in base64Files) + { + delFile(x.FullName); + } + + // Try to delete the .dll file + if (!delFile(baseFileName)) + { + return false; + } + + try + { + // Now try to remove the .delete file + f.Delete(); + } + catch { } + + return true; + } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs index 61e7d8ccdf..d8a3f158af 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs @@ -5,17 +5,17 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Web; using System.Web.Compilation; -using System.Runtime.InteropServices; using Microsoft.Web.Infrastructure.DynamicModuleHelper; +using SmartStore.Core.Data; +using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; -using SmartStore.Core.Plugins; +using SmartStore.Core.Logging; using SmartStore.Core.Packaging; +using SmartStore.Core.Plugins; using SmartStore.Utilities; -using SmartStore.Core.Data; -using SmartStore.Core.Logging; -using SmartStore.Core.Infrastructure; // Contributor: Umbraco (http://www.umbraco.com). Thanks a lot! // SEE THIS POST for full details of what this does @@ -29,13 +29,13 @@ namespace SmartStore.Core.Plugins /// public partial class PluginManager { - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetDllDirectory(string lpPathName); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetDllDirectory(string lpPathName); - private static readonly object _lock = new object(); + private static readonly object _lock = new object(); private static DirectoryInfo _shadowCopyDir; - private static readonly ConcurrentDictionary _referencedPlugins = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private static HashSet _inactiveAssemblies = new HashSet(); + private static readonly ConcurrentDictionary _referencedPlugins = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static readonly HashSet _inactiveAssemblies = new HashSet(); private static ILogger Logger { get; set; } = NullLogger.Instance; @@ -43,7 +43,7 @@ private static IDisposable ActivateLogger() { var logger = new TraceLogger(); Logger = logger; - return new ActionDisposable(() => + return new ActionDisposable(() => { logger.Dispose(); Logger = NullLogger.Instance; @@ -59,33 +59,30 @@ private static IDisposable ActivateLogger() /// Returns a collection of all referenced plugin assemblies that have been shadow copied /// public static IEnumerable ReferencedPlugins - { - get - { - return _referencedPlugins.Values; - } - // for unit testing purposes - internal set - { - foreach (var x in value) - { - if (!_referencedPlugins.ContainsKey(x.SystemName)) - { - _referencedPlugins[x.SystemName] = x; - } - } - } - } - - /// - /// Returns a collection of all plugins which are not compatible with the current version - /// - public static IEnumerable IncompatiblePlugins - { - get; - // for unit testing purposes - internal set; - } + { + get => _referencedPlugins.Values; + // for unit testing purposes + internal set + { + foreach (var x in value) + { + if (!_referencedPlugins.ContainsKey(x.SystemName)) + { + _referencedPlugins[x.SystemName] = x; + } + } + } + } + + /// + /// Returns a collection of all plugins which are not compatible with the current version + /// + public static IEnumerable IncompatiblePlugins + { + get; + // for unit testing purposes + internal set; + } /// /// Initialize @@ -97,15 +94,15 @@ public static void Initialize() Logger.Debug("===================== APPINIT START"); InitializeCore(); Logger.Debug("===================== APPINIT END"); - } - } + } + } private static void InitializeCore() { var isFullTrust = WebHelper.GetTrustLevel() == AspNetHostingPermissionLevel.Unrestricted; if (!isFullTrust) { - throw new ApplicationException("SmartStore.NET requires Full Trust mode. Please enable Full Trust for your web site or contact your hosting provider."); + throw new ApplicationException("Smartstore requires Full Trust mode. Please enable Full Trust for your web site or contact your hosting provider."); } using (var updater = new AppUpdater()) @@ -136,6 +133,7 @@ private static void InitializeCore() var plugins = LoadPluginDescriptors().ToArray(); var compatiblePlugins = plugins.Where(x => !x.Incompatible).ToArray(); + var hasher = new PluginsHasher(compatiblePlugins); Logger.DebugFormat("Loaded plugin descriptors. {0} total, {1} incompatible.", plugins.Length, plugins.Length - compatiblePlugins.Length); @@ -144,7 +142,7 @@ private static void InitializeCore() // If plugins state is dirty, we copy files over to the dynamic folder, // otherwise we just reference the previously copied file. - dirty = DetectAndCleanStalePlugins(compatiblePlugins); + dirty = DetectAndCleanStalePlugins(compatiblePlugins, hasher); // Perf: Initialize/probe all plugins in parallel plugins.AsParallel().ForAll(x => @@ -171,8 +169,7 @@ private static void InitializeCore() if (dirty && DataSettings.DatabaseIsInstalled()) { // Save current hash of all deployed plugins to disk - var hash = ComputePluginsHash(_referencedPlugins.Values.OrderBy(x => x.FolderName).ToArray()); - SavePluginsHash(hash); + hasher.Persist(); // Save names of all deployed assemblies to disk (so we can nuke them later) SavePluginsAssemblies(_referencedPlugins.Values); @@ -231,7 +228,7 @@ void FinalizePlugin(PluginDescriptor p) } if (firstFailedAssembly != null) { - Logger.ErrorFormat("Assembly probing failed for '{0}': {1}", firstFailedAssembly.File.Name, firstFailedAssembly.ActivationException.Message); + Logger.ErrorFormat("Assembly probing failed for '{0}': {1}", firstFailedAssembly.File?.Name.EmptyNull(), firstFailedAssembly.ActivationException.Message); p.Incompatible = true; incompatiblePlugins.Add(p.SystemName); } @@ -243,172 +240,132 @@ void FinalizePlugin(PluginDescriptor p) } } - /// - /// Loads and parses the descriptors of all installed plugins - /// - /// All descriptors - private static IEnumerable LoadPluginDescriptors() - { - // TODO: Add verbose exception handling / raising here since this is happening on app startup and could - // prevent app from starting altogether - - var pluginsDir = new DirectoryInfo(CommonHelper.MapPath(PluginsLocation)); - - if (!pluginsDir.Exists) - { - pluginsDir.Create(); - return Enumerable.Empty(); - } - - // Determine all plugin folders: ~/Plugins/{SystemName} - var allPluginDirs = pluginsDir.EnumerateDirectories().ToArray() - .Where(x => !x.Name.IsMatch("bin") && !x.Name.IsMatch("_Backup")) - .OrderBy(x => x.Name) - .ToArray(); - - var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile().AsSynchronized(); - - // Load/activate all plugins - return allPluginDirs - .AsParallel() - .AsOrdered() - .Select(d => LoadPluginDescriptor(d, installedPluginSystemNames)) - .Where(x => x != null); - } - - ///// - ///// Loads and parses the descriptors of all installed plugins - ///// - ///// All descriptors - //private static Task ReadPluginDescriptors(BlockingCollection bag) - //{ - // // TODO: Add verbose exception handling / raising here since this is happening on app startup and could - // // prevent app from starting altogether - - // return Task.Factory.StartNew(() => - // { - // var pluginsDir = new DirectoryInfo(CommonHelper.MapPath(_pluginsPath)); - - // if (!pluginsDir.Exists) - // { - // pluginsDir.Create(); - // } - // else - // { - // // Determine all plugin folders: ~/Plugins/{SystemName} - // var allPluginDirs = pluginsDir.EnumerateDirectories().ToArray() - // .Where(x => !x.Name.IsMatch("bin") && !x.Name.IsMatch("_Backup")) - // .OrderBy(x => x.Name) - // .ToArray(); - - // var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile().AsSynchronized(); - - // // Load/activate all plugins - // allPluginDirs - // .AsParallel() - // .AsOrdered() - // .Select(d => LoadPluginDescriptor(d, installedPluginSystemNames)) - // .Where(x => x != null) - // .ForAll(x => bag.Add(x)); - // } - - // bag.CompleteAdding(); - // }); - //} - - private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollection installedPluginSystemNames) - { - var descriptionFile = new FileInfo(Path.Combine(d.FullName, "Description.txt")); - if (!descriptionFile.Exists) - { - return null; - } - - // Load descriptor file (Description.txt) - var descriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName); - - // Some validation - if (descriptor.SystemName.IsEmpty()) - { - throw new SmartException("The plugin descriptor '{0}' does not define a plugin system name. Try assigning the plugin a unique name and recompile.".FormatInvariant(descriptionFile.FullName)); - } - - if (descriptor.PluginFileName.IsEmpty()) - { - 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 = PluginsLocation + "/" + descriptor.FolderName; - - // Set 'Installed' property - descriptor.Installed = installedPluginSystemNames.Contains(descriptor.SystemName); - - // Ensure that version of plugin is valid - if (!IsAssumedCompatible(descriptor)) - { - // Set 'Incompatible' property and return - descriptor.Incompatible = true; - return descriptor; - } - - var skipDlls = new HashSet(new[] { "log4net.dll" }, StringComparer.OrdinalIgnoreCase); - - // Get list of all DLLs in plugin folders (not in 'bin' or '_Backup'!) - var pluginBinaries = descriptionFile.Directory.EnumerateFiles("*.dll", SearchOption.AllDirectories).ToArray() - .Where(x => IsPackagePluginFolder(x.Directory) && !skipDlls.Contains(x.Name)) - .OrderBy(x => x.Name) - .ToDictionarySafe(x => x.Name, StringComparer.OrdinalIgnoreCase); - - // Set 'OriginalAssemblyFile' property - descriptor.Assembly.OriginalFile = pluginBinaries.Get(descriptor.PluginFileName); - - if (descriptor.Assembly.OriginalFile == null) - { - throw new SmartException("The main assembly '{0}' for plugin '{1}' could not be found.".FormatInvariant(descriptor.PluginFileName, descriptor.SystemName)); - } - - // Load all other referenced local assemblies now - var otherAssemblyFiles = pluginBinaries - .Where(x => !x.Key.IsCaseInsensitiveEqual(descriptor.PluginFileName)) - .Select(x => x.Value); - - descriptor.ReferencedLocalAssemblies = otherAssemblyFiles.Select(x => new AssemblyReference { OriginalFile = x }).ToArray(); - - return descriptor; - } - - private static void ActivatePlugin(PluginDescriptor plugin) - { - // Init plugin type (only one plugin per assembly is allowed) - bool pluginFound = false; - bool preStarterFound = !plugin.Installed; - var exportedTypes = plugin.Assembly.Assembly.GetExportedTypes(); - - foreach (var t in exportedTypes) - { - if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass && !t.IsAbstract) - { - plugin.PluginClrType = t; - plugin.IsConfigurable = typeof(IConfigurable).IsAssignableFrom(t); - pluginFound = true; - } - else if (plugin.Installed && typeof(IPreApplicationStart).IsAssignableFrom(t) && !t.IsInterface && t.IsClass && !t.IsAbstract && t.HasDefaultConstructor()) - { - try - { - var preStarter = Activator.CreateInstance(t) as IPreApplicationStart; - preStarter.Start(); - } - catch { } - preStarterFound = true; - } - - if (pluginFound && preStarterFound) - { - break; - } - } - } + /// + /// Loads and parses the descriptors of all installed plugins + /// + /// All descriptors + private static IEnumerable LoadPluginDescriptors() + { + // TODO: Add verbose exception handling / raising here since this is happening on app startup and could + // prevent app from starting altogether + + var pluginsDir = new DirectoryInfo(CommonHelper.MapPath(PluginsLocation)); + + if (!pluginsDir.Exists) + { + pluginsDir.Create(); + return Enumerable.Empty(); + } + + // Determine all plugin folders: ~/Plugins/{SystemName} + var allPluginDirs = pluginsDir.EnumerateDirectories().ToArray() + .Where(x => !x.Name.IsMatch("bin") && !x.Name.IsMatch("_Backup")) + .OrderBy(x => x.Name) + .ToArray(); + + var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile().AsSynchronized(); + + // Load/activate all plugins + return allPluginDirs + .AsParallel() + .AsOrdered() + .Select(d => LoadPluginDescriptor(d, installedPluginSystemNames)) + .Where(x => x != null); + } + + private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollection installedPluginSystemNames) + { + var descriptionFile = new FileInfo(Path.Combine(d.FullName, "Description.txt")); + if (!descriptionFile.Exists) + { + return null; + } + + // Load descriptor file (Description.txt) + var descriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName); + + // Some validation + if (descriptor.SystemName.IsEmpty()) + { + throw new SmartException("The plugin descriptor '{0}' does not define a plugin system name. Try assigning the plugin a unique name and recompile.".FormatInvariant(descriptionFile.FullName)); + } + + if (descriptor.PluginFileName.IsEmpty()) + { + 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 = PluginsLocation + "/" + descriptor.FolderName; + + // Set 'Installed' property + descriptor.Installed = installedPluginSystemNames.Contains(descriptor.SystemName); + + // Ensure that version of plugin is valid + if (!IsAssumedCompatible(descriptor)) + { + // Set 'Incompatible' property and return + descriptor.Incompatible = true; + return descriptor; + } + + var skipDlls = new HashSet(new[] { "log4net.dll" }, StringComparer.OrdinalIgnoreCase); + + // Get list of all DLLs in plugin folders (not in 'bin' or '_Backup'!) + var pluginBinaries = descriptionFile.Directory.EnumerateFiles("*.dll", SearchOption.AllDirectories).ToArray() + .Where(x => IsPackagePluginFolder(x.Directory) && !skipDlls.Contains(x.Name)) + .OrderBy(x => x.Name) + .ToDictionarySafe(x => x.Name, StringComparer.OrdinalIgnoreCase); + + // Set 'OriginalAssemblyFile' property + descriptor.Assembly.OriginalFile = pluginBinaries.Get(descriptor.PluginFileName); + + if (descriptor.Assembly.OriginalFile == null) + { + throw new SmartException("The main assembly '{0}' for plugin '{1}' could not be found.".FormatInvariant(descriptor.PluginFileName, descriptor.SystemName)); + } + + // Load all other referenced local assemblies now + var otherAssemblyFiles = pluginBinaries + .Where(x => !x.Key.IsCaseInsensitiveEqual(descriptor.PluginFileName)) + .Select(x => x.Value); + + descriptor.ReferencedLocalAssemblies = otherAssemblyFiles.Select(x => new AssemblyReference { OriginalFile = x }).ToArray(); + + return descriptor; + } + + private static void ActivatePlugin(PluginDescriptor plugin) + { + // Init plugin type (only one plugin per assembly is allowed) + bool pluginFound = false; + bool preStarterFound = !plugin.Installed; + var exportedTypes = plugin.Assembly.Assembly.GetExportedTypes(); + + foreach (var t in exportedTypes) + { + if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass && !t.IsAbstract) + { + plugin.PluginClrType = t; + plugin.IsConfigurable = typeof(IConfigurable).IsAssignableFrom(t); + pluginFound = true; + } + else if (plugin.Installed && typeof(IPreApplicationStart).IsAssignableFrom(t) && !t.IsInterface && t.IsClass && !t.IsAbstract && t.HasDefaultConstructor()) + { + try + { + var preStarter = Activator.CreateInstance(t) as IPreApplicationStart; + preStarter.Start(); + } + catch { } + preStarterFound = true; + } + + if (pluginFound && preStarterFound) + { + break; + } + } + } /// /// Mark plugin as installed @@ -416,17 +373,17 @@ private static void ActivatePlugin(PluginDescriptor plugin) /// Plugin system name public static void MarkPluginAsInstalled(string systemName) { - Guard.NotEmpty(systemName, nameof(systemName)); + Guard.NotEmpty(systemName, nameof(systemName)); - var installedPluginSystemNames = GetInstalledPluginNames(); + var installedPluginSystemNames = GetInstalledPluginNames(); - bool installed = installedPluginSystemNames.Contains(systemName); - if (!installed) - { - installedPluginSystemNames.Add(systemName); - } + bool installed = installedPluginSystemNames.Contains(systemName); + if (!installed) + { + installedPluginSystemNames.Add(systemName); + } - PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames); + PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames); } /// @@ -435,16 +392,16 @@ public static void MarkPluginAsInstalled(string systemName) /// Plugin system name public static void MarkPluginAsUninstalled(string systemName) { - Guard.NotEmpty(systemName, nameof(systemName)); + Guard.NotEmpty(systemName, nameof(systemName)); - var installedPluginSystemNames = GetInstalledPluginNames(); - bool installed = installedPluginSystemNames.Contains(systemName); - if (installed) - { - installedPluginSystemNames.Remove(systemName); - } + var installedPluginSystemNames = GetInstalledPluginNames(); + bool installed = installedPluginSystemNames.Contains(systemName); + if (installed) + { + installedPluginSystemNames.Remove(systemName); + } - PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames); + PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames); } /// @@ -457,20 +414,20 @@ public static void MarkAllPluginsAsUninstalled() File.Delete(filePath); } - private static ICollection GetInstalledPluginNames() - { - var filePath = PluginFileParser.InstalledPluginsFilePath; - if (!File.Exists(filePath)) - { - using (File.Create(filePath)) - { - // We use 'using' to close the file after it's created - } - } - - var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(); - return installedPluginSystemNames; - } + private static ICollection GetInstalledPluginNames() + { + var filePath = PluginFileParser.InstalledPluginsFilePath; + if (!File.Exists(filePath)) + { + using (File.Create(filePath)) + { + // We use 'using' to close the file after it's created + } + } + + var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(); + return installedPluginSystemNames; + } /// /// Gets a value indicating whether a plugin is assumed @@ -485,68 +442,68 @@ private static ICollection GetInstalledPluginNames() /// true when the plugin is assumed to be compatible public static bool IsAssumedCompatible(PluginDescriptor descriptor) { - Guard.NotNull(descriptor, nameof(descriptor)); + Guard.NotNull(descriptor, nameof(descriptor)); - return IsAssumedCompatible(descriptor.MinAppVersion); + return IsAssumedCompatible(descriptor.MinAppVersion); } - /// - /// Gets a value indicating whether the given min. required app version is assumed - /// to be compatible with the current app version - /// - /// - /// A plugin is generally compatible when both app version and plugin's - /// MinorAppVersion are equal, OR - when app version is greater - it is - /// assumed to be compatible when no breaking changes occured since MinorAppVersion. - /// - /// The min. app version to check for - /// true when the extension's version is assumed to be compatible - public static bool IsAssumedCompatible(Version minAppVersion) - { - Guard.NotNull(minAppVersion, nameof(minAppVersion)); - - if (SmartStoreVersion.Version == minAppVersion) - { - return true; - } - - if (SmartStoreVersion.Version < minAppVersion) - { - return false; - } - - bool compatible = true; - - foreach (var version in SmartStoreVersion.BreakingChangesHistory) - { - if (version > minAppVersion) - { - // there was a breaking change in a version greater - // than plugin's MinorAppVersion. - compatible = false; - break; - } - - if (version <= minAppVersion) - { - break; - } - } - - return compatible; - } - - /// - /// Gets a value indicating whether a plugin - /// is registered and installed. - /// - /// The system name of the plugin to check for - /// true if the plugin exists, false otherwise - public static bool PluginExists(string systemName) - { - Guard.NotEmpty(systemName, nameof(systemName)); - return _referencedPlugins.ContainsKey(systemName); - } + /// + /// Gets a value indicating whether the given min. required app version is assumed + /// to be compatible with the current app version + /// + /// + /// A plugin is generally compatible when both app version and plugin's + /// MinorAppVersion are equal, OR - when app version is greater - it is + /// assumed to be compatible when no breaking changes occured since MinorAppVersion. + /// + /// The min. app version to check for + /// true when the extension's version is assumed to be compatible + public static bool IsAssumedCompatible(Version minAppVersion) + { + Guard.NotNull(minAppVersion, nameof(minAppVersion)); + + if (SmartStoreVersion.Version == minAppVersion) + { + return true; + } + + if (SmartStoreVersion.Version < minAppVersion) + { + return false; + } + + bool compatible = true; + + foreach (var version in SmartStoreVersion.BreakingChangesHistory) + { + if (version > minAppVersion) + { + // there was a breaking change in a version greater + // than plugin's MinorAppVersion. + compatible = false; + break; + } + + if (version <= minAppVersion) + { + break; + } + } + + return compatible; + } + + /// + /// Gets a value indicating whether a plugin + /// is registered and installed. + /// + /// The system name of the plugin to check for + /// true if the plugin exists, false otherwise + public static bool PluginExists(string systemName) + { + Guard.NotEmpty(systemName, nameof(systemName)); + return _referencedPlugins.ContainsKey(systemName); + } /// /// Gets a value indicating whether the plugin assembly @@ -559,34 +516,34 @@ public static bool IsActivePluginAssembly(Assembly assembly) return !_inactiveAssemblies.Contains(assembly); } - private static void SetPrivateEnvPath() - { - string envPath = Environment.GetEnvironmentVariable("PATH"); + private static void SetPrivateEnvPath() + { + string envPath = Environment.GetEnvironmentVariable("PATH"); - if (Environment.Is64BitProcess) - { - envPath = envPath.EnsureEndsWith(";") + Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "amd64"); - envPath = envPath.EnsureEndsWith(";") + Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "x64"); - } - else - { - envPath = envPath.EnsureEndsWith(";") + Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "x86"); - } + if (Environment.Is64BitProcess) + { + envPath = envPath.EnsureEndsWith(";") + Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "amd64"); + envPath = envPath.EnsureEndsWith(";") + Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "x64"); + } + else + { + envPath = envPath.EnsureEndsWith(";") + Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "x86"); + } - Environment.SetEnvironmentVariable("PATH", envPath, EnvironmentVariableTarget.Process); - } + Environment.SetEnvironmentVariable("PATH", envPath, EnvironmentVariableTarget.Process); + } - private static void SetNativeDllPath() - { - var currentDomain = AppDomain.CurrentDomain; - var privateBinPath = currentDomain.SetupInformation.PrivateBinPath.NullEmpty() ?? currentDomain.BaseDirectory; - var dir = Path.Combine(privateBinPath, Environment.Is64BitProcess ? "x64" : "x86"); + private static void SetNativeDllPath() + { + var currentDomain = AppDomain.CurrentDomain; + var privateBinPath = currentDomain.SetupInformation.PrivateBinPath.NullEmpty() ?? currentDomain.BaseDirectory; + var dir = Path.Combine(privateBinPath, Environment.Is64BitProcess ? "x64" : "x86"); Logger.DebugFormat("Setting native DLL path to '{0}'.", dir); - SetDllDirectory(dir); - } + SetDllDirectory(dir); + } /// /// Perform file deploy @@ -595,74 +552,74 @@ private static void SetNativeDllPath() /// Reference to the shadow copied Assembly private static Assembly Probe(AssemblyReference ar, PluginDescriptor d, bool shadowCopy) { - var file = ar.OriginalFile; - - try - { - if (file.Directory == null || file.Directory.Parent == null) - { - throw new InvalidOperationException("The plugin directory for the " + file.Name + - " file exists in a folder outside of the allowed SmartStore folder hierarchy"); - } - - ar.File = InitializeFullTrust(file, shadowCopy); - - // Load assembly locked, because concurrent load calls - even with different assemblies - - // will result in strange app init behaviour. - lock (_lock) - { - // We can now register the plugin definition - ar.Assembly = Assembly.Load(AssemblyName.GetAssemblyName(ar.File.FullName)); - - // Add the reference to the build manager - if (ar.Assembly != null) - { - // Loading assembly can fail in parallel loops. - // In this case, we'll probe again later in a sequential loop. - BuildManager.AddReferencedAssembly(ar.Assembly); - } - } - - ar.ActivationException = null; - } - catch (UnauthorizedAccessException ex) - { + var file = ar.OriginalFile; + + try + { + if (file.Directory == null || file.Directory.Parent == null) + { + throw new InvalidOperationException("The plugin directory for the " + file.Name + + " file exists in a folder outside of the allowed SmartStore folder hierarchy"); + } + + ar.File = InitializeFullTrust(file, shadowCopy); + + // Load assembly locked, because concurrent load calls - even with different assemblies - + // will result in strange app init behaviour. + lock (_lock) + { + // We can now register the plugin definition + ar.Assembly = Assembly.Load(AssemblyName.GetAssemblyName(ar.File.FullName)); + + // Add the reference to the build manager + if (ar.Assembly != null) + { + // Loading assembly can fail in parallel loops. + // In this case, we'll probe again later in a sequential loop. + BuildManager.AddReferencedAssembly(ar.Assembly); + } + } + + ar.ActivationException = null; + } + catch (UnauthorizedAccessException ex) + { Logger.Error(ex.Message); // Throw the exception if its UnauthorizedAccessException as this will - // be because we most likely cannot copy to the dynamic folder. - throw; - } - catch (ReflectionTypeLoadException ex) - { - var msg = string.Empty; - foreach (var e in ex.LoaderExceptions) - { - msg += e.Message + Environment.NewLine; - } - - ar.ActivationException = CreateException(msg, ex); - } - catch (Exception ex) - { - var msg = string.Empty; - for (var e = ex; e != null; e = e.InnerException) - { - msg += e.Message + Environment.NewLine; - } - - ar.ActivationException = CreateException(msg, ex); - } - - return ar.Assembly; - - Exception CreateException(string message, Exception innerException) - { - return new SmartException( - "Error loading plugin '{0}'".FormatInvariant(d.SystemName) + Environment.NewLine + message, - innerException); - } - } + // be because we most likely cannot copy to the dynamic folder. + throw; + } + catch (ReflectionTypeLoadException ex) + { + var msg = string.Empty; + foreach (var e in ex.LoaderExceptions) + { + msg += e.Message + Environment.NewLine; + } + + ar.ActivationException = CreateException(msg, ex); + } + catch (Exception ex) + { + var msg = string.Empty; + for (var e = ex; e != null; e = e.InnerException) + { + msg += e.Message + Environment.NewLine; + } + + ar.ActivationException = CreateException(msg, ex); + } + + return ar.Assembly; + + Exception CreateException(string message, Exception innerException) + { + return new SmartException( + "Error loading plugin '{0}'".FormatInvariant(d.SystemName) + Environment.NewLine + message, + innerException); + } + } /// /// Used to initialize plugins when running in Full Trust @@ -673,19 +630,19 @@ private static FileInfo InitializeFullTrust(FileInfo dll, bool shadowCopy) { var probedDll = new FileInfo(Path.Combine(_shadowCopyDir.FullName, dll.Name)); - // If instructed to not perform the copy, just return the path to where it is supposed to be - if (!shadowCopy && probedDll.Exists) - return probedDll; + // If instructed to not perform the copy, just return the path to where it is supposed to be + if (!shadowCopy && probedDll.Exists) + return probedDll; - try + try { File.Copy(dll.FullName, probedDll.FullName, true); } - catch (UnauthorizedAccessException) - { - throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedDll.Directory.FullName)); - } - catch (IOException) + catch (UnauthorizedAccessException) + { + throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedDll.Directory.FullName)); + } + catch (IOException) { Logger.WarnFormat("'{0}' is locked, attempting to rename.", probedDll.FullName); @@ -694,15 +651,15 @@ private static FileInfo InitializeFullTrust(FileInfo dll, bool shadowCopy) // Which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy try { - // If all else fails during the cleanup and we cannot copy over so we need to rename with a GUID - var deleteName = GetNewDeleteName(probedDll); - File.Move(probedDll.FullName, deleteName); + // If all else fails during the cleanup and we cannot copy over so we need to rename with a GUID + var deleteName = GetNewDeleteName(probedDll); + File.Move(probedDll.FullName, deleteName); + } + catch (UnauthorizedAccessException) + { + throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedDll.Directory.FullName)); } - catch (UnauthorizedAccessException) - { - throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedDll.Directory.FullName)); - } - catch (IOException exc) + catch (IOException exc) { throw new IOException(probedDll.FullName + " rename failed, cannot initialize plugin", exc); } @@ -726,5 +683,5 @@ private static bool IsPackagePluginFolder(DirectoryInfo folder) if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false; return true; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/DependentWidgetsAttribute.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/DependentWidgetsAttribute.cs index 384b6a7627..6fe776760e 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/DependentWidgetsAttribute.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/DependentWidgetsAttribute.cs @@ -1,28 +1,24 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Core.Plugins { - /// - /// Enables provider developers to specify one or many widgets, which - /// should automatically get (de)activated when the provider gets (de)activated. - /// Useful in scenarios where separate widgets are responsible for the displaying of provider data. - /// - /// - /// A widget should definitely NOT depend on multiple providers as the activation - /// only occurs on a single item base. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class DependentWidgetsAttribute : Attribute - { - public DependentWidgetsAttribute(params string[] widgetSystemNames) - { - WidgetSystemNames = widgetSystemNames; - } + /// + /// Enables provider developers to specify one or many widgets, which + /// should automatically get (de)activated when the provider gets (de)activated. + /// Useful in scenarios where separate widgets are responsible for the displaying of provider data. + /// + /// + /// A widget should definitely NOT depend on multiple providers as the activation + /// only occurs on a single item base. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class DependentWidgetsAttribute : Attribute + { + public DependentWidgetsAttribute(params string[] widgetSystemNames) + { + WidgetSystemNames = widgetSystemNames; + } - public string[] WidgetSystemNames { get; private set; } - } + public string[] WidgetSystemNames { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/DisplayOrderAttribute.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/DisplayOrderAttribute.cs index 33a8b3b13e..a60b19aeed 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/DisplayOrderAttribute.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/DisplayOrderAttribute.cs @@ -2,14 +2,14 @@ namespace SmartStore.Core.Plugins { - [AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=false)] - public sealed class DisplayOrderAttribute : Attribute - { - public DisplayOrderAttribute(int displayOrder) - { - DisplayOrder = displayOrder; - } + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class DisplayOrderAttribute : Attribute + { + public DisplayOrderAttribute(int displayOrder) + { + DisplayOrder = displayOrder; + } - public int DisplayOrder { get; set; } - } + public int DisplayOrder { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/FriendlyNameAttribute.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/FriendlyNameAttribute.cs index ec406c905b..c95cea4e2d 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/FriendlyNameAttribute.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/FriendlyNameAttribute.cs @@ -2,18 +2,18 @@ namespace SmartStore.Core.Plugins { - [AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=false)] - public sealed class FriendlyNameAttribute : Attribute - { - public FriendlyNameAttribute(string name) - { - Guard.NotNull(name, nameof(name)); + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class FriendlyNameAttribute : Attribute + { + public FriendlyNameAttribute(string name) + { + Guard.NotNull(name, nameof(name)); - Name = name; - } + Name = name; + } - public string Name { get; set; } + public string Name { get; set; } - public string Description { get; set; } - } + public string Description { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs index fae747723b..409e07edd2 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs @@ -2,42 +2,33 @@ namespace SmartStore.Core.Plugins { - public interface IProvider - { - } - - public sealed class Provider where TProvider : IProvider - { - private readonly Lazy _lazy; - - public Provider(Lazy lazy) - { - this._lazy = lazy; - } - - public TProvider Value - { - get { return _lazy.Value; } - } - - public ProviderMetadata Metadata - { - get { return _lazy.Metadata; } - } - - public bool IsValueCreated - { - get { return _lazy.IsValueCreated; } - } - - public Lazy ToLazy() - { - return _lazy; - } - - public override string ToString() - { - return _lazy.Metadata.SystemName; - } - } + public interface IProvider + { + } + + public sealed class Provider where TProvider : IProvider + { + private readonly Lazy _lazy; + + public Provider(Lazy lazy) + { + this._lazy = lazy; + } + + public TProvider Value => _lazy.Value; + + public ProviderMetadata Metadata => _lazy.Metadata; + + public bool IsValueCreated => _lazy.IsValueCreated; + + public Lazy ToLazy() + { + return _lazy; + } + + public override string ToString() + { + return _lazy.Metadata.SystemName; + } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/IProviderManager.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/IProviderManager.cs index c2a10fe5b8..4585578355 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/IProviderManager.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/IProviderManager.cs @@ -1,17 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; namespace SmartStore.Core.Plugins { - public interface IProviderManager - { - Provider GetProvider(string systemName, int storeId = 0) where TProvider : IProvider; + public interface IProviderManager + { + Provider GetProvider(string systemName, int storeId = 0) where TProvider : IProvider; - Provider GetProvider(string systemName, int storeId = 0); + Provider GetProvider(string systemName, int storeId = 0); - IEnumerable> GetAllProviders(int storeId = 0) where TProvider : IProvider; + IEnumerable> GetAllProviders(int storeId = 0) where TProvider : IProvider; - IEnumerable> GetAllProviders(int storeId = 0); - } + IEnumerable> GetAllProviders(int storeId = 0); + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/IUserEditable.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/IUserEditable.cs index 8544372396..453b22f101 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/IUserEditable.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/IUserEditable.cs @@ -1,8 +1,6 @@ -using System; - -namespace SmartStore.Core.Plugins +namespace SmartStore.Core.Plugins { - public interface IUserEditable - { - } + public interface IUserEditable + { + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/IsHiddenAttribute.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/IsHiddenAttribute.cs index 6ba32b9d5a..b2b0036669 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/IsHiddenAttribute.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/IsHiddenAttribute.cs @@ -2,14 +2,14 @@ namespace SmartStore.Core.Plugins { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class IsHiddenAttribute : Attribute - { - public IsHiddenAttribute(bool isHidden) - { - IsHidden = isHidden; - } + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class IsHiddenAttribute : Attribute + { + public IsHiddenAttribute(bool isHidden) + { + IsHidden = isHidden; + } - public bool IsHidden { get; set; } - } + public bool IsHidden { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs index 8b806c43fc..ab44fa593b 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs @@ -4,143 +4,143 @@ namespace SmartStore.Core.Plugins { - public interface IProviderMetadata - { - /// - /// Gets the provider system name - /// - string SystemName { get; } - - /// - /// Gets the resource key pattern for user data (e.g. FriendlyName) - /// - /// - /// Plugins.{1}.{0} > 0 = provider system name, 1 = propertyname - /// - string ResourceKeyPattern { get; } - - /// - /// Gets the provider friendly name - /// - string FriendlyName { get; } - - /// - /// Gets the provider description - /// - string Description { get; } - - /// - /// Gets the provider display order - /// - int DisplayOrder { get; } - } - - public class ProviderMetadata : IProviderMetadata - { - /// - /// Gets or sets the provider type - /// - public Type ProviderType { get; set; } - - /// - /// Gets or sets the provider's group name - /// - public string GroupName { get; set; } - - /// - /// Gets or sets the provider system name - /// - public string SystemName { get; set; } - - /// - /// Gets or sets the resource key pattern for user data (e.g. FriendlyName) - /// - /// - /// Plugins.{1}.{0} > 0 = provider system name, 1 = propertyname - /// - public string ResourceKeyPattern { get; set; } - - /// - /// Gets or sets the setting key pattern for user data (e.g. DisplayOrder) - /// - /// - /// Plugins.{0}.{1} > 0 = provider system name, 1 = propertyname - /// - public string SettingKeyPattern { get; set; } - - /// - /// Gets or sets the provider friendly name - /// - public string FriendlyName { get; set; } - - /// - /// Gets or sets the provider description - /// - public string Description { get; set; } - - /// - /// Gets or sets the display order - /// - public int DisplayOrder { get; set; } - - /// - /// Gets or sets a value indicating whether the provider is configurable (by implementing ) - /// - public bool IsConfigurable { get; set; } - - /// - /// Gets or sets a value indicating whether the provider is editable by the user (by implementing ) - /// - /// - /// A provider is editable if the user is allowed to change display order and/or localize display name - /// - public bool IsEditable { get; set; } - - /// - /// Gets or sets a value indicating whether the provider is hidden (by decorating with ) - /// - public bool IsHidden { get; set; } - - /// - /// Gets or sets flags that reflects what features of export data processing is supported by a provider - /// - public ExportFeatures ExportFeatures { get; set; } - - /// - /// Gets or sets an array of widget system names, which depend on the current provider - /// - /// - /// Dependent widgets get automatically (de)activated when their parent providers get (de)activated - /// - public string[] DependentWidgets { get; set; } - - /// - /// Gets or sets the instance in which the provider is implemented - /// - /// The value is null, if the provider is part of the application core - public PluginDescriptor PluginDescriptor { get; set; } - - public string GetSettingKey(string name) - { - return SettingKeyPattern.FormatWith(SystemName, name); - } - - public override string ToString() - { - return "Provider '{0}' - {1}".FormatCurrent(SystemName, FriendlyName); - } - - public override bool Equals(object obj) - { - var other = obj as ProviderMetadata; - return other != null && - SystemName != null && - SystemName.Equals(other.SystemName); - } - - public override int GetHashCode() - { - return HashCodeCombiner.Start().Add(typeof(ProviderMetadata)).Add(SystemName).CombinedHash; - } - } + public interface IProviderMetadata + { + /// + /// Gets the provider system name + /// + string SystemName { get; } + + /// + /// Gets the resource key pattern for user data (e.g. FriendlyName) + /// + /// + /// Plugins.{1}.{0} > 0 = provider system name, 1 = propertyname + /// + string ResourceKeyPattern { get; } + + /// + /// Gets the provider friendly name + /// + string FriendlyName { get; } + + /// + /// Gets the provider description + /// + string Description { get; } + + /// + /// Gets the provider display order + /// + int DisplayOrder { get; } + } + + public class ProviderMetadata : IProviderMetadata + { + /// + /// Gets or sets the provider type + /// + public Type ProviderType { get; set; } + + /// + /// Gets or sets the provider's group name + /// + public string GroupName { get; set; } + + /// + /// Gets or sets the provider system name + /// + public string SystemName { get; set; } + + /// + /// Gets or sets the resource key pattern for user data (e.g. FriendlyName) + /// + /// + /// Plugins.{1}.{0} > 0 = provider system name, 1 = propertyname + /// + public string ResourceKeyPattern { get; set; } + + /// + /// Gets or sets the setting key pattern for user data (e.g. DisplayOrder) + /// + /// + /// Plugins.{0}.{1} > 0 = provider system name, 1 = propertyname + /// + public string SettingKeyPattern { get; set; } + + /// + /// Gets or sets the provider friendly name + /// + public string FriendlyName { get; set; } + + /// + /// Gets or sets the provider description + /// + public string Description { get; set; } + + /// + /// Gets or sets the display order + /// + public int DisplayOrder { get; set; } + + /// + /// Gets or sets a value indicating whether the provider is configurable (by implementing ) + /// + public bool IsConfigurable { get; set; } + + /// + /// Gets or sets a value indicating whether the provider is editable by the user (by implementing ) + /// + /// + /// A provider is editable if the user is allowed to change display order and/or localize display name + /// + public bool IsEditable { get; set; } + + /// + /// Gets or sets a value indicating whether the provider is hidden (by decorating with ) + /// + public bool IsHidden { get; set; } + + /// + /// Gets or sets flags that reflects what features of export data processing is supported by a provider + /// + public ExportFeatures ExportFeatures { get; set; } + + /// + /// Gets or sets an array of widget system names, which depend on the current provider + /// + /// + /// Dependent widgets get automatically (de)activated when their parent providers get (de)activated + /// + public string[] DependentWidgets { get; set; } + + /// + /// Gets or sets the instance in which the provider is implemented + /// + /// The value is null, if the provider is part of the application core + public PluginDescriptor PluginDescriptor { get; set; } + + public string GetSettingKey(string name) + { + return SettingKeyPattern.FormatWith(SystemName, name); + } + + public override string ToString() + { + return "Provider '{0}' - {1}".FormatCurrent(SystemName, FriendlyName); + } + + public override bool Equals(object obj) + { + var other = obj as ProviderMetadata; + return other != null && + SystemName != null && + SystemName.Equals(other.SystemName); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start().Add(typeof(ProviderMetadata)).Add(SystemName).CombinedHash; + } + } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/SystemNameAttribute.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/SystemNameAttribute.cs index 4bbb2e7a4e..0946be4c34 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/SystemNameAttribute.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/SystemNameAttribute.cs @@ -2,15 +2,15 @@ namespace SmartStore.Core.Plugins { - [AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=false)] - public sealed class SystemNameAttribute : Attribute - { - public SystemNameAttribute(string name) - { - Guard.NotEmpty(name, nameof(name)); - Name = name; - } + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class SystemNameAttribute : Attribute + { + public SystemNameAttribute(string name) + { + Guard.NotEmpty(name, nameof(name)); + Name = name; + } - public string Name { get; set; } - } + public string Name { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Properties/AssemblyInfo.cs b/src/Libraries/SmartStore.Core/Properties/AssemblyInfo.cs index ddde4c0b4a..7f42ecee30 100644 --- a/src/Libraries/SmartStore.Core/Properties/AssemblyInfo.cs +++ b/src/Libraries/SmartStore.Core/Properties/AssemblyInfo.cs @@ -2,6 +2,8 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SmartStore.Core")] +[assembly: InternalsVisibleTo("SmartStore.Services")] +[assembly: InternalsVisibleTo("SmartStore.Data")] [assembly: InternalsVisibleTo("SmartStore.Core.Tests")] [assembly: InternalsVisibleTo("SmartStore.Services.Tests")] [assembly: InternalsVisibleTo("SmartStore.Web.Mvc.Tests")] diff --git a/src/Libraries/SmartStore.Core/RouteInfo.cs b/src/Libraries/SmartStore.Core/RouteInfo.cs index 84827ddd44..e98f9d9004 100644 --- a/src/Libraries/SmartStore.Core/RouteInfo.cs +++ b/src/Libraries/SmartStore.Core/RouteInfo.cs @@ -1,29 +1,28 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Web.Routing; using Newtonsoft.Json; namespace SmartStore { - [JsonConverter(typeof(RouteInfoConverter))] - public class RouteInfo - { - public RouteInfo(RouteInfo cloneFrom) - : this(cloneFrom.Action, cloneFrom.Controller, new RouteValueDictionary(cloneFrom.RouteValues)) - { - Guard.NotNull(cloneFrom, nameof(cloneFrom)); - } + [JsonConverter(typeof(RouteInfoConverter))] + public class RouteInfo + { + public RouteInfo(RouteInfo cloneFrom) + : this(cloneFrom.Action, cloneFrom.Controller, new RouteValueDictionary(cloneFrom.RouteValues)) + { + Guard.NotNull(cloneFrom, nameof(cloneFrom)); + } public RouteInfo(string action, object routeValues) : this(action, null, routeValues) { } - public RouteInfo(string action, string controller, object routeValues) - : this(action, controller, new RouteValueDictionary(routeValues)) - { - } + public RouteInfo(string action, string controller, object routeValues) + : this(action, controller, new RouteValueDictionary(routeValues)) + { + } public RouteInfo(string action, IDictionary routeValues) : this(action, null, routeValues) @@ -31,103 +30,100 @@ public RouteInfo(string action, IDictionary routeValues) } public RouteInfo(string action, string controller, IDictionary routeValues) - : this(action, controller, new RouteValueDictionary(routeValues)) - { - Guard.NotNull(routeValues, nameof(routeValues)); - } + : this(action, controller, new RouteValueDictionary(routeValues)) + { + Guard.NotNull(routeValues, nameof(routeValues)); + } public RouteInfo(string action, RouteValueDictionary routeValues) : this(action, null, routeValues) { } - [JsonConstructor] + [JsonConstructor] public RouteInfo(string action, string controller, RouteValueDictionary routeValues) - { - Guard.NotEmpty(action, nameof(action)); - Guard.NotNull(routeValues, nameof(routeValues)); - - this.Action = action; - this.Controller = controller; - this.RouteValues = routeValues; - } - - public string Action - { - get; - private set; - } - - public string Controller - { - get; - private set; - } - - public RouteValueDictionary RouteValues - { - get; - private set; - } - } - - #region JsonConverter - - public class RouteInfoConverter : JsonConverter - { - public override bool CanWrite - { - get { return false; } - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(RouteInfo); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - string action = null; - string controller = null; - RouteValueDictionary routeValues = null; - - reader.Read(); - while (reader.TokenType == JsonToken.PropertyName) - { - string a = reader.Value.ToString(); - if (string.Equals(a, "Action", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - action = serializer.Deserialize(reader); - } - else if (string.Equals(a, "Controller", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - controller = serializer.Deserialize(reader); - } - else if (string.Equals(a, "RouteValues", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - routeValues = serializer.Deserialize(reader); - } - else - { - reader.Skip(); - } - - reader.Read(); - } - - var routeInfo = Activator.CreateInstance(objectType, new object[] { action, controller, routeValues }); - - return routeInfo; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotSupportedException(); - } - } - - #endregion + { + Guard.NotEmpty(action, nameof(action)); + Guard.NotNull(routeValues, nameof(routeValues)); + + this.Action = action; + this.Controller = controller; + this.RouteValues = routeValues; + } + + public string Action + { + get; + private set; + } + + public string Controller + { + get; + private set; + } + + public RouteValueDictionary RouteValues + { + get; + private set; + } + } + + #region JsonConverter + + public class RouteInfoConverter : JsonConverter + { + public override bool CanWrite => false; + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(RouteInfo); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string action = null; + string controller = null; + RouteValueDictionary routeValues = null; + + reader.Read(); + while (reader.TokenType == JsonToken.PropertyName) + { + string a = reader.Value.ToString(); + if (string.Equals(a, "Action", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + action = serializer.Deserialize(reader); + } + else if (string.Equals(a, "Controller", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + controller = serializer.Deserialize(reader); + } + else if (string.Equals(a, "RouteValues", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + routeValues = serializer.Deserialize(reader); + } + else + { + reader.Skip(); + } + + reader.Read(); + } + + var routeInfo = Activator.CreateInstance(objectType, new object[] { action, controller, routeValues }); + + return routeInfo; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } + + #endregion } diff --git a/src/Libraries/SmartStore.Core/Rules/Domain/RuleEntity.cs b/src/Libraries/SmartStore.Core/Rules/Domain/RuleEntity.cs index 7fc5677ce9..587be18e1f 100644 --- a/src/Libraries/SmartStore.Core/Rules/Domain/RuleEntity.cs +++ b/src/Libraries/SmartStore.Core/Rules/Domain/RuleEntity.cs @@ -37,9 +37,6 @@ public partial class RuleEntity : BaseEntity public int DisplayOrder { get; set; } [NotMapped] - public bool IsGroup - { - get => RuleType.IsCaseInsensitiveEqual("Group"); - } + public bool IsGroup => RuleType.IsCaseInsensitiveEqual("Group"); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Rules/Domain/RuleSetEntity.cs b/src/Libraries/SmartStore.Core/Rules/Domain/RuleSetEntity.cs index 232b478a02..96d4bb591e 100644 --- a/src/Libraries/SmartStore.Core/Rules/Domain/RuleSetEntity.cs +++ b/src/Libraries/SmartStore.Core/Rules/Domain/RuleSetEntity.cs @@ -53,8 +53,8 @@ public partial class RuleSetEntity : BaseEntity, IAuditable public virtual ICollection Rules { - get { return _rules ?? (_rules = new HashSet()); } - protected internal set { _rules = value; } + get => _rules ?? (_rules = new HashSet()); + protected internal set => _rules = value; } /// @@ -62,8 +62,8 @@ public virtual ICollection Rules /// public virtual ICollection Discounts { - get { return _discounts ?? (_discounts = new HashSet()); } - protected set { _discounts = value; } + get => _discounts ?? (_discounts = new HashSet()); + protected set => _discounts = value; } /// @@ -71,8 +71,8 @@ public virtual ICollection Discounts /// public virtual ICollection ShippingMethods { - get { return _shippingMethods ?? (_shippingMethods = new HashSet()); } - protected set { _shippingMethods = value; } + get => _shippingMethods ?? (_shippingMethods = new HashSet()); + protected set => _shippingMethods = value; } /// @@ -80,8 +80,8 @@ public virtual ICollection ShippingMethods /// public virtual ICollection PaymentMethods { - get { return _paymentMethods ?? (_paymentMethods = new HashSet()); } - protected set { _paymentMethods = value; } + get => _paymentMethods ?? (_paymentMethods = new HashSet()); + protected set => _paymentMethods = value; } /// @@ -89,8 +89,8 @@ public virtual ICollection PaymentMethods /// public virtual ICollection CustomerRoles { - get { return _customerRoles ?? (_customerRoles = new HashSet()); } - protected set { _customerRoles = value; } + get => _customerRoles ?? (_customerRoles = new HashSet()); + protected set => _customerRoles = value; } /// @@ -98,8 +98,8 @@ public virtual ICollection CustomerRoles /// public virtual ICollection Categories { - get { return _categories ?? (_categories = new HashSet()); } - protected set { _categories = value; } + get => _categories ?? (_categories = new HashSet()); + protected set => _categories = value; } } } diff --git a/src/Libraries/SmartStore.Core/Rules/Exceptions/InvalidRuleOperatorException.cs b/src/Libraries/SmartStore.Core/Rules/Exceptions/InvalidRuleOperatorException.cs index 3bb110bf4d..2361ffe53b 100644 --- a/src/Libraries/SmartStore.Core/Rules/Exceptions/InvalidRuleOperatorException.cs +++ b/src/Libraries/SmartStore.Core/Rules/Exceptions/InvalidRuleOperatorException.cs @@ -1,14 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Rules { public sealed class InvalidRuleOperatorException : NotSupportedException { - public InvalidRuleOperatorException(RuleExpression expression) + public InvalidRuleOperatorException(RuleExpression expression) : base("The rule type '{0}' does not support the rule operator '{1}'.".FormatInvariant(expression.Descriptor.Name, expression.Operator)) { // TODO: expression.Descriptor.Name could be null or empty (?) diff --git a/src/Libraries/SmartStore.Core/Rules/Filters/ExpressionHelper.cs b/src/Libraries/SmartStore.Core/Rules/Filters/ExpressionHelper.cs index e14befde6d..4ae4d8fa92 100644 --- a/src/Libraries/SmartStore.Core/Rules/Filters/ExpressionHelper.cs +++ b/src/Libraries/SmartStore.Core/Rules/Filters/ExpressionHelper.cs @@ -151,7 +151,7 @@ public static Expression CreateConstantExpression(object value, Type type = null public static LambdaExpression CreateLambdaExpression(ParameterExpression p, Expression body) { return Expression.Lambda( - new FilterExpressionVisitor(p).Visit(body), + new FilterExpressionVisitor(p).Visit(body), new[] { p }); } } diff --git a/src/Libraries/SmartStore.Core/Rules/Filters/FilterDescriptor.cs b/src/Libraries/SmartStore.Core/Rules/Filters/FilterDescriptor.cs index cba9a28dc7..b27e7df330 100644 --- a/src/Libraries/SmartStore.Core/Rules/Filters/FilterDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Rules/Filters/FilterDescriptor.cs @@ -1,13 +1,7 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Orders; namespace SmartStore.Rules.Filters { @@ -28,7 +22,7 @@ public virtual Expression GetExpression(RuleOperator op, Expression valueExpress } } - public class FilterDescriptor : FilterDescriptor where T : class + public class FilterDescriptor : FilterDescriptor where T : class { public FilterDescriptor(Expression> expression) : base(expression) // TODO { @@ -81,12 +75,12 @@ public override Expression GetExpression(RuleOperator op, Expression valueExpres } } - public class AnyFilterDescriptor : PredicateFilterDescriptor - where T : class + public class AnyFilterDescriptor : PredicateFilterDescriptor + where T : class where TAny : class { public AnyFilterDescriptor( - Expression>> path, + Expression>> path, Expression> anyPredicate) : base("Any", path, anyPredicate) { diff --git a/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpression.cs b/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpression.cs index 9d664f9042..1d3f1df065 100644 --- a/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpression.cs +++ b/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpression.cs @@ -1,7 +1,4 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace SmartStore.Rules.Filters { @@ -18,7 +15,7 @@ protected virtual Expression CreateBodyExpression(ParameterExpression node, bool { return this.Descriptor.GetExpression( this.Operator, - ExpressionHelper.CreateValueExpression(Descriptor.MemberExpression.Body.Type, this.Value), + ExpressionHelper.CreateValueExpression(Descriptor.MemberExpression.Body.Type, this.Value), liftToNull); } } diff --git a/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionGroup.cs b/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionGroup.cs index 94b002fb7b..a5e13fc09b 100644 --- a/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionGroup.cs +++ b/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionGroup.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Rules.Filters { @@ -27,10 +25,7 @@ public FilterExpressionGroup(Type entityType) public bool IsSubGroup { get; set; } public IRuleProvider Provider { get; set; } - public IEnumerable Expressions - { - get => _expressions; - } + public IEnumerable Expressions => _expressions; public void AddExpressions(params IRuleExpression[] expressions) { diff --git a/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionVisitor.cs b/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionVisitor.cs index d65a823a34..31ab7948b5 100644 --- a/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionVisitor.cs +++ b/src/Libraries/SmartStore.Core/Rules/Filters/FilterExpressionVisitor.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; +using System.Linq.Expressions; namespace SmartStore.Rules.Filters { diff --git a/src/Libraries/SmartStore.Core/Rules/Filters/QueryableExtensions.cs b/src/Libraries/SmartStore.Core/Rules/Filters/QueryableExtensions.cs index f82b7e60d6..7352727c7b 100644 --- a/src/Libraries/SmartStore.Core/Rules/Filters/QueryableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Rules/Filters/QueryableExtensions.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Rules.Filters { diff --git a/src/Libraries/SmartStore.Core/Rules/IRuleConstraint.cs b/src/Libraries/SmartStore.Core/Rules/IRuleConstraint.cs index b303e4cf28..7ecb6fbb36 100644 --- a/src/Libraries/SmartStore.Core/Rules/IRuleConstraint.cs +++ b/src/Libraries/SmartStore.Core/Rules/IRuleConstraint.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SmartStore.Rules +namespace SmartStore.Rules { public interface IRuleConstraint { diff --git a/src/Libraries/SmartStore.Core/Rules/IRuleProvider.cs b/src/Libraries/SmartStore.Core/Rules/IRuleProvider.cs index 8dcfbfd0c7..fd0039c0a6 100644 --- a/src/Libraries/SmartStore.Core/Rules/IRuleProvider.cs +++ b/src/Libraries/SmartStore.Core/Rules/IRuleProvider.cs @@ -45,6 +45,7 @@ protected virtual void ConvertRule(RuleEntity entity, RuleExpression expression) } expression.Id = entity.Id; + expression.RuleSetId = entity.RuleSetId; expression.Descriptor = descriptor; expression.Operator = entity.Operator; expression.RawValue = entity.Value; diff --git a/src/Libraries/SmartStore.Core/Rules/IRuleVisitor.cs b/src/Libraries/SmartStore.Core/Rules/IRuleVisitor.cs index 4c74dd3609..70b7cbfe4b 100644 --- a/src/Libraries/SmartStore.Core/Rules/IRuleVisitor.cs +++ b/src/Libraries/SmartStore.Core/Rules/IRuleVisitor.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using SmartStore.Rules.Domain; +using SmartStore.Rules.Domain; namespace SmartStore.Rules { diff --git a/src/Libraries/SmartStore.Core/Rules/Operators/CommonOperators.cs b/src/Libraries/SmartStore.Core/Rules/Operators/CommonOperators.cs index de57ade318..a732b49424 100644 --- a/src/Libraries/SmartStore.Core/Rules/Operators/CommonOperators.cs +++ b/src/Libraries/SmartStore.Core/Rules/Operators/CommonOperators.cs @@ -1,12 +1,11 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using SmartStore.Rules.Filters; namespace SmartStore.Rules.Operators { internal sealed class EqualOperator : RuleOperator { - internal EqualOperator() + internal EqualOperator() : base("=") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) @@ -23,7 +22,7 @@ protected override Expression GenerateExpression(Expression left, Expression rig internal sealed class NotEqualOperator : RuleOperator { - internal NotEqualOperator() + internal NotEqualOperator() : base("!=") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) @@ -40,26 +39,26 @@ protected override Expression GenerateExpression(Expression left, Expression rig internal sealed class IsNullOperator : RuleOperator { - internal IsNullOperator() + internal IsNullOperator() : base("IsNull") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) { return Expression.Equal( - left, + left, ExpressionHelper.CreateConstantExpression(null, GetBodyType(left))); } } internal sealed class IsNotNullOperator : RuleOperator { - internal IsNotNullOperator() + internal IsNotNullOperator() : base("IsNotNull") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) { return Expression.NotEqual( - left, + left, ExpressionHelper.CreateConstantExpression(null, GetBodyType(left))); } } diff --git a/src/Libraries/SmartStore.Core/Rules/Operators/ListOperators.cs b/src/Libraries/SmartStore.Core/Rules/Operators/ListOperators.cs index 3a0d101e17..12b3b240ca 100644 --- a/src/Libraries/SmartStore.Core/Rules/Operators/ListOperators.cs +++ b/src/Libraries/SmartStore.Core/Rules/Operators/ListOperators.cs @@ -13,7 +13,7 @@ internal NotInOperator() internal class InOperator : RuleOperator { - internal InOperator() + internal InOperator() : this("In", false) { } protected InOperator(string op, bool negate) @@ -39,7 +39,7 @@ protected override Expression GenerateExpression(Expression left /* member expre var containsMethod = ExpressionHelper.GetCollectionContainsMethod(itemType); return Expression.Equal( - Expression.Call(right, containsMethod, left), + Expression.Call(right, containsMethod, left), Negate ? ExpressionHelper.FalseLiteral : ExpressionHelper.TrueLiteral); } diff --git a/src/Libraries/SmartStore.Core/Rules/Operators/NumericOperators.cs b/src/Libraries/SmartStore.Core/Rules/Operators/NumericOperators.cs index be536465b1..0e6435c69e 100644 --- a/src/Libraries/SmartStore.Core/Rules/Operators/NumericOperators.cs +++ b/src/Libraries/SmartStore.Core/Rules/Operators/NumericOperators.cs @@ -1,11 +1,10 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace SmartStore.Rules.Operators { internal sealed class LessThanOperator : RuleOperator { - internal LessThanOperator() + internal LessThanOperator() : base("<") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) @@ -16,7 +15,7 @@ protected override Expression GenerateExpression(Expression left, Expression rig internal sealed class LessThanOrEqualOperator : RuleOperator { - internal LessThanOrEqualOperator() + internal LessThanOrEqualOperator() : base("<=") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) @@ -27,7 +26,7 @@ protected override Expression GenerateExpression(Expression left, Expression rig internal sealed class GreaterThanOperator : RuleOperator { - internal GreaterThanOperator() + internal GreaterThanOperator() : base(">") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) @@ -38,7 +37,7 @@ protected override Expression GenerateExpression(Expression left, Expression rig internal sealed class GreaterThanOrEqualOperator : RuleOperator { - internal GreaterThanOrEqualOperator() + internal GreaterThanOrEqualOperator() : base(">=") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) diff --git a/src/Libraries/SmartStore.Core/Rules/Operators/StringOperators.cs b/src/Libraries/SmartStore.Core/Rules/Operators/StringOperators.cs index 1ffa12af6b..731871e018 100644 --- a/src/Libraries/SmartStore.Core/Rules/Operators/StringOperators.cs +++ b/src/Libraries/SmartStore.Core/Rules/Operators/StringOperators.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using SmartStore.Rules.Filters; namespace SmartStore.Rules.Operators @@ -66,28 +65,28 @@ protected override Expression GenerateExpression(Expression left, Expression rig internal sealed class StartsWithOperator : RuleOperator { - internal StartsWithOperator() + internal StartsWithOperator() : base("StartsWith") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) { var methodInfo = ExpressionHelper.StringStartsWithMethod; return Expression.Equal( - methodInfo.ToCaseInsensitiveStringMethodCall(left, right, liftToNull), + methodInfo.ToCaseInsensitiveStringMethodCall(left, right, liftToNull), ExpressionHelper.TrueLiteral); } } internal sealed class EndsWithOperator : RuleOperator { - internal EndsWithOperator() + internal EndsWithOperator() : base("EndsWith") { } protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) { var methodInfo = ExpressionHelper.StringEndsWithMethod; return Expression.Equal( - methodInfo.ToCaseInsensitiveStringMethodCall(left, right, liftToNull), + methodInfo.ToCaseInsensitiveStringMethodCall(left, right, liftToNull), ExpressionHelper.TrueLiteral); } } @@ -100,7 +99,7 @@ internal NotContainsOperator() internal class ContainsOperator : RuleOperator { - internal ContainsOperator() + internal ContainsOperator() : this("Contains", false) { } protected ContainsOperator(string op, bool negate) @@ -114,7 +113,7 @@ protected ContainsOperator(string op, bool negate) protected override Expression GenerateExpression(Expression left, Expression right, bool liftToNull) { return Expression.Equal( - ExpressionHelper.StringContainsMethod.ToCaseInsensitiveStringMethodCall(left, right, liftToNull), + ExpressionHelper.StringContainsMethod.ToCaseInsensitiveStringMethodCall(left, right, liftToNull), Negate ? ExpressionHelper.FalseLiteral : ExpressionHelper.TrueLiteral); } } diff --git a/src/Libraries/SmartStore.Core/Rules/Rendering/IRuleOptionsProvider.cs b/src/Libraries/SmartStore.Core/Rules/Rendering/IRuleOptionsProvider.cs index 00e3dcc4d8..001a020e31 100644 --- a/src/Libraries/SmartStore.Core/Rules/Rendering/IRuleOptionsProvider.cs +++ b/src/Libraries/SmartStore.Core/Rules/Rendering/IRuleOptionsProvider.cs @@ -22,5 +22,23 @@ public partial interface IRuleOptionsProvider /// Optional search term entered by user in select control. /// Rule options result. RuleOptionsResult GetOptions(RuleOptionsRequestReason reason, IRuleExpression expression, int pageIndex, int pageSize, string searchTerm); + + /// + /// Gets options for a rule. + /// + /// The reason for the request. + /// Rule descriptor. + /// Expression value. + /// Page index if provided options are paged. + /// Page size if provided options are paged. + /// Optional search term entered by user in select control. + /// Rule options result. + RuleOptionsResult GetOptions( + RuleOptionsRequestReason reason, + RuleDescriptor descriptor, + string value, + int pageIndex, + int pageSize, + string searchTerm); } } diff --git a/src/Libraries/SmartStore.Core/Rules/Rendering/RuleTemplateSelector.cs b/src/Libraries/SmartStore.Core/Rules/Rendering/RuleTemplateSelector.cs index 3d8dc1680f..fe6353cd0f 100644 --- a/src/Libraries/SmartStore.Core/Rules/Rendering/RuleTemplateSelector.cs +++ b/src/Libraries/SmartStore.Core/Rules/Rendering/RuleTemplateSelector.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace SmartStore.Rules +namespace SmartStore.Rules { public class RuleTemplateSelector : IRuleTemplateSelector { @@ -19,8 +16,8 @@ public RuleTemplateInfo GetTemplate(RuleDescriptor descriptor) protected virtual string GetValueTemplateName(RuleDescriptor descriptor) { - if (descriptor.Metadata.TryGetValue("ValueTemplateName", out var val) && - val is string name && + if (descriptor.Metadata.TryGetValue("ValueTemplateName", out var val) && + val is string name && name.HasValue()) { return name; diff --git a/src/Libraries/SmartStore.Core/Rules/RuleDescriptor.cs b/src/Libraries/SmartStore.Core/Rules/RuleDescriptor.cs index 203a742a6f..8bba30e8e6 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleDescriptor.cs @@ -10,6 +10,7 @@ public abstract class RuleDescriptor protected RuleDescriptor(RuleScope scope) { Scope = scope; + Constraints = new IRuleConstraint[0]; Metadata = new Dictionary(); } @@ -19,13 +20,7 @@ protected RuleDescriptor(RuleScope scope) public string Description { get; set; } public string GroupKey { get; set; } - public bool IsValid - { - get - { - return false == (this is InvalidRuleDescriptor); - } - } + public bool IsValid => false == (this is InvalidRuleDescriptor); public RuleType RuleType { get; set; } public RuleValueSelectList SelectList { get; set; } @@ -48,7 +43,7 @@ public RuleOperator[] Operators public class InvalidRuleDescriptor : RuleDescriptor { - public InvalidRuleDescriptor(RuleScope scope) + public InvalidRuleDescriptor(RuleScope scope) : base(scope) { RuleType = RuleType.String; diff --git a/src/Libraries/SmartStore.Core/Rules/RuleExpression.cs b/src/Libraries/SmartStore.Core/Rules/RuleExpression.cs index 6a60f0a3fa..a6bcaf32f2 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleExpression.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleExpression.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using SmartStore.Utilities; namespace SmartStore.Rules { public interface IRuleExpression { int Id { get; } + int RuleSetId { get; } RuleDescriptor Descriptor { get; } RuleOperator Operator { get; } object Value { get; } @@ -20,10 +22,23 @@ public RuleExpression() } public int Id { get; set; } + public int RuleSetId { get; set; } public RuleDescriptor Descriptor { get; set; } public RuleOperator Operator { get; set; } public object Value { get; set; } public string RawValue { get; set; } public IDictionary Metadata { get; set; } + + public override int GetHashCode() + { + var combiner = HashCodeCombiner + .Start() + .Add(RuleSetId) + .Add(Id) + .Add(Operator) + .Add(RawValue); + + return combiner.CombinedHash; + } } } diff --git a/src/Libraries/SmartStore.Core/Rules/RuleExpressionGroup.cs b/src/Libraries/SmartStore.Core/Rules/RuleExpressionGroup.cs index 9ec260c8cb..06c7d0b244 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleExpressionGroup.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleExpressionGroup.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using SmartStore.Utilities; namespace SmartStore.Rules { @@ -31,15 +29,21 @@ public class RuleExpressionGroup : RuleExpression, IRuleExpressionGroup public bool IsSubGroup { get; set; } public IRuleProvider Provider { get; set; } - public IEnumerable Expressions - { - get => _expressions; - } + public IEnumerable Expressions => _expressions; public virtual void AddExpressions(params IRuleExpression[] expressions) { Guard.NotNull(expressions, nameof(expressions)); _expressions.AddRange(expressions); } + + public override int GetHashCode() + { + var combiner = HashCodeCombiner + .Start() + .Add(Expressions.Select(x => x.GetHashCode())); + + return combiner.CombinedHash; + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Rules/RuleModule.cs b/src/Libraries/SmartStore.Core/Rules/RuleModule.cs index f88b5dcfa4..680e80118d 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleModule.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleModule.cs @@ -4,9 +4,9 @@ namespace SmartStore.Rules { public class RuleModule : Autofac.Module - { - protected override void Load(ContainerBuilder builder) - { + { + protected override void Load(ContainerBuilder builder) + { builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -21,5 +21,5 @@ protected override void Load(ContainerBuilder builder) return key => cc.ResolveKeyed(key); }); } - } + } } diff --git a/src/Libraries/SmartStore.Core/Rules/RuleOperator.cs b/src/Libraries/SmartStore.Core/Rules/RuleOperator.cs index 73decf47ad..84846ea32c 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleOperator.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleOperator.cs @@ -41,11 +41,20 @@ protected RuleOperator(string op) public string Operator { get; set; } - public override string ToString() => Operator; + public override string ToString() + { + return Operator; + } - public static implicit operator string(RuleOperator obj) => obj.Operator; + public static implicit operator string(RuleOperator obj) + { + return obj.Operator; + } - public static implicit operator RuleOperator(string obj) => GetOperator(obj); + public static implicit operator RuleOperator(string obj) + { + return GetOperator(obj); + } public static RuleOperator GetOperator(string op) { @@ -57,7 +66,7 @@ public static RuleOperator GetOperator(string op) if (_map.TryGetValue(op, out var instance)) { return instance; - } + } throw new InvalidCastException("No rule operator has been registered for '{0}'.".FormatInvariant(op)); } @@ -91,8 +100,8 @@ public bool Match(object left, object right) public virtual bool Match(TLeft left, object right) { var body = GetExpression( - ExpressionHelper.CreateConstantExpression(left, typeof(TLeft)), - ExpressionHelper.CreateConstantExpression(right), + ExpressionHelper.CreateConstantExpression(left, typeof(TLeft)), + ExpressionHelper.CreateConstantExpression(right), true); var lambda = Expression.Lambda>(body); @@ -185,7 +194,7 @@ private bool TryConvertNullableValue(Expression left, ref Expression right) if (!handled) { right = Expression.Constant(value, targetType); - } + } } catch { diff --git a/src/Libraries/SmartStore.Core/Rules/RuleStorage.cs b/src/Libraries/SmartStore.Core/Rules/RuleStorage.cs index 4161838c7d..8b8226c2bd 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleStorage.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleStorage.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Data.Entity; using System.Linq; using SmartStore.Core; @@ -59,27 +60,26 @@ public RuleSetEntity GetCachedRuleSet(int id) if (id <= 0) return null; - return this.GetRuleSetById(id, false, true); - - //// TODO: uncomment later - //var cacheKey = RULESET_BY_ID_KEY.FormatInvariant(id); - //return _cache.Get(cacheKey, () => - //{ - // using (new DbContextScope(forceNoTracking: true, proxyCreation: false, lazyLoading: false)) - // { - // var ruleSet = this.GetRuleSetById(id, false, true); - - // if (ruleSet != null) - // { - // _rsRuleSets.Context.DetachEntity(ruleSet); - // // TODO: check if the above detach call already detached navigation prop instances - // // TODO: Sort rules (?) - // _rsRuleSets.Context.DetachEntities(ruleSet.Rules); - // } - - // return ruleSet; - // } - //}, TimeSpan.FromHours(8)); + //return this.GetRuleSetById(id, false, true); + + var cacheKey = RULESET_BY_ID_KEY.FormatInvariant(id); + return _cache.Get(cacheKey, () => + { + using (new DbContextScope(forceNoTracking: true, proxyCreation: false, lazyLoading: false)) + { + var ruleSet = this.GetRuleSetById(id, false, true); + + if (ruleSet != null) + { + _rsRuleSets.Context.DetachEntity(ruleSet, true); + //// TODO: check if the above detach call already detached navigation prop instances + //// TODO: Sort rules (?) + //_rsRuleSets.Context.DetachEntities(ruleSet.Rules); + } + + return ruleSet; + } + }, TimeSpan.FromHours(8)); } protected void InvalidateRuleSet(int id) diff --git a/src/Libraries/SmartStore.Core/Rules/RuleValueSelectList.cs b/src/Libraries/SmartStore.Core/Rules/RuleValueSelectList.cs index f084e5e6d7..4b4b582489 100644 --- a/src/Libraries/SmartStore.Core/Rules/RuleValueSelectList.cs +++ b/src/Libraries/SmartStore.Core/Rules/RuleValueSelectList.cs @@ -6,6 +6,7 @@ namespace SmartStore.Rules public abstract class RuleValueSelectList { public bool Multiple { get; set; } + public bool Tags { get; set; } } public class RuleValueSelectListOption diff --git a/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs b/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs index e0df140361..f1ce494345 100644 --- a/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs +++ b/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs @@ -8,43 +8,43 @@ namespace SmartStore.Core.Search { - public enum AcquirementReason - { - Indexing, - Deleting - } + public enum AcquirementReason + { + Indexing, + Deleting + } - public class AcquireWriterContext - { - public AcquireWriterContext(AcquirementReason reason) - { - Reason = reason; - Languages = new List(); - Currencies = new List(); - StoreMappings = new Multimap(); - CustomerRoleIds = new int[0]; - CustomerRoleMappings = new Multimap(); - DeliveryTimes = new Dictionary(); - Manufacturers = new Dictionary(); - Categories = new Dictionary(); - Translations = new Dictionary(StringComparer.OrdinalIgnoreCase); - CustomProperties = new Dictionary(); - } + public class AcquireWriterContext + { + public AcquireWriterContext(AcquirementReason reason) + { + Reason = reason; + Languages = new List(); + Currencies = new List(); + StoreMappings = new Multimap(); + CustomerRoleIds = new int[0]; + CustomerRoleMappings = new Multimap(); + DeliveryTimes = new Dictionary(); + Manufacturers = new Dictionary(); + Categories = new Dictionary(); + Translations = new Dictionary(StringComparer.OrdinalIgnoreCase); + CustomProperties = new Dictionary(); + } - /// - /// Reason for writer acquirement - /// - public AcquirementReason Reason { get; private set; } + /// + /// Reason for writer acquirement + /// + public AcquirementReason Reason { get; private set; } - /// - /// All languages - /// - public IList Languages { get; set; } + /// + /// All languages + /// + public IList Languages { get; set; } - /// - /// Currency codes used for indexing - /// - public IList Currencies { get; set; } + /// + /// Currency codes used for indexing + /// + public IList Currencies { get; set; } /// /// All stores @@ -56,40 +56,40 @@ public AcquireWriterContext(AcquirementReason reason) /// public Multimap StoreMappings { get; set; } - /// - /// Array of all customer role identifiers - /// - public int[] CustomerRoleIds { get; set; } + /// + /// Array of all customer role identifiers + /// + public int[] CustomerRoleIds { get; set; } - /// - /// Map of product to customer role identifiers if the product is limited to certain customer roles - /// - public Multimap CustomerRoleMappings { get; set; } + /// + /// Map of product to customer role identifiers if the product is limited to certain customer roles + /// + public Multimap CustomerRoleMappings { get; set; } /// /// All manufacturers /// public Dictionary Manufacturers { get; set; } - /// - /// All categories - /// - public Dictionary Categories { get; set; } + /// + /// All categories + /// + public Dictionary Categories { get; set; } - /// - /// All delivery times - /// - public Dictionary DeliveryTimes { get; set; } + /// + /// All delivery times + /// + public Dictionary DeliveryTimes { get; set; } - /// - /// All translations for global scopes (like Category, Manufacturer etc.) - /// - public Dictionary Translations { get; set; } + /// + /// All translations for global scopes (like Category, Manufacturer etc.) + /// + public Dictionary Translations { get; set; } - /// - /// Use this dictionary for any custom data required along indexing - /// - public Dictionary CustomProperties { get; set; } + /// + /// Use this dictionary for any custom data required along indexing + /// + public Dictionary CustomProperties { get; set; } public void Clear() { @@ -103,5 +103,5 @@ public void Clear() Translations.Clear(); CustomProperties.Clear(); } - } + } } diff --git a/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs b/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs index 0fe4f85550..941ab39578 100644 --- a/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs +++ b/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs @@ -5,22 +5,22 @@ namespace SmartStore.Core.Search { public class DefaultIndexManager : IIndexManager - { - private readonly IEnumerable> _providers; + { + private readonly IEnumerable> _providers; - public DefaultIndexManager(IEnumerable> providers) - { - _providers = providers; - } + public DefaultIndexManager(IEnumerable> providers) + { + _providers = providers; + } - public bool HasAnyProvider(string scope, bool activeOnly = true) - { + public bool HasAnyProvider(string scope, bool activeOnly = true) + { return _providers.Any(x => !activeOnly || x.Value.IsActive(scope)); - } + } - public IIndexProvider GetIndexProvider(string scope, bool activeOnly = true) - { - return _providers.FirstOrDefault(x => !activeOnly || x.Value.IsActive(scope))?.Value; - } - } + public IIndexProvider GetIndexProvider(string scope, bool activeOnly = true) + { + 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 index b235921138..32f6d85bd7 100644 --- a/src/Libraries/SmartStore.Core/Search/Events/IndexSegmentProcessedEvent.cs +++ b/src/Libraries/SmartStore.Core/Search/Events/IndexSegmentProcessedEvent.cs @@ -1,43 +1,42 @@ -using System; -using System.Collections.Generic; +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)); + 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(); - } + Scope = scope; + IsRebuild = isRebuild; + Operations = operations; + Metadata = new Dictionary(); + } - public string Scope - { - get; - private set; - } + public string Scope + { + get; + private set; + } - public bool IsRebuild - { - get; - private set; - } + public bool IsRebuild + { + get; + private set; + } - public IEnumerable Operations - { - get; - private set; - } + public IEnumerable Operations + { + get; + private set; + } - public IDictionary Metadata - { - 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 index b1304aae56..b15bf32629 100644 --- a/src/Libraries/SmartStore.Core/Search/Events/IndexingCompletedEvent.cs +++ b/src/Libraries/SmartStore.Core/Search/Events/IndexingCompletedEvent.cs @@ -1,27 +1,25 @@ -using System; - -namespace SmartStore.Core.Search +namespace SmartStore.Core.Search { - public class IndexingCompletedEvent - { - public IndexingCompletedEvent(IndexInfo indexInfo, bool wasRebuilt) - { - Guard.NotNull(indexInfo, nameof(indexInfo)); + public class IndexingCompletedEvent + { + public IndexingCompletedEvent(IndexInfo indexInfo, bool wasRebuilt) + { + Guard.NotNull(indexInfo, nameof(indexInfo)); - IndexInfo = indexInfo; - WasRebuilt = wasRebuilt; - } + IndexInfo = indexInfo; + WasRebuilt = wasRebuilt; + } - public IndexInfo IndexInfo - { - get; - private set; - } + public IndexInfo IndexInfo + { + get; + private set; + } - public bool WasRebuilt - { - get; - private set; - } - } + public bool WasRebuilt + { + get; + private set; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Search/Facets/Facet.cs b/src/Libraries/SmartStore.Core/Search/Facets/Facet.cs index 84a88b2109..1fd5f17bcc 100644 --- a/src/Libraries/SmartStore.Core/Search/Facets/Facet.cs +++ b/src/Libraries/SmartStore.Core/Search/Facets/Facet.cs @@ -3,69 +3,69 @@ namespace SmartStore.Core.Search.Facets { - [DebuggerDisplay("Key: {Key}, IsChoice: {IsChoice}, HitCount: {HitCount}, Value: {Value}")] - public class Facet - { - public Facet(FacetValue value) - : this(value.ToString(), value) - { - } + [DebuggerDisplay("Key: {Key}, IsChoice: {IsChoice}, HitCount: {HitCount}, Value: {Value}")] + public class Facet + { + public Facet(FacetValue value) + : this(value.ToString(), value) + { + } - public Facet(string key, FacetValue value) - { - Guard.NotEmpty(key, nameof(key)); - Guard.NotNull(value, nameof(value)); + public Facet(string key, FacetValue value) + { + Guard.NotEmpty(key, nameof(key)); + Guard.NotNull(value, nameof(value)); - Key = key; - Value = value; - Children = new List(); - IsChoice = true; - Published = true; - } + Key = key; + Value = value; + Children = new List(); + IsChoice = true; + Published = true; + } - public string Key - { - get; - private set; - } + public string Key + { + get; + private set; + } - public FacetValue Value - { - get; - private set; - } + public FacetValue Value + { + get; + private set; + } - /// - /// Gets or sets whether the facet can be selected - /// - public bool IsChoice - { - get; - set; - } + /// + /// Gets or sets whether the facet can be selected + /// + public bool IsChoice + { + get; + set; + } - public bool Published - { - get; - set; - } + public bool Published + { + get; + set; + } - public long HitCount - { - get; - set; - } + public long HitCount + { + get; + set; + } - public FacetGroup FacetGroup - { - get; - internal set; - } + public FacetGroup FacetGroup + { + get; + internal set; + } - public IList Children - { - get; - set; - } - } + public IList Children + { + get; + set; + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs index d0e8b5b8e7..4292617e5f 100644 --- a/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs @@ -5,131 +5,125 @@ namespace SmartStore.Core.Search.Facets { - public enum FacetSorting - { - HitsDesc, - ValueAsc, - LabelAsc, - DisplayOrder - } - - /// - /// A filter and its selection to be applied, e.g. Color=Red. - /// - [Serializable] - public class FacetDescriptor - { - private readonly List _values; - - public FacetDescriptor(string key) - { - Guard.NotEmpty(key, nameof(key)); - - _values = new List(); - Key = key; - } - - /// - /// Gets the key / field name. - /// - public string Key - { - get; - private set; - } - - /// - /// Gets or sets the label. - /// - public string Label - { - get; - set; - } - - /// - /// Gets the values for this facet. - /// - public ICollection Values - { - get - { - return _values; - } - } - - /// - /// Adds a facet value. - /// - /// Facet value - public FacetDescriptor AddValue(params FacetValue[] values) - { - _values.AddRange(values); - return this; - } - - /// - /// Gets or sets the boolean value operator. - /// - //public ValueOperator Operator - //{ - // get; - // set; - //} - - /// - /// Gets or sets whether selection of multiple values is allowed. - /// - public bool IsMultiSelect - { - get; - set; - } - - /// - /// Gets or sets the minimum number of hits a choice would need to have to be returned. - /// - public int MinHitCount - { - get; - set; - } - - /// - /// Gets or sets the maximum number of choices to return. Default = 0 which means all. - /// - public int MaxChoicesCount - { - get; - set; - } - - /// - /// Gets or sets the result choices sort order. - /// - public FacetSorting OrderBy - { - get; - set; - } - - /// - /// Gets or sets the display order. - /// - public int DisplayOrder - { - get; - set; - } - - public override string ToString() - { - var sb = new StringBuilder(); - - sb.Append("FieldName: ").Append(Key).Append(" "); - sb.Append("Values: " + string.Join(",", _values.Select(x => x.Value.ToString()))).Append(" "); - - return sb.ToString(); - } - } + public enum FacetSorting + { + HitsDesc, + ValueAsc, + LabelAsc, + DisplayOrder + } + + /// + /// A filter and its selection to be applied, e.g. Color=Red. + /// + [Serializable] + public class FacetDescriptor + { + private readonly List _values; + + public FacetDescriptor(string key) + { + Guard.NotEmpty(key, nameof(key)); + + _values = new List(); + Key = key; + } + + /// + /// Gets the key / field name. + /// + public string Key + { + get; + private set; + } + + /// + /// Gets or sets the label. + /// + public string Label + { + get; + set; + } + + /// + /// Gets the values for this facet. + /// + public ICollection Values => _values; + + /// + /// Adds a facet value. + /// + /// Facet value + public FacetDescriptor AddValue(params FacetValue[] values) + { + _values.AddRange(values); + return this; + } + + /// + /// Gets or sets the boolean value operator. + /// + //public ValueOperator Operator + //{ + // get; + // set; + //} + + /// + /// Gets or sets whether selection of multiple values is allowed. + /// + public bool IsMultiSelect + { + get; + set; + } + + /// + /// Gets or sets the minimum number of hits a choice would need to have to be returned. + /// + public int MinHitCount + { + get; + set; + } + + /// + /// Gets or sets the maximum number of choices to return. Default = 0 which means all. + /// + public int MaxChoicesCount + { + get; + set; + } + + /// + /// Gets or sets the result choices sort order. + /// + public FacetSorting OrderBy + { + get; + set; + } + + /// + /// Gets or sets the display order. + /// + public int DisplayOrder + { + get; + set; + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("FieldName: ").Append(Key).Append(" "); + sb.Append("Values: " + string.Join(",", _values.Select(x => x.Value.ToString()))).Append(" "); + + return sb.ToString(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetEnums.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetEnums.cs index 2ca081d194..5e6644f6f5 100644 --- a/src/Libraries/SmartStore.Core/Search/Facets/FacetEnums.cs +++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetEnums.cs @@ -1,20 +1,20 @@ namespace SmartStore.Core.Search.Facets { - public enum FacetTemplateHint - { - /// - /// Render facets as checkboxes - /// - Checkboxes = 0, + public enum FacetTemplateHint + { + /// + /// Render facets as checkboxes + /// + Checkboxes = 0, - /// - /// Custom facet rendering like color or picture boxes - /// - Custom, + /// + /// Custom facet rendering like color or picture boxes + /// + Custom, - /// - /// Render facets as a numeric range filter - /// - NumericRange - } + /// + /// Render facets as a numeric range filter + /// + NumericRange + } } diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetExtensions.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetExtensions.cs index 3951bb1a7e..e4c4333b86 100644 --- a/src/Libraries/SmartStore.Core/Search/Facets/FacetExtensions.cs +++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetExtensions.cs @@ -3,83 +3,94 @@ namespace SmartStore.Core.Search.Facets { - public static class FacetExtensions - { - /// - /// Orders a facet sequence - /// - /// Facets - /// Facet descriptor - /// Ordered facets - public static IOrderedEnumerable OrderBy(this IEnumerable source, FacetDescriptor descriptor) - { - Guard.NotNull(descriptor, nameof(descriptor)); + public static class FacetExtensions + { + /// + /// Orders a facet sequence + /// + /// Facets + /// Facet descriptor + /// Ordered facets + public static IOrderedEnumerable OrderBy(this IEnumerable source, FacetDescriptor descriptor) + { + Guard.NotNull(descriptor, nameof(descriptor)); - return source.OrderBy(descriptor.OrderBy, descriptor.IsMultiSelect); - } + return source.OrderBy(descriptor.OrderBy, descriptor.IsMultiSelect); + } - /// - /// Orders a facet sequence - /// - /// Facets - /// Type of sorting - /// Whether to display selected facets first - /// Ordered facets - public static IOrderedEnumerable OrderBy(this IEnumerable source, FacetSorting sorting, bool selectedFirst = true) - { - Guard.NotNull(source, nameof(source)); + /// + /// Orders a facet sequence + /// + /// Facets + /// Type of sorting + /// Whether to display selected facets first + /// Ordered facets + public static IOrderedEnumerable OrderBy(this IEnumerable source, FacetSorting sorting, bool selectedFirst = true) + { + Guard.NotNull(source, nameof(source)); - switch (sorting) - { - case FacetSorting.ValueAsc: - if (selectedFirst) - return source.OrderByDescending(x => x.Value.IsSelected).ThenBy(x => x.Value.Value); - else - return source.OrderBy(x => x.Value.Value); + switch (sorting) + { + case FacetSorting.ValueAsc: + if (selectedFirst) + return source + .OrderByDescending(x => x.Value.IsSelected) + .ThenBy(x => x.Value.Value); + else + return source.OrderBy(x => x.Value.Value); - case FacetSorting.LabelAsc: - if (selectedFirst) - return source.OrderByDescending(x => x.Value.IsSelected).ThenBy(x => x.Value.Label); - else - return source.OrderBy(x => x.Value.Label); + case FacetSorting.LabelAsc: + if (selectedFirst) + return source + .OrderByDescending(x => x.Value.IsSelected) + .ThenBy(x => x.Value.Label); + else + return source.OrderBy(x => x.Value.Label); - case FacetSorting.DisplayOrder: - if (selectedFirst) - return source.OrderByDescending(x => x.Value.IsSelected).ThenBy(x => x.Value.DisplayOrder); - else - return source.OrderBy(x => x.Value.DisplayOrder); + case FacetSorting.DisplayOrder: + if (selectedFirst) + return source + .OrderByDescending(x => x.Value.IsSelected) + .ThenBy(x => x.Value.DisplayOrder); + else + return source.OrderBy(x => x.Value.DisplayOrder); - default: - if (selectedFirst) - return source.OrderByDescending(x => x.Value.IsSelected).ThenByDescending(x => x.HitCount); - else - return source.OrderByDescending(x => x.HitCount); - } - } + default: + if (selectedFirst) + return source + .OrderByDescending(x => x.Value.IsSelected) + .ThenByDescending(x => x.HitCount) + .ThenBy(x => x.Value.Label); + else + return source + .OrderByDescending(x => x.HitCount) + .ThenBy(x => x.Value.Label); + } + } - /// - /// Removes a facet - /// - /// List of facets - /// Facet value - /// Whether to compare the upper value - public static void RemoveFacet(this IList facets, object value, bool upperValue) - { - Facet facet = null; + /// + /// Removes a facet + /// + /// List of facets + /// Facet value + /// Whether to compare the upper value + public static void RemoveFacet(this IList facets, object value, bool upperValue) + { + Facet facet = null; - if (upperValue) - { - facet = facets.FirstOrDefault(x => x.Value.UpperValue != null && x.Value.UpperValue.Equals(value)); - } - else - { - facet = facets.FirstOrDefault(x => x.Value.Value != null && x.Value.Value.Equals(value)); - } + if (upperValue) + { + facet = facets.FirstOrDefault(x => x.Value.UpperValue != null && x.Value.UpperValue.Equals(value)); + } + else + { + facet = facets.FirstOrDefault(x => x.Value.Value != null && x.Value.Value.Equals(value)); + } - if (facet != null) - { - facets.Remove(facet); - } - } - } + if (facet != null) + { + facets.Remove(facet); + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs index e6d9ffc48e..35b2f1b56d 100644 --- a/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs +++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs @@ -5,82 +5,82 @@ namespace SmartStore.Core.Search.Facets { - public enum FacetGroupKind - { - Unknown = -1, - Category, - Brand, - Price, - Rating, - DeliveryTime, - Availability, - NewArrivals, - Attribute, - Variant, + public enum FacetGroupKind + { + Unknown = -1, + Category, + Brand, + Price, + Rating, + DeliveryTime, + Availability, + NewArrivals, + Attribute, + Variant, Forum, Customer, Date - } + } - [DebuggerDisplay("Key: {Key}, Label: {Label}, Kind: {Kind}")] - public class FacetGroup - { - private readonly Dictionary _facets; - private FacetGroupKind? _kind; + [DebuggerDisplay("Key: {Key}, Label: {Label}, Kind: {Kind}")] + public class FacetGroup + { + private readonly Dictionary _facets; + private FacetGroupKind? _kind; - public FacetGroup() - : this (string.Empty, string.Empty, string.Empty, false, false, 0, Enumerable.Empty()) - { - } + public FacetGroup() + : this(string.Empty, string.Empty, string.Empty, false, false, 0, Enumerable.Empty()) + { + } - public FacetGroup( + public FacetGroup( string scope, - string key, - string label, - bool isMultiSelect, - bool hasChildren, - int displayOrder, - IEnumerable facets) - { + string key, + string label, + bool isMultiSelect, + bool hasChildren, + int displayOrder, + IEnumerable facets) + { Guard.NotNull(scope, nameof(scope)); Guard.NotNull(key, nameof(key)); - Guard.NotNull(facets, nameof(facets)); + Guard.NotNull(facets, nameof(facets)); Scope = scope; - Key = key; - Label = label; - IsMultiSelect = isMultiSelect; - DisplayOrder = displayOrder; + Key = key; + Label = label; + IsMultiSelect = isMultiSelect; + DisplayOrder = displayOrder; IsScrollable = true; - _facets = new Dictionary(StringComparer.OrdinalIgnoreCase); - - facets.Each(x => - { - x.FacetGroup = this; - x.Children.Each(y => y.FacetGroup = this); - - try - { - _facets.Add(x.Key, x); - } - catch (Exception ex) - { - ex.Dump(); - } - }); - } - - public static FacetGroupKind GetKindByKey(string scope, string key) - { - if (key.StartsWith("attrid")) - { - return FacetGroupKind.Attribute; - } - else if (key.StartsWith("variantid")) - { - return FacetGroupKind.Variant; - } + _facets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + facets.Each(x => + { + x.FacetGroup = this; + x.Children.Each(y => y.FacetGroup = this); + + try + { + _facets.Add(x.Key, x); + } + catch (Exception ex) + { + ex.Dump(); + } + }); + } + + public static FacetGroupKind GetKindByKey(string scope, string key) + { + if (key.StartsWith("attrid")) + { + return FacetGroupKind.Attribute; + } + else if (key.StartsWith("variantid")) + { + return FacetGroupKind.Variant; + } switch (key) { @@ -105,7 +105,7 @@ public static FacetGroupKind GetKindByKey(string scope, string key) return FacetGroupKind.Customer; default: return FacetGroupKind.Unknown; - } + } } public string Scope @@ -115,34 +115,34 @@ public string Scope } public string Key - { - get; - private set; - } - - public string Label - { - get; - private set; - } - - public bool IsMultiSelect - { - get; - private set; - } + { + get; + private set; + } + + public string Label + { + get; + private set; + } + + public bool IsMultiSelect + { + get; + private set; + } public bool HasChildren - { - get; - private set; - } + { + get; + private set; + } - public int DisplayOrder - { - get; - private set; - } + public int DisplayOrder + { + get; + private set; + } public bool IsScrollable { @@ -150,49 +150,43 @@ public bool IsScrollable set; } - public IEnumerable Facets - { - get - { - return _facets.Values; - } - } - - public IEnumerable SelectedFacets - { - get - { - var parents = _facets.Values.Where(x => x.Value.IsSelected); - var children = _facets.Values.SelectMany(x => x.Children).Where(x => x.Value.IsSelected); - - return parents.Concat(children); - } - } - - public Facet GetFacet(string key) - { - Guard.NotEmpty(key, nameof(key)); - - return _facets.Get(key); - } - - public FacetGroupKind Kind - { - get - { - if (_kind == null) - { - _kind = GetKindByKey(Scope, Key); - } - - return _kind.Value; - } - } - - public FacetTemplateHint TemplateHint - { - get; - set; - } - } + public IEnumerable Facets => _facets.Values; + + public IEnumerable SelectedFacets + { + get + { + var parents = _facets.Values.Where(x => x.Value.IsSelected); + var children = _facets.Values.SelectMany(x => x.Children).Where(x => x.Value.IsSelected); + + return parents.Concat(children); + } + } + + public Facet GetFacet(string key) + { + Guard.NotEmpty(key, nameof(key)); + + return _facets.Get(key); + } + + public FacetGroupKind Kind + { + get + { + if (_kind == null) + { + _kind = GetKindByKey(Scope, Key); + } + + return _kind.Value; + } + } + + public FacetTemplateHint TemplateHint + { + get; + set; + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs index 80e54b44af..b3d68a6c20 100644 --- a/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs +++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs @@ -4,156 +4,150 @@ namespace SmartStore.Core.Search.Facets { - [Serializable] - public class FacetValue : IEquatable, ICloneable - { - public FacetValue() - { - } - - public FacetValue(object value, IndexTypeCode typeCode) - { - Value = value; - TypeCode = typeCode; - IsRange = false; - } - - public FacetValue(object value, object upperValue, IndexTypeCode typeCode, bool includesLower, bool includesUpper) - { - Value = value; - UpperValue = upperValue; - TypeCode = typeCode; - IncludesLower = includesLower; - IncludesUpper = includesUpper; - IsRange = true; - } - - public object Value - { - get; - set; - } - - public object UpperValue - { - get; - set; - } - - public IndexTypeCode TypeCode - { - get; - set; - } - - public bool IncludesLower - { - get; - set; - } - - public bool IncludesUpper - { - get; - set; - } - - public bool IsRange - { - get; - set; - } - - public bool IsSelected - { - get; - set; - } - - public bool IsEmpty - { - get - { - return TypeCode == IndexTypeCode.Empty && Value == null; - } - } - - #region Metadata - - public string Label { get; set; } - - public int ParentId { get; set; } - - public int DisplayOrder { get; set; } - - public FacetSorting? Sorting { get; set; } - - public string PictureUrl { get; set; } - - public string Color { get; set; } - - #endregion - - public override int GetHashCode() - { - if (Value != null && UpperValue != null) - { - var combiner = HashCodeCombiner - .Start() - .Add(Value) - .Add(UpperValue); - - return combiner.CombinedHash; - } - else if (UpperValue != null) - { - return UpperValue.GetHashCode(); - } - else if (Value != null) - { - return Value.GetHashCode(); - } - - return 0; - } - - public bool Equals(FacetValue other) - { - if (other == null || other.TypeCode != TypeCode || other.IsRange != IsRange) - { - return false; - } - - if (other.IsRange) - { - if (other.IncludesLower != IncludesLower || other.IncludesUpper != IncludesUpper) - { - return false; - } - - if (other.Value == null && Value == null && other.UpperValue == null && UpperValue == null) - { - return true; - } - - if (other.UpperValue != null && !other.UpperValue.Equals(UpperValue)) - { - return false; - } - } - - if (other.Value == null && Value == null) - { - return true; - } - - return other.Value != null && other.Value.Equals(Value); - } - - public override bool Equals(object obj) - { - return this.Equals(obj as FacetValue); - } + [Serializable] + public class FacetValue : IEquatable, ICloneable + { + public FacetValue() + { + } + + public FacetValue(object value, IndexTypeCode typeCode) + { + Value = value; + TypeCode = typeCode; + IsRange = false; + } + + public FacetValue(object value, object upperValue, IndexTypeCode typeCode, bool includesLower, bool includesUpper) + { + Value = value; + UpperValue = upperValue; + TypeCode = typeCode; + IncludesLower = includesLower; + IncludesUpper = includesUpper; + IsRange = true; + } + + public object Value + { + get; + set; + } + + public object UpperValue + { + get; + set; + } + + public IndexTypeCode TypeCode + { + get; + set; + } + + public bool IncludesLower + { + get; + set; + } + + public bool IncludesUpper + { + get; + set; + } + + public bool IsRange + { + get; + set; + } + + public bool IsSelected + { + get; + set; + } + + public bool IsEmpty => TypeCode == IndexTypeCode.Empty && Value == null; + + #region Metadata + + public string Label { get; set; } + + public int ParentId { get; set; } + + public int DisplayOrder { get; set; } + + public FacetSorting? Sorting { get; set; } + + public string PictureUrl { get; set; } + + public string Color { get; set; } + + #endregion + + public override int GetHashCode() + { + if (Value != null && UpperValue != null) + { + var combiner = HashCodeCombiner + .Start() + .Add(Value) + .Add(UpperValue); + + return combiner.CombinedHash; + } + else if (UpperValue != null) + { + return UpperValue.GetHashCode(); + } + else if (Value != null) + { + return Value.GetHashCode(); + } + + return 0; + } + + public bool Equals(FacetValue other) + { + if (other == null || other.TypeCode != TypeCode || other.IsRange != IsRange) + { + return false; + } + + if (other.IsRange) + { + if (other.IncludesLower != IncludesLower || other.IncludesUpper != IncludesUpper) + { + return false; + } + + if (other.Value == null && Value == null && other.UpperValue == null && UpperValue == null) + { + return true; + } + + if (other.UpperValue != null && !other.UpperValue.Equals(UpperValue)) + { + return false; + } + } + + if (other.Value == null && Value == null) + { + return true; + } + + return other.Value != null && other.Value.Equals(Value); + } + + public override bool Equals(object obj) + { + return this.Equals(obj as FacetValue); + } protected virtual string ConvertToString(object value) { @@ -179,39 +173,39 @@ protected virtual string ConvertToString(object value) return string.Empty; } - public override string ToString() - { - var result = string.Empty; + public override string ToString() + { + var result = string.Empty; var valueStr = ConvertToString(Value); - if (IsRange) - { + if (IsRange) + { var upperValueStr = ConvertToString(UpperValue); - if (upperValueStr.HasValue()) - { - result = string.Concat(valueStr, "~", upperValueStr); - } - else - { - result = valueStr; - } - } - else - { - result = valueStr; - } - - return result; - } - - public FacetValue Clone() - { - return (FacetValue)this.MemberwiseClone(); - } - - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } - } + if (upperValueStr.HasValue()) + { + result = string.Concat(valueStr, "~", upperValueStr); + } + else + { + result = valueStr; + } + } + else + { + result = valueStr; + } + + return result; + } + + public FacetValue Clone() + { + return (FacetValue)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Filter/CombinedSearchFilter.cs b/src/Libraries/SmartStore.Core/Search/Filter/CombinedSearchFilter.cs index 46f00c7d3c..e984b56574 100644 --- a/src/Libraries/SmartStore.Core/Search/Filter/CombinedSearchFilter.cs +++ b/src/Libraries/SmartStore.Core/Search/Filter/CombinedSearchFilter.cs @@ -3,51 +3,45 @@ namespace SmartStore.Core.Search { - public class CombinedSearchFilter : SearchFilterBase, ICombinedSearchFilter - { - private readonly IList _filters; + public class CombinedSearchFilter : SearchFilterBase, ICombinedSearchFilter + { + private readonly IList _filters; - public CombinedSearchFilter() - { - _filters = new List(); - } + public CombinedSearchFilter() + { + _filters = new List(); + } - public CombinedSearchFilter(IEnumerable filters) - { - Guard.NotNull(filters, nameof(filters)); + public CombinedSearchFilter(IEnumerable filters) + { + Guard.NotNull(filters, nameof(filters)); - _filters = new List(filters); - } + _filters = new List(filters); + } - public ICollection Filters - { - get - { - return _filters; - } - } + public ICollection Filters => _filters; - #region Fluent builder + #region Fluent builder - public CombinedSearchFilter Add(ISearchFilter filter) - { - Guard.NotNull(filter, nameof(filter)); + public CombinedSearchFilter Add(ISearchFilter filter) + { + Guard.NotNull(filter, nameof(filter)); - _filters.Add(filter); + _filters.Add(filter); - return this; - } + return this; + } - #endregion + #endregion - public override string ToString() - { - if (_filters.Any()) - { - return string.Concat("(", string.Join(", ", _filters.Select(x => x.ToString())), ")"); - } + public override string ToString() + { + if (_filters.Any()) + { + return string.Concat("(", string.Join(", ", _filters.Select(x => x.ToString())), ")"); + } - return ""; - } - } + return ""; + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Filter/ISearchFilter.cs b/src/Libraries/SmartStore.Core/Search/Filter/ISearchFilter.cs index d0adf644f1..ca96bc8536 100644 --- a/src/Libraries/SmartStore.Core/Search/Filter/ISearchFilter.cs +++ b/src/Libraries/SmartStore.Core/Search/Filter/ISearchFilter.cs @@ -1,33 +1,32 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Core.Search { - public interface ISearchFilter - { - SearchFilterOccurence Occurence { get; } - float Boost { get; } - } + public interface ISearchFilter + { + SearchFilterOccurence Occurence { get; } + float Boost { get; } + } - public interface ICombinedSearchFilter : ISearchFilter - { - ICollection Filters { get; } - } + public interface ICombinedSearchFilter : ISearchFilter + { + ICollection Filters { get; } + } - public interface IAttributeSearchFilter : ISearchFilter - { - string FieldName { get; } - IndexTypeCode TypeCode { get; } - object Term { get; } - bool IsExactMatch { get; } - bool IsNotAnalyzed { get; } - int ParentId { get; } - } + public interface IAttributeSearchFilter : ISearchFilter + { + string FieldName { get; } + IndexTypeCode TypeCode { get; } + object Term { get; } + bool IsExactMatch { get; } + bool IsNotAnalyzed { get; } + int ParentId { get; } + } - public interface IRangeSearchFilter : IAttributeSearchFilter - { - object UpperTerm { get; } - bool IncludesLower { get; } - bool IncludesUpper { get; } - } + public interface IRangeSearchFilter : IAttributeSearchFilter + { + object UpperTerm { get; } + bool IncludesLower { get; } + bool IncludesUpper { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Filter/RangeSearchFilter.cs b/src/Libraries/SmartStore.Core/Search/Filter/RangeSearchFilter.cs index fda82e8457..78b4615ec5 100644 --- a/src/Libraries/SmartStore.Core/Search/Filter/RangeSearchFilter.cs +++ b/src/Libraries/SmartStore.Core/Search/Filter/RangeSearchFilter.cs @@ -1,31 +1,31 @@ namespace SmartStore.Core.Search { - public class RangeSearchFilter : SearchFilter, IRangeSearchFilter - { - public object UpperTerm - { - get; - protected internal set; - } + public class RangeSearchFilter : SearchFilter, IRangeSearchFilter + { + public object UpperTerm + { + get; + protected internal set; + } - public bool IncludesLower - { - get; - protected internal set; - } + public bool IncludesLower + { + get; + protected internal set; + } - public bool IncludesUpper - { - get; - protected internal set; - } + public bool IncludesUpper + { + get; + protected internal set; + } - public override string ToString() - { - return "{0}: {1} - {2}".FormatInvariant( - FieldName, - Term != null ? Term.ToString() : "*", - UpperTerm != null ? UpperTerm.ToString() : "*"); - } - } + public override string ToString() + { + return "{0}: {1} - {2}".FormatInvariant( + FieldName, + Term != null ? Term.ToString() : "*", + UpperTerm != null ? UpperTerm.ToString() : "*"); + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Filter/SearchFilter.cs b/src/Libraries/SmartStore.Core/Search/Filter/SearchFilter.cs index f212279f70..aa6ddba8a6 100644 --- a/src/Libraries/SmartStore.Core/Search/Filter/SearchFilter.cs +++ b/src/Libraries/SmartStore.Core/Search/Filter/SearchFilter.cs @@ -2,211 +2,211 @@ namespace SmartStore.Core.Search { - public class SearchFilter : SearchFilterBase, IAttributeSearchFilter - { - protected SearchFilter() - : base() - { - } - - public string FieldName - { - get; - protected set; - } - - public IndexTypeCode TypeCode - { - get; - protected set; - } - - public object Term - { - get; - protected set; - } - - public bool IsExactMatch - { - get; - protected set; - } - - public bool IsNotAnalyzed - { - get; - protected set; - } - - public int ParentId - { - get; - protected set; - } - - #region Fluent builder - - /// - /// Mark a clause as a mandatory match. By default all clauses are optional. - /// + public class SearchFilter : SearchFilterBase, IAttributeSearchFilter + { + protected SearchFilter() + : base() + { + } + + public string FieldName + { + get; + protected set; + } + + public IndexTypeCode TypeCode + { + get; + protected set; + } + + public object Term + { + get; + protected set; + } + + public bool IsExactMatch + { + get; + protected set; + } + + public bool IsNotAnalyzed + { + get; + protected set; + } + + public int ParentId + { + get; + protected set; + } + + #region Fluent builder + + /// + /// Mark a clause as a mandatory match. By default all clauses are optional. + /// /// Whether the clause is mandatory or not. - public SearchFilter Mandatory(bool mandatory = true) - { + public SearchFilter Mandatory(bool mandatory = true) + { Occurence = mandatory ? SearchFilterOccurence.Must : SearchFilterOccurence.MustNot; return this; - } + } /// /// Mark a clause as a forbidden match (MustNot). /// public SearchFilter Forbidden() - { - Occurence = SearchFilterOccurence.MustNot; - return this; - } + { + Occurence = SearchFilterOccurence.MustNot; + return this; + } /// /// Specifies whether the clause should be matched exactly, like 'app' won't match 'apple' (applied on string clauses only). /// public SearchFilter ExactMatch() - { - IsExactMatch = true; - return this; - } - - /// - /// Specifies that the searched value will not be tokenized (applied on string clauses only) - /// - public SearchFilter NotAnalyzed() - { - IsNotAnalyzed = true; - return this; - } - - /// - /// Applies a parent identifier. - /// - /// Parent identifier - public SearchFilter HasParent(int parentId) - { - ParentId = parentId; - return this; - } - - /// - /// Applies a specific boost to a clause. - /// - /// A value greater than zero, by which the score will be multiplied. - /// If greater than 1, it will improve the weight of a clause - public SearchFilter Weighted(float weight) - { - Boost = weight; - return this; - } - - #endregion - - #region Static factories - - public static CombinedSearchFilter Combined(params ISearchFilter[] filters) - { - var filter = new CombinedSearchFilter(filters); - filter.Occurence = SearchFilterOccurence.Must; - return filter; - } - - public static SearchFilter ByField(string fieldName, string term) - { - return ByField(fieldName, term, IndexTypeCode.String); - } - - public static SearchFilter ByField(string fieldName, int term) - { - return ByField(fieldName, term, IndexTypeCode.Int32); - } - - public static SearchFilter ByField(string fieldName, bool term) - { - return ByField(fieldName, term, IndexTypeCode.Boolean); - } - - public static SearchFilter ByField(string fieldName, double term) - { - return ByField(fieldName, term, IndexTypeCode.Double); - } - - public static SearchFilter ByField(string fieldName, DateTime term) - { - return ByField(fieldName, term, IndexTypeCode.DateTime); - } - - private static SearchFilter ByField(string fieldName, object term, IndexTypeCode typeCode) - { - Guard.NotEmpty(fieldName, nameof(fieldName)); - - return new SearchFilter - { - FieldName = fieldName, - Term = term, - TypeCode = typeCode - }; - } - - - public static RangeSearchFilter ByRange(string fieldName, string lower, string upper, bool includeLower = true, bool includeUpper = true) - { - return ByRange(fieldName, lower, upper, IndexTypeCode.String, includeLower, includeUpper); - } - - public static RangeSearchFilter ByRange(string fieldName, int? lower, int? upper, bool includeLower = true, bool includeUpper = true) - { - return ByRange(fieldName, lower, upper, IndexTypeCode.Int32, includeLower, includeUpper); - } - - public static RangeSearchFilter ByRange(string fieldName, double? lower, double? upper, bool includeLower = true, bool includeUpper = true) - { - return ByRange(fieldName, lower, upper, IndexTypeCode.Double, includeLower, includeUpper); - } - - public static RangeSearchFilter ByRange(string fieldName, DateTime? lower, DateTime? upper, bool includeLower = true, bool includeUpper = true) - { - return ByRange(fieldName, lower, upper, IndexTypeCode.DateTime, includeLower, includeUpper); - } - - private static RangeSearchFilter ByRange( - string fieldName, - object lowerTerm, - object upperTerm, - IndexTypeCode typeCode, - bool includeLower, - bool includeUpper) - { - Guard.NotEmpty(fieldName, nameof(fieldName)); - - return new RangeSearchFilter - { - FieldName = fieldName, - Term = lowerTerm, - UpperTerm = upperTerm, - TypeCode = typeCode, - IncludesLower = includeLower, - IncludesUpper = includeUpper - }; - } - - #endregion - - public override string ToString() - { - return $"{FieldName}: {Term.ToString()}"; - } - } - - public enum SearchFilterOccurence - { - Must = 0, - Should = 1, - MustNot = 2 - } + { + IsExactMatch = true; + return this; + } + + /// + /// Specifies that the searched value will not be tokenized (applied on string clauses only) + /// + public SearchFilter NotAnalyzed() + { + IsNotAnalyzed = true; + return this; + } + + /// + /// Applies a parent identifier. + /// + /// Parent identifier + public SearchFilter HasParent(int parentId) + { + ParentId = parentId; + return this; + } + + /// + /// Applies a specific boost to a clause. + /// + /// A value greater than zero, by which the score will be multiplied. + /// If greater than 1, it will improve the weight of a clause + public SearchFilter Weighted(float weight) + { + Boost = weight; + return this; + } + + #endregion + + #region Static factories + + public static CombinedSearchFilter Combined(params ISearchFilter[] filters) + { + var filter = new CombinedSearchFilter(filters); + filter.Occurence = SearchFilterOccurence.Must; + return filter; + } + + public static SearchFilter ByField(string fieldName, string term) + { + return ByField(fieldName, term, IndexTypeCode.String); + } + + public static SearchFilter ByField(string fieldName, int term) + { + return ByField(fieldName, term, IndexTypeCode.Int32); + } + + public static SearchFilter ByField(string fieldName, bool term) + { + return ByField(fieldName, term, IndexTypeCode.Boolean); + } + + public static SearchFilter ByField(string fieldName, double term) + { + return ByField(fieldName, term, IndexTypeCode.Double); + } + + public static SearchFilter ByField(string fieldName, DateTime term) + { + return ByField(fieldName, term, IndexTypeCode.DateTime); + } + + private static SearchFilter ByField(string fieldName, object term, IndexTypeCode typeCode) + { + Guard.NotEmpty(fieldName, nameof(fieldName)); + + return new SearchFilter + { + FieldName = fieldName, + Term = term, + TypeCode = typeCode + }; + } + + + public static RangeSearchFilter ByRange(string fieldName, string lower, string upper, bool includeLower = true, bool includeUpper = true) + { + return ByRange(fieldName, lower, upper, IndexTypeCode.String, includeLower, includeUpper); + } + + public static RangeSearchFilter ByRange(string fieldName, int? lower, int? upper, bool includeLower = true, bool includeUpper = true) + { + return ByRange(fieldName, lower, upper, IndexTypeCode.Int32, includeLower, includeUpper); + } + + public static RangeSearchFilter ByRange(string fieldName, double? lower, double? upper, bool includeLower = true, bool includeUpper = true) + { + return ByRange(fieldName, lower, upper, IndexTypeCode.Double, includeLower, includeUpper); + } + + public static RangeSearchFilter ByRange(string fieldName, DateTime? lower, DateTime? upper, bool includeLower = true, bool includeUpper = true) + { + return ByRange(fieldName, lower, upper, IndexTypeCode.DateTime, includeLower, includeUpper); + } + + private static RangeSearchFilter ByRange( + string fieldName, + object lowerTerm, + object upperTerm, + IndexTypeCode typeCode, + bool includeLower, + bool includeUpper) + { + Guard.NotEmpty(fieldName, nameof(fieldName)); + + return new RangeSearchFilter + { + FieldName = fieldName, + Term = lowerTerm, + UpperTerm = upperTerm, + TypeCode = typeCode, + IncludesLower = includeLower, + IncludesUpper = includeUpper + }; + } + + #endregion + + public override string ToString() + { + return $"{FieldName}: {Term.ToString()}"; + } + } + + public enum SearchFilterOccurence + { + Must = 0, + Should = 1, + MustNot = 2 + } } diff --git a/src/Libraries/SmartStore.Core/Search/Filter/SearchFilterBase.cs b/src/Libraries/SmartStore.Core/Search/Filter/SearchFilterBase.cs index 29bb1f590b..f8d8ae3d14 100644 --- a/src/Libraries/SmartStore.Core/Search/Filter/SearchFilterBase.cs +++ b/src/Libraries/SmartStore.Core/Search/Filter/SearchFilterBase.cs @@ -1,30 +1,28 @@ -using System; - -namespace SmartStore.Core.Search +namespace SmartStore.Core.Search { - public abstract class SearchFilterBase : ISearchFilter - { - protected SearchFilterBase() - { - Occurence = SearchFilterOccurence.Should; - } + public abstract class SearchFilterBase : ISearchFilter + { + protected SearchFilterBase() + { + Occurence = SearchFilterOccurence.Should; + } - public float Boost - { - get; - protected internal set; - } + public float Boost + { + get; + protected internal set; + } - public SearchFilterOccurence Occurence - { - get; - protected internal set; - } + public SearchFilterOccurence Occurence + { + get; + protected internal set; + } - #region Fluent builder + #region Fluent builder - // [...] + // [...] - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Search/IIndexDocument.cs b/src/Libraries/SmartStore.Core/Search/IIndexDocument.cs index 72ad6f3ccb..6c3db5a6d1 100644 --- a/src/Libraries/SmartStore.Core/Search/IIndexDocument.cs +++ b/src/Libraries/SmartStore.Core/Search/IIndexDocument.cs @@ -2,53 +2,53 @@ namespace SmartStore.Core.Search { - /// - /// Abstraction of document to be stored in the underlying index providers - /// - public interface IIndexDocument : IEnumerable - { - /// - /// The primary key of the indexed entity - /// - /// Implementors: the id must be persisted internally as an - int Id { get; } + /// + /// Abstraction of document to be stored in the underlying index providers + /// + public interface IIndexDocument : IEnumerable + { + /// + /// The primary key of the indexed entity + /// + /// Implementors: the id must be persisted internally as an + int Id { get; } - /// - /// Identifies the type of a document, can be null - /// - SearchDocumentType? DocumentType { get; } + /// + /// Identifies the type of a document, can be null + /// + SearchDocumentType? DocumentType { get; } - /// - /// The number of fields in this document - /// - int Count { get; } + /// + /// The number of fields in this document + /// + int Count { get; } - /// - /// Adds a field to this document - /// - /// The field to add - void Add(IndexField field); + /// + /// Adds a field to this document + /// + /// The field to add + void Add(IndexField field); - /// - /// Removes a field from this document - /// - /// The name of the field - /// The number of removed fields - /// A document can contain fields with the same name. This method removes ALL fields with the passed name. - int Remove(string name); + /// + /// Removes a field from this document + /// + /// The name of the field + /// The number of removed fields + /// A document can contain fields with the same name. This method removes ALL fields with the passed name. + int Remove(string name); - /// - /// Whether at least one field with the passed name exists in the document - /// - /// The name of the field(s) - /// true if field exists, false otherwise - bool Contains(string name); + /// + /// Whether at least one field with the passed name exists in the document + /// + /// The name of the field(s) + /// true if field exists, false otherwise + bool Contains(string name); - /// - /// Enumerates all fields with the passed name - /// - /// The name of the field(s) - /// The sequence - IEnumerable this[string name] { get; } - } + /// + /// Enumerates all fields with the passed name + /// + /// The name of the field(s) + /// The sequence + IEnumerable this[string name] { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Search/IIndexManager.cs b/src/Libraries/SmartStore.Core/Search/IIndexManager.cs index 72868d85b5..8e4301c158 100644 --- a/src/Libraries/SmartStore.Core/Search/IIndexManager.cs +++ b/src/Libraries/SmartStore.Core/Search/IIndexManager.cs @@ -4,7 +4,7 @@ /// A simple factory for registered search index providers /// public interface IIndexManager - { + { /// /// Whether at least one provider is available, which implements /// @@ -14,12 +14,12 @@ public interface IIndexManager /// 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) - /// + /// + /// 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(string scope, bool activeOnly = true); - } + /// Whether only active providers should be queried for + /// The index provider implementation instance + 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 index 27453c616e..553e3f24fc 100644 --- a/src/Libraries/SmartStore.Core/Search/IIndexOperation.cs +++ b/src/Libraries/SmartStore.Core/Search/IIndexOperation.cs @@ -1,35 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SmartStore.Core.Search +namespace SmartStore.Core.Search { - public enum IndexOperationType - { - Index, - Delete - } + public enum IndexOperationType + { + Index, + Delete + } - /// - /// Represents an indexing operation - /// - public interface IIndexOperation - { - /// - /// The type of the operation - /// - IndexOperationType OperationType { get; } + /// + /// 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 document being inserted to or deleted from the index storage + /// + IIndexDocument Document { get; } - /// - /// The database entity from which was created - /// - BaseEntity Entity { 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 b63ac0261c..6b9464f068 100644 --- a/src/Libraries/SmartStore.Core/Search/IIndexProvider.cs +++ b/src/Libraries/SmartStore.Core/Search/IIndexProvider.cs @@ -2,8 +2,8 @@ namespace SmartStore.Core.Search { - public interface IIndexProvider - { + public interface IIndexProvider + { /// /// Gets a value indicating whether the search index given by scope is active /// @@ -15,32 +15,32 @@ public interface IIndexProvider /// IEnumerable EnumerateIndexes(); - /// - /// Creates an empty document - /// - /// The primary key of the indexed entity - /// Identifies the type of a document, can be null - /// The document instance - IIndexDocument CreateDocument(int id, SearchDocumentType? documentType); + /// + /// Creates an empty document + /// + /// The primary key of the indexed entity + /// Identifies the type of a document, can be null + /// The document instance + IIndexDocument CreateDocument(int id, SearchDocumentType? documentType); - /// - /// Returns a provider specific implementation of the interface - /// which allows interaction with the underlying index store for managing the index and containing documents. - /// - /// The index name - /// The index store - /// - /// This methods always returns an object instance, even if the index does not exist yet. - /// - IIndexStore GetIndexStore(string scope); + /// + /// Returns a provider specific implementation of the interface + /// which allows interaction with the underlying index store for managing the index and containing documents. + /// + /// The index name + /// The index store + /// + /// This methods always returns an object instance, even if the index does not exist yet. + /// + IIndexStore GetIndexStore(string scope); - /// - /// Returns a provider specific implementation of the interface - /// which allows executing queries against an index store. - /// - /// The index store - /// The query to execute against the store - /// The search engine instance - ISearchEngine GetSearchEngine(IIndexStore store, ISearchQuery query); - } + /// + /// Returns a provider specific implementation of the interface + /// which allows executing queries against an index store. + /// + /// The index store + /// The query to execute against the store + /// The search engine instance + ISearchEngine GetSearchEngine(IIndexStore store, ISearchQuery query); + } } diff --git a/src/Libraries/SmartStore.Core/Search/IIndexStore.cs b/src/Libraries/SmartStore.Core/Search/IIndexStore.cs index 36e7fe6a23..6a0d6df230 100644 --- a/src/Libraries/SmartStore.Core/Search/IIndexStore.cs +++ b/src/Libraries/SmartStore.Core/Search/IIndexStore.cs @@ -3,27 +3,27 @@ namespace SmartStore.Core.Search { - public interface IIndexStore - { - /// - /// Creates a new index if it doesn't exist already - /// - void CreateIfNotExists(); + public interface IIndexStore + { + /// + /// Creates a new index if it doesn't exist already + /// + void CreateIfNotExists(); - /// - /// Deletes the index - /// - void Delete(); + /// + /// Deletes the index + /// + void Delete(); - /// - /// Returns the name of the index - /// - string Scope { get; } + /// + /// Returns the name of the index + /// + string Scope { get; } - /// - /// Checks whether the index is already existing or not - /// - bool Exists { get; } + /// + /// Checks whether the index is already existing or not + /// + bool Exists { get; } /// /// Gets the size of the index in bytes. @@ -41,64 +41,64 @@ public interface IIndexStore /// Type of document, use null to get all documents int GetDocumentCount(SearchDocumentType? documentType); - /// - /// Returns every field's name available in the index - /// - IEnumerable GetAllFields(); + /// + /// Returns every field's name available in the index + /// + IEnumerable GetAllFields(); - /// - /// Removes all documents from the index - /// - void Clear(); + /// + /// Removes all documents from the index + /// + void Clear(); - /// - /// Acquires an index writer - /// - /// Provides information about the indexing operation - /// - /// This method creates a transient writer instance which automatically gets released on dispose. - /// - IDisposable AcquireWriter(AcquireWriterContext writerContext); + /// + /// Acquires an index writer + /// + /// Provides information about the indexing operation + /// + /// This method creates a transient writer instance which automatically gets released on dispose. + /// + IDisposable AcquireWriter(AcquireWriterContext writerContext); - /// - /// Adds a set of new documents to the index - /// - /// - /// This method will delete already existing documents before saving them - /// - void SaveDocuments(IEnumerable documents); + /// + /// Adds a set of new documents to the index + /// + /// + /// This method will delete already existing documents before saving them + /// + void SaveDocuments(IEnumerable documents); - /// - /// Removes a set of existing documents from the index - /// - void DeleteDocuments(IEnumerable documents); + /// + /// Removes a set of existing documents from the index + /// + void DeleteDocuments(IEnumerable documents); - /// - /// Removes a set of existing documents from the index - /// - /// Type of document - void DeleteDocuments(SearchDocumentType documentType); - } + /// + /// Removes a set of existing documents from the index + /// + /// Type of document + void DeleteDocuments(SearchDocumentType documentType); + } - public static class IIndexStoreExtensions - { - /// - /// Adds a new document to the index - /// - /// - /// This method will delete a document with the same entity id - if it exists - before saving it. - /// - public static void SaveDocument(this IIndexStore store, IIndexDocument document) - { - store.SaveDocuments(new[] { document }); - } + public static class IIndexStoreExtensions + { + /// + /// Adds a new document to the index + /// + /// + /// This method will delete a document with the same entity id - if it exists - before saving it. + /// + public static void SaveDocument(this IIndexStore store, IIndexDocument document) + { + store.SaveDocuments(new[] { document }); + } - /// - /// Removes an existing document from the index - /// - public static void DeleteDocument(this IIndexStore store, IIndexDocument document) - { - store.DeleteDocuments(new[] { document }); - } - } + /// + /// Removes an existing document from the index + /// + public static void DeleteDocument(this IIndexStore store, IIndexDocument document) + { + store.DeleteDocuments(new[] { document }); + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/ISearchAlias.cs b/src/Libraries/SmartStore.Core/Search/ISearchAlias.cs index a822492057..81f1129163 100644 --- a/src/Libraries/SmartStore.Core/Search/ISearchAlias.cs +++ b/src/Libraries/SmartStore.Core/Search/ISearchAlias.cs @@ -1,12 +1,12 @@ namespace SmartStore.Core.Search { - /// - /// Represents an entity which supports SEO friendly search alias - /// - public interface ISearchAlias - { - int Id { get; set; } + /// + /// Represents an entity which supports SEO friendly search alias + /// + public interface ISearchAlias + { + int Id { get; set; } - string Alias { get; set; } - } + string Alias { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs b/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs index 12235f2d44..96a651765e 100644 --- a/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs +++ b/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs @@ -3,43 +3,43 @@ namespace SmartStore.Core.Search { - public interface ISearchEngine - { - /// - /// Search query - /// - ISearchQuery Query { get; } + public interface ISearchEngine + { + /// + /// Search query + /// + ISearchQuery Query { get; } - /// - /// Get search hit by entity identifier - /// - /// Entity identifier - /// Search hit - ISearchHit Get(int id); + /// + /// Get search hit by entity identifier + /// + /// Entity identifier + /// Search hit + ISearchHit Get(int id); - /// - /// Get total number of search hits - /// - /// Total number of search hits - int Count(); + /// + /// Get total number of search hits + /// + /// Total number of search hits + int Count(); - /// - /// Search - /// - /// Search hits - IEnumerable Search(); + /// + /// Search + /// + /// Search hits + IEnumerable Search(); - /// - /// Gets the facet map for drilldown navigation - /// - /// The facet groups - IDictionary GetFacetMap(); + /// + /// Gets the facet map for drilldown navigation + /// + /// The facet groups + IDictionary GetFacetMap(); - /// - /// Performs spell checking ("Diy you mean?") - /// - /// Suggestions/corrections or an empty array - string[] CheckSpelling(); + /// + /// Performs spell checking ("Diy you mean?") + /// + /// Suggestions/corrections or an empty array + string[] CheckSpelling(); /// /// Highlights chosen terms in a text, extracting the most relevant sections diff --git a/src/Libraries/SmartStore.Core/Search/ISearchHit.cs b/src/Libraries/SmartStore.Core/Search/ISearchHit.cs index af39c9a495..03c09a4299 100644 --- a/src/Libraries/SmartStore.Core/Search/ISearchHit.cs +++ b/src/Libraries/SmartStore.Core/Search/ISearchHit.cs @@ -3,18 +3,18 @@ namespace SmartStore.Core.Search { - public interface ISearchHit - { - int EntityId { get; } - float Score { get; } + public interface ISearchHit + { + int EntityId { get; } + float Score { get; } - int GetInt(string name); - double GetDouble(string name); - bool GetBoolean(string name); - string GetString(string name); - string GetString(string name, string languageSeoCode); - DateTime GetDateTime(string name); + int GetInt(string name); + double GetDouble(string name); + bool GetBoolean(string name); + string GetString(string name); + string GetString(string name, string languageSeoCode); + DateTime GetDateTime(string name); - IEnumerable GetStoredFieldNames(); - } + IEnumerable GetStoredFieldNames(); + } } diff --git a/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs b/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs index 28ba070494..46d60e6761 100644 --- a/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs +++ b/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs @@ -4,50 +4,50 @@ namespace SmartStore.Core.Search { public interface ISearchQuery - { - // Language, Currency & Store - int? LanguageId { get; } - string LanguageCulture { get; } - string CurrencyCode { get; } + { + // Language, Currency & Store + int? LanguageId { get; } + string LanguageCulture { get; } + string CurrencyCode { get; } int? StoreId { get; } // Search term string[] Fields { get; } - string Term { get; } - SearchMode Mode { get; } - bool EscapeTerm { get; } - bool IsFuzzySearch { get; } - - // Filtering - ICollection Filters { get; } - - // Facets - IReadOnlyDictionary FacetDescriptors { get; } - - // Paging - int Skip { get; } - int Take { get; } - - // Sorting - ICollection Sorting { get; } - - /// - /// Maximum number of suggestions returned from spell checker - /// - int SpellCheckerMaxSuggestions { get; } - - /// - /// Defines how many characters must be in the query before suggestions are provided - /// - int SpellCheckerMinQueryLength { get; } - - /// - /// The maximum number of product hits up to which suggestions are provided - /// - int SpellCheckerMaxHitCount { get; } - - // Misc - string Origin { get; } - IDictionary CustomData { get; } - } + string Term { get; } + SearchMode Mode { get; } + bool EscapeTerm { get; } + bool IsFuzzySearch { get; } + + // Filtering + ICollection Filters { get; } + + // Facets + IReadOnlyDictionary FacetDescriptors { get; } + + // Paging + int Skip { get; } + int Take { get; } + + // Sorting + ICollection Sorting { get; } + + /// + /// Maximum number of suggestions returned from spell checker + /// + int SpellCheckerMaxSuggestions { get; } + + /// + /// Defines how many characters must be in the query before suggestions are provided + /// + int SpellCheckerMinQueryLength { get; } + + /// + /// The maximum number of product hits up to which suggestions are provided + /// + int SpellCheckerMaxHitCount { get; } + + // Misc + string Origin { get; } + IDictionary CustomData { get; } + } } diff --git a/src/Libraries/SmartStore.Core/Search/IndexDocument.cs b/src/Libraries/SmartStore.Core/Search/IndexDocument.cs index 0fce13d090..a63c2a9f7f 100644 --- a/src/Libraries/SmartStore.Core/Search/IndexDocument.cs +++ b/src/Libraries/SmartStore.Core/Search/IndexDocument.cs @@ -6,103 +6,97 @@ namespace SmartStore.Core.Search { - public class IndexDocument : IIndexDocument - { - protected readonly Multimap _fields; - - public IndexDocument(int id) - : this(id, null) - { - } - - public IndexDocument(int id, SearchDocumentType? documentType) - { - _fields = new Multimap(StringComparer.OrdinalIgnoreCase); - - Add(new IndexField("id", id).Store()); - - if (documentType.HasValue) - { - Add(new IndexField("doctype", (int)documentType.Value).Store()); - } - } - - public int Id - { - get - { - return (int)_fields["id"].FirstOrDefault().Value; - } - } - - public virtual SearchDocumentType? DocumentType - { - get - { - if (_fields.ContainsKey("doctype")) - { - return (SearchDocumentType)_fields["doctype"].FirstOrDefault().Value; - } - return null; - } - } - - public int Count => _fields.TotalValueCount; - - public virtual void Add(IndexField field) - { - if (field.Name.IsCaseInsensitiveEqual("id") && _fields.ContainsKey("id")) - { - // special treatment for id field: allow only one! - _fields.RemoveAll("id"); - } - - if (field.Name.IsCaseInsensitiveEqual("doctype") && _fields.ContainsKey("doctype")) - { - _fields.RemoveAll("doctype"); - } - - _fields.Add(field.Name, field); - } - - public int Remove(string name) - { - if (_fields.ContainsKey(name)) - { - var num = _fields[name].Count; - _fields.RemoveAll(name); - return num; - } - - return 0; - } - - public bool Contains(string name) - { - return _fields.ContainsKey(name); - } - - public IEnumerable this[string name] - { - get - { - if (_fields.ContainsKey(name)) - { - return _fields[name]; - } - - return Enumerable.Empty(); - } - } - - public IEnumerator GetEnumerator() - { - return _fields.SelectMany(x => x.Value).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } + public class IndexDocument : IIndexDocument + { + protected readonly Multimap _fields; + + public IndexDocument(int id) + : this(id, null) + { + } + + public IndexDocument(int id, SearchDocumentType? documentType) + { + _fields = new Multimap(StringComparer.OrdinalIgnoreCase); + + Add(new IndexField("id", id).Store()); + + if (documentType.HasValue) + { + Add(new IndexField("doctype", (int)documentType.Value).Store()); + } + } + + public int Id => (int)_fields["id"].FirstOrDefault().Value; + + public virtual SearchDocumentType? DocumentType + { + get + { + if (_fields.ContainsKey("doctype")) + { + return (SearchDocumentType)_fields["doctype"].FirstOrDefault().Value; + } + return null; + } + } + + public int Count => _fields.TotalValueCount; + + public virtual void Add(IndexField field) + { + if (field.Name.IsCaseInsensitiveEqual("id") && _fields.ContainsKey("id")) + { + // special treatment for id field: allow only one! + _fields.RemoveAll("id"); + } + + if (field.Name.IsCaseInsensitiveEqual("doctype") && _fields.ContainsKey("doctype")) + { + _fields.RemoveAll("doctype"); + } + + _fields.Add(field.Name, field); + } + + public int Remove(string name) + { + if (_fields.ContainsKey(name)) + { + var num = _fields[name].Count; + _fields.RemoveAll(name); + return num; + } + + return 0; + } + + public bool Contains(string name) + { + return _fields.ContainsKey(name); + } + + public IEnumerable this[string name] + { + get + { + if (_fields.ContainsKey(name)) + { + return _fields[name]; + } + + return Enumerable.Empty(); + } + } + + public IEnumerator GetEnumerator() + { + return _fields.SelectMany(x => x.Value).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/IndexField.cs b/src/Libraries/SmartStore.Core/Search/IndexField.cs index 396b9abc8f..3bb2801287 100644 --- a/src/Libraries/SmartStore.Core/Search/IndexField.cs +++ b/src/Libraries/SmartStore.Core/Search/IndexField.cs @@ -2,121 +2,121 @@ namespace SmartStore.Core.Search { - public class IndexField - { - public IndexField(string name, bool value) - : this(name, value, IndexTypeCode.Boolean) - { - } - - public IndexField(string name, int value) - : this(name, value, IndexTypeCode.Int32) - { - } - - public IndexField(string name, double value) - : this(name, value, IndexTypeCode.Double) - { - } - - public IndexField(string name, DateTime value) - : this(name, value, IndexTypeCode.DateTime) - { - } - - public IndexField(string name, string value) - : this(name, value, IndexTypeCode.String) - { - } - - private IndexField(string name, object value, IndexTypeCode typeCode) - { - Guard.NotEmpty(name, nameof(name)); - - Name = name; - Value = value; - TypeCode = typeCode; - } - - public IndexField Store(bool store = true) - { - Stored = store; - return this; - } - - public IndexField Analyze(bool analyze = true) - { - Analyzed = analyze; - return this; - } - - public IndexField RemoveTags(bool removeTags = true) - { - ShouldRemoveTags = removeTags; - return this; - } - - public IndexField Boost(float factor) - { - Boosted = factor; - return this; - } - - public string Name - { - get; - private set; - } - - public object Value - { - get; - private set; - } - - public IndexTypeCode TypeCode - { - get; - private set; - } - - public bool Stored - { - get; - private set; - } - - public bool Analyzed - { - get; - private set; - } - - public bool ShouldRemoveTags - { - get; - private set; - } - - public float Boosted - { - get; - private set; - } - - public override string ToString() - { - return Name + ": " + Value.ToString().NaIfEmpty(); - } - } - - public enum IndexTypeCode - { - Empty = 0, - Boolean = 3, - Int32 = 9, - Double = 14, - DateTime = 16, - String = 18 - } + public class IndexField + { + public IndexField(string name, bool value) + : this(name, value, IndexTypeCode.Boolean) + { + } + + public IndexField(string name, int value) + : this(name, value, IndexTypeCode.Int32) + { + } + + public IndexField(string name, double value) + : this(name, value, IndexTypeCode.Double) + { + } + + public IndexField(string name, DateTime value) + : this(name, value, IndexTypeCode.DateTime) + { + } + + public IndexField(string name, string value) + : this(name, value, IndexTypeCode.String) + { + } + + private IndexField(string name, object value, IndexTypeCode typeCode) + { + Guard.NotEmpty(name, nameof(name)); + + Name = name; + Value = value; + TypeCode = typeCode; + } + + public IndexField Store(bool store = true) + { + Stored = store; + return this; + } + + public IndexField Analyze(bool analyze = true) + { + Analyzed = analyze; + return this; + } + + public IndexField RemoveTags(bool removeTags = true) + { + ShouldRemoveTags = removeTags; + return this; + } + + public IndexField Boost(float factor) + { + Boosted = factor; + return this; + } + + public string Name + { + get; + private set; + } + + public object Value + { + get; + private set; + } + + public IndexTypeCode TypeCode + { + get; + private set; + } + + public bool Stored + { + get; + private set; + } + + public bool Analyzed + { + get; + private set; + } + + public bool ShouldRemoveTags + { + get; + private set; + } + + public float Boosted + { + get; + private set; + } + + public override string ToString() + { + return Name + ": " + Value.ToString().NaIfEmpty(); + } + } + + public enum IndexTypeCode + { + Empty = 0, + Boolean = 3, + Int32 = 9, + Double = 14, + DateTime = 16, + String = 18 + } } diff --git a/src/Libraries/SmartStore.Core/Search/IndexInfo.cs b/src/Libraries/SmartStore.Core/Search/IndexInfo.cs index 7389b7a5cf..b0fbc4b5cc 100644 --- a/src/Libraries/SmartStore.Core/Search/IndexInfo.cs +++ b/src/Libraries/SmartStore.Core/Search/IndexInfo.cs @@ -6,103 +6,103 @@ namespace SmartStore.Core.Search { - public enum IndexingStatus - { - Unavailable = -1, - Idle = 0, - Rebuilding = 1, - Updating = 2 - } - - public class IndexInfo - { - public IndexInfo(string scope) - { + 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]; - } + Fields = new string[0]; + } - public string Scope { get; private set; } - public int DocumentCount { get; set; } + public string Scope { get; private set; } + public int DocumentCount { get; set; } public long IndexSize { get; set; } public int LastAddedDocumentId { 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), + // 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("index-size", IndexSize), new XElement("last-added-document-id", LastAddedDocumentId), new XElement("fields", string.Join(", ", Fields ?? Enumerable.Empty())) - )).ToString(); - } + )).ToString(); + } - public static IndexInfo FromXml(string xml, string scope) - { + 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(); - } + 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 indexSize = doc.Descendants("index-size").FirstOrDefault()?.Value; if (indexSize.HasValue() && CommonHelper.TryConvert(indexSize, out long size)) @@ -117,20 +117,20 @@ public static IndexInfo FromXml(string xml, string scope) } 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; - } - } + 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 15662c3768..a6e57e771b 100644 --- a/src/Libraries/SmartStore.Core/Search/IndexProviderBase.cs +++ b/src/Libraries/SmartStore.Core/Search/IndexProviderBase.cs @@ -2,8 +2,8 @@ namespace SmartStore.Core.Search { - public abstract class IndexProviderBase : IIndexProvider - { + public abstract class IndexProviderBase : IIndexProvider + { public virtual bool IsActive(string scope) { return true; @@ -11,15 +11,15 @@ public virtual bool IsActive(string scope) public abstract IEnumerable EnumerateIndexes(); - public virtual IIndexDocument CreateDocument(int id, SearchDocumentType? documentType) - { - Guard.IsPositive(id, nameof(id)); + public virtual IIndexDocument CreateDocument(int id, SearchDocumentType? documentType) + { + Guard.IsPositive(id, nameof(id)); - return new IndexDocument(id, documentType); - } + return new IndexDocument(id, documentType); + } - public abstract IIndexStore GetIndexStore(string scope); + public abstract IIndexStore GetIndexStore(string scope); - public abstract ISearchEngine GetSearchEngine(IIndexStore store, ISearchQuery query); - } + public abstract ISearchEngine GetSearchEngine(IIndexStore store, ISearchQuery query); + } } diff --git a/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs b/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs index c2597f571d..a919aabc71 100644 --- a/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs +++ b/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs @@ -1,17 +1,17 @@ namespace SmartStore.Core.Search { - public enum SearchDocumentType - { - Product = 0, - Category, - Manufacturer, - DeliveryTime, - Attribute, - AttributeValue, - Variant, - VariantValue, + public enum SearchDocumentType + { + Product = 0, + Category, + Manufacturer, + DeliveryTime, + Attribute, + AttributeValue, + Variant, + VariantValue, Customer, Forum, ForumPost - } + } } diff --git a/src/Libraries/SmartStore.Core/Search/SearchEnums.cs b/src/Libraries/SmartStore.Core/Search/SearchEnums.cs index a280714d51..dd85018a4e 100644 --- a/src/Libraries/SmartStore.Core/Search/SearchEnums.cs +++ b/src/Libraries/SmartStore.Core/Search/SearchEnums.cs @@ -1,20 +1,20 @@ namespace SmartStore.Core.Search { - public enum SearchMode - { - /// - /// Term search - /// - ExactMatch = 0, - - /// - /// Prefix term search - /// - StartsWith, + public enum SearchMode + { + /// + /// Term search + /// + ExactMatch = 0, - /// - /// Wildcard search - /// - Contains - } + /// + /// Prefix term search + /// + StartsWith, + + /// + /// Wildcard search + /// + Contains + } } diff --git a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs index c74e37e286..b9e10af195 100644 --- a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs +++ b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs @@ -10,164 +10,155 @@ namespace SmartStore.Core.Search { public enum SearchResultFlags - { - WithHits = 1 << 0, - WithFacets = 1 << 1, - WithSuggestions = 1 << 2, - Full = WithHits | WithFacets | WithSuggestions - } - - public class SearchQuery : SearchQuery - { - /// - /// Initializes a new instance of the class without a search term being set - /// - public SearchQuery() - : base((string[])null, null) - { - } - - public SearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) - : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) - { - } - - public SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) - : base(fields, term, mode, escape, isFuzzySearch) - { - } - } - - public class SearchQuery : ISearchQuery where TQuery : class, ISearchQuery - { - private readonly Dictionary _facetDescriptors; - private Dictionary _customData; - - protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) - { - Fields = fields; - Term = term; - Mode = mode; - EscapeTerm = escape; - IsFuzzySearch = isFuzzySearch; - - Filters = new List(); - Sorting = new List(); - _facetDescriptors = new Dictionary(StringComparer.OrdinalIgnoreCase); - - Take = int.MaxValue; - - SpellCheckerMinQueryLength = 4; - SpellCheckerMaxHitCount = 3; - - ResultFlags = SearchResultFlags.WithHits; - } - - // Language, Currency & Store - public int? LanguageId { get; protected set; } - public string LanguageCulture { get; protected set; } - public string CurrencyCode { get; protected set; } + { + WithHits = 1 << 0, + WithFacets = 1 << 1, + WithSuggestions = 1 << 2, + Full = WithHits | WithFacets | WithSuggestions + } + + public class SearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class without a search term being set + /// + public SearchQuery() + : base((string[])null, null) + { + } + + public SearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) + : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) + { + } + + public SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) + : base(fields, term, mode, escape, isFuzzySearch) + { + } + } + + public class SearchQuery : ISearchQuery where TQuery : class, ISearchQuery + { + private readonly Dictionary _facetDescriptors; + private Dictionary _customData; + + protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) + { + Fields = fields; + Term = term; + Mode = mode; + EscapeTerm = escape; + IsFuzzySearch = isFuzzySearch; + + Filters = new List(); + Sorting = new List(); + _facetDescriptors = new Dictionary(StringComparer.OrdinalIgnoreCase); + + Take = int.MaxValue; + + SpellCheckerMinQueryLength = 4; + SpellCheckerMaxHitCount = 3; + + ResultFlags = SearchResultFlags.WithHits; + } + + // Language, Currency & Store + public int? LanguageId { get; protected set; } + public string LanguageCulture { get; protected set; } + public string CurrencyCode { get; protected set; } public int? StoreId { get; protected 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; } - public bool IsFuzzySearch { get; protected set; } + public string Term { get; set; } + public bool EscapeTerm { get; protected set; } + public SearchMode Mode { get; protected set; } + public bool IsFuzzySearch { get; protected set; } - // Filtering - public ICollection Filters { get; } + // Filtering + public ICollection Filters { get; } - // Facets - public IReadOnlyDictionary FacetDescriptors - { - get { return _facetDescriptors; } - } + // Facets + public IReadOnlyDictionary FacetDescriptors => _facetDescriptors; - // Paging - public int Skip { get; protected set; } - public int Take { get; protected set; } - public int PageIndex - { - get - { - if (Take == 0) - return 0; + // Paging + public int Skip { get; protected set; } + public int Take { get; protected set; } + public int PageIndex + { + get + { + if (Take == 0) + return 0; - return Math.Max(Skip / Take, 0); - } - } + return Math.Max(Skip / Take, 0); + } + } - // Sorting - public ICollection Sorting { get; } + // Sorting + public ICollection Sorting { get; } - // Spell checker - public int SpellCheckerMaxSuggestions { get; protected set; } - public int SpellCheckerMinQueryLength { get; protected set; } - public int SpellCheckerMaxHitCount { get; protected set; } + // Spell checker + public int SpellCheckerMaxSuggestions { get; protected set; } + public int SpellCheckerMinQueryLength { get; protected set; } + public int SpellCheckerMaxHitCount { get; protected set; } - // Result control - public SearchResultFlags ResultFlags { get; protected set; } + // Result control + public SearchResultFlags ResultFlags { get; protected set; } - // Misc - public string Origin { get; protected set; } + // Misc + public string Origin { get; protected set; } - public IDictionary CustomData - { - get - { - return _customData ?? (_customData = new Dictionary(StringComparer.OrdinalIgnoreCase)); - } - } + public IDictionary CustomData => _customData ?? (_customData = new Dictionary(StringComparer.OrdinalIgnoreCase)); - #region Fluent builder + #region Fluent builder - public virtual TQuery HasStoreId(int id) - { - Guard.NotNegative(id, nameof(id)); + public virtual TQuery HasStoreId(int id) + { + Guard.NotNegative(id, nameof(id)); - StoreId = id; + StoreId = id; - return (this as TQuery); - } + return (this as TQuery); + } - public TQuery WithLanguage(Language language) - { - Guard.NotNull(language, nameof(language)); - Guard.NotEmpty(language.LanguageCulture, nameof(language.LanguageCulture)); + public TQuery WithLanguage(Language language) + { + Guard.NotNull(language, nameof(language)); + Guard.NotEmpty(language.LanguageCulture, nameof(language.LanguageCulture)); - LanguageId = language.Id; - LanguageCulture = language.LanguageCulture; + LanguageId = language.Id; + LanguageCulture = language.LanguageCulture; - return (this as TQuery); - } + return (this as TQuery); + } - public TQuery WithCurrency(Currency currency) - { - Guard.NotNull(currency, nameof(currency)); - Guard.NotEmpty(currency.CurrencyCode, nameof(currency.CurrencyCode)); + public TQuery WithCurrency(Currency currency) + { + Guard.NotNull(currency, nameof(currency)); + Guard.NotEmpty(currency.CurrencyCode, nameof(currency.CurrencyCode)); - CurrencyCode = currency.CurrencyCode; + CurrencyCode = currency.CurrencyCode; - return (this as TQuery); - } + return (this as TQuery); + } - public TQuery Slice(int skip, int take) - { - Guard.NotNegative(skip, nameof(skip)); - Guard.NotNegative(take, nameof(take)); + public TQuery Slice(int skip, int take) + { + Guard.NotNegative(skip, nameof(skip)); + Guard.NotNegative(take, nameof(take)); - Skip = skip; - Take = take; + Skip = skip; + Take = take; - return (this as TQuery); - } + return (this as TQuery); + } - public TQuery CheckSpelling(int maxSuggestions, int minQueryLength = 4, int maxHitCount = 3) - { - Guard.IsPositive(minQueryLength, nameof(minQueryLength)); - Guard.IsPositive(maxHitCount, nameof(maxHitCount)); + public TQuery CheckSpelling(int maxSuggestions, int minQueryLength = 4, int maxHitCount = 3) + { + Guard.IsPositive(minQueryLength, nameof(minQueryLength)); + Guard.IsPositive(maxHitCount, nameof(maxHitCount)); if (maxSuggestions > 0) { @@ -178,117 +169,117 @@ public TQuery CheckSpelling(int maxSuggestions, int minQueryLength = 4, int maxH ResultFlags &= ~SearchResultFlags.WithSuggestions; } - SpellCheckerMaxSuggestions = Math.Max(maxSuggestions, 0); - SpellCheckerMinQueryLength = minQueryLength; - SpellCheckerMaxHitCount = maxHitCount; + SpellCheckerMaxSuggestions = Math.Max(maxSuggestions, 0); + SpellCheckerMinQueryLength = minQueryLength; + SpellCheckerMaxHitCount = maxHitCount; + + return (this as TQuery); + } - return (this as TQuery); - } + public TQuery WithFilter(ISearchFilter filter) + { + Guard.NotNull(filter, nameof(filter)); - public TQuery WithFilter(ISearchFilter filter) - { - Guard.NotNull(filter, nameof(filter)); + Filters.Add(filter); - Filters.Add(filter); + return (this as TQuery); + } - return (this as TQuery); - } + public TQuery SortBy(SearchSort sort) + { + Guard.NotNull(sort, nameof(sort)); - public TQuery SortBy(SearchSort sort) - { - Guard.NotNull(sort, nameof(sort)); + Sorting.Add(sort); - Sorting.Add(sort); + return (this as TQuery); + } - return (this as TQuery); - } + public TQuery BuildHits(bool build) + { + if (build) + { + ResultFlags = ResultFlags | SearchResultFlags.WithHits; + } + else + { + ResultFlags &= ~SearchResultFlags.WithHits; + } - public TQuery BuildHits(bool build) - { - if (build) - { - ResultFlags = ResultFlags | SearchResultFlags.WithHits; - } - else - { - ResultFlags &= ~SearchResultFlags.WithHits; - } + return (this as TQuery); + } - return (this as TQuery); - } + public TQuery BuildFacetMap(bool build) + { + if (build) + { + ResultFlags = ResultFlags | SearchResultFlags.WithFacets; + } + else + { + ResultFlags &= ~SearchResultFlags.WithFacets; + } - public TQuery BuildFacetMap(bool build) - { - if (build) - { - ResultFlags = ResultFlags | SearchResultFlags.WithFacets; - } - else - { - ResultFlags &= ~SearchResultFlags.WithFacets; - } - - return (this as TQuery); - } + return (this as TQuery); + } - public TQuery WithFacet(FacetDescriptor facetDescription) - { - Guard.NotNull(facetDescription, nameof(facetDescription)); + public TQuery WithFacet(FacetDescriptor facetDescription) + { + Guard.NotNull(facetDescription, nameof(facetDescription)); - if (_facetDescriptors.ContainsKey(facetDescription.Key)) - { - throw new InvalidOperationException("A facet description object with the same key has already been added. Key: {0}".FormatInvariant(facetDescription.Key)); - } + if (_facetDescriptors.ContainsKey(facetDescription.Key)) + { + throw new InvalidOperationException("A facet description object with the same key has already been added. Key: {0}".FormatInvariant(facetDescription.Key)); + } - _facetDescriptors.Add(facetDescription.Key, facetDescription); + _facetDescriptors.Add(facetDescription.Key, facetDescription); - return (this as TQuery); - } + return (this as TQuery); + } - public TQuery OriginatesFrom(string origin) - { - Guard.NotEmpty(origin, nameof(origin)); + public TQuery OriginatesFrom(string origin) + { + Guard.NotEmpty(origin, nameof(origin)); - Origin = origin; + Origin = origin; - return (this as TQuery); - } + return (this as TQuery); + } - #endregion + #endregion - public override string ToString() - { + public override string ToString() + { var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; if (Term.HasValue()) - { - var fields = (Fields != null && Fields.Length > 0 ? string.Join(", ", Fields) : "".NaIfEmpty()); + { + var fields = (Fields != null && Fields.Length > 0 ? string.Join(", ", Fields) : "".NaIfEmpty()); - sb.AppendFormat("'{0}' in {1}", Term, fields); + sb.AppendFormat("'{0}' in {1}", Term, fields); - var parameters = string.Join(" ", EscapeTerm ? "escape" : "", IsFuzzySearch ? "fuzzy" : Mode.ToString()).TrimSafe(); + var parameters = string.Join(" ", EscapeTerm ? "escape" : "", IsFuzzySearch ? "fuzzy" : Mode.ToString()).TrimSafe(); - if (parameters.HasValue()) - { - sb.AppendFormat(" ({0})", parameters); - } - } + if (parameters.HasValue()) + { + sb.AppendFormat(" ({0})", parameters); + } + } - foreach (var filter in Filters) - { - if (sb.Length > 0) - { - sb.Append(" "); - } + foreach (var filter in Filters) + { + if (sb.Length > 0) + { + sb.Append(" "); + } - sb.Append(filter.ToString()); - } + sb.Append(filter.ToString()); + } - return psb.ToStringAndReturn(); - } + return psb.ToStringAndReturn(); + } #region Utilities diff --git a/src/Libraries/SmartStore.Core/Search/SearchSort.cs b/src/Libraries/SmartStore.Core/Search/SearchSort.cs index 423ad608db..388fd97d3f 100644 --- a/src/Libraries/SmartStore.Core/Search/SearchSort.cs +++ b/src/Libraries/SmartStore.Core/Search/SearchSort.cs @@ -1,82 +1,82 @@ namespace SmartStore.Core.Search { - public class SearchSort - { - private SearchSort(string name, IndexTypeCode typeCode, bool descending) - { - FieldName = name; - TypeCode = typeCode; - Descending = descending; - } + public class SearchSort + { + private SearchSort(string name, IndexTypeCode typeCode, bool descending) + { + FieldName = name; + TypeCode = typeCode; + Descending = descending; + } - public string FieldName - { - get; - private set; - } + public string FieldName + { + get; + private set; + } - /// - /// In this context, actually means Score - /// - public IndexTypeCode TypeCode - { - get; - private set; - } + /// + /// In this context, actually means Score + /// + public IndexTypeCode TypeCode + { + get; + private set; + } - public bool Descending - { - get; - private set; - } + public bool Descending + { + get; + private set; + } - public override string ToString() - { - if (FieldName.IsEmpty()) - { - return "RELEVANCE"; - } - else - { - return "{0} {1}".FormatInvariant(FieldName, Descending ? "DESC" : "ASC"); - } - } + public override string ToString() + { + if (FieldName.IsEmpty()) + { + return "RELEVANCE"; + } + else + { + return "{0} {1}".FormatInvariant(FieldName, Descending ? "DESC" : "ASC"); + } + } - public static SearchSort ByRelevance(bool descending = false) - { - return new SearchSort(null, IndexTypeCode.Empty, descending); - } + public static SearchSort ByRelevance(bool descending = false) + { + return new SearchSort(null, IndexTypeCode.Empty, descending); + } - public static SearchSort ByStringField(string fieldName, bool descending = false) - { - return ByField(fieldName, IndexTypeCode.String, descending); - } + public static SearchSort ByStringField(string fieldName, bool descending = false) + { + return ByField(fieldName, IndexTypeCode.String, descending); + } - public static SearchSort ByIntField(string fieldName, bool descending = false) - { - return ByField(fieldName, IndexTypeCode.Int32, descending); - } + public static SearchSort ByIntField(string fieldName, bool descending = false) + { + return ByField(fieldName, IndexTypeCode.Int32, descending); + } - public static SearchSort ByBooleanField(string fieldName, bool descending = false) - { - return ByField(fieldName, IndexTypeCode.Boolean, descending); - } + public static SearchSort ByBooleanField(string fieldName, bool descending = false) + { + return ByField(fieldName, IndexTypeCode.Boolean, descending); + } - public static SearchSort ByDoubleField(string fieldName, bool descending = false) - { - return ByField(fieldName, IndexTypeCode.Double, descending); - } + public static SearchSort ByDoubleField(string fieldName, bool descending = false) + { + return ByField(fieldName, IndexTypeCode.Double, descending); + } - public static SearchSort ByDateTimeField(string fieldName, bool descending = false) - { - return ByField(fieldName, IndexTypeCode.DateTime, descending); - } + public static SearchSort ByDateTimeField(string fieldName, bool descending = false) + { + return ByField(fieldName, IndexTypeCode.DateTime, descending); + } - private static SearchSort ByField(string fieldName, IndexTypeCode typeCode, bool descending = false) - { - Guard.NotEmpty(fieldName, nameof(fieldName)); + private static SearchSort ByField(string fieldName, IndexTypeCode typeCode, bool descending = false) + { + Guard.NotEmpty(fieldName, nameof(fieldName)); - return new SearchSort(fieldName, typeCode, descending); - } - } + return new SearchSort(fieldName, typeCode, descending); + } + } } diff --git a/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs b/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs index b213e2e56b..8831ea2c2d 100644 --- a/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs +++ b/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs @@ -5,15 +5,15 @@ namespace SmartStore.Core.Search { public class ForumSearchSettings : ISettings - { - public ForumSearchSettings() - { - SearchMode = SearchMode.Contains; - SearchFields = new List { "username", "text" }; + { + public ForumSearchSettings() + { + SearchMode = SearchMode.Contains; + SearchFields = new List { "username", "text" }; DefaultSortOrder = ForumTopicSorting.Relevance; InstantSearchEnabled = true; - InstantSearchNumberOfHits = 10; - InstantSearchTermMinLength = 2; + InstantSearchNumberOfHits = 10; + InstantSearchTermMinLength = 2; FilterMinHitCount = 1; FilterMaxChoicesCount = 20; @@ -22,15 +22,15 @@ public ForumSearchSettings() DateDisplayOrder = 3; } - /// - /// Gets or sets the search mode. - /// - public SearchMode SearchMode { get; set; } + /// + /// 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 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. @@ -42,15 +42,15 @@ public ForumSearchSettings() /// 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 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 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. diff --git a/src/Libraries/SmartStore.Core/Search/Settings/SearchSettings.cs b/src/Libraries/SmartStore.Core/Search/Settings/SearchSettings.cs index 58d74c9206..9a581da895 100644 --- a/src/Libraries/SmartStore.Core/Search/Settings/SearchSettings.cs +++ b/src/Libraries/SmartStore.Core/Search/Settings/SearchSettings.cs @@ -4,67 +4,67 @@ namespace SmartStore.Core.Search { - public class SearchSettings : ISettings - { - public SearchSettings() - { - SearchMode = SearchMode.Contains; - SearchFields = new List { "sku", "shortdescription", "tagname", "manufacturer", "category" }; - InstantSearchEnabled = true; - ShowProductImagesInInstantSearch = true; - InstantSearchNumberOfProducts = 10; - InstantSearchTermMinLength = 2; - FilterMinHitCount = 1; - FilterMaxChoicesCount = 20; + public class SearchSettings : ISettings + { + public SearchSettings() + { + SearchMode = SearchMode.Contains; + SearchFields = new List { "sku", "shortdescription", "tagname", "manufacturer", "category" }; + InstantSearchEnabled = true; + ShowProductImagesInInstantSearch = true; + InstantSearchNumberOfProducts = 10; + InstantSearchTermMinLength = 2; + FilterMinHitCount = 1; + FilterMaxChoicesCount = 20; DefaultSortOrder = ProductSortingEnum.Relevance; - BrandDisplayOrder = 1; - PriceDisplayOrder = 2; - RatingDisplayOrder = 3; - DeliveryTimeDisplayOrder = 4; - AvailabilityDisplayOrder = 5; - NewArrivalsDisplayOrder = 6; - } - - /// - /// 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 a value indicating whether instant-search is enabled - /// - public bool InstantSearchEnabled { get; set; } - - /// - /// Gets or sets a value indicating whether to show product images in instant-search result - /// - public bool ShowProductImagesInInstantSearch { get; set; } - - /// - /// Gets or sets the number of products to return when using "instant-search" feature - /// - public int InstantSearchNumberOfProducts { 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; } + BrandDisplayOrder = 1; + PriceDisplayOrder = 2; + RatingDisplayOrder = 3; + DeliveryTimeDisplayOrder = 4; + AvailabilityDisplayOrder = 5; + NewArrivalsDisplayOrder = 6; + } + + /// + /// 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 a value indicating whether instant-search is enabled + /// + public bool InstantSearchEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether to show product images in instant-search result + /// + public bool ShowProductImagesInInstantSearch { get; set; } + + /// + /// Gets or sets the number of products to return when using "instant-search" feature + /// + public int InstantSearchNumberOfProducts { 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; } /// /// Gets or sets the default sort order in search results @@ -76,6 +76,11 @@ public SearchSettings() /// public bool UseCatalogSearchInBackend { get; set; } + /// + /// Specifies whether the product page should be opened directly if the search term matches a SKU, MPN or GTIN. + /// + public bool SearchProductByIdentificationNumber { get; set; } + // TBD: what about area specific searchin setting (product, blog, etc.) #region Common facet settings @@ -85,20 +90,20 @@ public SearchSettings() /// public bool IncludeNotAvailable { get; set; } - public bool BrandDisabled { get; set; } - public bool PriceDisabled { get; set; } - public bool RatingDisabled { get; set; } - public bool DeliveryTimeDisabled { get; set; } - public bool AvailabilityDisabled { get; set; } - public bool NewArrivalsDisabled { get; set; } - - public int BrandDisplayOrder { get; set; } - public int PriceDisplayOrder { get; set; } - public int RatingDisplayOrder { get; set; } - public int DeliveryTimeDisplayOrder { get; set; } - public int AvailabilityDisplayOrder { get; set; } - public int NewArrivalsDisplayOrder { get; set; } - - #endregion - } + public bool BrandDisabled { get; set; } + public bool PriceDisabled { get; set; } + public bool RatingDisabled { get; set; } + public bool DeliveryTimeDisabled { get; set; } + public bool AvailabilityDisabled { get; set; } + public bool NewArrivalsDisabled { get; set; } + + public int BrandDisplayOrder { get; set; } + public int PriceDisplayOrder { get; set; } + public int RatingDisplayOrder { get; set; } + public int DeliveryTimeDisplayOrder { get; set; } + public int AvailabilityDisplayOrder { get; set; } + public int NewArrivalsDisplayOrder { get; set; } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Security/IPermissionService.cs b/src/Libraries/SmartStore.Core/Security/IPermissionService.cs index 5b472a1e4e..4acfba9ad5 100644 --- a/src/Libraries/SmartStore.Core/Security/IPermissionService.cs +++ b/src/Libraries/SmartStore.Core/Security/IPermissionService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Collections; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Security; diff --git a/src/Libraries/SmartStore.Core/Security/SmartStorePrincipal.cs b/src/Libraries/SmartStore.Core/Security/SmartStorePrincipal.cs index 201ee0a082..48d48e2d78 100644 --- a/src/Libraries/SmartStore.Core/Security/SmartStorePrincipal.cs +++ b/src/Libraries/SmartStore.Core/Security/SmartStorePrincipal.cs @@ -7,32 +7,32 @@ namespace SmartStore.Core { public class SmartStoreIdentity : ClaimsIdentity - { - [SecuritySafeCritical] - public SmartStoreIdentity(int customerId, string name, string type) - : base(new GenericIdentity(name, type)) - { - CustomerId = customerId; - } + { + [SecuritySafeCritical] + public SmartStoreIdentity(int customerId, string name, string type) + : base(new GenericIdentity(name, type)) + { + CustomerId = customerId; + } - public int CustomerId { get; private set; } + public int CustomerId { get; private set; } - public override bool IsAuthenticated { get { return CustomerId != 0; } } - } + public override bool IsAuthenticated => CustomerId != 0; + } - public class SmartStorePrincipal : IPrincipal - { - public SmartStorePrincipal(Customer customer, string type) - { - Identity = new SmartStoreIdentity(customer.Id, customer.Username.NullEmpty() ?? customer.Email, type); - } + public class SmartStorePrincipal : IPrincipal + { + public SmartStorePrincipal(Customer customer, string type) + { + Identity = new SmartStoreIdentity(customer.Id, customer.Username.NullEmpty() ?? customer.Email, type); + } - public bool IsInRole(string role) - { - return Identity != null && Identity.IsAuthenticated && role.HasValue() && Roles.IsUserInRole(Identity.Name, role); - } + public bool IsInRole(string role) + { + return Identity != null && Identity.IsAuthenticated && role.HasValue() && Roles.IsUserInRole(Identity.Name, role); + } - public IIdentity Identity { get; private set; } - } + public IIdentity Identity { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Core/SmartException.cs b/src/Libraries/SmartStore.Core/SmartException.cs index 0b620fa609..d3137ece3f 100644 --- a/src/Libraries/SmartStore.Core/SmartException.cs +++ b/src/Libraries/SmartStore.Core/SmartException.cs @@ -31,9 +31,9 @@ public SmartException(string message) /// The exception message format. /// The exception message arguments. public SmartException(string messageFormat, params object[] args) - : base(string.Format(messageFormat, args)) - { - } + : base(string.Format(messageFormat, args)) + { + } /// /// Initializes a new instance of the Exception class with serialized data. diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 76f4ec4387..cd84ae4268 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Core SmartStore.Core - v4.6.1 + v4.7.2 512 @@ -65,19 +65,18 @@ ..\..\packages\AngleSharp.0.9.11\lib\net45\AngleSharp.dll - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll - True + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll True - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll True @@ -87,6 +86,9 @@ ..\..\packages\log4net.2.0.8\lib\net45-full\log4net.dll True + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -94,8 +96,8 @@ ..\..\packages\Microsoft.Web.Xdt.3.0.0\lib\net40\Microsoft.Web.XmlTransform.dll - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll ..\..\packages\NuGet.Core.2.14.0\lib\net40-Client\NuGet.Core.dll @@ -111,16 +113,18 @@ True - - ..\..\packages\System.Linq.Dynamic.1.0.7\lib\net40\System.Linq.Dynamic.dll - True + + ..\..\packages\System.Linq.Dynamic.Core.1.2.1\lib\net46\System.Linq.Dynamic.Core.dll + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + - - ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll @@ -204,12 +208,11 @@ - - + @@ -223,6 +226,7 @@ + @@ -262,6 +266,7 @@ + @@ -295,6 +300,7 @@ + @@ -492,16 +498,13 @@ - - - @@ -790,12 +793,7 @@ - - - - - - + diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index e5bc5e5e48..b34a1f8625 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -1,35 +1,34 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using System.Reflection; -using SmartStore; namespace SmartStore.Core { - 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 + 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 + private static readonly List s_breakingChangesHistory = new List { // IMPORTANT: Add app versions from low to high // NOTE: do not specify build & revision unless you have good reasons for it. @@ -38,20 +37,25 @@ public static class SmartStoreVersion new Version("1.2"), new Version("1.2.1"), new Version("2.0"), - new Version("2.1"), - new Version("2.2"), + new Version("2.1"), + new Version("2.2"), new Version("2.5"), - new Version("3.0"), - new Version("3.1"), - new Version("3.1.5"), + new Version("3.0"), + new Version("3.1"), + new Version("3.1.5"), new Version("3.2"), new Version("3.2.1"), - new Version("3.2.2") + new Version("3.2.2"), + new Version("4.0.0"), + new Version("4.0.1"), + new Version("4.1.0"), + new Version("4.1.1"), + new Version("4.2.0") }; - private const string HELP_BASEURL = "https://docs.smartstore.com/display/"; + private const string HELP_BASEURL = "https://docs.smartstore.com/display/"; - static SmartStoreVersion() + static SmartStoreVersion() { s_breakingChangesHistory.Reverse(); @@ -60,80 +64,56 @@ static SmartStoreVersion() if (infoVersionAttr != null) { s_infoVersion = new Version(infoVersionAttr.InformationalVersion); - } + } } /// /// Gets the app version /// - public static string CurrentVersion - { - get - { - return "{0}.{1}".FormatInvariant(s_infoVersion.Major, s_infoVersion.Minor); - } - } + public static string CurrentVersion => "{0}.{1}".FormatInvariant(s_infoVersion.Major, s_infoVersion.Minor); /// /// Gets the app full version /// - public static string CurrentFullVersion + public static string CurrentFullVersion => s_infoVersion.ToString(); + + public static Version Version => s_infoVersion; + + public static string GenerateHelpUrl(string languageCode, HelpTopic topic) { - get - { - return s_infoVersion.ToString(); - } + 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 Version Version + public static string GenerateHelpUrl(string languageCode, string path) { - get - { - return s_infoVersion; - } + Guard.NotEmpty(languageCode, nameof(languageCode)); + + return String.Concat( + HELP_BASEURL, + GetUserGuideSpaceKey(languageCode), + "/", + path.EmptyNull().Trim().TrimStart('/', '\\')); } - 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), - "/", - path.EmptyNull().Trim().TrimStart('/', '\\')); - } - - public static string GetUserGuideSpaceKey(string languageCode) - { - return languageCode.IsCaseInsensitiveEqual("de") - ? "SDDE32" - : "SMNET32"; - } + public static string GetUserGuideSpaceKey(string languageCode) + { + return languageCode.IsCaseInsensitiveEqual("de") + ? "SDDE40" + : "SMNET40"; + } /// - /// Gets a list of SmartStore.NET versions in which breaking changes occured, + /// Gets a list of Smartstore versions in which breaking changes occured, /// which could lead to plugins malfunctioning. /// /// /// A plugin's MinAppVersion is checked against this list to assume /// it's compatibility with the current app version. /// - internal static IEnumerable BreakingChangesHistory - { - get - { - return s_breakingChangesHistory.AsEnumerable(); - } - } + internal static IEnumerable BreakingChangesHistory => s_breakingChangesHistory.AsEnumerable(); } } diff --git a/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs b/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs index 8395d04364..482d2ea87c 100644 --- a/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs +++ b/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs @@ -4,66 +4,66 @@ namespace SmartStore.Templating { - public partial class DefaultTemplateManager : ITemplateManager - { - private readonly ConcurrentDictionary _templates; - private readonly ITemplateEngine _engine; + public partial class DefaultTemplateManager : ITemplateManager + { + private readonly ConcurrentDictionary _templates; + private readonly ITemplateEngine _engine; - public DefaultTemplateManager(ITemplateEngine engine) - { - _templates = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _engine = engine; - } + public DefaultTemplateManager(ITemplateEngine engine) + { + _templates = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _engine = engine; + } - public IReadOnlyDictionary All() - { - return _templates; - } + public IReadOnlyDictionary All() + { + return _templates; + } - public bool Contains(string name) - { - Guard.NotEmpty(name, nameof(name)); + public bool Contains(string name) + { + Guard.NotEmpty(name, nameof(name)); - return _templates.ContainsKey(name); - } + return _templates.ContainsKey(name); + } - public ITemplate Get(string name) - { - Guard.NotEmpty(name, nameof(name)); + public ITemplate Get(string name) + { + Guard.NotEmpty(name, nameof(name)); - _templates.TryGetValue(name, out var template); - return template; - } + _templates.TryGetValue(name, out var template); + return template; + } - public void Put(string name, ITemplate template) - { - Guard.NotEmpty(name, nameof(name)); - Guard.NotNull(template, nameof(template)); + public void Put(string name, ITemplate template) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(template, nameof(template)); - _templates[name] = template; - } + _templates[name] = template; + } - public ITemplate GetOrAdd(string name, Func sourceFactory) - { - Guard.NotEmpty(name, nameof(name)); - Guard.NotNull(sourceFactory, nameof(sourceFactory)); + public ITemplate GetOrAdd(string name, Func sourceFactory) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(sourceFactory, nameof(sourceFactory)); - return _templates.GetOrAdd(name, key => - { - return _engine.Compile(sourceFactory()); - }); - } + return _templates.GetOrAdd(name, key => + { + return _engine.Compile(sourceFactory()); + }); + } - public bool TryRemove(string name, out ITemplate template) - { - Guard.NotEmpty(name, nameof(name)); + public bool TryRemove(string name, out ITemplate template) + { + Guard.NotEmpty(name, nameof(name)); - return _templates.TryRemove(name, out template); - } + return _templates.TryRemove(name, out template); + } - public void Clear() - { - _templates.Clear(); - } - } + public void Clear() + { + _templates.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplate.cs b/src/Libraries/SmartStore.Core/Templating/ITemplate.cs index 3014ba0e20..c1a796f060 100644 --- a/src/Libraries/SmartStore.Core/Templating/ITemplate.cs +++ b/src/Libraries/SmartStore.Core/Templating/ITemplate.cs @@ -2,26 +2,26 @@ namespace SmartStore.Templating { - /// - /// A compiled template intended to be stored in a singleton storage. - /// - public interface ITemplate - { - /// - /// Gets the original template source - /// - string Source { get; } + /// + /// A compiled template intended to be stored in a singleton storage. + /// + public interface ITemplate + { + /// + /// Gets the original template source + /// + string Source { get; } - /// - /// Renders the template in - /// - /// - /// The model object which contains the data for the template. - /// Can be a subclass of , - /// a plain class object, or an anonymous type. - /// - /// Provider to use for formatting numbers, dates, money etc. - /// The processed template result - string Render(object model, IFormatProvider formatProvider); - } + /// + /// Renders the template in + /// + /// + /// The model object which contains the data for the template. + /// Can be a subclass of , + /// a plain class object, or an anonymous type. + /// + /// Provider to use for formatting numbers, dates, money etc. + /// The processed template result + string Render(object model, IFormatProvider formatProvider); + } } diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs b/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs index 56e5868375..c6f6092cbb 100644 --- a/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs +++ b/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs @@ -4,50 +4,50 @@ namespace SmartStore.Templating { - /// - /// Represents a model object for testing purposes. A test model - /// becomes necessary when the database does not contain any data - /// for the previews/tested model. - /// - public interface ITestModel - { - /// - /// The name of the model to use as hash key, e.g. Product, Order, Customer etc. - /// - string ModelName { get; } - } + /// + /// Represents a model object for testing purposes. A test model + /// becomes necessary when the database does not contain any data + /// for the previews/tested model. + /// + public interface ITestModel + { + /// + /// The name of the model to use as hash key, e.g. Product, Order, Customer etc. + /// + string ModelName { get; } + } - /// - /// Responsible for compiling and rendering the templates - /// - public interface ITemplateEngine - { - /// - /// Compiles a template for faster rendering - /// - /// The template source - /// Compiled template - ITemplate Compile(string source); + /// + /// Responsible for compiling and rendering the templates + /// + public interface ITemplateEngine + { + /// + /// Compiles a template for faster rendering + /// + /// The template source + /// Compiled template + ITemplate Compile(string source); - /// - /// Directly renders a template source - /// - /// The template source - /// - /// The model object which contains the data for the template. - /// Can be a subclass of , - /// a plain class object, or an anonymous type. - /// - /// Provider to use for formatting numbers, dates, money etc. - /// The processed template result - string Render(string source, object model, IFormatProvider formatProvider); + /// + /// Directly renders a template source + /// + /// The template source + /// + /// The model object which contains the data for the template. + /// Can be a subclass of , + /// a plain class object, or an anonymous type. + /// + /// Provider to use for formatting numbers, dates, money etc. + /// The processed template result + string Render(string source, object model, IFormatProvider formatProvider); - /// - /// Creates a test model for the passed entity to be used during preview and test. - /// - /// The entity to create a test model for. - /// The model prefix - /// An object which implements and contains some test data for the declared properties of - ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix); - } + /// + /// Creates a test model for the passed entity to be used during preview and test. + /// + /// The entity to create a test model for. + /// The model prefix + /// An object which implements and contains some test data for the declared properties of + ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix); + } } diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs b/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs index affe05aeb1..80e75d44c2 100644 --- a/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs +++ b/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs @@ -3,58 +3,58 @@ namespace SmartStore.Templating { - /// - /// Responsible for managing all compiled templates - /// - public interface ITemplateManager - { - /// - /// Gets all compiled templates - /// - /// - IReadOnlyDictionary All(); + /// + /// Responsible for managing all compiled templates + /// + public interface ITemplateManager + { + /// + /// Gets all compiled templates + /// + /// + IReadOnlyDictionary All(); - /// - /// Checks whether a compiled template exists in the storage - /// - /// Template name - /// true when the template exists - bool Contains(string name); + /// + /// Checks whether a compiled template exists in the storage + /// + /// Template name + /// true when the template exists + bool Contains(string name); - /// - /// Gets a compiled template matching the passed name - /// - /// Template name to get. - /// An instance of or null - ITemplate Get(string name); + /// + /// Gets a compiled template matching the passed name + /// + /// Template name to get. + /// An instance of or null + ITemplate Get(string name); - /// - /// Saves a compiled template in the storage. Any existing template gets overridden. - /// - /// The name to use as template key - /// The compiled template - void Put(string name, ITemplate template); + /// + /// Saves a compiled template in the storage. Any existing template gets overridden. + /// + /// The name to use as template key + /// The compiled template + void Put(string name, ITemplate template); - /// - /// Either gets a template with the passed name, or compiles and stores - /// a template if it does not exist yet. - /// - /// Template name - /// The factory used to generate/obtain the template source. - /// The compiled template - ITemplate GetOrAdd(string name, Func sourceFactory); + /// + /// Either gets a template with the passed name, or compiles and stores + /// a template if it does not exist yet. + /// + /// Template name + /// The factory used to generate/obtain the template source. + /// The compiled template + ITemplate GetOrAdd(string name, Func sourceFactory); - /// - /// Attempts to remove and return the compiled template that has the specified name. - /// - /// The name of the template to remove and return. - /// The removed template or null - /// true if the template was removed, false otherwise. - bool TryRemove(string name, out ITemplate template); + /// + /// Attempts to remove and return the compiled template that has the specified name. + /// + /// The name of the template to remove and return. + /// The removed template or null + /// true if the template was removed, false otherwise. + bool TryRemove(string name, out ITemplate template); - /// - /// Removes all compiled templates from the storage. - /// - void Clear(); - } + /// + /// Removes all compiled templates from the storage. + /// + void Clear(); + } } diff --git a/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs b/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs index 04f166dbf9..ec3a7d99b9 100644 --- a/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs +++ b/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs @@ -3,47 +3,47 @@ namespace SmartStore.Templating { - public class NullTemplateEngine : ITemplateEngine - { - private readonly static ITemplateEngine _instance = new NullTemplateEngine(); - - public static ITemplateEngine Instance => _instance; - - public ITemplate Compile(string template) - { - return new NullTemplate(template); - } - - public string Render(string source, object data, IFormatProvider formatProvider) - { - return source; - } - - public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix) - { - return new NullTestModel(); - } - - internal class NullTestModel : ITestModel - { - public string ModelName => "TestModel"; - } - - internal class NullTemplate : ITemplate - { - private readonly string _source; - - public NullTemplate(string source) - { - _source = source; - } - - public string Source => _source; - - public string Render(object data, IFormatProvider formatProvider) - { - return _source; - } - } - } + public class NullTemplateEngine : ITemplateEngine + { + private readonly static ITemplateEngine _instance = new NullTemplateEngine(); + + public static ITemplateEngine Instance => _instance; + + public ITemplate Compile(string template) + { + return new NullTemplate(template); + } + + public string Render(string source, object data, IFormatProvider formatProvider) + { + return source; + } + + public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix) + { + return new NullTestModel(); + } + + internal class NullTestModel : ITestModel + { + public string ModelName => "TestModel"; + } + + internal class NullTemplate : ITemplate + { + private readonly string _source; + + public NullTemplate(string source) + { + _source = source; + } + + public string Source => _source; + + public string Render(object data, IFormatProvider formatProvider) + { + return _source; + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs index 2776b10b5f..0c9aaa55e5 100644 --- a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs +++ b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs @@ -8,7 +8,6 @@ using System.Threading; using SmartStore.Collections; using SmartStore.Core.Events; -using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.Core.Logging; using SmartStore.Utilities; @@ -16,519 +15,519 @@ namespace SmartStore.Core.Themes { public partial class DefaultThemeRegistry : DisposableObject, IThemeRegistry { - private readonly bool _enableMonitoring; - private readonly string _themesBasePath; - private readonly IEventPublisher _eventPublisher; - private readonly IApplicationEnvironment _env; - private readonly ConcurrentDictionary _themes = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly ConcurrentDictionary _eventQueue = new ConcurrentDictionary(); + private readonly bool _enableMonitoring; + private readonly string _themesBasePath; + private readonly IEventPublisher _eventPublisher; + private readonly IApplicationEnvironment _env; + private readonly ConcurrentDictionary _themes = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly ConcurrentDictionary _eventQueue = new ConcurrentDictionary(); - private readonly Regex _fileFilterPattern = new Regex(@"^\.(config|png|gif|jpg|jpeg|css|less|js|cshtml|svg|json)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex _fileFilterPattern = new Regex(@"^\.(config|png|gif|jpg|jpeg|css|scss|js|cshtml|svg|json)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private FileSystemWatcher _monitorFolders; - private FileSystemWatcher _monitorFiles; + private FileSystemWatcher _monitorFolders; + private FileSystemWatcher _monitorFiles; - public DefaultThemeRegistry(IEventPublisher eventPublisher, IApplicationEnvironment env, bool? enableMonitoring, string themesBasePath, bool autoLoadThemes) + public DefaultThemeRegistry(IEventPublisher eventPublisher, IApplicationEnvironment env, bool? enableMonitoring, string themesBasePath, bool autoLoadThemes) { - _enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); - _eventPublisher = eventPublisher; - _env = env; - _themesBasePath = themesBasePath.NullEmpty() ?? _env.ThemesFolder.RootPath; + _enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); + _eventPublisher = eventPublisher; + _env = env; + _themesBasePath = themesBasePath.NullEmpty() ?? _env.ThemesFolder.RootPath; - Logger = NullLogger.Instance; + Logger = NullLogger.Instance; - if (autoLoadThemes) - { - // load all themes initially - ReloadThemes(); - } + if (autoLoadThemes) + { + // load all themes initially + ReloadThemes(); + } - CreateFileSystemWatchers(); + CreateFileSystemWatchers(); - // start FS watcher - this.StartMonitoring(false); + // start FS watcher + this.StartMonitoring(false); } - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } - #region IThemeRegistry + #region IThemeRegistry - public bool ThemeManifestExists(string themeName) + public bool ThemeManifestExists(string themeName) { if (themeName.IsEmpty()) return false; - ThemeManifest manifest; - if (_themes.TryGetValue(themeName, out manifest)) - { - return manifest.State == ThemeManifestState.Active; - } + ThemeManifest manifest; + if (_themes.TryGetValue(themeName, out manifest)) + { + return manifest.State == ThemeManifestState.Active; + } - return false; + return false; } - public ThemeManifest GetThemeManifest(string themeName) - { - ThemeManifest value; - if (themeName.HasValue() && _themes.TryGetValue(themeName, out value)) - { - return value; - } - return null; - } - - public ICollection GetThemeManifests(bool includeHidden = false) - { - var allThemes = _themes.Values; - - if (includeHidden) - { - return allThemes.AsReadOnly(); - } - else - { - return allThemes.Where(x => x.State == ThemeManifestState.Active).AsReadOnly(); - } - } - - public void AddThemeManifest(ThemeManifest manifest) - { - AddThemeManifestInternal(manifest, false); - } - - private void AddThemeManifestInternal(ThemeManifest manifest, bool isInit) - { - Guard.NotNull(manifest, nameof(manifest)); - - if (!isInit) - { - TryRemoveManifest(manifest.ThemeName); - } - - ThemeManifest baseManifest = null; - if (manifest.BaseThemeName != null) - { - if (!_themes.TryGetValue(manifest.BaseThemeName, out baseManifest)) - { - manifest.State = ThemeManifestState.MissingBaseTheme; - } - } - - manifest.BaseTheme = baseManifest; - var added = _themes.TryAdd(manifest.ThemeName, manifest); - if (added && !isInit) - { - // post process - var children = GetChildrenOf(manifest.ThemeName, false); - foreach (var child in children) - { - child.BaseTheme = manifest; - child.State = ThemeManifestState.Active; - } - } - } - - [SuppressMessage("ReSharper", "AssignmentInConditionalExpression")] - private bool TryRemoveManifest(string themeName) - { - bool result; - ThemeManifest existing; - if (result = _themes.TryRemove(themeName, out existing)) - { - _eventPublisher.Publish(new ThemeTouchedEvent(themeName)); - - existing.BaseTheme = null; - - // set all direct children as broken - var children = GetChildrenOf(themeName, false); - foreach (var child in children) - { - child.BaseTheme = null; - child.State = ThemeManifestState.MissingBaseTheme; - _eventPublisher.Publish(new ThemeTouchedEvent(child.ThemeName)); - } - } - - return result; - } - - public bool IsChildThemeOf(string themeName, string baseTheme) - { - if (themeName.IsEmpty() && baseTheme.IsEmpty()) - { - return false; - } - - if (themeName.Equals(baseTheme, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - var current = GetThemeManifest(themeName); - if (current == null) - return false; - - while (current.BaseThemeName != null) - { - if (baseTheme.Equals(current.BaseThemeName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (!_themes.TryGetValue(current.BaseThemeName, out current)) - { - return false; - } - //currentBaseName = current.BaseThemeName; - } - - return false; - } - - public IEnumerable GetChildrenOf(string themeName, bool deep = true) - { - Guard.NotEmpty(themeName, nameof(themeName)); - - if (!ThemeManifestExists(themeName)) - Enumerable.Empty(); - - var derivedThemes = _themes.Values.Where(x => x.BaseThemeName != null && !x.ThemeName.IsCaseInsensitiveEqual(themeName)); - if (!deep) - { - derivedThemes = derivedThemes.Where(x => x.BaseThemeName.IsCaseInsensitiveEqual(themeName)); - } - else - { - derivedThemes = derivedThemes.Where(x => IsChildThemeOf(x.ThemeName, themeName)); - } - - return derivedThemes; - } - - public void ReloadThemes() - { - _themes.Clear(); - - var folder = _env.ThemesFolder; - var folderDatas = new List(); - var dirs = folder.ListDirectories(""); - - // create folder (meta)datas first - foreach (var path in dirs) - { - try - { - var folderData = ThemeManifest.CreateThemeFolderData(folder.MapPath(path), _themesBasePath); - if (folderData != null) - { - folderDatas.Add(folderData); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Unable to create folder data for folder '{0}'".FormatCurrent(path)); - } - } - - // perform topological sort (BaseThemes first...) - IEnumerable sortedThemeFolders; - try - { - sortedThemeFolders = folderDatas.ToArray().SortTopological(StringComparer.OrdinalIgnoreCase).Cast(); - } - catch (CyclicDependencyException) - { - var ex = new CyclicDependencyException("Cyclic theme dependencies detected. Please check the 'baseTheme' attribute of your themes and ensure that they do not reference themselves (in)directly."); - Logger.Error(ex); - throw ex; - } - catch - { - throw; - } - - // create theme manifests - foreach (var themeFolder in sortedThemeFolders) - { - try - { - var manifest = ThemeManifest.Create(themeFolder); - if (manifest != null) - { - AddThemeManifestInternal(manifest, true); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Unable to create manifest for theme '{0}'".FormatCurrent(themeFolder.FolderName)); - } - } - } + public ThemeManifest GetThemeManifest(string themeName) + { + ThemeManifest value; + if (themeName.HasValue() && _themes.TryGetValue(themeName, out value)) + { + return value; + } + return null; + } + + public ICollection GetThemeManifests(bool includeHidden = false) + { + var allThemes = _themes.Values; + + if (includeHidden) + { + return allThemes.AsReadOnly(); + } + else + { + return allThemes.Where(x => x.State == ThemeManifestState.Active).AsReadOnly(); + } + } + + public void AddThemeManifest(ThemeManifest manifest) + { + AddThemeManifestInternal(manifest, false); + } + + private void AddThemeManifestInternal(ThemeManifest manifest, bool isInit) + { + Guard.NotNull(manifest, nameof(manifest)); + + if (!isInit) + { + TryRemoveManifest(manifest.ThemeName); + } + + ThemeManifest baseManifest = null; + if (manifest.BaseThemeName != null) + { + if (!_themes.TryGetValue(manifest.BaseThemeName, out baseManifest)) + { + manifest.State = ThemeManifestState.MissingBaseTheme; + } + } + + manifest.BaseTheme = baseManifest; + var added = _themes.TryAdd(manifest.ThemeName, manifest); + if (added && !isInit) + { + // post process + var children = GetChildrenOf(manifest.ThemeName, false); + foreach (var child in children) + { + child.BaseTheme = manifest; + child.State = ThemeManifestState.Active; + } + } + } + + [SuppressMessage("ReSharper", "AssignmentInConditionalExpression")] + private bool TryRemoveManifest(string themeName) + { + bool result; + ThemeManifest existing; + if (result = _themes.TryRemove(themeName, out existing)) + { + _eventPublisher.Publish(new ThemeTouchedEvent(themeName)); + + existing.BaseTheme = null; + + // set all direct children as broken + var children = GetChildrenOf(themeName, false); + foreach (var child in children) + { + child.BaseTheme = null; + child.State = ThemeManifestState.MissingBaseTheme; + _eventPublisher.Publish(new ThemeTouchedEvent(child.ThemeName)); + } + } + + return result; + } + + public bool IsChildThemeOf(string themeName, string baseTheme) + { + if (themeName.IsEmpty() && baseTheme.IsEmpty()) + { + return false; + } + + if (themeName.Equals(baseTheme, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var current = GetThemeManifest(themeName); + if (current == null) + return false; + + while (current.BaseThemeName != null) + { + if (baseTheme.Equals(current.BaseThemeName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (!_themes.TryGetValue(current.BaseThemeName, out current)) + { + return false; + } + //currentBaseName = current.BaseThemeName; + } + + return false; + } + + public IEnumerable GetChildrenOf(string themeName, bool deep = true) + { + Guard.NotEmpty(themeName, nameof(themeName)); + + if (!ThemeManifestExists(themeName)) + return Enumerable.Empty(); + + var derivedThemes = _themes.Values.Where(x => x.BaseThemeName != null && !x.ThemeName.IsCaseInsensitiveEqual(themeName)); + if (!deep) + { + derivedThemes = derivedThemes.Where(x => x.BaseThemeName.IsCaseInsensitiveEqual(themeName)); + } + else + { + derivedThemes = derivedThemes.Where(x => IsChildThemeOf(x.ThemeName, themeName)); + } + + return derivedThemes; + } + + public void ReloadThemes() + { + _themes.Clear(); + + var folder = _env.ThemesFolder; + var folderDatas = new List(); + var dirs = folder.ListDirectories(""); + + // create folder (meta)datas first + foreach (var path in dirs) + { + try + { + var folderData = ThemeManifest.CreateThemeFolderData(folder.MapPath(path), _themesBasePath); + if (folderData != null) + { + folderDatas.Add(folderData); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Unable to create folder data for folder '{0}'".FormatCurrent(path)); + } + } + + // perform topological sort (BaseThemes first...) + IEnumerable sortedThemeFolders; + try + { + sortedThemeFolders = folderDatas.ToArray().SortTopological(StringComparer.OrdinalIgnoreCase).Cast(); + } + catch (CyclicDependencyException) + { + var ex = new CyclicDependencyException("Cyclic theme dependencies detected. Please check the 'baseTheme' attribute of your themes and ensure that they do not reference themselves (in)directly."); + Logger.Error(ex); + throw ex; + } + catch + { + throw; + } + + // create theme manifests + foreach (var themeFolder in sortedThemeFolders) + { + try + { + var manifest = ThemeManifest.Create(themeFolder); + if (manifest != null) + { + AddThemeManifestInternal(manifest, true); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Unable to create manifest for theme '{0}'".FormatCurrent(themeFolder.FolderName)); + } + } + } + + #endregion + + #region Monitoring & Events + + private void CreateFileSystemWatchers() + { + _monitorFiles = new FileSystemWatcher(); + _monitorFiles.Path = CommonHelper.MapPath(_themesBasePath); + _monitorFiles.InternalBufferSize = 32768; // 32 instead of the default 8 KB + _monitorFiles.Filter = "*.*"; + _monitorFiles.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.FileName; + _monitorFiles.IncludeSubdirectories = true; + _monitorFiles.Changed += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Modified); + _monitorFiles.Deleted += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Deleted); + _monitorFiles.Created += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Created); + _monitorFiles.Renamed += (s, e) => + { + OnThemeFileChanged(e.OldName, e.OldFullPath, ThemeFileChangeType.Deleted); + OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Created); + }; + + _monitorFolders = new FileSystemWatcher(); + _monitorFolders.Path = CommonHelper.MapPath(_themesBasePath); + _monitorFolders.Filter = "*"; + _monitorFolders.NotifyFilter = NotifyFilters.DirectoryName; + _monitorFolders.IncludeSubdirectories = false; + _monitorFolders.Renamed += (s, e) => OnThemeFolderRenamed(e.Name, e.FullPath, e.OldName, e.OldFullPath); + _monitorFolders.Deleted += (s, e) => OnThemeFolderDeleted(e.Name, e.FullPath); + } + + public void StartMonitoring(bool force) + { + var shouldStart = force || _enableMonitoring; + + if (shouldStart && !_monitorFiles.EnableRaisingEvents) + _monitorFiles.EnableRaisingEvents = true; + if (shouldStart && !_monitorFolders.EnableRaisingEvents) + _monitorFolders.EnableRaisingEvents = true; + } + + public void StopMonitoring() + { + if (_monitorFiles.EnableRaisingEvents) + _monitorFiles.EnableRaisingEvents = false; + if (_monitorFolders.EnableRaisingEvents) + _monitorFolders.EnableRaisingEvents = false; + } + + private bool ShouldThrottleEvent(EventThrottleKey key) + { + Timer timer; + if (_eventQueue.TryGetValue(key, out timer)) + { + // do nothing. The same event was published a tick ago. + return true; + } + + _eventQueue[key] = new Timer(RemoveFromEventQueue, key, 500, Timeout.Infinite); + return false; + } + + private void RemoveFromEventQueue(object key) + { + Timer timer; + if (_eventQueue.TryRemove((EventThrottleKey)key, out timer)) + { + timer.Dispose(); + } + } + + private void OnThemeFileChanged(string name, string fullPath, ThemeFileChangeType changeType) + { + // Enable event throttling by allowing the very same event to be published only all 500 ms. + var throttleKey = new EventThrottleKey(name, changeType); + if (ShouldThrottleEvent(throttleKey)) + { + return; + } + + if (!_fileFilterPattern.IsMatch(Path.GetExtension(name))) + return; + + var idx = name.IndexOf('\\'); + if (idx < 0) + { + // must be a subfolder of "~/Themes/" + return; + } + + var themeName = name.Substring(0, idx); + var relativePath = name.Substring(themeName.Length + 1).Replace('\\', '/'); + var isConfigFile = relativePath.IsCaseInsensitiveEqual("theme.config"); + + if (changeType == ThemeFileChangeType.Modified && !isConfigFile) + { + // Monitor changes only for root theme.config + return; + } + + BaseThemeChangedEventArgs baseThemeChangedArgs = null; + + if (isConfigFile) + { + // config file changes always result in refreshing the corresponding theme manifest + var di = new DirectoryInfo(Path.GetDirectoryName(fullPath)); + + string oldBaseThemeName = null; + var oldManifest = this.GetThemeManifest(di.Name); + if (oldManifest != null) + { + oldBaseThemeName = oldManifest.BaseThemeName; + } + + try + { + // FS watcher in conjunction with some text editors fires change events twice and locks the file. + // Let's wait max. 250ms till the lock is gone (hopefully). + var fi = new FileInfo(fullPath); + fi.WaitForUnlock(250); + + var newManifest = ThemeManifest.Create(di.FullName, _themesBasePath); + if (newManifest != null) + { + this.AddThemeManifestInternal(newManifest, false); + + if (!oldBaseThemeName.IsCaseInsensitiveEqual(newManifest.BaseThemeName)) + { + baseThemeChangedArgs = new BaseThemeChangedEventArgs + { + ThemeName = newManifest.ThemeName, + BaseTheme = newManifest.BaseTheme != null ? newManifest.BaseTheme.ThemeName : null, + OldBaseTheme = oldBaseThemeName + }; + } + + Logger.Debug("Changed theme manifest for '{0}'".FormatCurrent(name)); + } + else + { + // something went wrong (most probably no 'theme.config'): remove the manifest + TryRemoveManifest(di.Name); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Could not touch theme manifest '{0}': {1}".FormatCurrent(name, ex.Message)); + TryRemoveManifest(di.Name); + } + } + + if (baseThemeChangedArgs != null) + { + RaiseBaseThemeChanged(baseThemeChangedArgs); + } + + RaiseThemeFileChanged(new ThemeFileChangedEventArgs + { + ChangeType = changeType, + FullPath = fullPath, + ThemeName = themeName, + RelativePath = relativePath, + IsConfigurationFile = isConfigFile + }); + } + + private void OnThemeFolderRenamed(string name, string fullPath, string oldName, string oldFullPath) + { + TryRemoveManifest(oldName); + + try + { + var newManifest = GetThemeManifest(name); + if (newManifest != null) + { + this.AddThemeManifestInternal(newManifest, false); + Logger.Debug("Changed theme manifest for '{0}'".FormatCurrent(name)); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Could not touch theme manifest '{0}'".FormatCurrent(name)); + } + + RaiseThemeFolderRenamed(new ThemeFolderRenamedEventArgs + { + FullPath = fullPath, + Name = name, + OldFullPath = oldFullPath, + OldName = oldName + }); + } + + private void OnThemeFolderDeleted(string name, string fullPath) + { + TryRemoveManifest(name); + + RaiseThemeFolderDeleted(new ThemeFolderDeletedEventArgs + { + FullPath = fullPath, + Name = name + }); + } + + public event EventHandler ThemeFileChanged; + protected void RaiseThemeFileChanged(ThemeFileChangedEventArgs e) + { + if (ThemeFileChanged != null) + ThemeFileChanged(this, e); + } + + public event EventHandler ThemeFolderRenamed; + protected void RaiseThemeFolderRenamed(ThemeFolderRenamedEventArgs e) + { + if (ThemeFolderRenamed != null) + ThemeFolderRenamed(this, e); + } + + public event EventHandler ThemeFolderDeleted; + protected void RaiseThemeFolderDeleted(ThemeFolderDeletedEventArgs e) + { + if (ThemeFolderDeleted != null) + ThemeFolderDeleted(this, e); + } + + public event EventHandler BaseThemeChanged; + protected void RaiseBaseThemeChanged(BaseThemeChangedEventArgs e) + { + if (BaseThemeChanged != null) + BaseThemeChanged(this, e); + } #endregion - #region Monitoring & Events - - private void CreateFileSystemWatchers() - { - _monitorFiles = new FileSystemWatcher(); - _monitorFiles.Path = CommonHelper.MapPath(_themesBasePath); - _monitorFiles.InternalBufferSize = 32768; // 32 instead of the default 8 KB - _monitorFiles.Filter = "*.*"; - _monitorFiles.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.FileName; - _monitorFiles.IncludeSubdirectories = true; - _monitorFiles.Changed += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Modified); - _monitorFiles.Deleted += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Deleted); - _monitorFiles.Created += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Created); - _monitorFiles.Renamed += (s, e) => - { - OnThemeFileChanged(e.OldName, e.OldFullPath, ThemeFileChangeType.Deleted); - OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Created); - }; - - _monitorFolders = new FileSystemWatcher(); - _monitorFolders.Path = CommonHelper.MapPath(_themesBasePath); - _monitorFolders.Filter = "*"; - _monitorFolders.NotifyFilter = NotifyFilters.DirectoryName; - _monitorFolders.IncludeSubdirectories = false; - _monitorFolders.Renamed += (s, e) => OnThemeFolderRenamed(e.Name, e.FullPath, e.OldName, e.OldFullPath); - _monitorFolders.Deleted += (s, e) => OnThemeFolderDeleted(e.Name, e.FullPath); - } - - public void StartMonitoring(bool force) - { - var shouldStart = force || _enableMonitoring; - - if (shouldStart && !_monitorFiles.EnableRaisingEvents) - _monitorFiles.EnableRaisingEvents = true; - if (shouldStart && !_monitorFolders.EnableRaisingEvents) - _monitorFolders.EnableRaisingEvents = true; - } - - public void StopMonitoring() - { - if (_monitorFiles.EnableRaisingEvents) - _monitorFiles.EnableRaisingEvents = false; - if (_monitorFolders.EnableRaisingEvents) - _monitorFolders.EnableRaisingEvents = false; - } - - private bool ShouldThrottleEvent(EventThrottleKey key) - { - Timer timer; - if (_eventQueue.TryGetValue(key, out timer)) - { - // do nothing. The same event was published a tick ago. - return true; - } - - _eventQueue[key] = new Timer(RemoveFromEventQueue, key, 500, Timeout.Infinite); - return false; - } - - private void RemoveFromEventQueue(object key) - { - Timer timer; - if (_eventQueue.TryRemove((EventThrottleKey)key, out timer)) - { - timer.Dispose(); - } - } - - private void OnThemeFileChanged(string name, string fullPath, ThemeFileChangeType changeType) - { - // Enable event throttling by allowing the very same event to be published only all 500 ms. - var throttleKey = new EventThrottleKey(name, changeType); - if (ShouldThrottleEvent(throttleKey)) - { - return; - } - - if (!_fileFilterPattern.IsMatch(Path.GetExtension(name))) - return; - - var idx = name.IndexOf('\\'); - if (idx < 0) - { - // must be a subfolder of "~/Themes/" - return; - } - - var themeName = name.Substring(0, idx); - var relativePath = name.Substring(themeName.Length + 1).Replace('\\', '/'); - var isConfigFile = relativePath.IsCaseInsensitiveEqual("theme.config"); - - if (changeType == ThemeFileChangeType.Modified && !isConfigFile) - { - // Monitor changes only for root theme.config - return; - } - - BaseThemeChangedEventArgs baseThemeChangedArgs = null; - - if (isConfigFile) - { - // config file changes always result in refreshing the corresponding theme manifest - var di = new DirectoryInfo(Path.GetDirectoryName(fullPath)); - - string oldBaseThemeName = null; - var oldManifest = this.GetThemeManifest(di.Name); - if (oldManifest != null) - { - oldBaseThemeName = oldManifest.BaseThemeName; - } - - try - { - // FS watcher in conjunction with some text editors fires change events twice and locks the file. - // Let's wait max. 250ms till the lock is gone (hopefully). - var fi = new FileInfo(fullPath); - fi.WaitForUnlock(250); - - var newManifest = ThemeManifest.Create(di.FullName, _themesBasePath); - if (newManifest != null) - { - this.AddThemeManifestInternal(newManifest, false); - - if (!oldBaseThemeName.IsCaseInsensitiveEqual(newManifest.BaseThemeName)) - { - baseThemeChangedArgs = new BaseThemeChangedEventArgs - { - ThemeName = newManifest.ThemeName, - BaseTheme = newManifest.BaseTheme != null ? newManifest.BaseTheme.ThemeName : null, - OldBaseTheme = oldBaseThemeName - }; - } - - Logger.Debug("Changed theme manifest for '{0}'".FormatCurrent(name)); - } - else - { - // something went wrong (most probably no 'theme.config'): remove the manifest - TryRemoveManifest(di.Name); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Could not touch theme manifest '{0}': {1}".FormatCurrent(name, ex.Message)); - TryRemoveManifest(di.Name); - } - } - - if (baseThemeChangedArgs != null) - { - RaiseBaseThemeChanged(baseThemeChangedArgs); - } - - RaiseThemeFileChanged(new ThemeFileChangedEventArgs - { - ChangeType = changeType, - FullPath = fullPath, - ThemeName = themeName, - RelativePath = relativePath, - IsConfigurationFile = isConfigFile - }); - } - - private void OnThemeFolderRenamed(string name, string fullPath, string oldName, string oldFullPath) - { - TryRemoveManifest(oldName); - - try - { - var newManifest = GetThemeManifest(name); - if (newManifest != null) - { - this.AddThemeManifestInternal(newManifest, false); - Logger.Debug("Changed theme manifest for '{0}'".FormatCurrent(name)); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Could not touch theme manifest '{0}'".FormatCurrent(name)); - } - - RaiseThemeFolderRenamed(new ThemeFolderRenamedEventArgs - { - FullPath = fullPath, - Name = name, - OldFullPath = oldFullPath, - OldName = oldName - }); - } - - private void OnThemeFolderDeleted(string name, string fullPath) - { - TryRemoveManifest(name); - - RaiseThemeFolderDeleted(new ThemeFolderDeletedEventArgs - { - FullPath = fullPath, - Name = name - }); - } - - public event EventHandler ThemeFileChanged; - protected void RaiseThemeFileChanged(ThemeFileChangedEventArgs e) - { - if (ThemeFileChanged != null) - ThemeFileChanged(this, e); - } - - public event EventHandler ThemeFolderRenamed; - protected void RaiseThemeFolderRenamed(ThemeFolderRenamedEventArgs e) - { - if (ThemeFolderRenamed != null) - ThemeFolderRenamed(this, e); - } - - public event EventHandler ThemeFolderDeleted; - protected void RaiseThemeFolderDeleted(ThemeFolderDeletedEventArgs e) - { - if (ThemeFolderDeleted != null) - ThemeFolderDeleted(this, e); - } - - public event EventHandler BaseThemeChanged; - protected void RaiseBaseThemeChanged(BaseThemeChangedEventArgs e) - { - if (BaseThemeChanged != null) - BaseThemeChanged(this, e); - } - - #endregion - - #region Disposable - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - if (_monitorFiles != null) - { - _monitorFiles.EnableRaisingEvents = false; - _monitorFiles.Dispose(); - _monitorFiles = null; - } - - if (_monitorFolders != null) - { - _monitorFolders.EnableRaisingEvents = false; - _monitorFolders.Dispose(); - _monitorFolders = null; - } - } - } - - #endregion - - private class EventThrottleKey : Tuple - { - public EventThrottleKey(string name, ThemeFileChangeType changeType) - : base(name, changeType) - { - } - } - } + #region Disposable + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + if (_monitorFiles != null) + { + _monitorFiles.EnableRaisingEvents = false; + _monitorFiles.Dispose(); + _monitorFiles = null; + } + + if (_monitorFolders != null) + { + _monitorFolders.EnableRaisingEvents = false; + _monitorFolders.Dispose(); + _monitorFolders = null; + } + } + } + + #endregion + + private class EventThrottleKey : Tuple + { + public EventThrottleKey(string name, ThemeFileChangeType changeType) + : base(name, changeType) + { + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs b/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs index 1162ab331d..7398f169a7 100644 --- a/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs +++ b/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs @@ -2,7 +2,7 @@ namespace SmartStore.Core.Themes { /// - /// Work context + /// Theme context /// public interface IThemeContext { @@ -11,29 +11,29 @@ public interface IThemeContext /// string WorkingThemeName { get; set; } - /// - /// Sets a theme override to be used for the current request - /// - /// The theme override or null to remove the override - void SetRequestTheme(string theme); + /// + /// Sets a theme override to be used for the current request + /// + /// The theme override or null to remove the override + void SetRequestTheme(string theme); - /// - /// Sets a theme override to be used for the current user's session (e.g. for preview mode) - /// - /// The theme override or null to remove the override - void SetPreviewTheme(string theme); + /// + /// Sets a theme override to be used for the current user's session (e.g. for preview mode) + /// + /// The theme override or null to remove the override + void SetPreviewTheme(string theme); - /// - /// Gets the theme override for the current request - /// - /// The theme override or null - string GetRequestTheme(); + /// + /// Gets the theme override for the current request + /// + /// The theme override or null + string GetRequestTheme(); - /// - /// Gets the theme override for the current session - /// - /// The theme override or null - string GetPreviewTheme(); + /// + /// Gets the theme override for the current session + /// + /// The theme override or null + string GetPreviewTheme(); /// /// Gets or sets the manifest of the current working theme diff --git a/src/Libraries/SmartStore.Core/Themes/IThemeFileResolver.cs b/src/Libraries/SmartStore.Core/Themes/IThemeFileResolver.cs index 6c76b038ab..9e2f5d879a 100644 --- a/src/Libraries/SmartStore.Core/Themes/IThemeFileResolver.cs +++ b/src/Libraries/SmartStore.Core/Themes/IThemeFileResolver.cs @@ -1,9 +1,7 @@ -using System; - -namespace SmartStore.Core.Themes +namespace SmartStore.Core.Themes { - public interface IThemeFileResolver - { - InheritedThemeFileResult Resolve(string virtualPath); - } + public interface IThemeFileResolver + { + InheritedThemeFileResult Resolve(string virtualPath); + } } diff --git a/src/Libraries/SmartStore.Core/Themes/IThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/IThemeRegistry.cs index 94bade5e85..c095783f33 100644 --- a/src/Libraries/SmartStore.Core/Themes/IThemeRegistry.cs +++ b/src/Libraries/SmartStore.Core/Themes/IThemeRegistry.cs @@ -5,130 +5,130 @@ namespace SmartStore.Core.Themes { public partial interface IThemeRegistry { - - /// - /// Gets all registered theme manifests - /// - /// Specifies whether inactive themes should also be included in the return list - /// A collection of manifests - ICollection GetThemeManifests(bool includeHidden = false); - - /// - /// Gets a single theme manifest by theme name - /// - /// The name of the theme to get a manifest for - /// A ThemeManifest instance or null, if theme is not registered - ThemeManifest GetThemeManifest(string themeName); - - /// - /// Gets a value indicating whether a theme is registered - /// - /// The theme name to check - /// true if theme exists, false otherwise + + /// + /// Gets all registered theme manifests + /// + /// Specifies whether inactive themes should also be included in the return list + /// A collection of manifests + ICollection GetThemeManifests(bool includeHidden = false); + + /// + /// Gets a single theme manifest by theme name + /// + /// The name of the theme to get a manifest for + /// A ThemeManifest instance or null, if theme is not registered + ThemeManifest GetThemeManifest(string themeName); + + /// + /// Gets a value indicating whether a theme is registered + /// + /// The theme name to check + /// true if theme exists, false otherwise bool ThemeManifestExists(string themeName); - /// - /// Registers a theme manifest - /// - /// The theme manifest to register - /// If an equal theme exists already, it gets removed first. - void AddThemeManifest(ThemeManifest manifest); - - /// - /// Gets a value indicating whether a theme is a child of another theme - /// - /// The name of the theme to test - /// The name of the base theme - /// true when is based on , false othwerise - /// - /// This method walks up the complete hierarchy chain of to determine the result. - /// - bool IsChildThemeOf(string themeName, string baseTheme); - - /// - /// Gets all derived child themes - /// - /// The name of the theme to get the children for - /// When true, the method gets all child themes in the hierarchy chain, otherwise it only returns direct children. - /// The manifests of matching themes - IEnumerable GetChildrenOf(string themeName, bool deep = true); - - /// - /// Starts/resumes raising file system events - /// - /// - /// When true, monitoring gets started regardless of the global setting (web.config > appSettings > sm:MonitorThemesFolder), - /// otherwise this method does nothing when the setting is false. - /// - void StartMonitoring(bool force); - - /// - /// Stops/pauses raising file system events - /// - void StopMonitoring(); - - /// - /// Clears all parsed theme manifests and reloads them - /// - void ReloadThemes(); - - /// - /// Event raised when an inheritable (static) theme file has been created or deleted, - /// OR when the theme.config file has been modified. - /// - event EventHandler ThemeFileChanged; - - /// - /// Event raised when a theme folder has been renamed. - /// - event EventHandler ThemeFolderRenamed; - - /// - /// Event raised when a theme folder has been deleted. - /// - event EventHandler ThemeFolderDeleted; - - /// - /// Event raised when a theme's base theme changes. - /// - event EventHandler BaseThemeChanged; + /// + /// Registers a theme manifest + /// + /// The theme manifest to register + /// If an equal theme exists already, it gets removed first. + void AddThemeManifest(ThemeManifest manifest); + + /// + /// Gets a value indicating whether a theme is a child of another theme + /// + /// The name of the theme to test + /// The name of the base theme + /// true when is based on , false othwerise + /// + /// This method walks up the complete hierarchy chain of to determine the result. + /// + bool IsChildThemeOf(string themeName, string baseTheme); + + /// + /// Gets all derived child themes + /// + /// The name of the theme to get the children for + /// When true, the method gets all child themes in the hierarchy chain, otherwise it only returns direct children. + /// The manifests of matching themes + IEnumerable GetChildrenOf(string themeName, bool deep = true); + + /// + /// Starts/resumes raising file system events + /// + /// + /// When true, monitoring gets started regardless of the global setting (web.config > appSettings > sm:MonitorThemesFolder), + /// otherwise this method does nothing when the setting is false. + /// + void StartMonitoring(bool force); + + /// + /// Stops/pauses raising file system events + /// + void StopMonitoring(); + + /// + /// Clears all parsed theme manifests and reloads them + /// + void ReloadThemes(); + + /// + /// Event raised when an inheritable (static) theme file has been created or deleted, + /// OR when the theme.config file has been modified. + /// + event EventHandler ThemeFileChanged; + + /// + /// Event raised when a theme folder has been renamed. + /// + event EventHandler ThemeFolderRenamed; + + /// + /// Event raised when a theme folder has been deleted. + /// + event EventHandler ThemeFolderDeleted; + + /// + /// Event raised when a theme's base theme changes. + /// + event EventHandler BaseThemeChanged; + } + + + public class ThemeFileChangedEventArgs : EventArgs + { + public string FullPath { get; set; } + public string ThemeName { get; set; } + public string RelativePath { get; set; } + public bool IsConfigurationFile { get; set; } + public ThemeFileChangeType ChangeType { get; set; } } + public class ThemeFolderRenamedEventArgs : EventArgs + { + public string FullPath { get; set; } + public string Name { get; set; } + public string OldFullPath { get; set; } + public string OldName { get; set; } + } - public class ThemeFileChangedEventArgs : EventArgs - { - public string FullPath { get; set; } - public string ThemeName { get; set; } - public string RelativePath { get; set; } - public bool IsConfigurationFile { get; set; } - public ThemeFileChangeType ChangeType { get; set; } - } - - public class ThemeFolderRenamedEventArgs : EventArgs - { - public string FullPath { get; set; } - public string Name { get; set; } - public string OldFullPath { get; set; } - public string OldName { get; set; } - } - - public class ThemeFolderDeletedEventArgs : EventArgs - { - public string FullPath { get; set; } - public string Name { get; set; } - } - - public class BaseThemeChangedEventArgs : EventArgs - { - public string ThemeName { get; set; } - public string BaseTheme { get; set; } - public string OldBaseTheme { get; set; } - } - - public enum ThemeFileChangeType - { - Created, - Deleted, - Modified - } + public class ThemeFolderDeletedEventArgs : EventArgs + { + public string FullPath { get; set; } + public string Name { get; set; } + } + + public class BaseThemeChangedEventArgs : EventArgs + { + public string ThemeName { get; set; } + public string BaseTheme { get; set; } + public string OldBaseTheme { get; set; } + } + + public enum ThemeFileChangeType + { + Created, + Deleted, + Modified + } } diff --git a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs index 77d708c5c8..99d38490e9 100644 --- a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs +++ b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs @@ -1,44 +1,42 @@ -using System; - -namespace SmartStore.Core.Themes +namespace SmartStore.Core.Themes { - public class InheritedThemeFileResult - { - /// - /// The unrooted relative path of the file (without ~/Themes/ThemeName/) - /// - public string RelativePath { get; set; } - - /// - /// The original virtual path - /// - public string OriginalVirtualPath { get; set; } - - /// - /// The result virtual path (the path in which the file is actually located) - /// - public string ResultVirtualPath { get; set; } - - /// - /// The result physical path (the path in which the file is actually located) - /// - public string ResultPhysicalPath { get; set; } - - /// - /// The name of the requesting theme - /// - public string OriginalThemeName { get; set; } - - /// - /// The name of the resulting theme where the file is actually located - /// - public string ResultThemeName { get; set; } - - public bool IsBased { get; set; } - - /// - /// The query string, e.g. '?base' - /// - public string Query { get; set; } - } + public class InheritedThemeFileResult + { + /// + /// The unrooted relative path of the file (without ~/Themes/ThemeName/) + /// + public string RelativePath { get; set; } + + /// + /// The original virtual path + /// + public string OriginalVirtualPath { get; set; } + + /// + /// The result virtual path (the path in which the file is actually located) + /// + public string ResultVirtualPath { get; set; } + + /// + /// The result physical path (the path in which the file is actually located) + /// + public string ResultPhysicalPath { get; set; } + + /// + /// The name of the requesting theme + /// + public string OriginalThemeName { get; set; } + + /// + /// The name of the resulting theme where the file is actually located + /// + public string ResultThemeName { get; set; } + + public bool IsBased { get; set; } + + /// + /// The query string, e.g. '?base' + /// + public string Query { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs b/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs index e557505cc9..62f866de60 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs @@ -3,29 +3,26 @@ namespace SmartStore.Core.Themes { - 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; } + 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; } - string ITopologicSortable.Key - { - get { return FolderName; } - } + string ITopologicSortable.Key => FolderName; - string[] ITopologicSortable.DependsOn - { - get - { - if (BaseTheme == null) - return null; + string[] ITopologicSortable.DependsOn + { + get + { + if (BaseTheme == null) + return null; - return new [] { BaseTheme }; - } - } - } + return new[] { BaseTheme }; + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs index 1efdae47d2..f107635a81 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs @@ -1,348 +1,336 @@ using System; using System.Collections.Generic; +using System.IO; using System.Xml; using SmartStore.Collections; -using System.IO; namespace SmartStore.Core.Themes { - public class ThemeManifest : ComparableObject + public class ThemeManifest : ComparableObject { internal ThemeManifest() { } - #region Methods + #region Methods - public static ThemeManifest Create(string themePath, string virtualBasePath = "~/Themes/") - { - Guard.NotEmpty(themePath, nameof(themePath)); + public static ThemeManifest Create(string themePath, string virtualBasePath = "~/Themes/") + { + Guard.NotEmpty(themePath, nameof(themePath)); - var folderData = CreateThemeFolderData(themePath, virtualBasePath); - if (folderData != null) - { - return Create(folderData); - } + var folderData = CreateThemeFolderData(themePath, virtualBasePath); + if (folderData != null) + { + return Create(folderData); + } - return null; - } + return null; + } - internal static ThemeManifest Create(ThemeFolderData folderData) - { - Guard.NotNull(folderData, nameof(folderData)); + internal static ThemeManifest Create(ThemeFolderData folderData) + { + Guard.NotNull(folderData, nameof(folderData)); - var materializer = new ThemeManifestMaterializer(folderData); - var manifest = materializer.Materialize(); + var materializer = new ThemeManifestMaterializer(folderData); + var manifest = materializer.Materialize(); - return manifest; - } + return manifest; + } - internal static ThemeFolderData CreateThemeFolderData(string themePath, string virtualBasePath = "~/Themes/") - { - if (themePath.IsEmpty()) - return null; + internal static ThemeFolderData CreateThemeFolderData(string themePath, string virtualBasePath = "~/Themes/") + { + if (themePath.IsEmpty()) + return null; - virtualBasePath = virtualBasePath.EnsureEndsWith("/"); + virtualBasePath = virtualBasePath.EnsureEndsWith("/"); - var themeDirectory = new DirectoryInfo(themePath); + var themeDirectory = new DirectoryInfo(themePath); - var isSymLink = themeDirectory.IsSymbolicLink(out var finalPathName); - if (isSymLink) - { - themeDirectory = new DirectoryInfo(finalPathName); - } + var isSymLink = themeDirectory.IsSymbolicLink(out var finalPathName); + if (isSymLink) + { + themeDirectory = new DirectoryInfo(finalPathName); + } - var themeConfigFile = new FileInfo(System.IO.Path.Combine(themeDirectory.FullName, "theme.config")); + var themeConfigFile = new FileInfo(System.IO.Path.Combine(themeDirectory.FullName, "theme.config")); - if (themeConfigFile.Exists) - { - var doc = new XmlDocument(); - doc.Load(themeConfigFile.FullName); + if (themeConfigFile.Exists) + { + var doc = new XmlDocument(); + doc.Load(themeConfigFile.FullName); - Guard.Against(doc.DocumentElement == null, "The theme configuration document must have a root element."); + Guard.Against(doc.DocumentElement == null, "The theme configuration document must have a root element."); - var root = doc.DocumentElement; + var root = doc.DocumentElement; - var baseTheme = root.GetAttribute("baseTheme").TrimSafe().NullEmpty(); - if (baseTheme != null && baseTheme.IsCaseInsensitiveEqual(themeDirectory.Name)) - { - // Don't let theme point to itself! - baseTheme = null; - } + var baseTheme = root.GetAttribute("baseTheme").TrimSafe().NullEmpty(); + if (baseTheme != null && baseTheme.IsCaseInsensitiveEqual(themeDirectory.Name)) + { + // Don't let theme point to itself! + baseTheme = null; + } - return new ThemeFolderData - { - FolderName = themeDirectory.Name, - FullPath = themeDirectory.FullName, - IsSymbolicLink = isSymLink, - Configuration = doc, - VirtualBasePath = virtualBasePath, - BaseTheme = baseTheme - }; - } + return new ThemeFolderData + { + FolderName = themeDirectory.Name, + FullPath = themeDirectory.FullName, + IsSymbolicLink = isSymLink, + Configuration = doc, + VirtualBasePath = virtualBasePath, + BaseTheme = baseTheme + }; + } - return null; - } + return null; + } - #endregion + #endregion - #region Properties + #region Properties - public XmlElement ConfigurationNode - { - get; - protected internal set; - } + public XmlElement ConfigurationNode + { + get; + protected internal set; + } /// /// Gets the virtual theme base path (e.g.: ~/Themes) /// - public string Location - { - get; - protected internal set; - } - - /// - /// Determines whether the theme directory is a symbolic link to another target. - /// - public bool IsSymbolicLink - { - 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 PreviewImageUrl - { - get; - protected internal set; - } - - public string PreviewText - { - get; - protected internal set; - } - - [ObjectSignature] - public string ThemeName - { - get; - protected internal set; - } - - public string BaseThemeName - { - get; - internal set; - } - - public ThemeManifest BaseTheme - { - get; - internal set; - } - - public string ThemeTitle - { - get; - protected internal set; - } - - public string Author - { - get; - protected internal set; - } - - public string Url - { - get; - protected internal set; - } - - public string Version - { - get; - protected internal set; - } - - private IDictionary _variables; - public IDictionary Variables - { - get - { - if (this.BaseTheme == null) - { - return _variables; - } - - var baseVars = this.BaseTheme.Variables; - var mergedVars = new Dictionary(baseVars, StringComparer.OrdinalIgnoreCase); - var newVars = new List(); - - foreach (var localVar in _variables) - { - if (mergedVars.ContainsKey(localVar.Key)) - { - // Overridden var in child: update existing. - var baseVar = mergedVars[localVar.Key]; - mergedVars[localVar.Key] = new ThemeVariableInfo - { - Name = baseVar.Name, - Type = baseVar.Type, - SelectRef = baseVar.SelectRef, - DefaultValue = localVar.Value.DefaultValue, - Manifest = localVar.Value.Manifest - }; - } - else - { - // 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 - { - _variables = value; - } - } - - private Multimap _selects; - public Multimap Selects - { - get - { - if (this.BaseTheme == null) - { - return _selects; - } - - var baseSelects = this.BaseTheme.Selects; - var merged = new Multimap(); - baseSelects.Each(x => merged.AddRange(x.Key, x.Value)); - foreach (var localSelect in _selects) - { - if (!merged.ContainsKey(localSelect.Key)) - { - // New Select in child: add to list. - merged.AddRange(localSelect.Key, localSelect.Value); - } - else - { - // Do nothing: we don't support overriding Selects - } - } - - return merged; - } - internal set - { - _selects = value; - } - } - - private ThemeManifestState _state; - public ThemeManifestState State - { - get - { - if (_state == ThemeManifestState.Active) - { - // active state does not mean, that it actually IS active: check state of base themes! - var baseTheme = this.BaseTheme; - while (baseTheme != null) - { - if (baseTheme.State != ThemeManifestState.Active) - { - return baseTheme.State; - } - baseTheme = baseTheme.BaseTheme; - } - } - - return _state; - } - protected internal set - { - _state = value; - } - } - - internal string FullPath + public string Location + { + get; + protected internal set; + } + + /// + /// Determines whether the theme directory is a symbolic link to another target. + /// + public bool IsSymbolicLink + { + 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 PreviewImageUrl + { + get; + protected internal set; + } + + public string PreviewText + { + get; + protected internal set; + } + + [ObjectSignature] + public string ThemeName { - get { return System.IO.Path.Combine(this.Path, "theme.config"); } - } - - public override string ToString() - { - return "{0} (Parent: {1}, State: {2})".FormatInvariant(ThemeName, BaseThemeName ?? "-", State.ToString()); - } - - #endregion - - #region Dispose - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected void Dispose(bool disposing) - { - if (disposing) - { - BaseTheme = null; - if (_variables != null) - { - foreach (var pair in _variables) - { - pair.Value.Dispose(); - } - _variables.Clear(); - } - } - } - - ~ThemeManifest() + get; + protected internal set; + } + + public string BaseThemeName + { + get; + internal set; + } + + public ThemeManifest BaseTheme + { + get; + internal set; + } + + public string ThemeTitle + { + get; + protected internal set; + } + + public string Author + { + get; + protected internal set; + } + + public string Url + { + get; + protected internal set; + } + + public string Version + { + get; + protected internal set; + } + + private IDictionary _variables; + public IDictionary Variables + { + get + { + if (this.BaseTheme == null) + { + return _variables; + } + + var baseVars = this.BaseTheme.Variables; + var mergedVars = new Dictionary(baseVars, StringComparer.OrdinalIgnoreCase); + var newVars = new List(); + + foreach (var localVar in _variables) + { + if (mergedVars.ContainsKey(localVar.Key)) + { + // Overridden var in child: update existing. + var baseVar = mergedVars[localVar.Key]; + mergedVars[localVar.Key] = new ThemeVariableInfo + { + Name = baseVar.Name, + Type = baseVar.Type, + SelectRef = baseVar.SelectRef, + DefaultValue = localVar.Value.DefaultValue, + Manifest = localVar.Value.Manifest + }; + } + else + { + // 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 => _variables = value; + } + + private Multimap _selects; + public Multimap Selects + { + get + { + if (this.BaseTheme == null) + { + return _selects; + } + + var baseSelects = this.BaseTheme.Selects; + var merged = new Multimap(); + baseSelects.Each(x => merged.AddRange(x.Key, x.Value)); + foreach (var localSelect in _selects) + { + if (!merged.ContainsKey(localSelect.Key)) + { + // New Select in child: add to list. + merged.AddRange(localSelect.Key, localSelect.Value); + } + else + { + // Do nothing: we don't support overriding Selects + } + } + + return merged; + } + internal set => _selects = value; + } + + private ThemeManifestState _state; + public ThemeManifestState State + { + get + { + if (_state == ThemeManifestState.Active) + { + // active state does not mean, that it actually IS active: check state of base themes! + var baseTheme = this.BaseTheme; + while (baseTheme != null) + { + if (baseTheme.State != ThemeManifestState.Active) + { + return baseTheme.State; + } + baseTheme = baseTheme.BaseTheme; + } + } + + return _state; + } + protected internal set => _state = value; + } + + internal string FullPath => System.IO.Path.Combine(this.Path, "theme.config"); + + public override string ToString() + { + return "{0} (Parent: {1}, State: {2})".FormatInvariant(ThemeName, BaseThemeName ?? "-", State.ToString()); + } + + #endregion + + #region Dispose + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (disposing) + { + BaseTheme = null; + if (_variables != null) + { + foreach (var pair in _variables) + { + pair.Value.Dispose(); + } + _variables.Clear(); + } + } + } + + ~ThemeManifest() { Dispose(false); } - #endregion - } + #endregion + } - public enum ThemeManifestState - { - MissingBaseTheme = -1, - Active = 0, - } + public enum ThemeManifestState + { + MissingBaseTheme = -1, + Active = 0, + } } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs index 98f1058002..371dc9cd34 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs @@ -2,39 +2,38 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using System.Xml; using SmartStore.Collections; namespace SmartStore.Core.Themes -{ +{ internal class ThemeManifestMaterializer { private readonly ThemeManifest _manifest; public ThemeManifestMaterializer(ThemeFolderData folderData) { - Guard.NotNull(folderData, nameof(folderData)); + Guard.NotNull(folderData, nameof(folderData)); _manifest = new ThemeManifest(); - _manifest.ThemeName = folderData.FolderName; - _manifest.BaseThemeName = folderData.BaseTheme; + _manifest.ThemeName = folderData.FolderName; + _manifest.BaseThemeName = folderData.BaseTheme; _manifest.Location = folderData.VirtualBasePath; _manifest.Path = folderData.FullPath; - _manifest.IsSymbolicLink = folderData.IsSymbolicLink; + _manifest.IsSymbolicLink = folderData.IsSymbolicLink; _manifest.ConfigurationNode = folderData.Configuration.DocumentElement; } - + public ThemeManifest Materialize() { var root = _manifest.ConfigurationNode; - _manifest.ThemeTitle = root.GetAttribute("title").NullEmpty() ?? _manifest.ThemeName; + _manifest.ThemeTitle = root.GetAttribute("title").NullEmpty() ?? _manifest.ThemeName; _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(); - _manifest.Url = root.GetAttribute("url").ToSafe(); + _manifest.Url = root.GetAttribute("url").ToSafe(); _manifest.Version = root.GetAttribute("version").ToSafe().HasValue() ? root.GetAttribute("version") : "1.0"; _manifest.Selects = MaterializeSelects(); @@ -43,9 +42,9 @@ public ThemeManifest Materialize() return _manifest; } - [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private Multimap MaterializeSelects() + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + private Multimap MaterializeSelects() { var selects = new Multimap(); var root = _manifest.ConfigurationNode; @@ -115,15 +114,14 @@ private ThemeVariableInfo MaterializeVariable(XmlElement xel) string type = xel.GetAttribute("type").ToSafe("String"); - string selectRef = null; - var varType = ConvertVarType(type, xel, out selectRef); + var varType = ConvertVarType(type, xel, out var selectRef); - if (varType != ThemeVariableType.String && value.IsEmpty()) - { - throw new SmartException("A value is required for non-string 'Var' elements. Affected: '{0}' - element: {1}", _manifest.FullPath, xel.OuterXml); - } + if (varType != ThemeVariableType.String && value.IsEmpty()) + { + throw new SmartException("A value is required for non-string 'Var' elements. Affected: '{0}' - element: {1}", _manifest.FullPath, xel.OuterXml); + } - var info = new ThemeVariableInfo + var info = new ThemeVariableInfo { Name = name, DefaultValue = value, diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeSwitchedEvent.cs b/src/Libraries/SmartStore.Core/Themes/ThemeSwitchedEvent.cs index c076712e4b..25a50006c9 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeSwitchedEvent.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeSwitchedEvent.cs @@ -1,6 +1,4 @@ -using System; - -namespace SmartStore.Core.Themes +namespace SmartStore.Core.Themes { public class ThemeSwitchedEvent { diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeTouchedEvent.cs b/src/Libraries/SmartStore.Core/Themes/ThemeTouchedEvent.cs index cdef95b538..7d8622181b 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeTouchedEvent.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeTouchedEvent.cs @@ -1,15 +1,13 @@ -using System; - -namespace SmartStore.Core.Themes +namespace SmartStore.Core.Themes { - public class ThemeTouchedEvent - { - public ThemeTouchedEvent(string themeName) - { - Guard.NotEmpty(themeName, nameof(themeName)); - this.ThemeName = themeName; - } - - public string ThemeName { get; set; } - } + public class ThemeTouchedEvent + { + public ThemeTouchedEvent(string themeName) + { + Guard.NotEmpty(themeName, nameof(themeName)); + this.ThemeName = themeName; + } + + public string ThemeName { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs b/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs index 0fbb373a9d..f6609f6cc0 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SmartStore.Core.Themes -{ +namespace SmartStore.Core.Themes +{ /// /// Represents deserialized metadata for a theme variable /// diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeVariableType.cs b/src/Libraries/SmartStore.Core/Themes/ThemeVariableType.cs index f07e5cf7ef..a36cb21465 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeVariableType.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeVariableType.cs @@ -1,6 +1,4 @@ -using System; - -namespace SmartStore.Core.Themes +namespace SmartStore.Core.Themes { public enum ThemeVariableType { diff --git a/src/Libraries/SmartStore.Core/Utilities/ActionDisposable.cs b/src/Libraries/SmartStore.Core/Utilities/ActionDisposable.cs index dd99242538..b568462120 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ActionDisposable.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ActionDisposable.cs @@ -2,7 +2,6 @@ namespace SmartStore.Utilities { - /// /// Allows action to be executed when it is disposed /// @@ -14,7 +13,7 @@ public struct ActionDisposable : IDisposable public ActionDisposable(Action action) { - Guard.NotNull(action, nameof(action)); + Guard.NotNull(action, nameof(action)); _action = action; } @@ -23,8 +22,5 @@ public void Dispose() { _action(); } - } - - } diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs index 5f413627b1..0b1cd338e5 100644 --- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs @@ -3,165 +3,165 @@ using System.Collections.Generic; using System.Configuration; using System.Dynamic; -using System.Linq; using System.Globalization; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization.Formatters.Binary; using System.Security.Cryptography; +using System.Text; using System.Web.Hosting; using System.Web.Mvc; -using SmartStore.ComponentModel; -using System.Text; using Newtonsoft.Json; -using System.Runtime.Serialization.Formatters.Binary; -using System.Runtime.CompilerServices; +using SmartStore.ComponentModel; namespace SmartStore.Utilities { public static partial class CommonHelper { - private static bool? _isDevEnvironment; - - [ThreadStatic] - private static Random _random; - - private static Random GetRandomizer() - { - if (_random == null) - { - _random = new Random(); - } - - return _random; - } - - /// + private static bool? _isDevEnvironment; + + [ThreadStatic] + private static Random _random; + + private static Random GetRandomizer() + { + if (_random == null) + { + _random = new Random(); + } + + return _random; + } + + /// /// Generate random digit code /// /// Length /// Result string public static string GenerateRandomDigitCode(int length) { - var buffer = new int[length]; - for (int i = 0; i < length; ++i) - { - buffer[i] = GetRandomizer().Next(10); - } - - return string.Join("", buffer); - } - - /// - /// Returns a random number within the range to - 1. - /// - /// Minimum number - /// Maximum number (exclusive!). - /// Random integer number. - public static int GenerateRandomInteger(int min = 0, int max = 2147483647) - { - var randomNumberBuffer = new byte[10]; - new RNGCryptoServiceProvider().GetBytes(randomNumberBuffer); - return new Random(BitConverter.ToInt32(randomNumberBuffer, 0)).Next(min, max); - } - - /// - /// Maps a virtual path to a physical disk path. - /// - /// The path to map. E.g. "~/bin" - /// Specifies if the app root should be resolved when mapped directory does not exist - /// The physical path. E.g. "c:\inetpub\wwwroot\bin" - /// - /// This method is able to resolve the web application root - /// even when it's called during design-time (e.g. from EF design-time tools). - /// - public static string MapPath(string path, bool findAppRoot = true) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - if (HostingEnvironment.IsHosted) - { - // hosted - return HostingEnvironment.MapPath(path); - } - else - { - // not hosted. For example, running in unit tests or EF tooling - string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; - path = path.Replace("~/", "").TrimStart('/').Replace('/', '\\'); - - var testPath = Path.Combine(baseDirectory, path); - - if (findAppRoot /* && !Directory.Exists(testPath)*/) - { - // most likely we're in unit tests or design-mode (EF migration scaffolding)... - // find solution root directory first - var dir = FindSolutionRoot(baseDirectory); - - // concat the web root - if (dir != null) - { - baseDirectory = Path.Combine(dir.FullName, "Presentation\\SmartStore.Web"); - testPath = Path.Combine(baseDirectory, path); - } - } - - return testPath; - } - } - - public static bool IsDevEnvironment - { - get - { - if (!_isDevEnvironment.HasValue) - { - _isDevEnvironment = IsDevEnvironmentInternal(); - } - - return _isDevEnvironment.Value; - } - } - - private static bool IsDevEnvironmentInternal() - { - if (!HostingEnvironment.IsHosted) - return true; - - if (HostingEnvironment.IsDevelopmentEnvironment) - return true; - - if (System.Diagnostics.Debugger.IsAttached) - return true; - - // if there's a 'SmartStore.NET.sln' in one of the parent folders, - // then we're likely in a dev environment - if (FindSolutionRoot(HostingEnvironment.MapPath("~/")) != null) - return true; - - return false; - } - - private static DirectoryInfo FindSolutionRoot(string currentDir) - { - var dir = Directory.GetParent(currentDir); - while (true) - { - if (dir == null || IsSolutionRoot(dir)) - break; - - dir = dir.Parent; - } - - return dir; - } - - private static bool IsSolutionRoot(DirectoryInfo dir) - { - return File.Exists(Path.Combine(dir.FullName, "SmartStoreNET.sln")); - } - - public static bool TryConvert(object value, out T convertedValue) - { + var buffer = new int[length]; + for (int i = 0; i < length; ++i) + { + buffer[i] = GetRandomizer().Next(10); + } + + return string.Join("", buffer); + } + + /// + /// Returns a random number within the range to - 1. + /// + /// Minimum number + /// Maximum number (exclusive!). + /// Random integer number. + public static int GenerateRandomInteger(int min = 0, int max = 2147483647) + { + var randomNumberBuffer = new byte[10]; + new RNGCryptoServiceProvider().GetBytes(randomNumberBuffer); + return new Random(BitConverter.ToInt32(randomNumberBuffer, 0)).Next(min, max); + } + + /// + /// Maps a virtual path to a physical disk path. + /// + /// The path to map. E.g. "~/bin" + /// Specifies if the app root should be resolved when mapped directory does not exist + /// The physical path. E.g. "c:\inetpub\wwwroot\bin" + /// + /// This method is able to resolve the web application root + /// even when it's called during design-time (e.g. from EF design-time tools). + /// + public static string MapPath(string path, bool findAppRoot = true) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + if (HostingEnvironment.IsHosted) + { + // hosted + return HostingEnvironment.MapPath(path); + } + else + { + // not hosted. For example, running in unit tests or EF tooling + string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + path = path.Replace("~/", "").TrimStart('/').Replace('/', '\\'); + + var testPath = Path.Combine(baseDirectory, path); + + if (findAppRoot /* && !Directory.Exists(testPath)*/) + { + // most likely we're in unit tests or design-mode (EF migration scaffolding)... + // find solution root directory first + var dir = FindSolutionRoot(baseDirectory); + + // concat the web root + if (dir != null) + { + baseDirectory = Path.Combine(dir.FullName, "Presentation\\SmartStore.Web"); + testPath = Path.Combine(baseDirectory, path); + } + } + + return testPath; + } + } + + public static bool IsDevEnvironment + { + get + { + if (!_isDevEnvironment.HasValue) + { + _isDevEnvironment = IsDevEnvironmentInternal(); + } + + return _isDevEnvironment.Value; + } + } + + private static bool IsDevEnvironmentInternal() + { + if (!HostingEnvironment.IsHosted) + return true; + + if (HostingEnvironment.IsDevelopmentEnvironment) + return true; + + if (System.Diagnostics.Debugger.IsAttached) + return true; + + // if there's a 'SmartStore.NET.sln' in one of the parent folders, + // then we're likely in a dev environment + if (FindSolutionRoot(HostingEnvironment.MapPath("~/")) != null) + return true; + + return false; + } + + private static DirectoryInfo FindSolutionRoot(string currentDir) + { + var dir = Directory.GetParent(currentDir); + while (true) + { + if (dir == null || IsSolutionRoot(dir)) + break; + + dir = dir.Parent; + } + + return dir; + } + + private static bool IsSolutionRoot(DirectoryInfo dir) + { + return File.Exists(Path.Combine(dir.FullName, "SmartStoreNET.sln")); + } + + public static bool TryConvert(object value, out T convertedValue) + { convertedValue = default(T); if (TryConvert(value, typeof(T), CultureInfo.InvariantCulture, out object result)) @@ -190,7 +190,7 @@ public static bool TryConvert(object value, CultureInfo culture, out T conver [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryConvert(object value, Type to, out object convertedValue) - { + { return TryConvert(value, to, CultureInfo.InvariantCulture, out convertedValue); } @@ -205,7 +205,7 @@ public static bool TryConvert(object value, Type to, CultureInfo culture, out ob { return to == typeof(string) || to.IsPredefinedSimpleType() == false; } - + if (to != typeof(object) && to.IsInstanceOfType(value)) { convertedValue = value; @@ -246,202 +246,202 @@ public static bool TryConvert(object value, Type to, CultureInfo culture, out ob } public static ExpandoObject ToExpando(object value) - { - Guard.NotNull(value, nameof(value)); - - var anonymousDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(value); - IDictionary expando = new ExpandoObject(); - foreach (var item in anonymousDictionary) - { - expando.Add(item); - } - return (ExpandoObject)expando; - } - - public static IDictionary ObjectToDictionary(object obj) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - return FastProperty.ObjectToDictionary( - obj, - key => key.Replace("_", "-").Replace("@", "")); - } - - /// - /// Gets a setting from the application's web.config appSettings node - /// - /// The type to convert the setting value to - /// The key of the setting - /// The default value to return if the setting does not exist - /// The casted setting value - public static T GetAppSetting(string key, T defValue = default(T)) - { - Guard.NotEmpty(key, nameof(key)); - - var setting = ConfigurationManager.AppSettings[key]; - - if (setting == null) - { - return defValue; - } - - return setting.Convert(); - } - - public static bool HasConnectionString(string connectionStringName) - { - var conString = ConfigurationManager.ConnectionStrings[connectionStringName]; - if (conString != null && conString.ConnectionString.HasValue()) - { - return true; - } - - return false; - } - - private static bool TryAction(Func func, out T output) - { - Guard.NotNull(func, nameof(func)); - - try - { - output = func(); - return true; - } - catch - { - output = default(T); - return false; - } - } - - public static bool IsTruthy(object value) - { - if (value == null) - return false; - - switch (value) - { - case string x: - return x.HasValue(); - case bool x: - return x == true; - case DateTime x: - return x > DateTime.MinValue; - case TimeSpan x: - return x > TimeSpan.MinValue; - case Guid x: - return x != Guid.Empty; - case IComparable x: - return x.CompareTo(0) != 0; - case IEnumerable x: - return x.Any(); - case IEnumerable x: - return x.GetEnumerator().MoveNext(); - } - - if (value.GetType().IsNullable(out var wrappedType)) - { - return IsTruthy(Convert.ChangeType(value, wrappedType)); - } - - return true; - } - - public static long GetObjectSizeInBytes(object obj, HashSet instanceLookup = null) - { - if (obj == null) - return 0; - - var type = obj.GetType(); - var genericArguments = type.GetGenericArguments(); - - long size = 0; - - if (obj is string str) - { - size = Encoding.Default.GetByteCount(str); - } - else if (obj is StringBuilder sb) - { - size = Encoding.Default.GetByteCount(sb.ToString()); - } - else if (type.IsEnum) - { - size = System.Runtime.InteropServices.Marshal.SizeOf(Enum.GetUnderlyingType(type)); - } - else if (type.IsPredefinedSimpleType() || type.IsPredefinedGenericType()) - { - //size = System.Runtime.InteropServices.Marshal.SizeOf(Nullable.GetUnderlyingType(type) ?? type); // crashes often - size = 8; // mean/average - } - else if (obj is Stream stream) - { - size = stream.Length; - } - else if (obj is IDictionary dic) - { - foreach (var item in dic.Values) - { - size += GetObjectSizeInBytes(item, instanceLookup); - } - } - else if (obj is IEnumerable e) - { - foreach (var item in e) - { - size += GetObjectSizeInBytes(item, instanceLookup); - } - } - else - { - if (instanceLookup == null) - { - instanceLookup = new HashSet(ReferenceEqualityComparer.Default); - } - - if (!type.IsValueType && instanceLookup.Contains(obj)) - { - return 0; - } - - instanceLookup.Add(obj); - - var serialized = false; - - if (type.IsSerializable && genericArguments.All(x => x.IsSerializable)) - { - try - { - using (var s = new MemoryStream()) - { - var formatter = new BinaryFormatter(); - formatter.Serialize(s, obj); - size = s.Length; - - serialized = true; - } - } - catch { } - } - - if (!serialized) - { - // Serialization failed or is not supported: make JSON. - var json = JsonConvert.SerializeObject(obj, new JsonSerializerSettings - { + { + Guard.NotNull(value, nameof(value)); + + var anonymousDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(value); + IDictionary expando = new ExpandoObject(); + foreach (var item in anonymousDictionary) + { + expando.Add(item); + } + return (ExpandoObject)expando; + } + + public static IDictionary ObjectToDictionary(object obj) + { + if (obj == null) + return new Dictionary(); + + return FastProperty.ObjectToDictionary( + obj, + key => key.Replace("_", "-").Replace("@", "")); + } + + /// + /// Gets a setting from the application's web.config appSettings node + /// + /// The type to convert the setting value to + /// The key of the setting + /// The default value to return if the setting does not exist + /// The casted setting value + public static T GetAppSetting(string key, T defValue = default(T)) + { + Guard.NotEmpty(key, nameof(key)); + + var setting = ConfigurationManager.AppSettings[key]; + + if (setting == null) + { + return defValue; + } + + return setting.Convert(); + } + + public static bool HasConnectionString(string connectionStringName) + { + var conString = ConfigurationManager.ConnectionStrings[connectionStringName]; + if (conString != null && conString.ConnectionString.HasValue()) + { + return true; + } + + return false; + } + + private static bool TryAction(Func func, out T output) + { + Guard.NotNull(func, nameof(func)); + + try + { + output = func(); + return true; + } + catch + { + output = default(T); + return false; + } + } + + public static bool IsTruthy(object value) + { + if (value == null) + return false; + + switch (value) + { + case string x: + return x.HasValue(); + case bool x: + return x == true; + case DateTime x: + return x > DateTime.MinValue; + case TimeSpan x: + return x > TimeSpan.MinValue; + case Guid x: + return x != Guid.Empty; + case IComparable x: + return x.CompareTo(0) != 0; + case IEnumerable x: + return x.Any(); + case IEnumerable x: + return x.GetEnumerator().MoveNext(); + } + + if (value.GetType().IsNullable(out var wrappedType)) + { + return IsTruthy(Convert.ChangeType(value, wrappedType)); + } + + return true; + } + + public static long GetObjectSizeInBytes(object obj, HashSet instanceLookup = null) + { + if (obj == null) + return 0; + + var type = obj.GetType(); + var genericArguments = type.GetGenericArguments(); + + long size = 0; + + if (obj is string str) + { + size = Encoding.Default.GetByteCount(str); + } + else if (obj is StringBuilder sb) + { + size = Encoding.Default.GetByteCount(sb.ToString()); + } + else if (type.IsEnum) + { + size = System.Runtime.InteropServices.Marshal.SizeOf(Enum.GetUnderlyingType(type)); + } + else if (type.IsPredefinedSimpleType() || type.IsPredefinedGenericType()) + { + //size = System.Runtime.InteropServices.Marshal.SizeOf(Nullable.GetUnderlyingType(type) ?? type); // crashes often + size = 8; // mean/average + } + else if (obj is Stream stream) + { + size = stream.Length; + } + else if (obj is IDictionary dic) + { + foreach (var item in dic.Values) + { + size += GetObjectSizeInBytes(item, instanceLookup); + } + } + else if (obj is IEnumerable e) + { + foreach (var item in e) + { + size += GetObjectSizeInBytes(item, instanceLookup); + } + } + else + { + if (instanceLookup == null) + { + instanceLookup = new HashSet(ReferenceEqualityComparer.Default); + } + + if (!type.IsValueType && instanceLookup.Contains(obj)) + { + return 0; + } + + instanceLookup.Add(obj); + + var serialized = false; + + if (type.IsSerializable && genericArguments.All(x => x.IsSerializable)) + { + try + { + using (var s = new MemoryStream()) + { + var formatter = new BinaryFormatter(); + formatter.Serialize(s, obj); + size = s.Length; + + serialized = true; + } + } + catch { } + } + + if (!serialized) + { + // Serialization failed or is not supported: make JSON. + var json = JsonConvert.SerializeObject(obj, new JsonSerializerSettings + { ContractResolver = SmartContractResolver.Instance, DateFormatHandling = DateFormatHandling.IsoDateFormat, - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - MaxDepth = 10, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }); - size = Encoding.Default.GetByteCount(json); - } - } - - return size; - } - } + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + MaxDepth = 10, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }); + size = Encoding.Default.GetByteCount(json); + } + } + + return size; + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs b/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs index d60545f8fa..80417db07f 100644 --- a/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs +++ b/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Collections; using System.Reflection; using System.Runtime.Serialization; +using System.Text; using SmartStore.ComponentModel; namespace SmartStore.Utilities @@ -140,7 +140,7 @@ public static void Populate(IDictionary source, object target, o { foreach (var fastProp in FastProperty.GetProperties(t).Values) { - var pi = fastProp.Property; + var pi = fastProp.Property; if (!pi.PropertyType.IsPredefinedSimpleType() && source.TryGetValue(pi.Name, out var value) && value is IDictionary) { @@ -197,7 +197,7 @@ private static object RetrieveArrayValues(PropertyInfo arrayProp, IDictionary problems) { - var pi = prop.Property; + var pi = prop.Property; - if (!pi.CanWrite) + if (!pi.CanWrite) return; try diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs index d2a1171d6c..b54d69e003 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs @@ -15,150 +15,150 @@ namespace SmartStore.Utilities { - public class FileDownloadManager - { - private const int _bufferSize = 16384; - - private readonly HttpRequestBase _httpRequest; - - public FileDownloadManager(HttpRequestBase httpRequest) - { - _httpRequest = httpRequest; - } - - /// - /// Downloads a single file synchronously - /// - /// The URL to download the file from (either a fully qualified URL or an app relative/absolute path) - /// Specifies whether the FormsAuthenticationCookie should be sent - /// Timeout in milliseconds - /// Specifiers whether the file is located on the local server - public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null, bool isLocal = false) - { - Guard.NotEmpty(url, nameof(url)); - - url = WebHelper.GetAbsoluteUrl(url, _httpRequest); - - HttpWebRequest req; - - if (isLocal) - { - req = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); - } - else - { - req = WebRequest.CreateHttp(url); - req.UserAgent = "SmartStore.NET"; - } - - if (timeout.HasValue) - { - req.Timeout = timeout.Value; - } - - if (sendAuthCookie) - { - req.SetFormsAuthenticationCookie(_httpRequest); - req.SetAnonymousIdentCookie(_httpRequest); + public class FileDownloadManager + { + private const int _bufferSize = 16384; + + private readonly HttpRequestBase _httpRequest; + + public FileDownloadManager(HttpRequestBase httpRequest) + { + _httpRequest = httpRequest; + } + + /// + /// Downloads a single file synchronously + /// + /// The URL to download the file from (either a fully qualified URL or an app relative/absolute path) + /// Specifies whether the FormsAuthenticationCookie should be sent + /// Timeout in milliseconds + /// Specifiers whether the file is located on the local server + public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null, bool isLocal = false) + { + Guard.NotEmpty(url, nameof(url)); + + url = WebHelper.GetAbsoluteUrl(url, _httpRequest); + + HttpWebRequest req; + + if (isLocal) + { + req = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); + } + else + { + req = WebRequest.CreateHttp(url); + req.UserAgent = "Smartstore"; + } + + if (timeout.HasValue) + { + req.Timeout = timeout.Value; + } + + if (sendAuthCookie) + { + req.SetFormsAuthenticationCookie(_httpRequest); + req.SetAnonymousIdentCookie(_httpRequest); req.SetVisitorCookie(_httpRequest); - } - - using (var resp = (HttpWebResponse)req.GetResponse()) - using (var stream = resp.GetResponseStream()) - { - if (resp.StatusCode == HttpStatusCode.OK) - { - var data = stream.ToByteArray(); - if (data != null && data.Length != 0) - { - string fileName = null; - - var contentDisposition = resp.Headers["Content-Disposition"]; - if (contentDisposition.HasValue()) - { - var cd = new ContentDisposition(contentDisposition); - fileName = cd.FileName; - } - - if (fileName.IsEmpty()) - { - try - { - var uri = new Uri(url); - fileName = Path.GetFileName(uri.LocalPath); - } - catch { } - } - - return new FileDownloadResponse(data, fileName, resp.ContentType); - } - } - } - - return null; - } - - /// - /// Starts asynchronous download of files and saves them to disk - /// - /// Download context - /// Items to be downloaded - public async Task DownloadAsync(FileDownloadManagerContext context, IEnumerable items) - { - await DownloadFilesAsync(context, items); - } - - private async Task DownloadFilesAsync(FileDownloadManagerContext context, IEnumerable items) - { - try - { - using (var client = new HttpClient()) - { - client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(); - client.DefaultRequestHeaders.CacheControl.NoCache = true; - client.DefaultRequestHeaders.Add("Connection", "Keep-alive"); - - if (context.Timeout.TotalMilliseconds > 0 && context.Timeout != Timeout.InfiniteTimeSpan) - client.Timeout = context.Timeout; - - IEnumerable downloadTasksQuery = - from item in items - select ProcessUrl(context, client, item); - - // now execute the bunch - List downloadTasks = downloadTasksQuery.ToList(); - - while (downloadTasks.Count > 0) - { - // identify the first task that completes - Task firstFinishedTask = await Task.WhenAny(downloadTasks); - - // process only once - downloadTasks.Remove(firstFinishedTask); - - await firstFinishedTask; - } - } - } - catch (Exception ex) - { + } + + using (var resp = (HttpWebResponse)req.GetResponse()) + using (var stream = resp.GetResponseStream()) + { + if (resp.StatusCode == HttpStatusCode.OK) + { + var data = stream.ToByteArray(); + if (data != null && data.Length != 0) + { + string fileName = null; + + var contentDisposition = resp.Headers["Content-Disposition"]; + if (contentDisposition.HasValue()) + { + var cd = new ContentDisposition(contentDisposition); + fileName = cd.FileName; + } + + if (fileName.IsEmpty()) + { + try + { + var uri = new Uri(url); + fileName = Path.GetFileName(uri.LocalPath); + } + catch { } + } + + return new FileDownloadResponse(data, fileName, resp.ContentType); + } + } + } + + return null; + } + + /// + /// Starts asynchronous download of files and saves them to disk + /// + /// Download context + /// Items to be downloaded + public async Task DownloadAsync(FileDownloadManagerContext context, IEnumerable items) + { + await DownloadFilesAsync(context, items); + } + + private async Task DownloadFilesAsync(FileDownloadManagerContext context, IEnumerable items) + { + try + { + using (var client = new HttpClient()) + { + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(); + client.DefaultRequestHeaders.CacheControl.NoCache = true; + client.DefaultRequestHeaders.Add("Connection", "Keep-alive"); + + if (context.Timeout.TotalMilliseconds > 0 && context.Timeout != Timeout.InfiniteTimeSpan) + client.Timeout = context.Timeout; + + IEnumerable downloadTasksQuery = + from item in items + select ProcessUrl(context, client, item); + + // now execute the bunch + List downloadTasks = downloadTasksQuery.ToList(); + + while (downloadTasks.Count > 0) + { + // identify the first task that completes + Task firstFinishedTask = await Task.WhenAny(downloadTasks); + + // process only once + downloadTasks.Remove(firstFinishedTask); + + await firstFinishedTask; + } + } + } + catch (Exception ex) + { if (context.Logger != null) { context.Logger.ErrorsAll(ex); } - } - } - - private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient client, FileDownloadManagerItem item) - { - try - { - var count = 0; - var canceled = false; - var bytes = new byte[_bufferSize]; - - using (var response = await client.GetAsync(item.Url)) - { + } + } + + private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient client, FileDownloadManagerItem item) + { + try + { + var count = 0; + var canceled = false; + var bytes = new byte[_bufferSize]; + + using (var response = await client.GetAsync(item.Url)) + { if (response.IsSuccessStatusCode) { if (response.Content.Headers.ContentType != null) @@ -200,15 +200,15 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli item.ErrorMessage = response.StatusCode.ToString(); } } - } - catch (Exception ex) - { - try - { - item.Success = false; - item.ErrorMessage = ex.ToAllMessages(); - - var webExc = ex.InnerException as WebException; + } + catch (Exception ex) + { + try + { + item.Success = false; + item.ErrorMessage = ex.ToAllMessages(); + + var webExc = ex.InnerException as WebException; if (webExc != null) { item.ExceptionStatus = webExc.Status; @@ -218,135 +218,129 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli { context.Logger.Error(ex, item.ToString()); } - } - catch { } - } - } - } - - - public class FileDownloadResponse - { - public FileDownloadResponse(byte[] data, string fileName, string contentType) - { - Guard.NotNull(data, nameof(data)); - - Data = data; - FileName = fileName; - ContentType = contentType; - } - - /// - /// The downloaded file's byte array - /// - public byte[] Data { get; private set; } - - /// - /// The file name - /// - public string FileName { get; private set; } - - /// - /// The mime type opf the downloaded file - /// - public string ContentType { get; private set; } - } - - public class FileDownloadManagerContext - { - /// - /// Optional logger to log errors - /// - public ILogger Logger { get; set; } - - /// - /// Cancellation token - /// - public CancellationToken CancellationToken { get; set; } - - /// - /// Timeout for the HTTP client - /// - public TimeSpan Timeout { get; set; } - } - - public class FileDownloadManagerItem - { - /// - /// Identifier of the item - /// - public int Id { get; set; } - - /// - /// New identifier of the downloaded item - /// - public int NewId { get; set; } - - /// - /// Display order of the item - /// - public int DisplayOrder { get; set; } - - /// - /// Download URL - /// - public string Url { get; set; } - - /// - /// Absolute path for saving the item - /// - public string Path { get; set; } - - /// - /// File name without file extension - /// - public string FileName { get; set; } - - /// - /// Mime type of the item - /// - public string MimeType { get; set; } - - /// - /// Whether the operation succeeded - /// - public bool? Success { get; set; } - - /// - /// Exception status if an exception of type WebException occurred - /// - public WebExceptionStatus ExceptionStatus { get; set; } - - /// - /// Error message - /// - public string ErrorMessage { get; set; } - - /// - /// Whether the operation timed out - /// - public bool HasTimedOut - { - get - { - return ExceptionStatus == WebExceptionStatus.Timeout || ExceptionStatus == WebExceptionStatus.RequestCanceled; - } - } - - /// - /// Use dictionary for any required extra data - /// - public IDictionary CustomProperties { get; set; } - - public override string ToString() - { - var str = "Result: {0} {1}{2}, {3}".FormatInvariant( - Success, - ExceptionStatus.ToString(), - ErrorMessage.HasValue() ? " ({0})".FormatInvariant(ErrorMessage) : "", - Path); - - return str; - } - } + } + catch { } + } + } + } + + + public class FileDownloadResponse + { + public FileDownloadResponse(byte[] data, string fileName, string contentType) + { + Guard.NotNull(data, nameof(data)); + + Data = data; + FileName = fileName; + ContentType = contentType; + } + + /// + /// The downloaded file's byte array + /// + public byte[] Data { get; private set; } + + /// + /// The file name + /// + public string FileName { get; private set; } + + /// + /// The mime type opf the downloaded file + /// + public string ContentType { get; private set; } + } + + public class FileDownloadManagerContext + { + /// + /// Optional logger to log errors + /// + public ILogger Logger { get; set; } + + /// + /// Cancellation token + /// + public CancellationToken CancellationToken { get; set; } + + /// + /// Timeout for the HTTP client + /// + public TimeSpan Timeout { get; set; } + } + + public class FileDownloadManagerItem + { + /// + /// Identifier of the item + /// + public int Id { get; set; } + + /// + /// New identifier of the downloaded item + /// + public int NewId { get; set; } + + /// + /// Display order of the item + /// + public int DisplayOrder { get; set; } + + /// + /// Download URL + /// + public string Url { get; set; } + + /// + /// Absolute path for saving the item + /// + public string Path { get; set; } + + /// + /// Name of downloaded file (including extension) + /// + public string FileName { get; set; } + + /// + /// Mime type of the item + /// + public string MimeType { get; set; } + + /// + /// Whether the operation succeeded + /// + public bool? Success { get; set; } + + /// + /// Exception status if an exception of type WebException occurred + /// + public WebExceptionStatus ExceptionStatus { get; set; } + + /// + /// Error message + /// + public string ErrorMessage { get; set; } + + /// + /// Whether the operation timed out + /// + public bool HasTimedOut => ExceptionStatus == WebExceptionStatus.Timeout || ExceptionStatus == WebExceptionStatus.RequestCanceled; + + /// + /// Use dictionary for any required extra data + /// + public IDictionary CustomProperties { get; set; } + + public override string ToString() + { + var str = "Result: {0} {1}{2}, {3}".FormatInvariant( + Success, + ExceptionStatus.ToString(), + ErrorMessage.HasValue() ? " ({0})".FormatInvariant(ErrorMessage) : "", + Path); + + return str; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs index 59bdf70326..3eafe1afae 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs @@ -1,321 +1,319 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using SmartStore.Core.Data; namespace SmartStore.Utilities { public static class FileSystemHelper - { - private static readonly string _pathTemp = CommonHelper.MapPath(CommonHelper.GetAppSetting("sm:TempDirectory", "~/App_Data/_temp")); - private static readonly string _pathTempTenant = CommonHelper.MapPath(DataSettings.Current.TenantPath + "/_temp"); - - /// - /// Returns physical path to application temp directory - /// - /// Name of a sub directory to be created and returned (optional) - public static string TempDir(string subDirectory = null) - { - return TempDirInternal(_pathTemp, subDirectory); - } - - /// - /// Returns physical path to current tenant temp directory - /// - /// Name of a sub directory to be created and returned (optional) - public static string TempDirTenant(string subDirectory = null) - { - return TempDirInternal(_pathTempTenant, subDirectory); - } - - private static string TempDirInternal(string path, string subDirectory = null) - { - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - - if (subDirectory.HasValue()) - { - path = Path.Combine(path, subDirectory); - - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - } - - return path; - } + { + private static readonly string _pathTemp = CommonHelper.MapPath(CommonHelper.GetAppSetting("sm:TempDirectory", "~/App_Data/_temp")); + private static readonly string _pathTempTenant = CommonHelper.MapPath(DataSettings.Current.TenantPath + "/_temp"); + + /// + /// Returns physical path to application temp directory + /// + /// Name of a sub directory to be created and returned (optional) + public static string TempDir(string subDirectory = null) + { + return TempDirInternal(_pathTemp, subDirectory); + } + + /// + /// Returns physical path to current tenant temp directory + /// + /// Name of a sub directory to be created and returned (optional) + public static string TempDirTenant(string subDirectory = null) + { + return TempDirInternal(_pathTempTenant, subDirectory); + } + + private static string TempDirInternal(string path, string subDirectory = null) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + if (subDirectory.HasValue()) + { + path = Path.Combine(path, subDirectory); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + return path; + } /// /// Safe way to cleanup temporary directories. Should be called via scheduled task. /// public static void ClearTempDirectories() - { - var olderThan = TimeSpan.FromHours(5); - - ClearDirectory(new DirectoryInfo(TempDir()), false, olderThan); - ClearDirectory(new DirectoryInfo(TempDirTenant()), false, olderThan); - } - - /// - /// Safe way to delete all directory content - /// - /// Directory path - /// Delete dir too if it doesn't contain any entries after deletion anymore - /// Delete only files older than this TimeSpan - /// Name of files not to be deleted - public static void ClearDirectory(string path, bool deleteIfEmpfy, TimeSpan olderThan, params string[] exceptFileNames) - { - if (path.IsEmpty()) - return; - - ClearDirectory(new DirectoryInfo(path), deleteIfEmpfy, olderThan, exceptFileNames); - } - - /// - /// Safe way to delete all directory content - /// - /// Directory path - /// Delete dir too if it doesn't contain any entries after deletion anymore - /// Name of files not to be deleted - public static void ClearDirectory(string path, bool deleteIfEmpfy, params string[] exceptFileNames) - { - if (path.IsEmpty()) - return; - - ClearDirectory(new DirectoryInfo(path), deleteIfEmpfy, TimeSpan.Zero, exceptFileNames); - } - - /// - /// Safe way to delete all directory content - /// - /// Directory info object - /// Delete dir too if it doesn't contain any entries after deletion anymore - /// Name of files not to be deleted - public static void ClearDirectory(DirectoryInfo dir, bool deleteIfEmpfy, params string[] exceptFileNames) - { - Guard.NotNull(dir, nameof(dir)); - - ClearDirectory(dir, deleteIfEmpfy, TimeSpan.Zero, exceptFileNames); - } - - /// - /// Safe way to delete all directory content - /// - /// Directory info object - /// Delete dir too if it doesn't contain any entries after deletion anymore - /// Delete only files older than this TimeSpan - /// Name of files not to be deleted - public static void ClearDirectory(DirectoryInfo dir, bool deleteIfEmpfy, TimeSpan olderThan, params string[] exceptFileNames) - { - Guard.NotNull(dir, nameof(dir)); - - if (!dir.Exists) - return; - - var olderThanDate = DateTime.UtcNow.Subtract(olderThan); - - for (int i = 0; i < 10; i++) - { - try - { - foreach (var fsi in dir.EnumerateFileSystemInfos()) - { - if (fsi is FileInfo file) - { - if (file.LastWriteTimeUtc >= olderThanDate) - continue; - - if (exceptFileNames.Any(x => x.IsCaseInsensitiveEqual(file.Name))) - continue; - - if (file.IsReadOnly) - { - file.IsReadOnly = false; - } - - file.Delete(); - } - else if (fsi is DirectoryInfo subDir) - { - ClearDirectory(subDir, true, olderThan, exceptFileNames); - } - - } - - break; - } - catch (Exception ex) - { - ex.Dump(); - } - } - - if (deleteIfEmpfy) - { - try - { - if (!dir.EnumerateFileSystemInfos().Any()) - { - dir.Delete(true); - } - } - catch (Exception ex) - { - ex.Dump(); - } - } - } - - /// - /// Safe way to delete a file. - /// - public static bool DeleteFile(string path) - { - if (path.IsEmpty()) - return true; - - try - { - if (Directory.Exists(path)) - { - throw new UnauthorizedAccessException("Deleting folders not possible due to security reasons: {0}".FormatWith(path)); - } - - // No exception, if file doesn't exists - File.Delete(path); - return true; - } - catch (Exception ex) - { - ex.Dump(); - return false; - } - } - - /// - /// Safe way to copy a file. - /// - public static bool CopyFile(string sourcePath, string destinationPath, bool overwrite = true, bool deleteSource = false) - { - bool result = true; - try - { - File.Copy(sourcePath, destinationPath, overwrite); - - if (deleteSource) - DeleteFile(sourcePath); - } - catch (Exception exc) - { - result = false; - exc.Dump(); - } - - return result; - } - - /// - /// Safe way to copy a directory and all content. - /// - /// Source directory - /// Target directory - /// Whether to overwrite existing files - public static bool CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) - { - if (target.FullName.EnsureEndsWith("\\").StartsWith(source.FullName.EnsureEndsWith("\\"), StringComparison.CurrentCultureIgnoreCase)) - { - // Cannot copy a folder into itself. - return false; - } - - var result = true; - - foreach (FileInfo fi in source.GetFiles()) - { - try - { - fi.CopyTo(Path.Combine(target.ToString(), fi.Name), overwrite); - } - catch (Exception ex) - { - result = false; - ex.Dump(); - } - } - - foreach (DirectoryInfo sourceSubDir in source.GetDirectories()) - { - try - { - DirectoryInfo targetSubDir = target.CreateSubdirectory(sourceSubDir.Name); - CopyDirectory(sourceSubDir, targetSubDir, overwrite); - } - catch (Exception ex) - { - result = false; - ex.Dump(); - } - } - - return result; - } - - /// - /// Creates a non existing directory name - /// - /// Path of a directory - /// Default name for directory. null to use a guid. - /// Non existing directory name - public static string CreateNonExistingDirectoryName(string directoryPath, string defaultName) - { - if (defaultName.IsEmpty()) - defaultName = Guid.NewGuid().ToString(); - - if (directoryPath.IsEmpty() || !Directory.Exists(directoryPath)) - return defaultName; - - var newName = defaultName; - - for (int i = 1; i < 999999 && Directory.Exists(Path.Combine(directoryPath, newName)); ++i) - { - newName = defaultName + i.ToString(); - } - - return newName; - } - - /// - /// Safe way to count files in a directory - /// - /// A directory path - /// File count - public static int CountFiles(string directoryPath) - { - try - { - return Directory.GetFiles(directoryPath).Length; - } - catch { } - - return 0; - } - - /// - /// Safe way to empty a file - /// - /// File path - public static void ClearFile(string path) - { - try - { - if (path.HasValue()) File.WriteAllText(path, ""); - } - catch { } - } - } + { + var olderThan = TimeSpan.FromHours(5); + + ClearDirectory(new DirectoryInfo(TempDir()), false, olderThan); + ClearDirectory(new DirectoryInfo(TempDirTenant()), false, olderThan); + } + + /// + /// Safe way to delete all directory content + /// + /// Directory path + /// Delete dir too if it doesn't contain any entries after deletion anymore + /// Delete only files older than this TimeSpan + /// Name of files not to be deleted + public static void ClearDirectory(string path, bool deleteIfEmpfy, TimeSpan olderThan, params string[] exceptFileNames) + { + if (path.IsEmpty()) + return; + + ClearDirectory(new DirectoryInfo(path), deleteIfEmpfy, olderThan, exceptFileNames); + } + + /// + /// Safe way to delete all directory content + /// + /// Directory path + /// Delete dir too if it doesn't contain any entries after deletion anymore + /// Name of files not to be deleted + public static void ClearDirectory(string path, bool deleteIfEmpfy, params string[] exceptFileNames) + { + if (path.IsEmpty()) + return; + + ClearDirectory(new DirectoryInfo(path), deleteIfEmpfy, TimeSpan.Zero, exceptFileNames); + } + + /// + /// Safe way to delete all directory content + /// + /// Directory info object + /// Delete dir too if it doesn't contain any entries after deletion anymore + /// Name of files not to be deleted + public static void ClearDirectory(DirectoryInfo dir, bool deleteIfEmpfy, params string[] exceptFileNames) + { + Guard.NotNull(dir, nameof(dir)); + + ClearDirectory(dir, deleteIfEmpfy, TimeSpan.Zero, exceptFileNames); + } + + /// + /// Safe way to delete all directory content + /// + /// Directory info object + /// Delete dir too if it doesn't contain any entries after deletion anymore + /// Delete only files older than this TimeSpan + /// Name of files not to be deleted + public static void ClearDirectory(DirectoryInfo dir, bool deleteIfEmpfy, TimeSpan olderThan, params string[] exceptFileNames) + { + Guard.NotNull(dir, nameof(dir)); + + if (!dir.Exists) + return; + + var olderThanDate = DateTime.UtcNow.Subtract(olderThan); + + for (int i = 0; i < 10; i++) + { + try + { + foreach (var fsi in dir.EnumerateFileSystemInfos()) + { + if (fsi is FileInfo file) + { + if (file.LastWriteTimeUtc >= olderThanDate) + continue; + + if (exceptFileNames.Any(x => x.IsCaseInsensitiveEqual(file.Name))) + continue; + + if (file.IsReadOnly) + { + file.IsReadOnly = false; + } + + file.Delete(); + } + else if (fsi is DirectoryInfo subDir) + { + ClearDirectory(subDir, true, olderThan, exceptFileNames); + } + + } + + break; + } + catch (Exception ex) + { + ex.Dump(); + } + } + + if (deleteIfEmpfy) + { + try + { + if (!dir.EnumerateFileSystemInfos().Any()) + { + dir.Delete(true); + } + } + catch (Exception ex) + { + ex.Dump(); + } + } + } + + /// + /// Safe way to delete a file. + /// + public static bool DeleteFile(string path) + { + if (path.IsEmpty()) + return true; + + try + { + if (Directory.Exists(path)) + { + throw new UnauthorizedAccessException("Deleting folders not possible due to security reasons: {0}".FormatWith(path)); + } + + // No exception, if file doesn't exists + File.Delete(path); + return true; + } + catch (Exception ex) + { + ex.Dump(); + return false; + } + } + + /// + /// Safe way to copy a file. + /// + public static bool CopyFile(string sourcePath, string destinationPath, bool overwrite = true, bool deleteSource = false) + { + bool result = true; + try + { + File.Copy(sourcePath, destinationPath, overwrite); + + if (deleteSource) + DeleteFile(sourcePath); + } + catch (Exception exc) + { + result = false; + exc.Dump(); + } + + return result; + } + + /// + /// Safe way to copy a directory and all content. + /// + /// Source directory + /// Target directory + /// Whether to overwrite existing files + public static bool CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) + { + if (target.FullName.EnsureEndsWith("\\").StartsWith(source.FullName.EnsureEndsWith("\\"), StringComparison.CurrentCultureIgnoreCase)) + { + // Cannot copy a folder into itself. + return false; + } + + var result = true; + + foreach (FileInfo fi in source.GetFiles()) + { + try + { + fi.CopyTo(Path.Combine(target.ToString(), fi.Name), overwrite); + } + catch (Exception ex) + { + result = false; + ex.Dump(); + } + } + + foreach (DirectoryInfo sourceSubDir in source.GetDirectories()) + { + try + { + DirectoryInfo targetSubDir = target.CreateSubdirectory(sourceSubDir.Name); + CopyDirectory(sourceSubDir, targetSubDir, overwrite); + } + catch (Exception ex) + { + result = false; + ex.Dump(); + } + } + + return result; + } + + /// + /// Creates a non existing directory name + /// + /// Path of a directory + /// Default name for directory. null to use a guid. + /// Non existing directory name + public static string CreateNonExistingDirectoryName(string directoryPath, string defaultName) + { + if (defaultName.IsEmpty()) + defaultName = Guid.NewGuid().ToString(); + + if (directoryPath.IsEmpty() || !Directory.Exists(directoryPath)) + return defaultName; + + var newName = defaultName; + + for (int i = 1; i < 999999 && Directory.Exists(Path.Combine(directoryPath, newName)); ++i) + { + newName = defaultName + i.ToString(); + } + + return newName; + } + + /// + /// Safe way to count files in a directory + /// + /// A directory path + /// File count + public static int CountFiles(string directoryPath) + { + try + { + return Directory.GetFiles(directoryPath).Length; + } + catch { } + + return 0; + } + + /// + /// Safe way to empty a file + /// + /// File path + public static void ClearFile(string path) + { + try + { + if (path.HasValue()) File.WriteAllText(path, ""); + } + catch { } + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs b/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs index e5f178c72f..a3627f2a66 100644 --- a/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs +++ b/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs @@ -11,126 +11,126 @@ Copied over from Microsoft.Framework.Internal namespace SmartStore.Utilities { - public struct HashCodeCombiner - { - private long _combinedHash64; - - public int CombinedHash - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return _combinedHash64.GetHashCode(); } - } - - public string CombinedHashString - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return _combinedHash64.GetHashCode().ToString("x", CultureInfo.InvariantCulture); } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private HashCodeCombiner(long seed) - { - _combinedHash64 = seed; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HashCodeCombiner Add(IEnumerable e) - { - if (e == null) - { - Add(0); - } - else - { - var count = 0; - foreach (object o in e) - { - Add(o); - count++; - } - Add(count); - } - return this; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator int (HashCodeCombiner self) - { - return self.CombinedHash; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HashCodeCombiner Add(int i) - { - _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; - return this; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HashCodeCombiner Add(string s) - { - var hashCode = (s != null) ? s.GetHashCode() : 0; - return Add(hashCode); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HashCodeCombiner Add(object o) - { - var hashCode = (o != null) ? o.GetHashCode() : 0; - return Add(hashCode); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HashCodeCombiner Add(DateTime d) - { - return Add(d.GetHashCode()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HashCodeCombiner Add(TValue value, IEqualityComparer comparer) - { - var hashCode = value != null ? comparer.GetHashCode(value) : 0; - return Add(hashCode); - } - - public HashCodeCombiner Add(FileSystemInfo fi, bool deep = true) - { - Guard.NotNull(fi, nameof(fi)); - - if (!fi.Exists) - return this; - - Add(fi.FullName.ToLower()); - Add(fi.CreationTimeUtc); - Add(fi.LastWriteTimeUtc); - - if (fi is FileInfo file) - { - Add(file.Length.GetHashCode()); - } - - if (fi is DirectoryInfo dir) - { - foreach (var f in dir.GetFiles()) - { - Add(f); - } - if (deep) - { - foreach (var s in dir.GetDirectories()) - { - Add(s); - } - } - } - - return this; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HashCodeCombiner Start() - { - return new HashCodeCombiner(0x1505L); - } - } + public struct HashCodeCombiner + { + private long _combinedHash64; + + public int CombinedHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _combinedHash64.GetHashCode(); + } + + public string CombinedHashString + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _combinedHash64.GetHashCode().ToString("x", CultureInfo.InvariantCulture); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private HashCodeCombiner(long seed) + { + _combinedHash64 = seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(IEnumerable e) + { + if (e == null) + { + Add(0); + } + else + { + var count = 0; + foreach (object o in e) + { + Add(o); + count++; + } + Add(count); + } + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator int(HashCodeCombiner self) + { + return self.CombinedHash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(string s) + { + var hashCode = (s != null) ? s.GetHashCode() : 0; + return Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(object o) + { + var hashCode = (o != null) ? o.GetHashCode() : 0; + return Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(DateTime d) + { + return Add(d.GetHashCode()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(TValue value, IEqualityComparer comparer) + { + var hashCode = value != null ? comparer.GetHashCode(value) : 0; + return Add(hashCode); + } + + public HashCodeCombiner Add(FileSystemInfo fi, bool deep = true) + { + Guard.NotNull(fi, nameof(fi)); + + if (!fi.Exists) + return this; + + Add(fi.FullName.ToLower()); + Add(fi.CreationTimeUtc); + Add(fi.LastWriteTimeUtc); + + if (fi is FileInfo file) + { + Add(file.Length.GetHashCode()); + } + + if (fi is DirectoryInfo dir) + { + foreach (var f in dir.GetFiles()) + { + Add(f); + } + if (deep) + { + foreach (var s in dir.GetDirectories()) + { + Add(s); + } + } + } + + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(0x1505L); + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs b/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs index 3d6ef4d3c5..2833cac71f 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs @@ -3,65 +3,65 @@ namespace SmartStore.Utilities { - public static class ImagingHelper - { - public static int GetPerceivedBrightness(string htmlColor) - { + public static class ImagingHelper + { + public static int GetPerceivedBrightness(string htmlColor) + { if (String.IsNullOrEmpty(htmlColor)) htmlColor = "#ffffff"; - + return GetPerceivedBrightness(ColorTranslator.FromHtml(htmlColor)); - } + } - /// - /// Calculates the perceived brightness of a color. - /// - /// The color - /// - /// A number in the range of 0 (black) to 255 (White). - /// For text contrast colors, an optimal cutoff value is 130. - /// - public static int GetPerceivedBrightness(Color color) - { - return (int)Math.Sqrt( - color.R * color.R * .241 + - color.G * color.G * .691 + - color.B * color.B * .068); - } + /// + /// Calculates the perceived brightness of a color. + /// + /// The color + /// + /// A number in the range of 0 (black) to 255 (White). + /// For text contrast colors, an optimal cutoff value is 130. + /// + public static int GetPerceivedBrightness(Color color) + { + return (int)Math.Sqrt( + color.R * color.R * .241 + + color.G * color.G * .691 + + color.B * color.B * .068); + } - /// - /// Recalculates an image size while keeping aspect ratio - /// - /// Original size - /// New max size - /// The rescaled size - public static Size Rescale(Size original, int maxSize) - { - Guard.IsPositive(maxSize, nameof(maxSize)); + /// + /// Recalculates an image size while keeping aspect ratio + /// + /// Original size + /// New max size + /// The rescaled size + public static Size Rescale(Size original, int maxSize) + { + Guard.IsPositive(maxSize, nameof(maxSize)); - return Rescale(original, new Size(maxSize, maxSize)); - } + return Rescale(original, new Size(maxSize, maxSize)); + } - /// - /// Recalculates an image size while keeping aspect ratio - /// - /// Original size - /// New max size - /// The rescaled size - public static Size Rescale(Size original, Size maxSize) - { - if (original.IsEmpty || maxSize.IsEmpty || (original.Width <= maxSize.Width && original.Height <= maxSize.Height)) - { - return original; - } + /// + /// Recalculates an image size while keeping aspect ratio + /// + /// Original size + /// New max size + /// The rescaled size + public static Size Rescale(Size original, Size maxSize) + { + if (original.IsEmpty || maxSize.IsEmpty || (original.Width <= maxSize.Width && original.Height <= maxSize.Height)) + { + return original; + } - // Figure out the ratio - double ratioX = (double)maxSize.Width / (double)original.Width; - double ratioY = (double)maxSize.Height / (double)original.Height; - // use whichever multiplier is smaller - double ratio = ratioX < ratioY ? ratioX : ratioY; + // Figure out the ratio + double ratioX = (double)maxSize.Width / (double)original.Width; + double ratioY = (double)maxSize.Height / (double)original.Height; + // use whichever multiplier is smaller + double ratio = ratioX < ratioY ? ratioX : ratioY; - return new Size(Convert.ToInt32(original.Width * ratio), Convert.ToInt32(original.Height * ratio)); - } - } + return new Size(Convert.ToInt32(original.Width * ratio), Convert.ToInt32(original.Height * ratio)); + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/Inflector.cs b/src/Libraries/SmartStore.Core/Utilities/Inflector.cs index a9f1c73e39..dfac59b3a9 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Inflector.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Inflector.cs @@ -4,7 +4,6 @@ namespace SmartStore.Utilities { - /// /// Implementation of the Inflector in Ruby that transforms words from singular to plural, /// class names to table names, modularized class names to ones without, and class names to foreign keys @@ -12,14 +11,14 @@ namespace SmartStore.Utilities // TODO: Inflector ist leider Englisch! Irgend 'ne Chance das zu lokalisieren??!! public static class Inflector { - #region fields + #region fields - private static readonly Regex HtmlCaseRegex = new Regex( - "(? _plurals = new List(); + private static readonly List _plurals = new List(); private static readonly List _singulars = new List(); private static readonly List _uncountables = new List(); @@ -116,7 +115,7 @@ public string Apply(string word) } #endregion - #region private rule buolder methods + #region private rule builder methods private static void AddIrregular(string singular, string plural) { AddPlural("(" + singular[0] + ")" + singular.Substring(1) + "$", "$1" + plural.Substring(1)); @@ -186,7 +185,7 @@ public static string Singularize(string word) public static string Titleize(string word) { return Regex.Replace(Humanize(Underscore(word)), @"\b([a-z])", - delegate(Match match) + delegate (Match match) { return match.Captures[0].Value.ToUpper(); }); @@ -211,7 +210,7 @@ public static string Humanize(string lowercaseAndUnderscoredWord) public static string Pascalize(string lowercaseAndUnderscoredWord) { return Regex.Replace(lowercaseAndUnderscoredWord, "(?:^|_)(.)", - delegate(Match match) + delegate (Match match) { return match.Groups[1].Value.ToUpper(); }); @@ -227,17 +226,17 @@ public static string Camelize(string lowercaseAndUnderscoredWord) return Uncapitalize(Pascalize(lowercaseAndUnderscoredWord)); } - public static string Handleize(string input) - { - return HtmlCaseRegex.Replace(input, "-$1$2").ToLowerInvariant(); - } - - /// - /// Makes an underscored form from the expression in the string. - /// - /// string. The word to underscore. - /// string. The word with underscore seperators. - public static string Underscore(string pascalCasedWord) + public static string Handleize(string input) + { + return HtmlCaseRegex.Replace(input, "-$1$2").ToLowerInvariant(); + } + + /// + /// Makes an underscored form from the expression in the string. + /// + /// string. The word to underscore. + /// string. The word with underscore seperators. + public static string Underscore(string pascalCasedWord) { return Regex.Replace( Regex.Replace( @@ -303,7 +302,7 @@ public static string Dasherize(string underscoredWord) { return underscoredWord.Replace('_', '-'); } + #endregion } - } diff --git a/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs b/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs index 788859fa99..da44bc6362 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs @@ -5,26 +5,26 @@ namespace SmartStore.Core.Utilities { - public static class ObjectDumper - { - public static void Dump(object value, TextWriter writer) - { - writer.WriteLine(Dump(value)); - } + public static class ObjectDumper + { + public static void Dump(object value, TextWriter writer) + { + writer.WriteLine(Dump(value)); + } - public static string Dump(object value) - { - return JsonConvert.SerializeObject(value, Formatting.Indented); - } + public static string Dump(object value) + { + return JsonConvert.SerializeObject(value, Formatting.Indented); + } - public static void ToConsole(object value) - { - Console.WriteLine(Dump(value)); - } + public static void ToConsole(object value) + { + Console.WriteLine(Dump(value)); + } - public static void ToDebug(object value) - { - Debug.WriteLine(Dump(value)); - } - } + public static void ToDebug(object value) + { + Debug.WriteLine(Dump(value)); + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/IPooledObject.cs b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/IPooledObject.cs index 72623e66ae..902051f3a3 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/IPooledObject.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/IPooledObject.cs @@ -1,6 +1,4 @@ -using System; - -namespace SmartStore.Utilities.ObjectPools +namespace SmartStore.Utilities.ObjectPools { public interface IPooledObject { diff --git a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/LeakTrackingObjectPool.cs b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/LeakTrackingObjectPool.cs index 240d7fae8d..bfd40d991f 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/LeakTrackingObjectPool.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/LeakTrackingObjectPool.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; diff --git a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/ObjectPool.cs b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/ObjectPool.cs index 94aa2c99b3..29cdf9e79f 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/ObjectPool.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/ObjectPool.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Threading; -using System.Threading.Tasks; namespace SmartStore.Utilities.ObjectPools { diff --git a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/PooledStringBuilder.cs b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/PooledStringBuilder.cs index 4cd909e00b..fca491cc22 100644 --- a/src/Libraries/SmartStore.Core/Utilities/ObjectPools/PooledStringBuilder.cs +++ b/src/Libraries/SmartStore.Core/Utilities/ObjectPools/PooledStringBuilder.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; @@ -63,10 +62,7 @@ public static PooledStringBuilder Rent(string value = null) public int MaxCapacity { get; private set; } - public int Length - { - get { return this.Builder.Length; } - } + public int Length => this.Builder.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public StringBuilder Append(string value) diff --git a/src/Libraries/SmartStore.Core/Utilities/PathHelper.cs b/src/Libraries/SmartStore.Core/Utilities/PathHelper.cs index d1cacbacbf..18159409ee 100644 --- a/src/Libraries/SmartStore.Core/Utilities/PathHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/PathHelper.cs @@ -1,202 +1,221 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace SmartStore.Utilities { - public static class PathHelper - { - private static readonly char[] _invalidPathChars; - private static readonly char[] _invalidFileNameChars; - private static readonly Regex _invalidCharsPattern; - - static PathHelper() - { - _invalidPathChars = Path.GetInvalidPathChars(); - _invalidFileNameChars = Path.GetInvalidFileNameChars(); - - var invalidChars = Regex.Escape(new string(_invalidPathChars) + new string(_invalidFileNameChars)); - _invalidCharsPattern = new Regex(string.Format(@"[{0}]+", invalidChars)); - } - - /// - /// Checks whether path is empty or starts with '/' or '\' - /// - public static bool IsRootedPath(string basepath) - { - return (string.IsNullOrEmpty(basepath) || basepath[0] == '/' || basepath[0] == '\\'); - } - - /// - /// Checks whether path is starts with '~/' - /// - public static bool IsAppRelativePath(string path) - { - if (path == null) - return false; - - int len = path.Length; - - // Empty string case - if (len == 0) return false; - - // It must start with ~ - if (path[0] != '~') - return false; - - // Single character case: "~" - if (len == 1) - return true; - - // If it's longer, checks if it starts with "~/" or "~\" - return path[1] == '\\' || path[1] == '/'; - } - - /// - /// Checks if virtual path contains a protocol, which is referred to as a scheme in the - /// URI spec. - /// - public static bool HasScheme(string virtualPath) - { - // URIs have the format :, e.g. mailto:user@ms.com, - // http://server/, nettcp://server/, etc. The cannot contain slashes. - // The virtualPath passed to this method may be absolute or relative. Although - // ':' is only allowed in the if it is encoded, the - // virtual path that we're receiving here may be decoded, so it is impossible - // for us to determine if virtualPath has a scheme. We will be conservative - // and err on the side of assuming it has a scheme when we cannot tell for certain. - // To do this, we first check for ':'. If not found, then it doesn't have a scheme. - // If ':' is found, then as long as we find a '/' before the ':', it cannot be - // a scheme because schemes don't contain '/'. Otherwise, we will assume it has a - // scheme. - int indexOfColon = virtualPath.IndexOf(':'); - if (indexOfColon == -1) - return false; - int indexOfSlash = virtualPath.IndexOf('/'); - return (indexOfSlash == -1 || indexOfColon < indexOfSlash); - } - - /// - /// Ensures that a path is a valid app root path by checking whether it starts - /// with '~/' and prepending it if needed and by replacing '\' with '/' - /// - /// Relative path - /// Normalized root path - public static string NormalizeAppRelativePath(string path) - { - if (path.IsEmpty()) - return path; - - path = path.Replace('\\', '/'); - - if (!path.StartsWith("~/")) - { - if (path.StartsWith("~")) - path = path.Substring(1); - - path = (path.StartsWith("/") ? "~" : "~/") + path; - } - - return path; - } - - /// - /// Checks whether a path is a safe app root path. - /// - /// Relative path - public static bool IsSafeAppRootPath(string path) - { - if (path.EmptyNull().Length > 2 && !path.IsCaseInsensitiveEqual("con") && !HasInvalidPathChars(path)) - { - try - { - var mappedPath = CommonHelper.MapPath(path); - var appPath = CommonHelper.MapPath("~/"); - return !mappedPath.IsCaseInsensitiveEqual(appPath); - } - catch { } - } - - return false; - } - - /// - /// Replaces all occurences of any illegal path or file name char by '-' - /// - /// Path/File name - /// Sanitized path/file name - public static string SanitizeFileName(string name) - { - if (name.IsEmpty()) - return name; - - return _invalidCharsPattern.Replace(name, "-"); - } - - public static bool HasInvalidPathChars(string path, bool checkWildcardChars = false) - { - if (path == null) - return false; - - return path.IndexOfAny(_invalidPathChars) >= 0 - || (checkWildcardChars && ContainsWildcardChars(path, 0)); - } - - public static bool HasInvalidFileNameChars(string fileName, bool checkWildcardChars = false) - { - if (fileName == null) - return false; - - return fileName.IndexOfAny(_invalidFileNameChars) >= 0 - || (checkWildcardChars && ContainsWildcardChars(fileName, 0)); - } - - private static bool ContainsWildcardChars(string path, int startIndex = 0) - { - for (int i = startIndex; i < path.Length; i++) - { - switch (path[i]) - { - case '*': - case '?': - return true; - } - } - - return false; - } - - /// - /// Checks whether the given path is a fully qualified absolute path (either UNC or rooted with drive letter) - /// - /// Path to check - /// true if path is fully qualified - public static bool IsAbsolutePhysicalPath(string path) - { - if ((path == null) || (path.Length < 3)) - { - return false; - } - - return (((path[1] == ':') && IsDirectorySeparatorChar(path[2])) || IsUncSharePath(path)); - } - - internal static bool IsUncSharePath(string path) => - (((path.Length > 2) && IsDirectorySeparatorChar(path[0])) && IsDirectorySeparatorChar(path[1])); - - - private static bool IsDirectorySeparatorChar(char ch) - { - if (ch != '\\') - { - return (ch == '/'); - } - - return true; - } - } + public static class PathHelper + { + private static readonly char[] _invalidPathChars; + private static readonly char[] _invalidFileNameChars; + private static readonly Regex _invalidCharsPattern; + + static PathHelper() + { + _invalidPathChars = Path.GetInvalidPathChars(); + _invalidFileNameChars = Path.GetInvalidFileNameChars(); + + var invalidChars = Regex.Escape(new string(_invalidPathChars) + new string(_invalidFileNameChars)); + _invalidCharsPattern = new Regex(string.Format(@"[{0}]+", invalidChars)); + } + + /// + /// Checks whether path is empty or starts with '/' or '\' + /// + public static bool IsRootedPath(string basepath) + { + return (string.IsNullOrEmpty(basepath) || basepath[0] == '/' || basepath[0] == '\\'); + } + + /// + /// Checks whether path starts with '~/' + /// + public static bool IsAppRelativePath(string path) + { + if (path == null) + return false; + + int len = path.Length; + + // Empty string case + if (len == 0) return false; + + // It must start with ~ + if (path[0] != '~') + return false; + + // Single character case: "~" + if (len == 1) + return true; + + // If it's longer, checks if it starts with "~/" or "~\" + return path[1] == '\\' || path[1] == '/'; + } + + /// + /// Determines the relative path from to + /// + /// From path + /// To path + /// Directory separator + /// The relative path + public static string MakeRelativePath(string fromPath, string toPath, string sep = "\\") + { + var fromParts = fromPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + var toParts = toPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + + var matchedParts = fromParts + .Zip(toParts, (x, y) => string.Compare(x, y, true) == 0) + .TakeWhile(x => x).Count(); + + return string.Join("", Enumerable.Range(0, fromParts.Length - matchedParts) + .Select(x => ".." + sep)) + + string.Join(sep, toParts.Skip(matchedParts)); + } + + /// + /// Checks if virtual path contains a protocol, which is referred to as a scheme in the + /// URI spec. + /// + public static bool HasScheme(string virtualPath) + { + // URIs have the format :, e.g. mailto:user@ms.com, + // http://server/, nettcp://server/, etc. The cannot contain slashes. + // The virtualPath passed to this method may be absolute or relative. Although + // ':' is only allowed in the if it is encoded, the + // virtual path that we're receiving here may be decoded, so it is impossible + // for us to determine if virtualPath has a scheme. We will be conservative + // and err on the side of assuming it has a scheme when we cannot tell for certain. + // To do this, we first check for ':'. If not found, then it doesn't have a scheme. + // If ':' is found, then as long as we find a '/' before the ':', it cannot be + // a scheme because schemes don't contain '/'. Otherwise, we will assume it has a + // scheme. + int indexOfColon = virtualPath.IndexOf(':'); + if (indexOfColon == -1) + return false; + int indexOfSlash = virtualPath.IndexOf('/'); + return (indexOfSlash == -1 || indexOfColon < indexOfSlash); + } + + /// + /// Ensures that a path is a valid app root path by checking whether it starts + /// with '~/' and prepending it if needed and by replacing '\' with '/' + /// + /// Relative path + /// Normalized root path + public static string NormalizeAppRelativePath(string path) + { + if (path.IsEmpty()) + return path; + + path = path.Replace('\\', '/'); + + if (!path.StartsWith("~/")) + { + if (path.StartsWith("~")) + path = path.Substring(1); + + path = (path.StartsWith("/") ? "~" : "~/") + path; + } + + return path; + } + + /// + /// Checks whether a path is a safe app root path. + /// + /// Relative path + public static bool IsSafeAppRootPath(string path) + { + if (path.EmptyNull().Length > 2 && !path.IsCaseInsensitiveEqual("con") && !HasInvalidPathChars(path)) + { + try + { + var mappedPath = CommonHelper.MapPath(path); + var appPath = CommonHelper.MapPath("~/"); + return !mappedPath.IsCaseInsensitiveEqual(appPath); + } + catch { } + } + + return false; + } + + /// + /// Replaces all occurences of any illegal path or file name char by '-' + /// + /// Path/File name + /// Sanitized path/file name + public static string SanitizeFileName(string name) + { + if (name.IsEmpty()) + return name; + + return _invalidCharsPattern.Replace(name, "-"); + } + + public static bool HasInvalidPathChars(string path, bool checkWildcardChars = false) + { + if (path == null) + return false; + + return path.IndexOfAny(_invalidPathChars) >= 0 + || (checkWildcardChars && ContainsWildcardChars(path, 0)); + } + + public static bool HasInvalidFileNameChars(string fileName, bool checkWildcardChars = false) + { + if (fileName == null) + return false; + + return fileName.IndexOfAny(_invalidFileNameChars) >= 0 + || (checkWildcardChars && ContainsWildcardChars(fileName, 0)); + } + + private static bool ContainsWildcardChars(string path, int startIndex = 0) + { + for (int i = startIndex; i < path.Length; i++) + { + switch (path[i]) + { + case '*': + case '?': + return true; + } + } + + return false; + } + + /// + /// Checks whether the given path is a fully qualified absolute path (either UNC or rooted with drive letter) + /// + /// Path to check + /// true if path is fully qualified + public static bool IsAbsolutePhysicalPath(string path) + { + if ((path == null) || (path.Length < 3)) + { + return false; + } + + return (((path[1] == ':') && IsDirectorySeparatorChar(path[2])) || IsUncSharePath(path)); + } + + internal static bool IsUncSharePath(string path) + { + return (((path.Length > 2) && IsDirectorySeparatorChar(path[0])) && IsDirectorySeparatorChar(path[1])); + } + + private static bool IsDirectorySeparatorChar(char ch) + { + if (ch != '\\') + { + return (ch == '/'); + } + + return true; + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs b/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs index d0d4cc2307..95e9c181a5 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Text; using System.Xml; @@ -10,8 +8,8 @@ namespace SmartStore.Utilities { public static class Prettifier { - [SuppressMessage("ReSharper", "RedundantAssignment")] - public static string BytesToString(long bytes) + [SuppressMessage("ReSharper", "RedundantAssignment")] + public static string BytesToString(long bytes) { double result = bytes; double dsize = bytes; @@ -46,7 +44,7 @@ public static string PrettifyXML(string xml) { if (xml.IsEmpty() || xml.IsWhiteSpace()) return xml; - + // first read the xml ignoring whitespace using (var xmlReader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { IgnoreWhitespace = true, CheckCharacters = false })) { @@ -60,9 +58,6 @@ public static string PrettifyXML(string xml) var result = sb.ToString(); return result; } - } - } - } diff --git a/src/Libraries/SmartStore.Core/Utilities/Range.cs b/src/Libraries/SmartStore.Core/Utilities/Range.cs index 42780fbe9a..7b10f3e6c3 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Range.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Range.cs @@ -4,7 +4,6 @@ namespace SmartStore.Utilities { - /// /// Helper class with shortcut methods for managing enumerables. /// Useful for inlining object generation in tests @@ -135,5 +134,4 @@ public static int[] Array(int count) return array; } } - } diff --git a/src/Libraries/SmartStore.Core/Utilities/Retry.cs b/src/Libraries/SmartStore.Core/Utilities/Retry.cs index 916b64e3b2..a7ec849ce6 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Retry.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Retry.cs @@ -3,140 +3,140 @@ namespace SmartStore.Utilities { - /// - /// Provides functionality to automatically try the given piece of logic some number of times before re-throwing the exception. - /// This is useful for any piece of code which may experience transient failures. Be cautious of passing code with two distinct - /// actions given that if the second or subsequent piece of logic fails, the first will also be retried upon each retry. - /// - public static class Retry - { - /// - /// Executes an action with retry logic. - /// - /// The action to be executed. - /// The maximum number of attempts. - /// Timespan to wait between attempts of the operation - /// The callback executed when an attempt is failed. - public static void Run(Action operation, int attempts, TimeSpan? wait = null, Action onFailed = null) - { - Guard.NotNull(operation, nameof(operation)); - - Run(Operation, attempts, wait, onFailed); - - bool Operation() - { - operation(); - return true; - } - } - - /// - /// Executes a function with retry logic. - /// - /// The function to be executed. - /// The maximum number of attempts. - /// Timespan to wait between attempts of the operation - /// The callback executed when an attempt is failed. - public static T Run(Func operation, int attempts, TimeSpan? wait, Action onFailed = null) - { - Guard.NotNull(operation, nameof(operation)); - - if (attempts < 1) - { - throw new ArgumentOutOfRangeException(nameof(attempts), attempts, "The maximum number of attempts must not be less than 1."); - } - - var attempt = 0; - - while (true) - { - if (attempt > 0 && wait != null) - { - Task.Delay(wait.Value).Wait(); - } - - try - { - // Call the function passed in by the caller. - return operation(); - } - catch (Exception ex) - { - attempt++; - - onFailed?.Invoke(attempt, ex); - - if (attempt >= attempts) - { - throw; - } - } - } - } - - - /// - /// Executes an asynchronous action with retry logic. - /// - /// The asynchronous action to be executed. - /// The maximum number of attempts. - /// Timespan to wait between attempts of the operation - /// The callback executed when an attempt is failed. - public static async Task RunAsync(Func operation, int attempts, TimeSpan? wait = null, Action onFailed = null) - { - Guard.NotNull(operation, nameof(operation)); - - async Task wrapper() - { - await operation(); - return true; - } - - await RunAsync(wrapper, attempts, wait, onFailed); - } - - /// - /// Executes an asynchronous function with retry logic. - /// - /// The asynchronous function to be executed. - /// The maximum number of attempts. - /// Timespan to wait between attempts of the operation - /// The callback executed when an attempt is failed. - public static async Task RunAsync(Func> operation, int attempts, TimeSpan? wait, Action onFailed = null) - { - Guard.NotNull(operation, nameof(operation)); - - if (attempts < 1) - { - throw new ArgumentOutOfRangeException(nameof(attempts), attempts, "The maximum number of attempts must not be less than 1."); - } - - var attempt = 0; - - while (true) - { - if (attempt > 0 && wait != null) - { - await Task.Delay(wait.Value); - } - - try - { - // Call the function passed in by the caller. - return await operation(); - } - catch (Exception ex) - { - attempt++; - - onFailed?.Invoke(attempt, ex); - - if (attempt >= attempts) - { - throw; - } - } - } - } - } + /// + /// Provides functionality to automatically try the given piece of logic some number of times before re-throwing the exception. + /// This is useful for any piece of code which may experience transient failures. Be cautious of passing code with two distinct + /// actions given that if the second or subsequent piece of logic fails, the first will also be retried upon each retry. + /// + public static class Retry + { + /// + /// Executes an action with retry logic. + /// + /// The action to be executed. + /// The maximum number of attempts. + /// Timespan to wait between attempts of the operation + /// The callback executed when an attempt is failed. + public static void Run(Action operation, int attempts, TimeSpan? wait = null, Action onFailed = null) + { + Guard.NotNull(operation, nameof(operation)); + + Run(Operation, attempts, wait, onFailed); + + bool Operation() + { + operation(); + return true; + } + } + + /// + /// Executes a function with retry logic. + /// + /// The function to be executed. + /// The maximum number of attempts. + /// Timespan to wait between attempts of the operation + /// The callback executed when an attempt is failed. + public static T Run(Func operation, int attempts, TimeSpan? wait, Action onFailed = null) + { + Guard.NotNull(operation, nameof(operation)); + + if (attempts < 1) + { + throw new ArgumentOutOfRangeException(nameof(attempts), attempts, "The maximum number of attempts must not be less than 1."); + } + + var attempt = 0; + + while (true) + { + if (attempt > 0 && wait != null) + { + Task.Delay(wait.Value).Wait(); + } + + try + { + // Call the function passed in by the caller. + return operation(); + } + catch (Exception ex) + { + attempt++; + + onFailed?.Invoke(attempt, ex); + + if (attempt >= attempts) + { + throw; + } + } + } + } + + + /// + /// Executes an asynchronous action with retry logic. + /// + /// The asynchronous action to be executed. + /// The maximum number of attempts. + /// Timespan to wait between attempts of the operation + /// The callback executed when an attempt is failed. + public static async Task RunAsync(Func operation, int attempts, TimeSpan? wait = null, Action onFailed = null) + { + Guard.NotNull(operation, nameof(operation)); + + async Task wrapper() + { + await operation(); + return true; + } + + await RunAsync(wrapper, attempts, wait, onFailed); + } + + /// + /// Executes an asynchronous function with retry logic. + /// + /// The asynchronous function to be executed. + /// The maximum number of attempts. + /// Timespan to wait between attempts of the operation + /// The callback executed when an attempt is failed. + public static async Task RunAsync(Func> operation, int attempts, TimeSpan? wait, Action onFailed = null) + { + Guard.NotNull(operation, nameof(operation)); + + if (attempts < 1) + { + throw new ArgumentOutOfRangeException(nameof(attempts), attempts, "The maximum number of attempts must not be less than 1."); + } + + var attempt = 0; + + while (true) + { + if (attempt > 0 && wait != null) + { + await Task.Delay(wait.Value); + } + + try + { + // Call the function passed in by the caller. + return await operation(); + } + catch (Exception ex) + { + attempt++; + + onFailed?.Invoke(attempt, ex); + + if (attempt >= attempts) + { + throw; + } + } + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs index 6dbfbd56f5..b3d32b4486 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs @@ -8,25 +8,25 @@ namespace SmartStore.Utilities { - public static class SeoHelper - { - private static readonly object _lock = new object(); - private static Dictionary _userSeoCharacterTable; - - /// - /// Get SEO friendly string - /// - /// String to be converted - /// A value indicating whether non western chars should be converted - /// A value indicating whether Unicode chars are allowed - /// Raw data of semicolon separated char conversions - /// SEO friendly string - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public static class SeoHelper + { + private static readonly object _lock = new object(); + private static Dictionary _userSeoCharacterTable; + + /// + /// Get SEO friendly string + /// + /// String to be converted + /// A value indicating whether non western chars should be converted + /// A value indicating whether Unicode chars are allowed + /// Raw data of semicolon separated char conversions + /// SEO friendly string + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeChars, string charConversions = null) - { + { return GetSeName(name, convertNonWesternChars, allowUnicodeChars, true, charConversions); - } + } /// /// Get SEO friendly string @@ -39,10 +39,10 @@ public static string GetSeName(string name, bool convertNonWesternChars, bool al /// SEO friendly string [SuppressMessage("ReSharper", "PossibleNullReferenceException")] public static string GetSeName( - string name, - bool convertNonWesternChars, - bool allowUnicodeChars, - bool allowForwardSlash, + string name, + bool convertNonWesternChars, + bool allowUnicodeChars, + bool allowForwardSlash, string charConversions = null) { // Return empty value if text is null @@ -136,31 +136,31 @@ public static string GetSeName( } public static void ResetUserSeoCharacterTable() - { - if (_userSeoCharacterTable != null) - { - _userSeoCharacterTable.Clear(); - _userSeoCharacterTable = null; - } - } - - private static void InitializeUserSeoCharacterTable(string charConversions) - { - lock (_lock) - { - if (_userSeoCharacterTable == null) - { - _userSeoCharacterTable = new Dictionary(); - - foreach (var conversion in charConversions.SplitSafe(Environment.NewLine)) - { - if (conversion.SplitToPair(out var strLeft, out var strRight, ";") && strLeft.HasValue() && !_userSeoCharacterTable.ContainsKey(strLeft[0])) - { - _userSeoCharacterTable.Add(strLeft[0], strRight); - } - } - } - } - } - } + { + if (_userSeoCharacterTable != null) + { + _userSeoCharacterTable.Clear(); + _userSeoCharacterTable = null; + } + } + + private static void InitializeUserSeoCharacterTable(string charConversions) + { + lock (_lock) + { + if (_userSeoCharacterTable == null) + { + _userSeoCharacterTable = new Dictionary(); + + foreach (var conversion in charConversions.SplitSafe(Environment.NewLine)) + { + if (conversion.SplitToPair(out var strLeft, out var strRight, ";") && strLeft.HasValue() && !_userSeoCharacterTable.ContainsKey(strLeft[0])) + { + _userSeoCharacterTable.Add(strLeft[0], strRight); + } + } + } + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs b/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs index 57302ecc08..b850c568ea 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs @@ -8,60 +8,60 @@ namespace SmartStore.Utilities { public class SmartSyndicationFeed : SyndicationFeed - { - public SmartSyndicationFeed(Uri feedAlternateLink, string title, string description = null) - : base(title, description ?? title, feedAlternateLink, null, DateTime.UtcNow) - { - } + { + public SmartSyndicationFeed(Uri feedAlternateLink, string title, string description = null) + : base(title, description ?? title, feedAlternateLink, null, DateTime.UtcNow) + { + } - public static string UrlAtom => "http://www.w3.org/2005/Atom"; - public static string UrlPurlContent => "http://purl.org/rss/1.0/modules/content/"; + public static string UrlAtom => "http://www.w3.org/2005/Atom"; + public static string UrlPurlContent => "http://purl.org/rss/1.0/modules/content/"; - public void AddNamespaces(bool purlContent) - { - AttributeExtensions.Add(new XmlQualifiedName("atom", XNamespace.Xmlns.NamespaceName), UrlAtom); + public void AddNamespaces(bool purlContent) + { + AttributeExtensions.Add(new XmlQualifiedName("atom", XNamespace.Xmlns.NamespaceName), UrlAtom); - if (purlContent) - { - AttributeExtensions.Add(new XmlQualifiedName("content", XNamespace.Xmlns.ToString()), UrlPurlContent); - } - } + if (purlContent) + { + AttributeExtensions.Add(new XmlQualifiedName("content", XNamespace.Xmlns.ToString()), UrlPurlContent); + } + } - public void Init(string selfLink, Language language = null) - { - ElementExtensions.Add( - new XElement(((XNamespace)UrlAtom) + "link", - new XAttribute("href", selfLink), - new XAttribute("rel", "self"), - new XAttribute("type", "application/rss+xml"))); + public void Init(string selfLink, Language language = null) + { + ElementExtensions.Add( + new XElement(((XNamespace)UrlAtom) + "link", + new XAttribute("href", selfLink), + new XAttribute("rel", "self"), + new XAttribute("type", "application/rss+xml"))); - if (language != null) - { - Language = language.LanguageCulture.EmptyNull().ToLower(); - } - } + if (language != null) + { + Language = language.LanguageCulture.EmptyNull().ToLower(); + } + } - public SyndicationItem CreateItem(string title, string synopsis, string url, DateTimeOffset published, string contentEncoded = null) - { - var item = new SyndicationItem( - title.RemoveInvalidXmlChars().EmptyNull(), - synopsis.RemoveInvalidXmlChars().EmptyNull(), - new Uri(url), - url, - published); + public SyndicationItem CreateItem(string title, string synopsis, string url, DateTimeOffset published, string contentEncoded = null) + { + var item = new SyndicationItem( + title.RemoveInvalidXmlChars().EmptyNull(), + synopsis.RemoveInvalidXmlChars().EmptyNull(), + new Uri(url), + url, + published); - if (contentEncoded != null) - { - item.ElementExtensions.Add("encoded", UrlPurlContent, contentEncoded.RemoveInvalidXmlChars().EmptyNull()); - } + if (contentEncoded != null) + { + item.ElementExtensions.Add("encoded", UrlPurlContent, contentEncoded.RemoveInvalidXmlChars().EmptyNull()); + } - return item; - } + return item; + } - public bool AddEnclosure(SyndicationItem item, MediaFile file, string fileUrl) - { - if (file != null && fileUrl.HasValue()) - { + public bool AddEnclosure(SyndicationItem item, MediaFile file, string fileUrl) + { + if (file != null && fileUrl.HasValue()) + { // We do not have the size behind fileUrl but the original file size should be fine. long fileLength = file.Size; if (fileLength <= 0) @@ -70,14 +70,14 @@ public bool AddEnclosure(SyndicationItem item, MediaFile file, string fileUrl) fileLength = 10000; } - var enclosure = SyndicationLink.CreateMediaEnclosureLink(new Uri(fileUrl), file.MimeType.EmptyNull(), fileLength); + var enclosure = SyndicationLink.CreateMediaEnclosureLink(new Uri(fileUrl), file.MimeType.EmptyNull(), fileLength); - item.Links.Add(enclosure); + item.Links.Add(enclosure); - return true; - } + return true; + } - return false; - } - } + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs b/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs index 4e863c3e87..72cd4bf8b6 100644 --- a/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs +++ b/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace SmartStore.Utilities { - public class StringTokenizer : IEnumerable { private const string _defaultDelim = " \t\n\r\f"; @@ -91,5 +87,4 @@ IEnumerator IEnumerable.GetEnumerator() #endregion } - } diff --git a/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs b/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs index a98e771f12..adaaec5992 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs @@ -6,117 +6,122 @@ namespace SmartStore.Utilities.Threading { - public sealed class KeyedLock - { - private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1); - private static readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + public sealed class KeyedLock + { + private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1); + private static readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); - private int _waiterCount; + private int _waiterCount; - private KeyedLock(object key) - { - Key = key; - Semaphore = new SemaphoreSlim(1, 1); - } + private KeyedLock(object key) + { + Key = key; + Semaphore = new SemaphoreSlim(1, 1); + } - private object Key { get; set; } - private SemaphoreSlim Semaphore { get; set; } + private object Key { get; set; } + private SemaphoreSlim Semaphore { get; set; } - public bool HasWaiters - { - get { return _waiterCount > 1; } - } + public bool HasWaiters => _waiterCount > 1; - public int WaiterCount - { - get { return _waiterCount; } - } + public int WaiterCount => _waiterCount; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void IncrementCount() => Interlocked.Increment(ref _waiterCount); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void IncrementCount() + { + Interlocked.Increment(ref _waiterCount); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void DecrementCount() => Interlocked.Decrement(ref _waiterCount); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrementCount() + { + Interlocked.Decrement(ref _waiterCount); + } - public static bool IsLockHeld(object key) - { - return _locks.ContainsKey(key); - } + public static bool IsLockHeld(object key) + { + return _locks.ContainsKey(key); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(object key) - { - return Lock(key, _noTimeout); - } + { + return Lock(key, _noTimeout); + } - public static IDisposable Lock(object key, TimeSpan timeout) - { - var keyedLock = GetOrCreateLock(key); - keyedLock.Semaphore.Wait(timeout); - return new Releaser(keyedLock); - } + public static IDisposable Lock(object key, TimeSpan timeout) + { + var keyedLock = GetOrCreateLock(key); + keyedLock.Semaphore.Wait(timeout); + return new Releaser(keyedLock); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task LockAsync(object key) - { - return LockAsync(key, _noTimeout, CancellationToken.None); - } + { + return LockAsync(key, _noTimeout, CancellationToken.None); + } - public static Task LockAsync(object key, TimeSpan timeout) - { - return LockAsync(key, timeout, CancellationToken.None); - } + public static Task LockAsync(object key, TimeSpan timeout) + { + return LockAsync(key, timeout, CancellationToken.None); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task LockAsync(object key, CancellationToken cancellationToken) - { - return LockAsync(key, _noTimeout, cancellationToken); - } - - public static Task LockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken) - { - var keyedLock = GetOrCreateLock(key); - var wait = keyedLock.Semaphore.WaitAsync(timeout, cancellationToken); - - return wait.IsCompleted - ? Task.FromResult(new Releaser(keyedLock) as IDisposable) - : wait.ContinueWith((t, s) => new Releaser((KeyedLock)s) as IDisposable, - keyedLock, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - - private static KeyedLock GetOrCreateLock(object key) - { - Guard.NotNull(key, nameof(key)); - - var item = _locks.GetOrAdd(key, k => new KeyedLock(key)); - item.IncrementCount(); - return item; - } - - sealed class Releaser : IDisposable - { - private KeyedLock _item; - - public Releaser(KeyedLock item) - { - _item = item; - } - - public void Dispose() - { - _item.DecrementCount(); - if (_item.WaiterCount == 0) - { - // Remove from dict - _locks.TryRemove(_item.Key, out _); - } - - _item.Semaphore.Release(); - _item = null; - } - } - } + { + return LockAsync(key, _noTimeout, cancellationToken); + } + + public static Task LockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken) + { + var keyedLock = GetOrCreateLock(key); + var wait = keyedLock.Semaphore.WaitAsync(timeout, cancellationToken); + + return wait.IsCompleted + ? Task.FromResult(new Releaser(keyedLock) as IDisposable) + : wait.ContinueWith((t, s) => new Releaser((KeyedLock)s) as IDisposable, + keyedLock, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + private static KeyedLock GetOrCreateLock(object key) + { + Guard.NotNull(key, nameof(key)); + + var item = _locks.GetOrAdd(key, k => new KeyedLock(key)); + item.IncrementCount(); + return item; + } + + sealed class Releaser : IDisposable + { + private KeyedLock _item; + + public Releaser(KeyedLock item) + { + _item = item; + } + + public void Dispose() + { + _item.DecrementCount(); + if (_item.WaiterCount == 0) + { + // Remove from dict + _locks.TryRemove(_item.Key, out _); + } + + if (_item.Semaphore.CurrentCount == 0) + { + _item.Semaphore.Release(); + } + + //_item.Semaphore.Dispose(); + _item = null; + } + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/Threading/LockExtensions.cs b/src/Libraries/SmartStore.Core/Utilities/Threading/LockExtensions.cs index ec05c3c650..4e4abcf113 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Threading/LockExtensions.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Threading/LockExtensions.cs @@ -1,7 +1,7 @@ using System; -using System.Threading; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Threading; namespace SmartStore.Utilities.Threading { diff --git a/src/Libraries/SmartStore.Core/Utilities/Throttle.cs b/src/Libraries/SmartStore.Core/Utilities/Throttle.cs index a380e15142..a203d90faf 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Throttle.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Throttle.cs @@ -1,13 +1,12 @@ using System; -using System.Collections.Generic; using System.Collections.Concurrent; using System.Runtime.CompilerServices; namespace SmartStore.Core.Utilities { - public static class Throttle - { - private readonly static ConcurrentDictionary _checks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + public static class Throttle + { + private readonly static ConcurrentDictionary _checks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// /// Performs a throttled check. @@ -18,54 +17,54 @@ public static class Throttle /// Check result [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Check(string key, TimeSpan interval, Func check) - { - return Check(key, interval, false, check); - } + { + return Check(key, interval, false, check); + } - /// - /// Performs a throttled check. - /// - /// Identifier for the check process - /// Interval between actual checks - /// - /// The check factory - /// Check result - public static bool Check(string key, TimeSpan interval, bool recheckWhenFalse, Func check) - { - Guard.NotEmpty(key, nameof(key)); - Guard.NotNull(check, nameof(check)); + /// + /// Performs a throttled check. + /// + /// Identifier for the check process + /// Interval between actual checks + /// + /// The check factory + /// Check result + public static bool Check(string key, TimeSpan interval, bool recheckWhenFalse, Func check) + { + Guard.NotEmpty(key, nameof(key)); + Guard.NotNull(check, nameof(check)); - bool added = false; - var now = DateTime.UtcNow; + bool added = false; + var now = DateTime.UtcNow; - var entry = _checks.GetOrAdd(key, x => - { - added = true; - return new CheckEntry { Value = check(), NextCheckUtc = (now + interval) }; - }); + var entry = _checks.GetOrAdd(key, x => + { + added = true; + return new CheckEntry { Value = check(), NextCheckUtc = (now + interval) }; + }); - if (added) - { - return entry.Value; - } + if (added) + { + return entry.Value; + } - var ok = entry.Value; - var isOverdue = (!ok && recheckWhenFalse) || (now > entry.NextCheckUtc); + var ok = entry.Value; + var isOverdue = (!ok && recheckWhenFalse) || (now > entry.NextCheckUtc); - if (isOverdue) - { - // Check is overdue: recheck - ok = check(); - _checks.TryUpdate(key, new CheckEntry { Value = ok, NextCheckUtc = (now + interval) }, entry); - } + if (isOverdue) + { + // Check is overdue: recheck + ok = check(); + _checks.TryUpdate(key, new CheckEntry { Value = ok, NextCheckUtc = (now + interval) }, entry); + } - return ok; - } + return ok; + } - class CheckEntry - { - public bool Value { get; set; } - public DateTime NextCheckUtc { get; set; } - } - } + class CheckEntry + { + public bool Value { get; set; } + public DateTime NextCheckUtc { get; set; } + } + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs b/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs index 2fb3f5bc09..6fbef73089 100644 --- a/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs @@ -1,11 +1,4 @@ using System; -using System.Collections.Generic; -using System.Reflection; -using System.Collections; -using System.ComponentModel; -using System.Linq; -using System.Globalization; -using System.IO; using System.Linq.Expressions; namespace SmartStore.Utilities @@ -37,25 +30,25 @@ public static Type GetElementType(Type type) return type; } - /// - /// Exctracts and returns the name of a property accessor lambda - /// - /// The containing type - /// The accessor lambda - /// When true, returns the result as '[TyoeName].[PropertyName]'. - /// The property name - public static string NameOf(Expression> propertyAccessor, bool includeTypeName = false) - { - Guard.NotNull(propertyAccessor, nameof(propertyAccessor)); + /// + /// Exctracts and returns the name of a property accessor lambda + /// + /// The containing type + /// The accessor lambda + /// When true, returns the result as '[TyoeName].[PropertyName]'. + /// The property name + public static string NameOf(Expression> propertyAccessor, bool includeTypeName = false) + { + Guard.NotNull(propertyAccessor, nameof(propertyAccessor)); - var name = propertyAccessor.ExtractPropertyInfo().Name; + var name = propertyAccessor.ExtractPropertyInfo().Name; - if (includeTypeName) - { - return typeof(T).Name + "." + name; - } + if (includeTypeName) + { + return typeof(T).Name + "." + name; + } - return name; - } + return name; + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs b/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs index 1d9ba4d877..3878d286fa 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs @@ -1,6 +1,6 @@ using System; -using System.Text; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using SmartStore.Utilities.ObjectPools; @@ -20,18 +20,18 @@ public class Wildcard : Regex /// private static bool m_isForward; - private readonly string _pattern; + private readonly string _pattern; - /// - /// Initializes a new instance of the class. - /// - /// The wildcard pattern. - /// - /// Specifies whether number ranges (e.g. 1234-5678) should - /// be converted to a regular expression pattern. - /// - public Wildcard(string pattern, bool parseNumberRanges = false) - : this(pattern, RegexOptions.None, parseNumberRanges) + /// + /// Initializes a new instance of the class. + /// + /// The wildcard pattern. + /// + /// Specifies whether number ranges (e.g. 1234-5678) should + /// be converted to a regular expression pattern. + /// + public Wildcard(string pattern, bool parseNumberRanges = false) + : this(pattern, RegexOptions.None, parseNumberRanges) { } @@ -44,25 +44,19 @@ public Wildcard(string pattern, bool parseNumberRanges = false) /// be converted to a regular expression pattern. /// /// The regular expression options. - public Wildcard(string pattern, RegexOptions options, bool parseNumberRanges = false) - : this(WildcardToRegex(pattern, parseNumberRanges), options, Timeout.InfiniteTimeSpan) + public Wildcard(string pattern, RegexOptions options, bool parseNumberRanges = false) + : this(WildcardToRegex(pattern, parseNumberRanges), options, Timeout.InfiniteTimeSpan) { - + } - internal Wildcard(string parsedPattern, RegexOptions options, TimeSpan matchTimeout) - : base(parsedPattern, options, matchTimeout) - { - _pattern = parsedPattern; - } + internal Wildcard(string parsedPattern, RegexOptions options, TimeSpan matchTimeout) + : base(parsedPattern, options, matchTimeout) + { + _pattern = parsedPattern; + } - public string Pattern - { - get - { - return _pattern; - } - } + public string Pattern => _pattern; /// /// Searches all number range terms and converts them @@ -74,26 +68,26 @@ private static string WildcardToRegex(string pattern, bool parseNumberRanges) { m_isForward = true; - // Replace ? with . and * with .* - // Prepend ^, append $ - // Escape all chars except []^ - pattern = ToGlobPattern(pattern); - - // convert the number ranges into regular expression - if (parseNumberRanges) - { - var re = new Regex("[0-9]+-[0-9]+"); - MatchCollection collection = re.Matches(pattern); - foreach (Match match in collection) - { - string[] split = match.Value.Split(new char[] { '-' }); - int leadingZeroesCount = split[0].TakeWhile(x => x == '0').Count(); - int min = Int32.Parse(split[0]); - int max = Int32.Parse(split[1]); - - pattern = pattern.Replace(match.Value, ConvertNumberRange(min, max, leadingZeroesCount)); - } - } + // Replace ? with . and * with .* + // Prepend ^, append $ + // Escape all chars except []^ + pattern = ToGlobPattern(pattern); + + // convert the number ranges into regular expression + if (parseNumberRanges) + { + var re = new Regex("[0-9]+-[0-9]+"); + MatchCollection collection = re.Matches(pattern); + foreach (Match match in collection) + { + string[] split = match.Value.Split(new char[] { '-' }); + int leadingZeroesCount = split[0].TakeWhile(x => x == '0').Count(); + int min = Int32.Parse(split[0]); + int max = Int32.Parse(split[1]); + + pattern = pattern.Replace(match.Value, ConvertNumberRange(min, max, leadingZeroesCount)); + } + } return pattern; } @@ -114,42 +108,42 @@ private static string ConvertNumberRange(int min, int max, int leadingZeroesCoun string prefix = new String('0', leadingZeroesCount); // for leading zeroes string pattern = string.Empty; - if (min > -1 && max - min < 10) - { - // special treatment: the below function has issues with too small ranges - for (var i = min; i <= max; i++) - { - pattern += prefix + i.ToString() + (i < max ? "|" : ""); - } - } - else - { - - int currentValue = min; + if (min > -1 && max - min < 10) + { + // special treatment: the below function has issues with too small ranges + for (var i = min; i <= max; i++) + { + pattern += prefix + i.ToString() + (i < max ? "|" : ""); + } + } + else + { + + int currentValue = min; int radix = 1; - while (m_isForward || radix != 0) - { + while (m_isForward || radix != 0) + { int tempMax = GetNextMaximum(currentValue, max, radix); if (tempMax >= currentValue) - { - pattern += prefix + ParseRange(currentValue, tempMax, radix); - if (!(!m_isForward && radix == 1)) - { - pattern += "|"; - } - } - radix += (m_isForward ? 1 : -1); - currentValue = tempMax + 1; - } - } + { + pattern += prefix + ParseRange(currentValue, tempMax, radix); + if (!(!m_isForward && radix == 1)) + { + pattern += "|"; + } + } + radix += (m_isForward ? 1 : -1); + currentValue = tempMax + 1; + } + } //add a negative look behind and a negative look ahead in order //to avoid that 122-321 is found in 2308. return @"((? /// Gets the next maximum value in condition to the current value. /// @@ -161,7 +155,7 @@ private static string ConvertNumberRange(int min, int max, int leadingZeroesCoun /// private static int GetNextMaximum(int currentValue, int maximum, int radix) { - //backward + //backward if (!m_isForward) { if (radix != 1) @@ -179,7 +173,7 @@ private static int GetNextMaximum(int currentValue, int maximum, int radix) radix--; tempMax = ((maximum / (int)Math.Pow(10, radix))) * (int)Math.Pow(10, radix) - 1; } - + return tempMax; } @@ -216,12 +210,12 @@ private static string GetRangePattern(int beginDigit, int endDigit, int count) { if (count == 0) { - return string.Empty; + return string.Empty; } if (beginDigit == endDigit) { - return beginDigit.ToString(); + return beginDigit.ToString(); } string pattern = string.Format("[{0}-{1}]", beginDigit, endDigit); @@ -244,27 +238,27 @@ private static int ExtractDigit(int value, int digit) return Int32.Parse(value.ToString()[digit].ToString()); } - #region Escaping + #region Escaping - /* -------------------------------------------------------------- + /* -------------------------------------------------------------- Stuff here partly copied over from .NET's internal RegexParser class and modified for performance reasons: we don't want to escape '[^]' chars, but Regex.Escape() does. Besides, wen need '*' and '?' as wildcard chars. -------------------------------------------------------------- */ - const byte W = 6; // wildcard char - const byte Q = 5; // quantifier - const byte S = 4; // ordinary stopper - const byte Z = 3; // ScanBlank stopper - const byte X = 2; // whitespace - const byte E = 1; // should be escaped + const byte W = 6; // wildcard char + const byte Q = 5; // quantifier + const byte S = 4; // ordinary stopper + const byte Z = 3; // ScanBlank stopper + const byte X = 2; // whitespace + const byte E = 1; // should be escaped - /* + /* * For categorizing ASCII characters. */ - private static readonly byte[] _category = new byte[] - { + private static readonly byte[] _category = new byte[] + { // 0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F 0,0,0,0,0,0,0,0,0,X,X,0,X,X,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @@ -273,85 +267,85 @@ Stuff here partly copied over from .NET's internal RegexParser 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,S,0,0,0, // ' a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,Q,S,0,0,0 - }; - - private static bool IsMetachar(char ch) - { - return (ch <= '|' && _category[ch] >= E); - } - - private static bool IsGlob(char ch) - { - return (ch <= '|' && _category[ch] >= W); - } - - private static string ToGlobPattern(string input) - { - Guard.NotNull(input, nameof(input)); - - for (int i = 0; i < input.Length; i++) - { - if (IsMetachar(input[i])) - { + }; + + private static bool IsMetachar(char ch) + { + return (ch <= '|' && _category[ch] >= E); + } + + private static bool IsGlob(char ch) + { + return (ch <= '|' && _category[ch] >= W); + } + + private static string ToGlobPattern(string input) + { + Guard.NotNull(input, nameof(input)); + + for (int i = 0; i < input.Length; i++) + { + if (IsMetachar(input[i])) + { var psb = PooledStringBuilder.Rent("^"); var sb = (StringBuilder)psb; - char ch = input[i]; - int lastpos; - - sb.Append(input, 0, i); - do - { - if (IsGlob(ch)) - { - sb.Append('.'); // '?' > '.' - if (ch == '*') sb.Append('*'); // '*' > '.*' - } - else - { - sb.Append('\\'); - switch (ch) - { - case '\n': - ch = 'n'; - break; - case '\r': - ch = 'r'; - break; - case '\t': - ch = 't'; - break; - case '\f': - ch = 'f'; - break; - } - sb.Append(ch); - } - - i++; - lastpos = i; - - while (i < input.Length) - { - ch = input[i]; - if (IsMetachar(ch)) - break; - - i++; - } - - sb.Append(input, lastpos, i - lastpos); - } while (i < input.Length); - - sb.Append('$'); - return psb.ToStringAndReturn(); - } - } - - return '^' + input + '$'; - } - - #endregion - } + char ch = input[i]; + int lastpos; + + sb.Append(input, 0, i); + do + { + if (IsGlob(ch)) + { + sb.Append('.'); // '?' > '.' + if (ch == '*') sb.Append('*'); // '*' > '.*' + } + else + { + sb.Append('\\'); + switch (ch) + { + case '\n': + ch = 'n'; + break; + case '\r': + ch = 'r'; + break; + case '\t': + ch = 't'; + break; + case '\f': + ch = 'f'; + break; + } + sb.Append(ch); + } + + i++; + lastpos = i; + + while (i < input.Length) + { + ch = input[i]; + if (IsMetachar(ch)) + break; + + i++; + } + + sb.Append(input, lastpos, i - lastpos); + } while (i < input.Length); + + sb.Append('$'); + return psb.ToStringAndReturn(); + } + } + + return '^' + input + '$'; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index 4617e0a0bd..14a332af68 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Configuration; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Web; @@ -12,33 +17,28 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.Infrastructure; using SmartStore.Utilities; -using System.Net; -using System.Text; -using System.Collections.Generic; -using System.Collections.Concurrent; -using System.Net.Sockets; namespace SmartStore.Core { public partial class WebHelper : IWebHelper { - private static object s_lock = new object(); - private static bool? s_optimizedCompilationsEnabled; - private static AspNetHostingPermissionLevel? s_trustLevel; - private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|webp|liquid|bmp|html|htm|xml|pdf|doc|xls|rar|zip|7z|ico|eot|svg|ttf|woff|woff2|otf)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex s_htmlPathPattern = new Regex(@"(?<=(?:href|src)=(?:""|'))(?!https?://)(?[^(?:""|')]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); - private static readonly Regex s_cssPathPattern = new Regex(@"url\('(?.+)'\)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); - private static ConcurrentDictionary s_safeLocalHostNames = new ConcurrentDictionary(); - - private readonly HttpContextBase _httpContext; + private static readonly object s_lock = new object(); + private static bool? s_optimizedCompilationsEnabled; + private static AspNetHostingPermissionLevel? s_trustLevel; + private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|webp|liquid|bmp|html|htm|xml|txt|pdf|doc|xls|rar|zip|7z|ico|eot|svg|ttf|woff|woff2|otf|json)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex s_htmlPathPattern = new Regex(@"(?<=(?:href|src)=(?:""|'))(?!https?://)(?[^(?:""|')]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex s_cssPathPattern = new Regex(@"url\('(?.+)'\)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly ConcurrentDictionary s_safeLocalHostNames = new ConcurrentDictionary(); + + private readonly HttpContextBase _httpContext; private bool? _isCurrentConnectionSecured; - private string _storeHost; - private string _storeHostSsl; - private string _ipAddress; - private bool? _appPathPossiblyAppended; - private bool? _appPathPossiblyAppendedSsl; + private string _storeHost; + private string _storeHostSsl; + private string _ipAddress; + private bool? _appPathPossiblyAppended; + private bool? _appPathPossiblyAppendedSsl; - private Store _currentStore; + private Store _currentStore; public WebHelper(HttpContextBase httpContext) { @@ -50,57 +50,57 @@ public virtual string GetUrlReferrer() return _httpContext.SafeGetHttpRequest()?.UrlReferrer?.ToString() ?? string.Empty; } - public virtual string GetClientIdent() - { - var ipAddress = this.GetCurrentIpAddress(); - var userAgent = _httpContext.SafeGetHttpRequest()?.UserAgent.EmptyNull(); - - if (ipAddress.HasValue() && userAgent.HasValue()) - { - return (ipAddress + userAgent).GetHashCode().ToString(); - } - - return null; - } - - public virtual string GetCurrentIpAddress() - { - if (_ipAddress != null) - { - return _ipAddress; - } - - var httpRequest = _httpContext.SafeGetHttpRequest(); - if (httpRequest == null) - { - return string.Empty; - } - - var vars = httpRequest.ServerVariables; - - var keysToCheck = new string[] - { - "HTTP_CLIENT_IP", - "HTTP_X_FORWARDED_FOR", - "HTTP_X_FORWARDED", - "HTTP_X_CLUSTER_CLIENT_IP", - "HTTP_FORWARDED_FOR", - "HTTP_FORWARDED", - "REMOTE_ADDR", - "HTTP_CF_CONNECTING_IP" - }; - - string result = null; + public virtual string GetClientIdent() + { + var ipAddress = this.GetCurrentIpAddress(); + var userAgent = _httpContext.SafeGetHttpRequest()?.UserAgent.EmptyNull(); + + if (ipAddress.HasValue() && userAgent.HasValue()) + { + return (ipAddress + userAgent).GetHashCode().ToString(); + } + + return null; + } + + public virtual string GetCurrentIpAddress() + { + if (_ipAddress != null) + { + return _ipAddress; + } + + var httpRequest = _httpContext.SafeGetHttpRequest(); + if (httpRequest == null) + { + return string.Empty; + } + + var vars = httpRequest.ServerVariables; + + var keysToCheck = new string[] + { + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "REMOTE_ADDR", + "HTTP_CF_CONNECTING_IP" + }; + + string result = null; IPAddress ipv6 = null; - foreach (var key in keysToCheck) - { - var ipString = vars[key]; + foreach (var key in keysToCheck) + { + var ipString = vars[key]; + + if (!string.IsNullOrEmpty(ipString)) + { + var arrStrings = ipString.Split(','); - if (!string.IsNullOrEmpty(ipString)) - { - var arrStrings = ipString.Split(','); - // Iterate list from end to start (IPv6 addresses usually have precedence) for (int i = arrStrings.Length - 1; i >= 0; i--) { @@ -118,21 +118,26 @@ public virtual string GetCurrentIpAddress() break; } } + } + + if (!string.IsNullOrEmpty(result)) + { + break; } - } - } + } + } if (string.IsNullOrEmpty(result) && ipv6 != null) { - result = ipv6.ToString() == "::1" + result = ipv6.ToString() == "::1" ? "127.0.0.1" : ipv6.MapToIPv4().ToString(); } - return (_ipAddress = result.EmptyNull()); - } + return (_ipAddress = result.EmptyNull()); + } - public virtual string GetThisPageUrl(bool includeQueryString) + public virtual string GetThisPageUrl(bool includeQueryString) { bool useSsl = IsCurrentConnectionSecured(); return GetThisPageUrl(includeQueryString, useSsl); @@ -141,44 +146,44 @@ public virtual string GetThisPageUrl(bool includeQueryString) public virtual string GetThisPageUrl(bool includeQueryString, bool useSsl) { string url = string.Empty; - var httpRequest = _httpContext.SafeGetHttpRequest(); + var httpRequest = _httpContext.SafeGetHttpRequest(); - if (httpRequest == null) + if (httpRequest == null) return url; - var authority = httpRequest.Url.GetLeftPart(UriPartial.Authority); - var path = httpRequest.RawUrl; - var schemeChanges = useSsl != IsCurrentConnectionSecured(); - - if (!schemeChanges && includeQueryString) - { - // Return as is - return authority + path; - } - - if (schemeChanges) - { - authority = GetStoreHost(useSsl, out bool appPathPossiblyAppended).TrimEnd('/'); - if (appPathPossiblyAppended) - { - path = _httpContext.GetOriginalAppRelativePath().TrimStart('~'); - if (includeQueryString && httpRequest.Url?.Query != null) - { - path += httpRequest.Url.Query; - } - } - } - - if (!includeQueryString) - { - var queryIndex = path.IndexOf('?'); - if (queryIndex > -1) - { - path = path.Substring(0, queryIndex); - } - } - - return authority + path; + var authority = httpRequest.Url.GetLeftPart(UriPartial.Authority); + var path = httpRequest.RawUrl; + var schemeChanges = useSsl != IsCurrentConnectionSecured(); + + if (!schemeChanges && includeQueryString) + { + // Return as is + return authority + path; + } + + if (schemeChanges) + { + authority = GetStoreHost(useSsl, out bool appPathPossiblyAppended).TrimEnd('/'); + if (appPathPossiblyAppended) + { + path = _httpContext.GetOriginalAppRelativePath().TrimStart('~'); + if (includeQueryString && httpRequest.Url?.Query != null) + { + path += httpRequest.Url.Query; + } + } + } + + if (!includeQueryString) + { + var queryIndex = path.IndexOf('?'); + if (queryIndex > -1) + { + path = path.Substring(0, queryIndex); + } + } + + return authority + path; } public virtual bool IsCurrentConnectionSecured() @@ -186,23 +191,23 @@ public virtual bool IsCurrentConnectionSecured() if (!_isCurrentConnectionSecured.HasValue) { _isCurrentConnectionSecured = false; - var httpRequest = _httpContext.SafeGetHttpRequest(); - if (httpRequest != null) + var httpRequest = _httpContext.SafeGetHttpRequest(); + if (httpRequest != null) { - _isCurrentConnectionSecured = httpRequest.IsSecureConnection(); + _isCurrentConnectionSecured = httpRequest.IsHttps(); } } return _isCurrentConnectionSecured.Value; } - + public virtual string ServerVariables(string name) { - return _httpContext.SafeGetHttpRequest()?.ServerVariables[name].EmptyNull(); + return _httpContext.SafeGetHttpRequest()?.ServerVariables[name].EmptyNull(); } - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private string GetHostPart(string url) + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private string GetHostPart(string url) { var uri = new Uri(url); var host = uri.GetComponents(UriComponents.Scheme | UriComponents.Host, UriFormat.Unescaped); @@ -211,14 +216,14 @@ private string GetHostPart(string url) private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) { - string cached = useSsl ? _storeHostSsl : _storeHost; - if (cached != null) - { - appPathPossiblyAppended = useSsl ? _appPathPossiblyAppendedSsl.Value : _appPathPossiblyAppended.Value; - return cached; - } - - appPathPossiblyAppended = false; + string cached = useSsl ? _storeHostSsl : _storeHost; + if (cached != null) + { + appPathPossiblyAppended = useSsl ? _appPathPossiblyAppendedSsl.Value : _appPathPossiblyAppended.Value; + return cached; + } + + appPathPossiblyAppended = false; var result = ""; var httpHost = ServerVariables("HTTP_HOST"); @@ -238,86 +243,86 @@ private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) } else { - // Let's resolve IWorkContext here. - // Do not inject it via contructor because it'll cause circular references - - if (_currentStore == null) - { - if (EngineContext.Current.ContainerManager.TryResolve(null, out IStoreContext storeContext)) // Unit test safe! - { - _currentStore = storeContext.CurrentStore; - if (_currentStore == null) - throw new Exception("Current store cannot be loaded"); - } - } - - if (_currentStore != null) - { - var securityMode = _currentStore.GetSecurityMode(); - - if (httpHost.IsEmpty()) - { - // HTTP_HOST variable is not available. - // It's possible only when HttpContext is not available (for example, running in a schedule task) - result = _currentStore.Url.EnsureEndsWith("/"); - - appPathPossiblyAppended = true; - } - - if (useSsl) - { - if (securityMode == HttpSecurityMode.SharedSsl) - { - // Secure URL for shared ssl specified. - // So a store owner doesn't want it to be resolved automatically. - // In this case let's use the specified secure URL - result = _currentStore.SecureUrl.EmptyNull(); - - if (!result.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - result = "https://" + result; - } - - appPathPossiblyAppended = true; - } - else - { - // Secure URL is not specified. - // So a store owner wants it to be resolved automatically. - result = result.Replace("http:/", "https:/"); - } - } - else // no ssl - { - if (securityMode == HttpSecurityMode.SharedSsl) - { - // SSL is enabled in this store and shared ssl URL is specified. - // So a store owner doesn't want it to be resolved automatically. - // In this case let's use the specified non-secure URL - - result = _currentStore.Url; - appPathPossiblyAppended = true; - } - } - } - } - - // cache results for request - result = result.EnsureEndsWith("/"); - if (useSsl) - { - _storeHostSsl = result; - _appPathPossiblyAppendedSsl = appPathPossiblyAppended; - } - else - { - _storeHost = result; - _appPathPossiblyAppended = appPathPossiblyAppended; - } + // Let's resolve IWorkContext here. + // Do not inject it via contructor because it'll cause circular references + + if (_currentStore == null) + { + if (EngineContext.Current.ContainerManager.TryResolve(null, out IStoreContext storeContext)) // Unit test safe! + { + _currentStore = storeContext.CurrentStore; + if (_currentStore == null) + throw new Exception("Current store cannot be loaded"); + } + } + + if (_currentStore != null) + { + var securityMode = _currentStore.GetSecurityMode(); + + if (httpHost.IsEmpty()) + { + // HTTP_HOST variable is not available. + // It's possible only when HttpContext is not available (for example, running in a schedule task) + result = _currentStore.Url.EnsureEndsWith("/"); + + appPathPossiblyAppended = true; + } + + if (useSsl) + { + if (securityMode == HttpSecurityMode.SharedSsl) + { + // Secure URL for shared ssl specified. + // So a store owner doesn't want it to be resolved automatically. + // In this case let's use the specified secure URL + result = _currentStore.SecureUrl.EmptyNull(); + + if (!result.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + result = "https://" + result; + } + + appPathPossiblyAppended = true; + } + else + { + // Secure URL is not specified. + // So a store owner wants it to be resolved automatically. + result = result.Replace("http:/", "https:/"); + } + } + else // no ssl + { + if (securityMode == HttpSecurityMode.SharedSsl) + { + // SSL is enabled in this store and shared ssl URL is specified. + // So a store owner doesn't want it to be resolved automatically. + // In this case let's use the specified non-secure URL + + result = _currentStore.Url; + appPathPossiblyAppended = true; + } + } + } + } + + // cache results for request + result = result.EnsureEndsWith("/"); + if (useSsl) + { + _storeHostSsl = result; + _appPathPossiblyAppendedSsl = appPathPossiblyAppended; + } + else + { + _storeHost = result; + _appPathPossiblyAppended = appPathPossiblyAppended; + } return result; } - + public virtual string GetStoreLocation() { bool useSsl = IsCurrentConnectionSecured(); @@ -333,7 +338,7 @@ public virtual string GetStoreLocation(bool useSsl) result = result.Substring(0, result.Length - 1); } - var httpRequest = _httpContext.SafeGetHttpRequest(); + var httpRequest = _httpContext.SafeGetHttpRequest(); if (httpRequest != null) { @@ -343,7 +348,7 @@ public virtual string GetStoreLocation(bool useSsl) // in a shared ssl scenario the user defined https url could contain // the app path already. In this case we must not append. result += appPath; - } + } } if (!result.EndsWith("/")) @@ -353,485 +358,485 @@ public virtual string GetStoreLocation(bool useSsl) return result; } - + public virtual bool IsStaticResource(HttpRequest request) { - return IsStaticResourceRequested(new HttpRequestWrapper(request)); + return IsStaticResourceRequested(new HttpRequestWrapper(request)); + } + + public static bool IsStaticResourceRequested(HttpRequest request) + { + Guard.NotNull(request, nameof(request)); + return s_staticExts.IsMatch(request.Path); + } + + public static bool IsStaticResourceRequested(HttpRequestBase request) + { + // unit testable + Guard.NotNull(request, nameof(request)); + return s_staticExts.IsMatch(request.Path); } - public static bool IsStaticResourceRequested(HttpRequest request) - { - Guard.NotNull(request, nameof(request)); - return s_staticExts.IsMatch(request.Path); - } - - public static bool IsStaticResourceRequested(HttpRequestBase request) - { - // unit testable - Guard.NotNull(request, nameof(request)); - return s_staticExts.IsMatch(request.Path); - } - public virtual string MapPath(string path) { - return CommonHelper.MapPath(path, false); + return CommonHelper.MapPath(path, false); } - + public virtual string ModifyQueryString(string url, string queryStringModification, string anchor) { - url = url.EmptyNull(); - queryStringModification = queryStringModification.EmptyNull(); - - string curAnchor = null; - - var hsIndex = url.LastIndexOf('#'); - if (hsIndex >= 0) - { - curAnchor = url.Substring(hsIndex); - url = url.Substring(0, hsIndex); - } - - var parts = url.Split(new[] { '?' }); - var current = new QueryString(parts.Length == 2 ? parts[1] : ""); - var modify = new QueryString(queryStringModification); - - foreach (var nv in modify.AllKeys) - { - current.Add(nv, modify[nv], true); - } - - var result = string.Concat( - parts[0], - current.ToString(false), - anchor.NullEmpty() == null ? (curAnchor == null ? "" : "#" + curAnchor) : "#" + anchor - ); - - return result; + url = url.EmptyNull(); + queryStringModification = queryStringModification.EmptyNull(); + + string curAnchor = null; + + var hsIndex = url.LastIndexOf('#'); + if (hsIndex >= 0) + { + curAnchor = url.Substring(hsIndex); + url = url.Substring(0, hsIndex); + } + + var parts = url.Split(new[] { '?' }); + var current = new QueryString(parts.Length == 2 ? parts[1] : ""); + var modify = new QueryString(queryStringModification); + + foreach (var nv in modify.AllKeys) + { + current.Add(nv, modify[nv], true); + } + + var result = string.Concat( + parts[0], + current.ToString(false), + anchor.NullEmpty() == null ? (curAnchor == null ? "" : "#" + curAnchor) : "#" + anchor + ); + + return result; } public virtual string RemoveQueryString(string url, string queryString) { - var parts = url.SplitSafe("?"); + var parts = url.SplitSafe("?"); - var current = new QueryString(parts.Length == 2 ? parts[1] : ""); + var current = new QueryString(parts.Length == 2 ? parts[1] : ""); - if (current.Count > 0 && queryString.HasValue()) - { - current.Remove(queryString); - } + if (current.Count > 0 && queryString.HasValue()) + { + current.Remove(queryString); + } - var result = string.Concat(parts[0], current.ToString(false)); - return result; + var result = string.Concat(parts[0], current.ToString(false)); + return result; } - + public virtual T QueryString(string name) { - var queryParam = _httpContext.SafeGetHttpRequest()?.QueryString[name]; + var queryParam = _httpContext.SafeGetHttpRequest()?.QueryString[name]; - if (!string.IsNullOrEmpty(queryParam)) - { - return queryParam.Convert(); - } + if (!string.IsNullOrEmpty(queryParam)) + { + return queryParam.Convert(); + } return default(T); } - + public virtual void RestartAppDomain(bool makeRedirect = false, string redirectUrl = "", bool aggressive = false) { - HttpRuntime.UnloadAppDomain(); - - if (aggressive) - { - // When plugins are (un)installed, 'aggressive' is always true. - if (OptimizedCompilationsEnabled) - { - // Very hackish: - // If optimizedCompilations is on per web.config, touching top-level resources - // like global.asax or bin folder is meaningless, 'cause ASP.NET skips these for - // hash calculation. This way we can throw in plugins like crazy without invalidating - // ASP.NET temp files, which boosts app startup performance dramatically. - // Unfortunately, MVC keeps a controller cache file in the temp files folder, which NEVER - // gets nuked, unless the 'compilation' element in web.config is changed. - // We MUST delete this file in order to ensure that it gets re-created with our new controller types in it. - DeleteMvcTypeCacheFiles(); - } - else - { - // Without optimizedCompilations, touching anything in the bin folder nukes ASP.NET temp folder completely, - // including compiled views, MVC cache files etc. - TryWriteBinFolder(); - } - } - else - { - // without this, MVC may fail resolving controllers for newly installed plugins after IIS restart - Thread.Sleep(250); - } + HttpRuntime.UnloadAppDomain(); + + if (aggressive) + { + // When plugins are (un)installed, 'aggressive' is always true. + if (OptimizedCompilationsEnabled) + { + // Very hackish: + // If optimizedCompilations is on per web.config, touching top-level resources + // like global.asax or bin folder is meaningless, 'cause ASP.NET skips these for + // hash calculation. This way we can throw in plugins like crazy without invalidating + // ASP.NET temp files, which boosts app startup performance dramatically. + // Unfortunately, MVC keeps a controller cache file in the temp files folder, which NEVER + // gets nuked, unless the 'compilation' element in web.config is changed. + // We MUST delete this file in order to ensure that it gets re-created with our new controller types in it. + DeleteMvcTypeCacheFiles(); + } + else + { + // Without optimizedCompilations, touching anything in the bin folder nukes ASP.NET temp folder completely, + // including compiled views, MVC cache files etc. + TryWriteBinFolder(); + } + } + else + { + // without this, MVC may fail resolving controllers for newly installed plugins after IIS restart + Thread.Sleep(250); + } // If setting up plugins requires an AppDomain restart, it's very unlikely the // current request can be processed correctly. So, we redirect to the same URL, so that the // new request will come to the newly started AppDomain. if (_httpContext != null && makeRedirect) { - if (_httpContext.Request.RequestType == "GET") - { - if (string.IsNullOrEmpty(redirectUrl)) - { - redirectUrl = GetThisPageUrl(true); - } - _httpContext.Response.Redirect(redirectUrl, true /*endResponse*/); - } - else - { - // Don't redirect posts... - _httpContext.Response.ContentType = "text/html"; - _httpContext.Response.WriteFile("~/refresh.html"); - _httpContext.Response.End(); - } + if (_httpContext.Request.RequestType == "GET") + { + if (string.IsNullOrEmpty(redirectUrl)) + { + redirectUrl = GetThisPageUrl(true); + } + _httpContext.Response.Redirect(redirectUrl, true /*endResponse*/); + } + else + { + // Don't redirect posts... + _httpContext.Response.ContentType = "text/html"; + _httpContext.Response.WriteFile("~/refresh.html"); + _httpContext.Response.End(); + } + } + } + + private void DeleteMvcTypeCacheFiles() + { + try + { + var userCacheDir = Path.Combine(HttpRuntime.CodegenDir, "UserCache"); + + File.Delete(Path.Combine(userCacheDir, "MVC-ControllerTypeCache.xml")); + File.Delete(Path.Combine(userCacheDir, "MVC-AreaRegistrationTypeCache.xml")); + } + catch { } + } + + private bool TryWriteBinFolder() + { + try + { + var binMarker = MapPath("~/bin/HostRestart"); + Directory.CreateDirectory(binMarker); + + using (var stream = File.CreateText(Path.Combine(binMarker, "marker.txt"))) + { + stream.WriteLine("Restart on '{0}'", DateTime.UtcNow); + stream.Flush(); + } + return true; + } + catch + { + return false; } } - private void DeleteMvcTypeCacheFiles() - { - try - { - var userCacheDir = Path.Combine(HttpRuntime.CodegenDir, "UserCache"); - - File.Delete(Path.Combine(userCacheDir, "MVC-ControllerTypeCache.xml")); - File.Delete(Path.Combine(userCacheDir, "MVC-AreaRegistrationTypeCache.xml")); - } - catch { } - } - - private bool TryWriteBinFolder() - { - try - { - var binMarker = MapPath("~/bin/HostRestart"); - Directory.CreateDirectory(binMarker); - - using (var stream = File.CreateText(Path.Combine(binMarker, "marker.txt"))) - { - stream.WriteLine("Restart on '{0}'", DateTime.UtcNow); - stream.Flush(); - } - return true; - } - catch - { - return false; - } - } - - internal static bool OptimizedCompilationsEnabled - { - get - { - if (!s_optimizedCompilationsEnabled.HasValue) - { - var section = (CompilationSection)ConfigurationManager.GetSection("system.web/compilation"); - s_optimizedCompilationsEnabled = section.OptimizeCompilations; - } - - return s_optimizedCompilationsEnabled.Value; - } - } - - /// - /// Finds the trust level of the running application (http://blogs.msdn.com/dmitryr/archive/2007/01/23/finding-out-the-current-trust-level-in-asp-net.aspx) - /// - /// The current trust level. - public static AspNetHostingPermissionLevel GetTrustLevel() - { - if (!s_trustLevel.HasValue) - { - // set minimum - s_trustLevel = AspNetHostingPermissionLevel.None; - - // determine maximum - foreach (AspNetHostingPermissionLevel trustLevel in - new [] { + internal static bool OptimizedCompilationsEnabled + { + get + { + if (!s_optimizedCompilationsEnabled.HasValue) + { + var section = (CompilationSection)ConfigurationManager.GetSection("system.web/compilation"); + s_optimizedCompilationsEnabled = section.OptimizeCompilations; + } + + return s_optimizedCompilationsEnabled.Value; + } + } + + /// + /// Finds the trust level of the running application (http://blogs.msdn.com/dmitryr/archive/2007/01/23/finding-out-the-current-trust-level-in-asp-net.aspx) + /// + /// The current trust level. + public static AspNetHostingPermissionLevel GetTrustLevel() + { + if (!s_trustLevel.HasValue) + { + // set minimum + s_trustLevel = AspNetHostingPermissionLevel.None; + + // determine maximum + foreach (AspNetHostingPermissionLevel trustLevel in + new[] { AspNetHostingPermissionLevel.Unrestricted, AspNetHostingPermissionLevel.High, AspNetHostingPermissionLevel.Medium, AspNetHostingPermissionLevel.Low, - AspNetHostingPermissionLevel.Minimal + AspNetHostingPermissionLevel.Minimal }) - { - try - { - new AspNetHostingPermission(trustLevel).Demand(); - s_trustLevel = trustLevel; - break; //we've set the highest permission we can - } - catch (System.Security.SecurityException) - { - continue; - } - } - } - return s_trustLevel.Value; - } - - /// - /// Prepends protocol and host to all (relative) urls in a html string - /// - /// The html string - /// Request object - /// The transformed result html - /// - /// All html attributed named src and href are affected, also occurences of url('path') within embedded stylesheets. - /// - public static string MakeAllUrlsAbsolute(string html, HttpRequestBase request) - { - Guard.NotNull(request, nameof(request)); - - if (request.Url == null) - { - return html; - } - - return MakeAllUrlsAbsolute(html, request.Url.Scheme, request.Url.Authority); - } - - /// - /// Prepends protocol and host to all (relative) urls in a html string - /// - /// The html string - /// The protocol to prepend, e.g. http - /// The host name to prepend, e.g. www.mysite.com - /// The transformed result html - /// - /// All html attributed named src and href are affected, also occurences of url('path') within embedded stylesheets. - /// - public static string MakeAllUrlsAbsolute(string html, string protocol, string host) - { - Guard.NotEmpty(html, nameof(html)); - Guard.NotEmpty(protocol, nameof(protocol)); - Guard.NotEmpty(host, nameof(host)); - - string baseUrl = protocol.EnsureEndsWith("://") + host.TrimEnd('/'); - - MatchEvaluator evaluator = (match) => - { - var url = match.Groups["url"].Value; - return "{0}{1}".FormatCurrent(baseUrl, url.EnsureStartsWith("/")); - }; - - html = s_htmlPathPattern.Replace(html, evaluator); - html = s_cssPathPattern.Replace(html, evaluator); - - return html; - } - - /// - /// Prepends protocol and host to the given (relative) url - /// - /// Changes the protocol if passed. - [SuppressMessage("ReSharper", "AccessToModifiedClosure")] - public static string GetAbsoluteUrl(string url, HttpRequestBase request, bool enforceScheme = false, string protocol = null) - { - Guard.NotEmpty(url, nameof(url)); - Guard.NotNull(request, nameof(request)); - - if (request.Url == null) - { - return url; - } - - if (url.Contains("://")) - { - return url; - } - - protocol = protocol ?? request.Url.Scheme; - - if (url.StartsWith("//")) - { - return enforceScheme - ? String.Concat(protocol, ":", url) - : url; - } - - if (url.StartsWith("~")) - { - url = VirtualPathUtility.ToAbsolute(url); - } - - url = string.Format("{0}://{1}{2}", protocol, request.Url.Authority, url); - return url; - } - - public static string GetPublicIPAddress() - { - string result = string.Empty; - - try - { - using (var client = new WebClient()) - { - client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; - try - { - byte[] arr = client.DownloadData("http://checkip.amazonaws.com/"); - string response = Encoding.UTF8.GetString(arr); - result = response.Trim(); - } - catch { } - } - } - catch { } - - var checkers = new string[] - { - "https://ipinfo.io/ip", - "https://api.ipify.org", - "https://icanhazip.com", - "https://wtfismyip.com/text", - "http://bot.whatismyipaddress.com/" - }; - - if (string.IsNullOrEmpty(result)) - { - using (var client = new WebClient()) - { - foreach (var checker in checkers) - { - try - { - result = client.DownloadString(checker).Replace("\n", ""); - if (!string.IsNullOrEmpty(result)) - { - break; - } - } - catch { } - } - } - } - - if (string.IsNullOrEmpty(result)) - { - try - { - var url = "http://checkip.dyndns.org"; - var req = WebRequest.Create(url); - using (var resp = req.GetResponse()) - { - using (var sr = new StreamReader(resp.GetResponseStream())) - { - var response = sr.ReadToEnd().Trim(); - var a = response.Split(':'); - var a2 = a[1].Substring(1); - var a3 = a2.Split('<'); - result = a3[0]; - } - } - } - catch { } - } - - return result; - } - - public static HttpWebRequest CreateHttpRequestForSafeLocalCall(Uri requestUri) - { - Guard.NotNull(requestUri, nameof(requestUri)); - - var safeHostName = GetSafeLocalHostName(requestUri); - - var uri = requestUri; - - if (!requestUri.Host.Equals(safeHostName, StringComparison.OrdinalIgnoreCase)) - { - var url = String.Format("{0}://{1}{2}", - requestUri.Scheme, - requestUri.IsDefaultPort ? safeHostName : safeHostName + ":" + requestUri.Port, - requestUri.PathAndQuery); - uri = new Uri(url); - } - - var request = WebRequest.CreateHttp(uri); - request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; - request.ServicePoint.Expect100Continue = false; - request.UserAgent = "SmartStore.NET {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); - - return request; - } - - private static string GetSafeLocalHostName(Uri requestUri) - { - return s_safeLocalHostNames.GetOrAdd(requestUri.Port, (port) => - { - // first try original host - if (TestHost(requestUri, requestUri.Host, 5000)) - { - return requestUri.Host; - } - - // try loopback - var hostName = Dns.GetHostName(); - var hosts = new List { "localhost", hostName, "127.0.0.1" }; - foreach (var host in hosts) - { - if (TestHost(requestUri, host, 500)) - { - return host; - } - } - - // try local IP addresses - hosts.Clear(); - var ipAddresses = Dns.GetHostAddresses(hostName).Where(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.ToString()); - hosts.AddRange(ipAddresses); - - foreach (var host in hosts) - { - if (TestHost(requestUri, host, 500)) - { - return host; - } - } - - // None of the hosts are callable. WTF? - return requestUri.Host; - }); - } - - private static bool TestHost(Uri originalUri, string host, int timeout) - { - var url = String.Format("{0}://{1}/taskscheduler/noop", - originalUri.Scheme, - originalUri.IsDefaultPort ? host : host + ":" + originalUri.Port); - var uri = new Uri(url); - - var request = WebRequest.CreateHttp(uri); - request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; - request.ServicePoint.Expect100Continue = false; - request.UserAgent = "SmartStore.NET"; - request.Timeout = timeout; - - HttpWebResponse response = null; - - try - { - response = (HttpWebResponse)request.GetResponse(); - if (response.StatusCode == HttpStatusCode.OK) - { - return true; - } - } - catch - { - // try the next host - } - finally - { - if (response != null) - response.Dispose(); - } - - return false; - } - } + { + try + { + new AspNetHostingPermission(trustLevel).Demand(); + s_trustLevel = trustLevel; + break; //we've set the highest permission we can + } + catch (System.Security.SecurityException) + { + continue; + } + } + } + return s_trustLevel.Value; + } + + /// + /// Prepends protocol and host to all (relative) urls in a html string + /// + /// The html string + /// Request object + /// The transformed result html + /// + /// All html attributed named src and href are affected, also occurences of url('path') within embedded stylesheets. + /// + public static string MakeAllUrlsAbsolute(string html, HttpRequestBase request) + { + Guard.NotNull(request, nameof(request)); + + if (request.Url == null) + { + return html; + } + + return MakeAllUrlsAbsolute(html, request.Url.Scheme, request.Url.Authority); + } + + /// + /// Prepends protocol and host to all (relative) urls in a html string + /// + /// The html string + /// The protocol to prepend, e.g. http + /// The host name to prepend, e.g. www.mysite.com + /// The transformed result html + /// + /// All html attributed named src and href are affected, also occurences of url('path') within embedded stylesheets. + /// + public static string MakeAllUrlsAbsolute(string html, string protocol, string host) + { + Guard.NotEmpty(html, nameof(html)); + Guard.NotEmpty(protocol, nameof(protocol)); + Guard.NotEmpty(host, nameof(host)); + + string baseUrl = protocol.EnsureEndsWith("://") + host.TrimEnd('/'); + + MatchEvaluator evaluator = (match) => + { + var url = match.Groups["url"].Value; + return "{0}{1}".FormatCurrent(baseUrl, url.EnsureStartsWith("/")); + }; + + html = s_htmlPathPattern.Replace(html, evaluator); + html = s_cssPathPattern.Replace(html, evaluator); + + return html; + } + + /// + /// Prepends protocol and host to the given (relative) url + /// + /// Changes the protocol if passed. + [SuppressMessage("ReSharper", "AccessToModifiedClosure")] + public static string GetAbsoluteUrl(string url, HttpRequestBase request, bool enforceScheme = false, string protocol = null) + { + Guard.NotEmpty(url, nameof(url)); + Guard.NotNull(request, nameof(request)); + + if (request.Url == null) + { + return url; + } + + if (url.Contains("://")) + { + return url; + } + + protocol = protocol ?? request.Url.Scheme; + + if (url.StartsWith("//")) + { + return enforceScheme + ? String.Concat(protocol, ":", url) + : url; + } + + if (url.StartsWith("~")) + { + url = VirtualPathUtility.ToAbsolute(url); + } + + url = string.Format("{0}://{1}{2}", protocol, request.Url.Authority, url); + return url; + } + + public static string GetPublicIPAddress() + { + string result = string.Empty; + + try + { + using (var client = new WebClient()) + { + client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; + try + { + byte[] arr = client.DownloadData("http://checkip.amazonaws.com/"); + string response = Encoding.UTF8.GetString(arr); + result = response.Trim(); + } + catch { } + } + } + catch { } + + var checkers = new string[] + { + "https://ipinfo.io/ip", + "https://api.ipify.org", + "https://icanhazip.com", + "https://wtfismyip.com/text", + "http://bot.whatismyipaddress.com/" + }; + + if (string.IsNullOrEmpty(result)) + { + using (var client = new WebClient()) + { + foreach (var checker in checkers) + { + try + { + result = client.DownloadString(checker).Replace("\n", ""); + if (!string.IsNullOrEmpty(result)) + { + break; + } + } + catch { } + } + } + } + + if (string.IsNullOrEmpty(result)) + { + try + { + var url = "http://checkip.dyndns.org"; + var req = WebRequest.Create(url); + using (var resp = req.GetResponse()) + { + using (var sr = new StreamReader(resp.GetResponseStream())) + { + var response = sr.ReadToEnd().Trim(); + var a = response.Split(':'); + var a2 = a[1].Substring(1); + var a3 = a2.Split('<'); + result = a3[0]; + } + } + } + catch { } + } + + return result; + } + + public static HttpWebRequest CreateHttpRequestForSafeLocalCall(Uri requestUri) + { + Guard.NotNull(requestUri, nameof(requestUri)); + + var safeHostName = GetSafeLocalHostName(requestUri); + + var uri = requestUri; + + if (!requestUri.Host.Equals(safeHostName, StringComparison.OrdinalIgnoreCase)) + { + var url = String.Format("{0}://{1}{2}", + requestUri.Scheme, + requestUri.IsDefaultPort ? safeHostName : safeHostName + ":" + requestUri.Port, + requestUri.PathAndQuery); + uri = new Uri(url); + } + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "Smartstore {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); + + return request; + } + + private static string GetSafeLocalHostName(Uri requestUri) + { + return s_safeLocalHostNames.GetOrAdd(requestUri.Port, (port) => + { + // first try original host + if (TestHost(requestUri, requestUri.Host, 5000)) + { + return requestUri.Host; + } + + // try loopback + var hostName = Dns.GetHostName(); + var hosts = new List { "localhost", hostName, "127.0.0.1" }; + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // try local IP addresses + hosts.Clear(); + var ipAddresses = Dns.GetHostAddresses(hostName).Where(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.ToString()); + hosts.AddRange(ipAddresses); + + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // None of the hosts are callable. WTF? + return requestUri.Host; + }); + } + + private static bool TestHost(Uri originalUri, string host, int timeout) + { + var url = String.Format("{0}://{1}/taskscheduler/noop", + originalUri.Scheme, + originalUri.IsDefaultPort ? host : host + ":" + originalUri.Port); + var uri = new Uri(url); + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "Smartstore"; + request.Timeout = timeout; + + HttpWebResponse response = null; + + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + return true; + } + } + catch + { + // try the next host + } + finally + { + if (response != null) + response.Dispose(); + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/XmlHelper.cs b/src/Libraries/SmartStore.Core/XmlHelper.cs index 47c806b66b..cc5f65923b 100644 --- a/src/Libraries/SmartStore.Core/XmlHelper.cs +++ b/src/Libraries/SmartStore.Core/XmlHelper.cs @@ -7,184 +7,184 @@ namespace SmartStore.Core { - /// - /// Xml helper class - /// - public partial class XmlHelper - { - private readonly static Regex s_xmlEncodePattern = new Regex(@"[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]", RegexOptions.Compiled); - - #region Methods - - /// - /// XML Encode - /// - /// String - /// Encoded string - public static string XmlEncode(string str) - { - if (str == null) - return null; - - str = s_xmlEncodePattern.Replace(str, ""); - return XmlEncodeAsIs(str); - } - - /// - /// XML Encode as is - /// - /// String - /// Encoded string - public static string XmlEncodeAsIs(string str) - { - if (str == null) - return null; - - using (var sw = new StringWriter()) - { - using (var xwr = new XmlTextWriter(sw)) - { - xwr.WriteString(str); - return sw.ToString(); - } - } - } - - /// - /// Encodes an attribute - /// - /// Attribute - /// Encoded attribute - public static string XmlEncodeAttribute(string str) - { - if (str == null) - return null; - str = s_xmlEncodePattern.Replace(str, ""); - return XmlEncodeAttributeAsIs(str); - } - - /// - /// Encodes an attribute as is - /// - /// Attribute - /// Encoded attribute - public static string XmlEncodeAttributeAsIs(string str) - { - return XmlEncodeAsIs(str).Replace("\"", """); - } - - /// - /// Decodes an attribute - /// - /// Attribute - /// Decoded attribute - public static string XmlDecode(string str) - { - var sb = new StringBuilder(str); - return sb.Replace(""", "\"").Replace("'", "'").Replace("<", "<").Replace(">", ">").Replace("&", "&").ToString(); - } - - /// - /// Serializes a datetime - /// - /// Datetime - /// Serialized datetime - public static string SerializeDateTime(DateTime dateTime) - { - var xmlS = new XmlSerializer(typeof(DateTime)); - var sb = new StringBuilder(); - using (var sw = new StringWriter(sb)) - { - xmlS.Serialize(sw, dateTime); - return sb.ToString(); - } - } - - /// - /// Deserializes a datetime - /// - /// Datetime - /// Deserialized datetime - public static DateTime DeserializeDateTime(string dateTime) - { - var xmlS = new XmlSerializer(typeof(DateTime)); - using (var sr = new StringReader(dateTime)) - { - object test = xmlS.Deserialize(sr); - return (DateTime)test; - } - } - - /// - /// Serializes an object instance to a XML formatted string - /// - /// Object type - /// Object instance - /// XML string - public static string Serialize(T instance) - { - return Serialize(instance, typeof(T)); - } - - /// - /// Serializes an object instance to a XML formatted string - /// - /// Object instance - /// Object type - /// XML string - public static string Serialize(object instance, Type type) - { - if (instance == null) - return null; - - Guard.NotNull(type, nameof(type)); - - using (var writer = new StringWriter()) - { - var xmlSerializer = new XmlSerializer(type); - - xmlSerializer.Serialize(writer, instance); - return writer.ToString(); - } - } - - /// - /// Deserializes a XML formatted string to an object instance - /// - /// Object type - /// XML string - /// Object instance - public static T Deserialize(string xml) - { - return (T)Deserialize(xml, typeof(T)); - } - - /// - /// Deserializes a XML formatted string to an object instance - /// - /// XML string - /// Object type - /// Object instance - public static object Deserialize(string xml, Type type) - { - Guard.NotNull(type, nameof(type)); - - try - { - if (xml.HasValue()) - { - using (var reader = new StringReader(xml)) - { - var serializer = new XmlSerializer(type); - return serializer.Deserialize(reader); - } - } - } - catch { } - - return Activator.CreateInstance(type); - } - - #endregion - } + /// + /// Xml helper class + /// + public partial class XmlHelper + { + private readonly static Regex s_xmlEncodePattern = new Regex(@"[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]", RegexOptions.Compiled); + + #region Methods + + /// + /// XML Encode + /// + /// String + /// Encoded string + public static string XmlEncode(string str) + { + if (str == null) + return null; + + str = s_xmlEncodePattern.Replace(str, ""); + return XmlEncodeAsIs(str); + } + + /// + /// XML Encode as is + /// + /// String + /// Encoded string + public static string XmlEncodeAsIs(string str) + { + if (str == null) + return null; + + using (var sw = new StringWriter()) + { + using (var xwr = new XmlTextWriter(sw)) + { + xwr.WriteString(str); + return sw.ToString(); + } + } + } + + /// + /// Encodes an attribute + /// + /// Attribute + /// Encoded attribute + public static string XmlEncodeAttribute(string str) + { + if (str == null) + return null; + str = s_xmlEncodePattern.Replace(str, ""); + return XmlEncodeAttributeAsIs(str); + } + + /// + /// Encodes an attribute as is + /// + /// Attribute + /// Encoded attribute + public static string XmlEncodeAttributeAsIs(string str) + { + return XmlEncodeAsIs(str).Replace("\"", """); + } + + /// + /// Decodes an attribute + /// + /// Attribute + /// Decoded attribute + public static string XmlDecode(string str) + { + var sb = new StringBuilder(str); + return sb.Replace(""", "\"").Replace("'", "'").Replace("<", "<").Replace(">", ">").Replace("&", "&").ToString(); + } + + /// + /// Serializes a datetime + /// + /// Datetime + /// Serialized datetime + public static string SerializeDateTime(DateTime dateTime) + { + var xmlS = new XmlSerializer(typeof(DateTime)); + var sb = new StringBuilder(); + using (var sw = new StringWriter(sb)) + { + xmlS.Serialize(sw, dateTime); + return sb.ToString(); + } + } + + /// + /// Deserializes a datetime + /// + /// Datetime + /// Deserialized datetime + public static DateTime DeserializeDateTime(string dateTime) + { + var xmlS = new XmlSerializer(typeof(DateTime)); + using (var sr = new StringReader(dateTime)) + { + object test = xmlS.Deserialize(sr); + return (DateTime)test; + } + } + + /// + /// Serializes an object instance to a XML formatted string + /// + /// Object type + /// Object instance + /// XML string + public static string Serialize(T instance) + { + return Serialize(instance, typeof(T)); + } + + /// + /// Serializes an object instance to a XML formatted string + /// + /// Object instance + /// Object type + /// XML string + public static string Serialize(object instance, Type type) + { + if (instance == null) + return null; + + Guard.NotNull(type, nameof(type)); + + using (var writer = new StringWriter()) + { + var xmlSerializer = new XmlSerializer(type); + + xmlSerializer.Serialize(writer, instance); + return writer.ToString(); + } + } + + /// + /// Deserializes a XML formatted string to an object instance + /// + /// Object type + /// XML string + /// Object instance + public static T Deserialize(string xml) + { + return (T)Deserialize(xml, typeof(T)); + } + + /// + /// Deserializes a XML formatted string to an object instance + /// + /// XML string + /// Object type + /// Object instance + public static object Deserialize(string xml, Type type) + { + Guard.NotNull(type, nameof(type)); + + try + { + if (xml.HasValue()) + { + using (var reader = new StringReader(xml)) + { + var serializer = new XmlSerializer(type); + return serializer.Deserialize(reader); + } + } + } + catch { } + + return Activator.CreateInstance(type); + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/app.config b/src/Libraries/SmartStore.Core/app.config index f9acd6487c..67916a0819 100644 --- a/src/Libraries/SmartStore.Core/app.config +++ b/src/Libraries/SmartStore.Core/app.config @@ -8,16 +8,24 @@ - - - - - + + + + + + + + + + + + + - + diff --git a/src/Libraries/SmartStore.Core/packages.config b/src/Libraries/SmartStore.Core/packages.config index 19d5cd9737..efb4850fdf 100644 --- a/src/Libraries/SmartStore.Core/packages.config +++ b/src/Libraries/SmartStore.Core/packages.config @@ -1,19 +1,21 @@  - - - + + + + - + - - + + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Caching/CacheTransactionInterceptor.cs b/src/Libraries/SmartStore.Data/Caching/CacheTransactionInterceptor.cs index a78b4ae4a6..6cea03e354 100644 --- a/src/Libraries/SmartStore.Data/Caching/CacheTransactionInterceptor.cs +++ b/src/Libraries/SmartStore.Data/Caching/CacheTransactionInterceptor.cs @@ -4,119 +4,118 @@ using System.Data; using System.Data.Common; using System.Data.Entity.Infrastructure.Interception; -using System.Linq; namespace SmartStore.Data.Caching { - public class CacheTransactionInterceptor : IDbTransactionInterceptor - { - private readonly ConcurrentDictionary> _affectedSetsInTransaction = new ConcurrentDictionary>(); - private readonly IDbCache _cache; - - public CacheTransactionInterceptor(IDbCache cache) - { - Guard.NotNull(cache, nameof(cache)); - - _cache = cache; - } - - public virtual bool GetItem(DbTransaction transaction, string key, out object value) - { - if (transaction == null) - { - return _cache.TryGet(key, out value); - } - - value = null; - - return false; - } - - public virtual void PutItem(DbTransaction transaction, string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration) - { - if (transaction == null) - { - _cache.Put(key, value, dependentEntitySets, duration); - } - } - - public virtual void InvalidateSets(DbTransaction transaction, IEnumerable entitySets) - { - if (transaction == null) - { - _cache.InvalidateSets(entitySets); - _cache.RequestInvalidateSets(entitySets); - } - else - { - AddAffectedEntitySets(transaction, entitySets); - } - } - - protected void AddAffectedEntitySets(DbTransaction transaction, IEnumerable affectedEntitySets) - { - Guard.NotNull(transaction, nameof(transaction)); - Guard.NotNull(affectedEntitySets, nameof(affectedEntitySets)); - - var entitySets = _affectedSetsInTransaction.GetOrAdd(transaction, new List()); - entitySets.AddRange(affectedEntitySets); - } - - private IEnumerable RemoveAffectedEntitySets(DbTransaction transaction) - { - List affectedEntitySets; - - _affectedSetsInTransaction.TryRemove(transaction, out affectedEntitySets); - - return affectedEntitySets; - } - - public void Committed(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - var entitySets = RemoveAffectedEntitySets(transaction); - - if (entitySets != null) - { - _cache.InvalidateSets(entitySets); - _cache.RequestInvalidateSets(entitySets); - } - } - - public void Committing(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void ConnectionGetting(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void ConnectionGot(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void Disposed(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void Disposing(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void IsolationLevelGetting(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void IsolationLevelGot(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - - public void RolledBack(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - RemoveAffectedEntitySets(transaction); - } - - public void RollingBack(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) - { - } - } + public class CacheTransactionInterceptor : IDbTransactionInterceptor + { + private readonly ConcurrentDictionary> _affectedSetsInTransaction = new ConcurrentDictionary>(); + private readonly IDbCache _cache; + + public CacheTransactionInterceptor(IDbCache cache) + { + Guard.NotNull(cache, nameof(cache)); + + _cache = cache; + } + + public virtual bool GetItem(DbTransaction transaction, string key, out object value) + { + if (transaction == null) + { + return _cache.TryGet(key, out value); + } + + value = null; + + return false; + } + + public virtual void PutItem(DbTransaction transaction, string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration) + { + if (transaction == null) + { + _cache.Put(key, value, dependentEntitySets, duration); + } + } + + public virtual void InvalidateSets(DbTransaction transaction, IEnumerable entitySets) + { + if (transaction == null) + { + _cache.InvalidateSets(entitySets); + _cache.RequestInvalidateSets(entitySets); + } + else + { + AddAffectedEntitySets(transaction, entitySets); + } + } + + protected void AddAffectedEntitySets(DbTransaction transaction, IEnumerable affectedEntitySets) + { + Guard.NotNull(transaction, nameof(transaction)); + Guard.NotNull(affectedEntitySets, nameof(affectedEntitySets)); + + var entitySets = _affectedSetsInTransaction.GetOrAdd(transaction, new List()); + entitySets.AddRange(affectedEntitySets); + } + + private IEnumerable RemoveAffectedEntitySets(DbTransaction transaction) + { + List affectedEntitySets; + + _affectedSetsInTransaction.TryRemove(transaction, out affectedEntitySets); + + return affectedEntitySets; + } + + public void Committed(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + var entitySets = RemoveAffectedEntitySets(transaction); + + if (entitySets != null) + { + _cache.InvalidateSets(entitySets); + _cache.RequestInvalidateSets(entitySets); + } + } + + public void Committing(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void ConnectionGetting(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void ConnectionGot(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void Disposed(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void Disposing(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void IsolationLevelGetting(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void IsolationLevelGot(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + + public void RolledBack(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + RemoveAffectedEntitySets(transaction); + } + + public void RollingBack(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext) + { + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/CachingCommand.cs b/src/Libraries/SmartStore.Data/Caching/CachingCommand.cs index 93dda75187..bd205c5956 100644 --- a/src/Libraries/SmartStore.Data/Caching/CachingCommand.cs +++ b/src/Libraries/SmartStore.Data/Caching/CachingCommand.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.Data; using System.Data.Common; -using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; @@ -11,396 +10,327 @@ namespace SmartStore.Data.Caching { - [DesignerCategory("")] - internal sealed class CachingCommand : DbCommand - { - private readonly DbCommand _command; - private readonly CommandTreeFacts _commandTreeFacts; - private readonly CacheTransactionInterceptor _cacheTransactionInterceptor; - private readonly DbCachingPolicy _policy; - - public CachingCommand( - DbCommand command, - CommandTreeFacts commandTreeFacts, - CacheTransactionInterceptor cacheTransactionInterceptor, - DbCachingPolicy policy) - { - Guard.NotNull(command, nameof(command)); - Guard.NotNull(commandTreeFacts, nameof(commandTreeFacts)); - Guard.NotNull(cacheTransactionInterceptor, nameof(cacheTransactionInterceptor)); - - _command = command; - _commandTreeFacts = commandTreeFacts; - _cacheTransactionInterceptor = cacheTransactionInterceptor; - _policy = policy; - } - - internal CommandTreeFacts CommandTreeFacts - { - get { return _commandTreeFacts; } - } - - internal CacheTransactionInterceptor CacheTransactionInterceptor - { - get { return _cacheTransactionInterceptor; } - } - - internal DbCachingPolicy CachingPolicy - { - get { return _policy; } - } - - internal DbCommand WrappedCommand - { - get { return _command; } - } - - private bool IsCacheable - { - get - { - return _commandTreeFacts.IsQuery && - (IsQueryCached || !_commandTreeFacts.UsesNonDeterministicFunctions - && _policy.CanBeCached(_commandTreeFacts.AffectedEntitySets, CommandText, - Parameters.Cast() - .Select(p => new KeyValuePair(p.ParameterName, p.Value)))); - } - } - - private bool IsQueryCached - { - get - { - return SingletonQueries.Current.IsQueryCached(_commandTreeFacts.MetadataWorkspace, CommandText); - } - } - - public override void Cancel() - { - _command.Cancel(); - } - - public override string CommandText - { - get - { - return _command.CommandText; - } - set - { - _command.CommandText = value; - } - } - - public override int CommandTimeout - { - get - { - return _command.CommandTimeout; - } - set - { - _command.CommandTimeout = value; - } - } - - public override CommandType CommandType - { - get - { - return _command.CommandType; - } - set - { - _command.CommandType = value; - } - } - - protected override DbParameter CreateDbParameter() - { - return _command.CreateParameter(); - } - - protected override DbConnection DbConnection - { - get - { - return _command.Connection; - } - set - { - _command.Connection = value; - } - } - - protected override DbParameterCollection DbParameterCollection - { - get { return _command.Parameters; } - } - - protected override DbTransaction DbTransaction - { - get - { - return _command.Transaction; - } - set - { - _command.Transaction = value; - } - } - - public override bool DesignTimeVisible - { - get - { - return _command.DesignTimeVisible; - } - set - { - _command.DesignTimeVisible = value; - } - } - - protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) - { - if (!IsCacheable) - { - var result = _command.ExecuteReader(behavior); - - if (!_commandTreeFacts.IsQuery) - { - _cacheTransactionInterceptor.InvalidateSets(Transaction, _commandTreeFacts.AffectedEntitySets.Select(s => s.Name)); - } - - return result; - } - - var key = CreateKey(); - - object value; - if (_cacheTransactionInterceptor.GetItem(Transaction, key, out value)) - { - return new CachingDataReader((CachedRecords)value); - } - - using (var reader = _command.ExecuteReader(behavior)) - { - var queryResults = new List(); - - while (reader.Read()) - { - var values = new object[reader.FieldCount]; - reader.GetValues(values); - queryResults.Add(values); - } - - return HandleCaching(reader, key, queryResults); - } - } - - protected async override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - { - if (!IsCacheable) - { - var result = await _command.ExecuteReaderAsync(behavior, cancellationToken); - - if (!_commandTreeFacts.IsQuery) - { - _cacheTransactionInterceptor.InvalidateSets(Transaction, _commandTreeFacts.AffectedEntitySets.Select(s => s.Name)); - } - - return result; - } - - var key = CreateKey(); - - object value; - if (_cacheTransactionInterceptor.GetItem(Transaction, key, out value)) - { - return new CachingDataReader((CachedRecords)value); - } - - using (var reader = await _command.ExecuteReaderAsync(behavior, cancellationToken)) - { - var queryResults = new List(); - - while (await reader.ReadAsync(cancellationToken)) - { - var values = new object[reader.FieldCount]; - reader.GetValues(values); - queryResults.Add(values); - } - - return HandleCaching(reader, key, queryResults); - } - } - - private DbDataReader HandleCaching(DbDataReader reader, string key, List queryResults) - { - var cachedResult = new CachedRecords - { - TableMetadata = GetTableMetadata(reader), - Records = queryResults, - RecordsAffected = reader.RecordsAffected - }; - - int minCacheableRows, maxCachableRows; - _policy.GetCacheableRows(_commandTreeFacts.AffectedEntitySets, out minCacheableRows, out maxCachableRows); - - if (IsQueryCached || (queryResults.Count >= minCacheableRows && queryResults.Count <= maxCachableRows)) - { - TimeSpan? duration = _policy.GetExpirationTimeout(_commandTreeFacts.AffectedEntitySets); - - _cacheTransactionInterceptor.PutItem( - Transaction, - key, - cachedResult, - _commandTreeFacts.AffectedEntitySets.Select(s => s.Name), - duration); - } - - return new CachingDataReader(cachedResult); - } - - protected override void Dispose(bool disposing) - { - _command.GetType() - .GetMethod("Dispose", BindingFlags.Instance | BindingFlags.NonPublic) - .Invoke(_command, new object[] { disposing }); - } - - private static ColumnMetadata[] GetTableMetadata(DbDataReader reader) - { - var columnMetadata = new ColumnMetadata[reader.FieldCount]; - - for (var i = 0; i < reader.FieldCount; i++) - { - columnMetadata[i] = new ColumnMetadata - { - Name = reader.GetName(i), - DataTypeName = reader.GetDataTypeName(i), - DataType = reader.GetFieldType(i) - }; - } - - return columnMetadata; - } - - public override int ExecuteNonQuery() - { - var recordsAffected = _command.ExecuteNonQuery(); - - InvalidateSetsForNonQuery(recordsAffected); - - return recordsAffected; - } - - public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) - { - var recordsAffected = await _command.ExecuteNonQueryAsync(cancellationToken); - - InvalidateSetsForNonQuery(recordsAffected); - - return recordsAffected; - } - - private void InvalidateSetsForNonQuery(int recordsAffected) - { - if (recordsAffected > 0 && _commandTreeFacts.AffectedEntitySets.Any()) - { - _cacheTransactionInterceptor.InvalidateSets(Transaction, _commandTreeFacts.AffectedEntitySets.Select(s => s.Name)); - } - } - - public override object ExecuteScalar() - { - if (!IsCacheable) - { - return _command.ExecuteScalar(); - } - - var key = CreateKey(); - - object value; - - if (_cacheTransactionInterceptor.GetItem(Transaction, key, out value)) - { - return value; - } - - value = _command.ExecuteScalar(); - - // TODO: somehow determine expiration - TimeSpan? duration = null; - - _cacheTransactionInterceptor.PutItem( - Transaction, - key, - value, - _commandTreeFacts.AffectedEntitySets.Select(s => s.Name), - duration); - - return value; - } - - public async override Task ExecuteScalarAsync(CancellationToken cancellationToken) - { - if (!IsCacheable) - { - return await _command.ExecuteScalarAsync(cancellationToken); - } - - var key = CreateKey(); - - if (_cacheTransactionInterceptor.GetItem(Transaction, key, out var value)) - { - return value; - } - - value = await _command.ExecuteScalarAsync(cancellationToken); - - // TODO: somehow determine expiration - TimeSpan? duration = null; - - _cacheTransactionInterceptor.PutItem( - Transaction, - key, - value, - _commandTreeFacts.AffectedEntitySets.Select(s => s.Name), - duration); - - return value; - } - - public override void Prepare() - { - _command.Prepare(); - } - - public override UpdateRowSource UpdatedRowSource - { - get - { - return _command.UpdatedRowSource; - } - set - { - _command.UpdatedRowSource = value; - } - } - - private string CreateKey() - { - return - string.Format( - "{0}_{1}_{2}", - Connection.Database, - CommandText, - string.Join( - "_", - Parameters.Cast() - .Select(p => string.Format("{0}={1}", p.ParameterName, p.Value)))); - } - } + [DesignerCategory("")] + internal sealed class CachingCommand : DbCommand + { + private readonly DbCommand _command; + private readonly CommandTreeFacts _commandTreeFacts; + private readonly CacheTransactionInterceptor _cacheTransactionInterceptor; + private readonly DbCachingPolicy _policy; + + public CachingCommand( + DbCommand command, + CommandTreeFacts commandTreeFacts, + CacheTransactionInterceptor cacheTransactionInterceptor, + DbCachingPolicy policy) + { + Guard.NotNull(command, nameof(command)); + Guard.NotNull(commandTreeFacts, nameof(commandTreeFacts)); + Guard.NotNull(cacheTransactionInterceptor, nameof(cacheTransactionInterceptor)); + + _command = command; + _commandTreeFacts = commandTreeFacts; + _cacheTransactionInterceptor = cacheTransactionInterceptor; + _policy = policy; + } + + internal CommandTreeFacts CommandTreeFacts => _commandTreeFacts; + + internal CacheTransactionInterceptor CacheTransactionInterceptor => _cacheTransactionInterceptor; + + internal DbCachingPolicy CachingPolicy => _policy; + + internal DbCommand WrappedCommand => _command; + + private bool IsCacheable => _commandTreeFacts.IsQuery && + (IsQueryCached || !_commandTreeFacts.UsesNonDeterministicFunctions + && _policy.CanBeCached(_commandTreeFacts.AffectedEntitySets, CommandText, + Parameters.Cast() + .Select(p => new KeyValuePair(p.ParameterName, p.Value)))); + + private bool IsQueryCached => SingletonQueries.Current.IsQueryCached(_commandTreeFacts.MetadataWorkspace, CommandText); + + public override void Cancel() + { + _command.Cancel(); + } + + public override string CommandText + { + get => _command.CommandText; + set => _command.CommandText = value; + } + + public override int CommandTimeout + { + get => _command.CommandTimeout; + set => _command.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => _command.CommandType; + set => _command.CommandType = value; + } + + protected override DbParameter CreateDbParameter() + { + return _command.CreateParameter(); + } + + protected override DbConnection DbConnection + { + get => _command.Connection; + set => _command.Connection = value; + } + + protected override DbParameterCollection DbParameterCollection => _command.Parameters; + + protected override DbTransaction DbTransaction + { + get => _command.Transaction; + set => _command.Transaction = value; + } + + public override bool DesignTimeVisible + { + get => _command.DesignTimeVisible; + set => _command.DesignTimeVisible = value; + } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + if (!IsCacheable) + { + var result = _command.ExecuteReader(behavior); + + if (!_commandTreeFacts.IsQuery) + { + _cacheTransactionInterceptor.InvalidateSets(Transaction, _commandTreeFacts.AffectedEntitySets.Select(s => s.Name)); + } + + return result; + } + + var key = CreateKey(); + + object value; + if (_cacheTransactionInterceptor.GetItem(Transaction, key, out value)) + { + return new CachingDataReader((CachedRecords)value); + } + + using (var reader = _command.ExecuteReader(behavior)) + { + var queryResults = new List(); + + while (reader.Read()) + { + var values = new object[reader.FieldCount]; + reader.GetValues(values); + queryResults.Add(values); + } + + return HandleCaching(reader, key, queryResults); + } + } + + protected async override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) + { + if (!IsCacheable) + { + var result = await _command.ExecuteReaderAsync(behavior, cancellationToken); + + if (!_commandTreeFacts.IsQuery) + { + _cacheTransactionInterceptor.InvalidateSets(Transaction, _commandTreeFacts.AffectedEntitySets.Select(s => s.Name)); + } + + return result; + } + + var key = CreateKey(); + + object value; + if (_cacheTransactionInterceptor.GetItem(Transaction, key, out value)) + { + return new CachingDataReader((CachedRecords)value); + } + + using (var reader = await _command.ExecuteReaderAsync(behavior, cancellationToken)) + { + var queryResults = new List(); + + while (await reader.ReadAsync(cancellationToken)) + { + var values = new object[reader.FieldCount]; + reader.GetValues(values); + queryResults.Add(values); + } + + return HandleCaching(reader, key, queryResults); + } + } + + private DbDataReader HandleCaching(DbDataReader reader, string key, List queryResults) + { + var cachedResult = new CachedRecords + { + TableMetadata = GetTableMetadata(reader), + Records = queryResults, + RecordsAffected = reader.RecordsAffected + }; + + int minCacheableRows, maxCachableRows; + _policy.GetCacheableRows(_commandTreeFacts.AffectedEntitySets, out minCacheableRows, out maxCachableRows); + + if (IsQueryCached || (queryResults.Count >= minCacheableRows && queryResults.Count <= maxCachableRows)) + { + TimeSpan? duration = _policy.GetExpirationTimeout(_commandTreeFacts.AffectedEntitySets); + + _cacheTransactionInterceptor.PutItem( + Transaction, + key, + cachedResult, + _commandTreeFacts.AffectedEntitySets.Select(s => s.Name), + duration); + } + + return new CachingDataReader(cachedResult); + } + + protected override void Dispose(bool disposing) + { + _command.GetType() + .GetMethod("Dispose", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(_command, new object[] { disposing }); + } + + private static ColumnMetadata[] GetTableMetadata(DbDataReader reader) + { + var columnMetadata = new ColumnMetadata[reader.FieldCount]; + + for (var i = 0; i < reader.FieldCount; i++) + { + columnMetadata[i] = new ColumnMetadata + { + Name = reader.GetName(i), + DataTypeName = reader.GetDataTypeName(i), + DataType = reader.GetFieldType(i) + }; + } + + return columnMetadata; + } + + public override int ExecuteNonQuery() + { + var recordsAffected = _command.ExecuteNonQuery(); + + InvalidateSetsForNonQuery(recordsAffected); + + return recordsAffected; + } + + public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) + { + var recordsAffected = await _command.ExecuteNonQueryAsync(cancellationToken); + + InvalidateSetsForNonQuery(recordsAffected); + + return recordsAffected; + } + + private void InvalidateSetsForNonQuery(int recordsAffected) + { + if (recordsAffected > 0 && _commandTreeFacts.AffectedEntitySets.Any()) + { + _cacheTransactionInterceptor.InvalidateSets(Transaction, _commandTreeFacts.AffectedEntitySets.Select(s => s.Name)); + } + } + + public override object ExecuteScalar() + { + if (!IsCacheable) + { + return _command.ExecuteScalar(); + } + + var key = CreateKey(); + + object value; + + if (_cacheTransactionInterceptor.GetItem(Transaction, key, out value)) + { + return value; + } + + value = _command.ExecuteScalar(); + + // TODO: somehow determine expiration + TimeSpan? duration = null; + + _cacheTransactionInterceptor.PutItem( + Transaction, + key, + value, + _commandTreeFacts.AffectedEntitySets.Select(s => s.Name), + duration); + + return value; + } + + public async override Task ExecuteScalarAsync(CancellationToken cancellationToken) + { + if (!IsCacheable) + { + return await _command.ExecuteScalarAsync(cancellationToken); + } + + var key = CreateKey(); + + if (_cacheTransactionInterceptor.GetItem(Transaction, key, out var value)) + { + return value; + } + + value = await _command.ExecuteScalarAsync(cancellationToken); + + // TODO: somehow determine expiration + TimeSpan? duration = null; + + _cacheTransactionInterceptor.PutItem( + Transaction, + key, + value, + _commandTreeFacts.AffectedEntitySets.Select(s => s.Name), + duration); + + return value; + } + + public override void Prepare() + { + _command.Prepare(); + } + + public override UpdateRowSource UpdatedRowSource + { + get => _command.UpdatedRowSource; + set => _command.UpdatedRowSource = value; + } + + private string CreateKey() + { + return + string.Format( + "{0}_{1}_{2}", + Connection.Database, + CommandText, + string.Join( + "_", + Parameters.Cast() + .Select(p => string.Format("{0}={1}", p.ParameterName, p.Value)))); + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/CachingCommandDefinition.cs b/src/Libraries/SmartStore.Data/Caching/CachingCommandDefinition.cs index 872f49658e..0f9e2754c5 100644 --- a/src/Libraries/SmartStore.Data/Caching/CachingCommandDefinition.cs +++ b/src/Libraries/SmartStore.Data/Caching/CachingCommandDefinition.cs @@ -5,43 +5,34 @@ namespace SmartStore.Data.Caching { - internal class CachingCommandDefinition : DbCommandDefinition - { - private readonly DbCommandDefinition _commandDefintion; - private readonly CommandTreeFacts _commandTreeFacts; - private readonly CacheTransactionInterceptor _cacheTransactionInterceptor; - private readonly DbCachingPolicy _policy; + internal class CachingCommandDefinition : DbCommandDefinition + { + private readonly DbCommandDefinition _commandDefintion; + private readonly CommandTreeFacts _commandTreeFacts; + private readonly CacheTransactionInterceptor _cacheTransactionInterceptor; + private readonly DbCachingPolicy _policy; - public CachingCommandDefinition( - DbCommandDefinition commandDefinition, - CommandTreeFacts commandTreeFacts, - CacheTransactionInterceptor cacheTransactionInterceptor, - DbCachingPolicy policy) - { - _commandDefintion = commandDefinition; - _commandTreeFacts = commandTreeFacts; - _cacheTransactionInterceptor = cacheTransactionInterceptor; - _policy = policy; - } + public CachingCommandDefinition( + DbCommandDefinition commandDefinition, + CommandTreeFacts commandTreeFacts, + CacheTransactionInterceptor cacheTransactionInterceptor, + DbCachingPolicy policy) + { + _commandDefintion = commandDefinition; + _commandTreeFacts = commandTreeFacts; + _cacheTransactionInterceptor = cacheTransactionInterceptor; + _policy = policy; + } - public bool IsQuery - { - get { return _commandTreeFacts.IsQuery; } - } + public bool IsQuery => _commandTreeFacts.IsQuery; - public bool IsCacheable - { - get { return _commandTreeFacts.IsQuery && !_commandTreeFacts.UsesNonDeterministicFunctions; } - } + public bool IsCacheable => _commandTreeFacts.IsQuery && !_commandTreeFacts.UsesNonDeterministicFunctions; - public ReadOnlyCollection AffectedEntitySets - { - get { return _commandTreeFacts.AffectedEntitySets; } - } + public ReadOnlyCollection AffectedEntitySets => _commandTreeFacts.AffectedEntitySets; - public override DbCommand CreateCommand() - { - return new CachingCommand(_commandDefintion.CreateCommand(), _commandTreeFacts, _cacheTransactionInterceptor, _policy); - } - } + public override DbCommand CreateCommand() + { + return new CachingCommand(_commandDefintion.CreateCommand(), _commandTreeFacts, _cacheTransactionInterceptor, _policy); + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/CachingDataReader.cs b/src/Libraries/SmartStore.Data/Caching/CachingDataReader.cs index 9f1f1fc3b0..b2467cdbbb 100644 --- a/src/Libraries/SmartStore.Data/Caching/CachingDataReader.cs +++ b/src/Libraries/SmartStore.Data/Caching/CachingDataReader.cs @@ -3,266 +3,247 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Diagnostics; namespace SmartStore.Data.Caching { - internal class CachingDataReader : DbDataReader - { - private enum State - { - BOF, - Reading, - EOF, - Closed, - Disposed - } - - private State _state; - private readonly ColumnMetadata[] _tableMetadata; - private readonly int _recordsAffected; - - // TODO: multiple resultsets? - private readonly IEnumerator _resultRowsEnumerator; - - internal CachingDataReader(CachedRecords cachedResult) - { - Guard.NotNull(cachedResult, nameof(cachedResult)); - - _tableMetadata = cachedResult.TableMetadata; - _recordsAffected = cachedResult.RecordsAffected; - _resultRowsEnumerator = cachedResult.Records.GetEnumerator(); - _state = State.BOF; - } - - public override void Close() - { - EnsureNotDisposed(); - - _state = State.Closed; - } - - public override int Depth - { - get { throw new NotImplementedException(); } - } - - public override int FieldCount - { - get { return _tableMetadata.Length; } - } - - public override bool GetBoolean(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override byte GetByte(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) - { - throw new NotImplementedException(); - } - - public override char GetChar(int ordinal) - { - throw new NotImplementedException(); - } - - public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) - { - throw new NotImplementedException(); - } - - public override string GetDataTypeName(int ordinal) - { - return _tableMetadata[ordinal].DataTypeName; - } - - public override DateTime GetDateTime(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override decimal GetDecimal(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override double GetDouble(int ordinal) - { - return (double)GetValue(ordinal); - } - - public override IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - public override Type GetFieldType(int ordinal) - { - return _tableMetadata[ordinal].DataType; - } - - public override float GetFloat(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override Guid GetGuid(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override short GetInt16(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override int GetInt32(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override long GetInt64(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override string GetName(int ordinal) - { - return _tableMetadata[ordinal].Name; - } - - public override int GetOrdinal(string name) - { - throw new NotImplementedException(); - } - - public override DataTable GetSchemaTable() - { - throw new NotSupportedException(); - } - - public override string GetString(int ordinal) - { - return GetValue(ordinal).Convert(); - } - - public override object GetValue(int ordinal) - { - EnsureReading(); - - return _resultRowsEnumerator.Current[ordinal]; - } - - public override int GetValues(object[] values) - { - throw new NotImplementedException(); - } - - public override bool HasRows - { - get { throw new NotImplementedException(); } - } - - public override bool IsClosed - { - get - { - EnsureNotDisposed(); - - return _state == State.Closed; - } - } - - public override bool IsDBNull(int ordinal) - { - return GetValue(ordinal) == DBNull.Value || GetValue(ordinal) == null; - } - - public override bool NextResult() - { - // TODO: Multiple resultsets - return false; - } - - public override bool Read() - { - EnsureNotClosed(); - - var result = _resultRowsEnumerator.MoveNext(); - - _state = result ? State.Reading : State.EOF; - - return result; - } - - public override int RecordsAffected - { - get { return _recordsAffected; } - } - - public override object this[string name] - { - get { throw new NotImplementedException(); } - } - - public override object this[int ordinal] - { - get { throw new NotImplementedException(); } - } - - private void EnsureReading() - { - EnsureNotClosed(); - EnsureNotBOF(); - EnsureNotEOF(); - } - - private void EnsureNotClosed() - { - if (IsClosed) - { - throw new InvalidOperationException("Reader has already been closed."); - } - } - - private void EnsureNotDisposed() - { - if (_state == State.Disposed) - { - throw new InvalidOperationException("Object has already been disposed."); - } - } - - private void EnsureNotBOF() - { - if (_state == State.BOF) - { - throw new InvalidOperationException("The operation is invalid before reading any data."); - } - } - - private void EnsureNotEOF() - { - if (_state == State.EOF) - { - throw new InvalidOperationException("The operation is invalid after reading all data."); - } - } - - protected override void Dispose(bool disposing) - { - // base.Dispose() will call Close() - base.Dispose(disposing); - - _resultRowsEnumerator.Dispose(); - - _state = State.Disposed; - } - } + internal class CachingDataReader : DbDataReader + { + private enum State + { + BOF, + Reading, + EOF, + Closed, + Disposed + } + + private State _state; + private readonly ColumnMetadata[] _tableMetadata; + private readonly int _recordsAffected; + + // TODO: multiple resultsets? + private readonly IEnumerator _resultRowsEnumerator; + + internal CachingDataReader(CachedRecords cachedResult) + { + Guard.NotNull(cachedResult, nameof(cachedResult)); + + _tableMetadata = cachedResult.TableMetadata; + _recordsAffected = cachedResult.RecordsAffected; + _resultRowsEnumerator = cachedResult.Records.GetEnumerator(); + _state = State.BOF; + } + + public override void Close() + { + EnsureNotDisposed(); + + _state = State.Closed; + } + + public override int Depth => throw new NotImplementedException(); + + public override int FieldCount => _tableMetadata.Length; + + public override bool GetBoolean(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override byte GetByte(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) + { + throw new NotImplementedException(); + } + + public override char GetChar(int ordinal) + { + throw new NotImplementedException(); + } + + public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) + { + throw new NotImplementedException(); + } + + public override string GetDataTypeName(int ordinal) + { + return _tableMetadata[ordinal].DataTypeName; + } + + public override DateTime GetDateTime(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override decimal GetDecimal(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override double GetDouble(int ordinal) + { + return (double)GetValue(ordinal); + } + + public override IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + public override Type GetFieldType(int ordinal) + { + return _tableMetadata[ordinal].DataType; + } + + public override float GetFloat(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override Guid GetGuid(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override short GetInt16(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override int GetInt32(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override long GetInt64(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override string GetName(int ordinal) + { + return _tableMetadata[ordinal].Name; + } + + public override int GetOrdinal(string name) + { + throw new NotImplementedException(); + } + + public override DataTable GetSchemaTable() + { + throw new NotSupportedException(); + } + + public override string GetString(int ordinal) + { + return GetValue(ordinal).Convert(); + } + + public override object GetValue(int ordinal) + { + EnsureReading(); + + return _resultRowsEnumerator.Current[ordinal]; + } + + public override int GetValues(object[] values) + { + throw new NotImplementedException(); + } + + public override bool HasRows => throw new NotImplementedException(); + + public override bool IsClosed + { + get + { + EnsureNotDisposed(); + + return _state == State.Closed; + } + } + + public override bool IsDBNull(int ordinal) + { + return GetValue(ordinal) == DBNull.Value || GetValue(ordinal) == null; + } + + public override bool NextResult() + { + // TODO: Multiple resultsets + return false; + } + + public override bool Read() + { + EnsureNotClosed(); + + var result = _resultRowsEnumerator.MoveNext(); + + _state = result ? State.Reading : State.EOF; + + return result; + } + + public override int RecordsAffected => _recordsAffected; + + public override object this[string name] => throw new NotImplementedException(); + + public override object this[int ordinal] => throw new NotImplementedException(); + + private void EnsureReading() + { + EnsureNotClosed(); + EnsureNotBOF(); + EnsureNotEOF(); + } + + private void EnsureNotClosed() + { + if (IsClosed) + { + throw new InvalidOperationException("Reader has already been closed."); + } + } + + private void EnsureNotDisposed() + { + if (_state == State.Disposed) + { + throw new InvalidOperationException("Object has already been disposed."); + } + } + + private void EnsureNotBOF() + { + if (_state == State.BOF) + { + throw new InvalidOperationException("The operation is invalid before reading any data."); + } + } + + private void EnsureNotEOF() + { + if (_state == State.EOF) + { + throw new InvalidOperationException("The operation is invalid after reading all data."); + } + } + + protected override void Dispose(bool disposing) + { + // base.Dispose() will call Close() + base.Dispose(disposing); + + _resultRowsEnumerator.Dispose(); + + _state = State.Disposed; + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/CachingProviderServices.cs b/src/Libraries/SmartStore.Data/Caching/CachingProviderServices.cs index 92bc8be666..06085ea2c7 100644 --- a/src/Libraries/SmartStore.Data/Caching/CachingProviderServices.cs +++ b/src/Libraries/SmartStore.Data/Caching/CachingProviderServices.cs @@ -5,130 +5,130 @@ using System.Data.Entity.Core.Common.CommandTrees; using System.Data.Entity.Core.Metadata.Edm; using System.Data.Entity.Spatial; -using SmartStore.Utilities; using SmartStore.Data.Setup; +using SmartStore.Utilities; namespace SmartStore.Data.Caching { - public class CachingProviderServices : DbProviderServices - { - private readonly DbProviderServices _providerServices; - private readonly CacheTransactionInterceptor _cacheTransactionInterceptor; - private readonly DbCachingPolicy _policy; - - public CachingProviderServices(DbProviderServices providerServices, CacheTransactionInterceptor cacheTransactionInterceptor, DbCachingPolicy policy = null) - { - _providerServices = providerServices; - _cacheTransactionInterceptor = cacheTransactionInterceptor; - _policy = policy ?? new DbCachingPolicy(); - } - - internal bool HasPendingMigrations - { - get - { - if (CommonHelper.IsDevEnvironment) - { - // Don't bother with database cache when we're in (IDE) debug mode - // and migrations are about to be executed: EF throws exceptions during migration - // on purpose, which are harmless in release mode, but really annoying and confusing - // in debug mode. Namely, execution will break within our 'CacheTransactionInterceptor', - // and we don't want that. - return DbSeedingMigrator.IsMigrating; - } - - return false; - } - } - - protected override DbCommandDefinition CreateDbCommandDefinition(DbProviderManifest providerManifest, DbCommandTree commandTree) - { - var commandDefinition = _providerServices.CreateCommandDefinition(providerManifest, commandTree); - - return HasPendingMigrations - ? commandDefinition - : new CachingCommandDefinition( - commandDefinition, - new CommandTreeFacts(commandTree), - _cacheTransactionInterceptor, - _policy); - } - - protected override DbProviderManifest GetDbProviderManifest(string manifestToken) - { - return _providerServices.GetProviderManifest(manifestToken); - } - - protected override string GetDbProviderManifestToken(DbConnection connection) - { - return _providerServices.GetProviderManifestToken(connection); - } - - protected override void DbCreateDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) - { - _providerServices.CreateDatabase(connection, commandTimeout, storeItemCollection); - } - - protected override string DbCreateDatabaseScript(string providerManifestToken, StoreItemCollection storeItemCollection) - { - return _providerServices.CreateDatabaseScript(providerManifestToken, storeItemCollection); - } - - protected override bool DbDatabaseExists(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) - { - return _providerServices.DatabaseExists(connection, commandTimeout, storeItemCollection); - } - - protected override void DbDeleteDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) - { - _providerServices.DeleteDatabase(connection, commandTimeout, storeItemCollection); - } - - protected override void SetDbParameterValue(DbParameter parameter, TypeUsage parameterType, object value) - { - _providerServices.SetParameterValue(parameter, parameterType, value); - } - - protected override DbSpatialDataReader GetDbSpatialDataReader(DbDataReader fromReader, string manifestToken) - { - return _providerServices.GetSpatialDataReader(fromReader, manifestToken); - } + public class CachingProviderServices : DbProviderServices + { + private readonly DbProviderServices _providerServices; + private readonly CacheTransactionInterceptor _cacheTransactionInterceptor; + private readonly DbCachingPolicy _policy; + + public CachingProviderServices(DbProviderServices providerServices, CacheTransactionInterceptor cacheTransactionInterceptor, DbCachingPolicy policy = null) + { + _providerServices = providerServices; + _cacheTransactionInterceptor = cacheTransactionInterceptor; + _policy = policy ?? new DbCachingPolicy(); + } + + internal bool HasPendingMigrations + { + get + { + if (CommonHelper.IsDevEnvironment) + { + // Don't bother with database cache when we're in (IDE) debug mode + // and migrations are about to be executed: EF throws exceptions during migration + // on purpose, which are harmless in release mode, but really annoying and confusing + // in debug mode. Namely, execution will break within our 'CacheTransactionInterceptor', + // and we don't want that. + return DbSeedingMigrator.IsMigrating; + } + + return false; + } + } + + protected override DbCommandDefinition CreateDbCommandDefinition(DbProviderManifest providerManifest, DbCommandTree commandTree) + { + var commandDefinition = _providerServices.CreateCommandDefinition(providerManifest, commandTree); + + return HasPendingMigrations + ? commandDefinition + : new CachingCommandDefinition( + commandDefinition, + new CommandTreeFacts(commandTree), + _cacheTransactionInterceptor, + _policy); + } + + protected override DbProviderManifest GetDbProviderManifest(string manifestToken) + { + return _providerServices.GetProviderManifest(manifestToken); + } + + protected override string GetDbProviderManifestToken(DbConnection connection) + { + return _providerServices.GetProviderManifestToken(connection); + } + + protected override void DbCreateDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) + { + _providerServices.CreateDatabase(connection, commandTimeout, storeItemCollection); + } + + protected override string DbCreateDatabaseScript(string providerManifestToken, StoreItemCollection storeItemCollection) + { + return _providerServices.CreateDatabaseScript(providerManifestToken, storeItemCollection); + } + + protected override bool DbDatabaseExists(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) + { + return _providerServices.DatabaseExists(connection, commandTimeout, storeItemCollection); + } + + protected override void DbDeleteDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) + { + _providerServices.DeleteDatabase(connection, commandTimeout, storeItemCollection); + } + + protected override void SetDbParameterValue(DbParameter parameter, TypeUsage parameterType, object value) + { + _providerServices.SetParameterValue(parameter, parameterType, value); + } + + protected override DbSpatialDataReader GetDbSpatialDataReader(DbDataReader fromReader, string manifestToken) + { + return _providerServices.GetSpatialDataReader(fromReader, manifestToken); + } #pragma warning disable 618, 672 - protected override DbSpatialServices DbGetSpatialServices(string manifestToken) - { - return _providerServices.GetSpatialServices(manifestToken); - } + protected override DbSpatialServices DbGetSpatialServices(string manifestToken) + { + return _providerServices.GetSpatialServices(manifestToken); + } #pragma warning restore 618, 672 - public override object GetService(Type type, object key) - { - return _providerServices.GetService(type, key); - } - - public override IEnumerable GetServices(Type type, object key) - { - return _providerServices.GetServices(type, key); - } - - public override void RegisterInfoMessageHandler(DbConnection connection, Action handler) - { - _providerServices.RegisterInfoMessageHandler(connection, handler); - } - - public override DbCommandDefinition CreateCommandDefinition(DbCommand prototype) - { - var command = prototype as CachingCommand; - - var commandDefinition = - _providerServices.CreateCommandDefinition( - command != null - ? command.WrappedCommand - : prototype); - - return command != null - ? new CachingCommandDefinition(commandDefinition, command.CommandTreeFacts, command.CacheTransactionInterceptor, command.CachingPolicy) - : commandDefinition; - } - } + public override object GetService(Type type, object key) + { + return _providerServices.GetService(type, key); + } + + public override IEnumerable GetServices(Type type, object key) + { + return _providerServices.GetServices(type, key); + } + + public override void RegisterInfoMessageHandler(DbConnection connection, Action handler) + { + _providerServices.RegisterInfoMessageHandler(connection, handler); + } + + public override DbCommandDefinition CreateCommandDefinition(DbCommand prototype) + { + var command = prototype as CachingCommand; + + var commandDefinition = + _providerServices.CreateCommandDefinition( + command != null + ? command.WrappedCommand + : prototype); + + return command != null + ? new CachingCommandDefinition(commandDefinition, command.CommandTreeFacts, command.CacheTransactionInterceptor, command.CachingPolicy) + : commandDefinition; + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/CommandTreeFacts.cs b/src/Libraries/SmartStore.Data/Caching/CommandTreeFacts.cs index f9410dd8e9..37e4bba8d6 100644 --- a/src/Libraries/SmartStore.Data/Caching/CommandTreeFacts.cs +++ b/src/Libraries/SmartStore.Data/Caching/CommandTreeFacts.cs @@ -9,111 +9,111 @@ namespace SmartStore.Data.Caching { - internal class CommandTreeFacts - { - private static readonly HashSet NonDeterministicFunctions = new HashSet( - new[] { - "Edm.CurrentDateTime", - "Edm.CurrentUtcDateTime", - "Edm.CurrentDateTimeOffsets", - "Edm.NewGuid", - - "SqlServer.NEWID", - "SqlServer.GETDATE", - "SqlServer.GETUTCDATE", - "SqlServer.SYSDATETIME", - "SqlServer.SYSUTCDATETIME", - "SqlServer.SYSDATETIMEOFFSET", - "SqlServer.CURRENT_USER", - "SqlServer.CURRENT_TIMESTAMP", - "SqlServer.HOST_NAME", - "SqlServer.USER_NAME" - }, - StringComparer.OrdinalIgnoreCase); - - public CommandTreeFacts(DbCommandTree commandTree) - { - IsQuery = commandTree is DbQueryCommandTree; - - var visitor = new CommandTreeVisitor(); - - if (commandTree.CommandTreeKind == DbCommandTreeKind.Function) - { - var edmFunction = ((DbFunctionCommandTree)commandTree).EdmFunction; - - var containerMapping = - commandTree.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace).GetItems().Single(); - - var entitySetMappings = - containerMapping.EntitySetMappings.Where( - esm => esm.ModificationFunctionMappings.Any( - mfm => mfm.DeleteFunctionMapping.Function == edmFunction || - mfm.InsertFunctionMapping.Function == edmFunction || - mfm.UpdateFunctionMapping.Function == edmFunction)); - - AffectedEntitySets = - (from esm in entitySetMappings - from etm in esm.EntityTypeMappings - from mappingFragment in etm.Fragments.Where(f => f.StoreEntitySet != null) - select mappingFragment.StoreEntitySet).Cast().ToList().AsReadOnly(); - } - else - { - if (commandTree.CommandTreeKind == DbCommandTreeKind.Query) - { - ((DbQueryCommandTree)commandTree).Query.Accept(visitor); - } - else - { - Debug.Assert(commandTree is DbModificationCommandTree, "Unexpected command tree kind"); - - ((DbModificationCommandTree)commandTree).Target.Expression.Accept(visitor); - } - - AffectedEntitySets = new ReadOnlyCollection(visitor.EntitySets); - UsesNonDeterministicFunctions = - visitor.Functions.Any(f => NonDeterministicFunctions.Contains( - string.Format("{0}.{1}", f.NamespaceName, f.Name))); - } - - MetadataWorkspace = commandTree.MetadataWorkspace; - } - - // testing only - internal CommandTreeFacts(ReadOnlyCollection affectedEntitySets, bool isQuery, bool usesNonDeterministicFunctions, MetadataWorkspace metadataWorkspace = null) - { - AffectedEntitySets = affectedEntitySets; - IsQuery = isQuery; - UsesNonDeterministicFunctions = usesNonDeterministicFunctions; - MetadataWorkspace = metadataWorkspace ?? new MetadataWorkspace(); - } - - public ReadOnlyCollection AffectedEntitySets { get; private set; } - - public bool IsQuery { get; private set; } - - public bool UsesNonDeterministicFunctions { get; private set; } - - public MetadataWorkspace MetadataWorkspace { get; private set; } - - private class CommandTreeVisitor : BasicCommandTreeVisitor - { - public readonly List EntitySets = new List(); - public readonly List Functions = new List(); - - public override void Visit(DbScanExpression expression) - { - EntitySets.Add(expression.Target); - - base.Visit(expression); - } - - public override void Visit(DbFunctionExpression expression) - { - Functions.Add(expression.Function); - - base.Visit(expression); - } - } - } + internal class CommandTreeFacts + { + private static readonly HashSet NonDeterministicFunctions = new HashSet( + new[] { + "Edm.CurrentDateTime", + "Edm.CurrentUtcDateTime", + "Edm.CurrentDateTimeOffsets", + "Edm.NewGuid", + + "SqlServer.NEWID", + "SqlServer.GETDATE", + "SqlServer.GETUTCDATE", + "SqlServer.SYSDATETIME", + "SqlServer.SYSUTCDATETIME", + "SqlServer.SYSDATETIMEOFFSET", + "SqlServer.CURRENT_USER", + "SqlServer.CURRENT_TIMESTAMP", + "SqlServer.HOST_NAME", + "SqlServer.USER_NAME" + }, + StringComparer.OrdinalIgnoreCase); + + public CommandTreeFacts(DbCommandTree commandTree) + { + IsQuery = commandTree is DbQueryCommandTree; + + var visitor = new CommandTreeVisitor(); + + if (commandTree.CommandTreeKind == DbCommandTreeKind.Function) + { + var edmFunction = ((DbFunctionCommandTree)commandTree).EdmFunction; + + var containerMapping = + commandTree.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace).GetItems().Single(); + + var entitySetMappings = + containerMapping.EntitySetMappings.Where( + esm => esm.ModificationFunctionMappings.Any( + mfm => mfm.DeleteFunctionMapping.Function == edmFunction || + mfm.InsertFunctionMapping.Function == edmFunction || + mfm.UpdateFunctionMapping.Function == edmFunction)); + + AffectedEntitySets = + (from esm in entitySetMappings + from etm in esm.EntityTypeMappings + from mappingFragment in etm.Fragments.Where(f => f.StoreEntitySet != null) + select mappingFragment.StoreEntitySet).Cast().ToList().AsReadOnly(); + } + else + { + if (commandTree.CommandTreeKind == DbCommandTreeKind.Query) + { + ((DbQueryCommandTree)commandTree).Query.Accept(visitor); + } + else + { + Debug.Assert(commandTree is DbModificationCommandTree, "Unexpected command tree kind"); + + ((DbModificationCommandTree)commandTree).Target.Expression.Accept(visitor); + } + + AffectedEntitySets = new ReadOnlyCollection(visitor.EntitySets); + UsesNonDeterministicFunctions = + visitor.Functions.Any(f => NonDeterministicFunctions.Contains( + string.Format("{0}.{1}", f.NamespaceName, f.Name))); + } + + MetadataWorkspace = commandTree.MetadataWorkspace; + } + + // testing only + internal CommandTreeFacts(ReadOnlyCollection affectedEntitySets, bool isQuery, bool usesNonDeterministicFunctions, MetadataWorkspace metadataWorkspace = null) + { + AffectedEntitySets = affectedEntitySets; + IsQuery = isQuery; + UsesNonDeterministicFunctions = usesNonDeterministicFunctions; + MetadataWorkspace = metadataWorkspace ?? new MetadataWorkspace(); + } + + public ReadOnlyCollection AffectedEntitySets { get; private set; } + + public bool IsQuery { get; private set; } + + public bool UsesNonDeterministicFunctions { get; private set; } + + public MetadataWorkspace MetadataWorkspace { get; private set; } + + private class CommandTreeVisitor : BasicCommandTreeVisitor + { + public readonly List EntitySets = new List(); + public readonly List Functions = new List(); + + public override void Visit(DbScanExpression expression) + { + EntitySets.Add(expression.Target); + + base.Visit(expression); + } + + public override void Visit(DbFunctionExpression expression) + { + Functions.Add(expression.Function); + + base.Visit(expression); + } + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/DbCacheEntry.cs b/src/Libraries/SmartStore.Data/Caching/DbCacheEntry.cs index 0734a6652d..3be735d59f 100644 --- a/src/Libraries/SmartStore.Data/Caching/DbCacheEntry.cs +++ b/src/Libraries/SmartStore.Data/Caching/DbCacheEntry.cs @@ -3,39 +3,39 @@ namespace SmartStore.Data.Caching { - [Serializable] - public class DbCacheEntry - { - public string Key { get; set; } - public DateTime CachedOnUtc { get; set; } - public TimeSpan? Duration { get; set; } - public string[] EntitySets { get; set; } - public object Value { get; set; } + [Serializable] + public class DbCacheEntry + { + public string Key { get; set; } + public DateTime CachedOnUtc { get; set; } + public TimeSpan? Duration { get; set; } + public string[] EntitySets { get; set; } + public object Value { get; set; } - public bool HasExpired(DateTime utcNow) - { - if (Duration.HasValue) - { - return utcNow > (CachedOnUtc + Duration.Value); - } + public bool HasExpired(DateTime utcNow) + { + if (Duration.HasValue) + { + return utcNow > (CachedOnUtc + Duration.Value); + } - return false; - } - } + return false; + } + } - [Serializable] - public class CachedRecords - { - public List Records { get; set; } - public int RecordsAffected { get; set; } - public ColumnMetadata[] TableMetadata { get; set; } - } + [Serializable] + public class CachedRecords + { + public List Records { get; set; } + public int RecordsAffected { get; set; } + public ColumnMetadata[] TableMetadata { get; set; } + } - [Serializable] - public class ColumnMetadata - { - public string Name { get; set; } - public string DataTypeName { get; set; } - public Type DataType { get; set; } - } + [Serializable] + public class ColumnMetadata + { + public string Name { get; set; } + public string DataTypeName { get; set; } + public Type DataType { get; set; } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs b/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs index 1b137911b2..b25df7670f 100644 --- a/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs +++ b/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs @@ -11,239 +11,239 @@ namespace SmartStore.Data.Caching { - public static class DbCacheExtensions - { - internal static IDbCache GetDbCacheInstance() - { - try - { - return EngineContext.Current.Resolve(); - } - catch - { - return new NullDbCache(); - } - } - - #region Request Cache - - /// - /// Returns the result of from the (HTTP)-REQUEST cache if available, - /// otherwise the query is materialized and cached before being returned. Cache result invalidation - /// is performed automatically by the underlying interceptor. - /// - /// The type of the data in the data source. - /// The query to be materialized. - /// The result of the query. - public static List ToListCached(this IQueryable source) - { - return ToListCached(source, null); - } - - /// - /// Returns the result of from the (HTTP)-REQUEST cache if available, - /// otherwise the query is materialized and cached before being returned. Cache result invalidation - /// is performed automatically by the underlying interceptor. - /// - /// The type of the data in the data source. - /// The query to be materialized. - /// A custom cache key to use instead of the computed one. - /// The result of the query. - /// - /// This overload is slightly faster, because the key does not need to be generated from the passed - /// - public static List ToListCached(this IQueryable source, string customKey) - { - var entry = GetCacheEntry(source, customKey, () => source.ToList()); - if (entry == null) - { - return source.ToList(); - } - - return (List)entry.Value; - } - - /// - /// Returns the first element of from the (HTTP)-REQUEST cache if available, - /// otherwise the query is materialized and cached before being returned. Cache result invalidation - /// is performed automatically by the underlying interceptor. - /// - /// The type of the data in the data source. - /// The query to be materialized. - /// The result of the query. - public static T FirstOrDefaultCached(this IQueryable source) - { - return FirstOrDefaultCached(source, null); - } - - /// - /// Returns the first element of from the (HTTP)-REQUEST cache if available, - /// otherwise the query is materialized and cached before being returned. Cache result invalidation - /// is performed automatically by the underlying interceptor. - /// - /// The type of the data in the data source. - /// The query to be materialized. - /// A custom cache key to use instead of the computed one. - /// The result of the query. - /// - /// This overload is slightly faster, because the key does not need to be generated from the passed - /// - public static T FirstOrDefaultCached(this IQueryable source, string customKey) - { - var entry = GetCacheEntry(source, customKey, () => source.FirstOrDefault()); - if (entry == null) - { - return source.FirstOrDefault(); - } - - return (T)entry.Value; - } - - internal static DbCacheEntry GetCacheEntry(this IQueryable source, string customKey, Func valueFactory) - { - // (Perf) Checking here means higher perf, as keys and entitysets does not need to be evaluated - var cache = GetDbCacheInstance(); - if (!cache.Enabled) - { - return null; - } - - Guard.NotNull(source, nameof(source)); - Guard.NotNull(valueFactory, nameof(valueFactory)); - - var cacheKey = new CacheKey(source, customKey); - if (cacheKey.Key.IsEmpty()) - { - return null; - } - - if (!cache.RequestTryGet(cacheKey.Key, out var entry)) - { - entry = cache.RequestPut(cacheKey.Key, valueFactory(), cacheKey.AffectedEntitySets); - } - //else - //{ - // Debug.WriteLine("FromRequestCache: " + cacheKey.Key); - //} - - return entry; - } - - #endregion - - #region Repository extensions - - /// - /// Gets an entity by id from the database, the local change tracker, or the (HTTP)-REQUEST cache. - /// Cache result invalidation is performed automatically by the underlying interceptor. - /// - /// The id of the entity. This can also be a composite key. - /// The resolved entity - public static T GetByIdCached(this IRepository rs, object id) - where T : BaseEntity - { - return GetByIdCached(rs, id, null); - } - - /// - /// Gets an entity by id from the database, the local change tracker, or the (HTTP)-REQUEST cache. - /// Cache result invalidation is performed automatically by the underlying interceptor. - /// - /// The id of the entity. This can also be a composite key. - /// A custom cache key to use instead of the computed one. - /// The resolved entity - /// - /// This overload is slightly faster, because the key does not need to be generated. - /// - public static T GetByIdCached(this IRepository rs, object id, string customKey) - where T : BaseEntity - { - var entry = rs.Table.GetCacheEntry(customKey, () => rs.GetById(id)); - if (entry == null) - { - return rs.GetById(id); - } - - return (T)entry.Value; - } - - #endregion - - /// - /// Forces query results to be cached. - /// Allows caching results for queries using non-deterministic functions. - /// - /// Query element type. - /// Query whose results will be cached. Must not be null. - public static IQueryable Cached(this IQueryable source) - where T : class - { - Guard.NotNull(source, nameof(source)); - - // TODO: implement Uncached() - - var objectQuery = DbCacheUtil.GetObjectQuery(source) ?? source as ObjectQuery; - - if (objectQuery != null) - { - SingletonQueries.Current.AddCachedQuery( - objectQuery.Context.MetadataWorkspace, - objectQuery.ToTraceString()); - } - - return source; - } - - private class CacheKey - { - public CacheKey(IQueryable source, string customKey = null) - { - Guard.NotNull(source, nameof(source)); - - GenerateKeyAndAffectedEntitySets(source, customKey); - } - - private void GenerateKeyAndAffectedEntitySets(IQueryable source, string customKey = null) - { - var objectQuery = DbCacheUtil.GetObjectQuery(source) ?? source as ObjectQuery; - - var key = customKey.NullEmpty(); - if (objectQuery != null && string.IsNullOrEmpty(key)) - { - var commandInfo = objectQuery.GetCommandInfo(); + public static class DbCacheExtensions + { + internal static IDbCache GetDbCacheInstance() + { + try + { + return EngineContext.Current.Resolve(); + } + catch + { + return new NullDbCache(); + } + } + + #region Request Cache + + /// + /// Returns the result of from the (HTTP)-REQUEST cache if available, + /// otherwise the query is materialized and cached before being returned. Cache result invalidation + /// is performed automatically by the underlying interceptor. + /// + /// The type of the data in the data source. + /// The query to be materialized. + /// The result of the query. + public static List ToListCached(this IQueryable source) + { + return ToListCached(source, null); + } + + /// + /// Returns the result of from the (HTTP)-REQUEST cache if available, + /// otherwise the query is materialized and cached before being returned. Cache result invalidation + /// is performed automatically by the underlying interceptor. + /// + /// The type of the data in the data source. + /// The query to be materialized. + /// A custom cache key to use instead of the computed one. + /// The result of the query. + /// + /// This overload is slightly faster, because the key does not need to be generated from the passed + /// + public static List ToListCached(this IQueryable source, string customKey) + { + var entry = GetCacheEntry(source, customKey, () => source.ToList()); + if (entry == null) + { + return source.ToList(); + } + + return (List)entry.Value; + } + + /// + /// Returns the first element of from the (HTTP)-REQUEST cache if available, + /// otherwise the query is materialized and cached before being returned. Cache result invalidation + /// is performed automatically by the underlying interceptor. + /// + /// The type of the data in the data source. + /// The query to be materialized. + /// The result of the query. + public static T FirstOrDefaultCached(this IQueryable source) + { + return FirstOrDefaultCached(source, null); + } + + /// + /// Returns the first element of from the (HTTP)-REQUEST cache if available, + /// otherwise the query is materialized and cached before being returned. Cache result invalidation + /// is performed automatically by the underlying interceptor. + /// + /// The type of the data in the data source. + /// The query to be materialized. + /// A custom cache key to use instead of the computed one. + /// The result of the query. + /// + /// This overload is slightly faster, because the key does not need to be generated from the passed + /// + public static T FirstOrDefaultCached(this IQueryable source, string customKey) + { + var entry = GetCacheEntry(source, customKey, () => source.FirstOrDefault()); + if (entry == null) + { + return source.FirstOrDefault(); + } + + return (T)entry.Value; + } + + internal static DbCacheEntry GetCacheEntry(this IQueryable source, string customKey, Func valueFactory) + { + // (Perf) Checking here means higher perf, as keys and entitysets does not need to be evaluated + var cache = GetDbCacheInstance(); + if (!cache.Enabled) + { + return null; + } + + Guard.NotNull(source, nameof(source)); + Guard.NotNull(valueFactory, nameof(valueFactory)); + + var cacheKey = new CacheKey(source, customKey); + if (cacheKey.Key.IsEmpty()) + { + return null; + } + + if (!cache.RequestTryGet(cacheKey.Key, out var entry)) + { + entry = cache.RequestPut(cacheKey.Key, valueFactory(), cacheKey.AffectedEntitySets); + } + //else + //{ + // Debug.WriteLine("FromRequestCache: " + cacheKey.Key); + //} + + return entry; + } + + #endregion + + #region Repository extensions + + /// + /// Gets an entity by id from the database, the local change tracker, or the (HTTP)-REQUEST cache. + /// Cache result invalidation is performed automatically by the underlying interceptor. + /// + /// The id of the entity. This can also be a composite key. + /// The resolved entity + public static T GetByIdCached(this IRepository rs, object id) + where T : BaseEntity + { + return GetByIdCached(rs, id, null); + } + + /// + /// Gets an entity by id from the database, the local change tracker, or the (HTTP)-REQUEST cache. + /// Cache result invalidation is performed automatically by the underlying interceptor. + /// + /// The id of the entity. This can also be a composite key. + /// A custom cache key to use instead of the computed one. + /// The resolved entity + /// + /// This overload is slightly faster, because the key does not need to be generated. + /// + public static T GetByIdCached(this IRepository rs, object id, string customKey) + where T : BaseEntity + { + var entry = rs.Table.GetCacheEntry(customKey, () => rs.GetById(id)); + if (entry == null) + { + return rs.GetById(id); + } + + return (T)entry.Value; + } + + #endregion + + /// + /// Forces query results to be cached. + /// Allows caching results for queries using non-deterministic functions. + /// + /// Query element type. + /// Query whose results will be cached. Must not be null. + public static IQueryable Cached(this IQueryable source) + where T : class + { + Guard.NotNull(source, nameof(source)); + + // TODO: implement Uncached() + + var objectQuery = DbCacheUtil.GetObjectQuery(source) ?? source as ObjectQuery; + + if (objectQuery != null) + { + SingletonQueries.Current.AddCachedQuery( + objectQuery.Context.MetadataWorkspace, + objectQuery.ToTraceString()); + } + + return source; + } + + private class CacheKey + { + public CacheKey(IQueryable source, string customKey = null) + { + Guard.NotNull(source, nameof(source)); + + GenerateKeyAndAffectedEntitySets(source, customKey); + } + + private void GenerateKeyAndAffectedEntitySets(IQueryable source, string customKey = null) + { + var objectQuery = DbCacheUtil.GetObjectQuery(source) ?? source as ObjectQuery; + + var key = customKey.NullEmpty(); + if (objectQuery != null && string.IsNullOrEmpty(key)) + { + var commandInfo = objectQuery.GetCommandInfo(); var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; sb.AppendLine(commandInfo.Sql); - foreach (DbParameter parameter in commandInfo.Parameters) - { - sb.Append(parameter.ParameterName); - sb.Append(";"); - sb.Append(parameter.Value); - sb.AppendLine(";"); - } - - key = psb.ToStringAndReturn(); - } - - Key = key; - AffectedEntitySets = objectQuery != null - ? objectQuery.GetAffectedEntitySets() - : new string[0]; - } - - public string Key - { - get; - private set; - } - - public string[] AffectedEntitySets - { - get; - private set; - } - } - } + foreach (DbParameter parameter in commandInfo.Parameters) + { + sb.Append(parameter.ParameterName); + sb.Append(";"); + sb.Append(parameter.Value); + sb.AppendLine(";"); + } + + key = psb.ToStringAndReturn(); + } + + Key = key; + AffectedEntitySets = objectQuery != null + ? objectQuery.GetAffectedEntitySets() + : new string[0]; + } + + public string Key + { + get; + private set; + } + + public string[] AffectedEntitySets + { + get; + private set; + } + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/DbCacheUtil.cs b/src/Libraries/SmartStore.Data/Caching/DbCacheUtil.cs index 86cab90ae7..aa426e1762 100644 --- a/src/Libraries/SmartStore.Data/Caching/DbCacheUtil.cs +++ b/src/Libraries/SmartStore.Data/Caching/DbCacheUtil.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data.Common; -using System.Data.Entity; using System.Data.Entity.Core.EntityClient; using System.Data.Entity.Core.Metadata.Edm; using System.Data.Entity.Core.Objects; @@ -12,86 +11,86 @@ namespace SmartStore.Data.Caching { - internal static class DbCacheUtil - { - public static ObjectQuery GetObjectQuery(this IQueryable source) - { - var dbQuery = source as DbQuery; - - if (dbQuery != null) - { - var internalQueryProperty = FastProperty.GetProperty(source.GetType(), "InternalQuery"); - var internalQuery = internalQueryProperty.GetValue(source); - - var objectQueryProperty = FastProperty.GetProperty(internalQuery.GetType(), "ObjectQuery"); - var objectQuery = objectQueryProperty.GetValue(internalQuery); - - return objectQuery as ObjectQuery; - } - - return null; - } - - public static CommandInfo GetCommandInfo(this ObjectQuery objectQuery) - { - var stateField = objectQuery.GetType().BaseType.GetField("_state", BindingFlags.NonPublic | BindingFlags.Instance); - var state = stateField.GetValue(objectQuery); - var getExecutionPlanMethod = state.GetType().GetMethod("GetExecutionPlan", BindingFlags.NonPublic | BindingFlags.Instance); - var getExecutionPlan = getExecutionPlanMethod.Invoke(state, new object[] { null }); - var prepareEntityCommandMethod = getExecutionPlan.GetType().GetMethod("PrepareEntityCommand", BindingFlags.NonPublic | BindingFlags.Instance); - - var sql = ""; - - using (var entityCommand = (EntityCommand)prepareEntityCommandMethod.Invoke(getExecutionPlan, new object[] { objectQuery.Context, objectQuery.Parameters })) - { - var getCommandDefinitionMethod = entityCommand.GetType().GetMethod("GetCommandDefinition", BindingFlags.NonPublic | BindingFlags.Instance); - var getCommandDefinition = getCommandDefinitionMethod.Invoke(entityCommand, new object[0]); - - var prepareEntityCommandBeforeExecutionMethod = getCommandDefinition.GetType().GetMethod("PrepareEntityCommandBeforeExecution", BindingFlags.NonPublic | BindingFlags.Instance); - var prepareEntityCommandBeforeExecution = (DbCommand)prepareEntityCommandBeforeExecutionMethod.Invoke(getCommandDefinition, new object[] { entityCommand }); - - sql = prepareEntityCommandBeforeExecution.CommandText; - var parameters = prepareEntityCommandBeforeExecution.Parameters; - - return new CommandInfo(sql, parameters); - } - } - - public static string[] GetAffectedEntitySets(this ObjectQuery objectQuery) - { - var result = new HashSet(); - - var entityType = objectQuery.GetResultType().EdmType as EntityType; - - if (entityType != null) - { - result.Add(entityType.Name); - foreach (var nav in entityType.NavigationProperties) - { - var edmType = nav.TypeUsage.EdmType; - if (edmType is CollectionType) - { - result.Add(((CollectionType)edmType).TypeUsage.EdmType.Name); - } - else - { - result.Add(edmType.Name); - } - } - } - - return result.ToArray(); - } - - internal class CommandInfo : Tuple - { - public CommandInfo(string sql, DbParameterCollection parameters) - : base(sql, parameters) - { - } - - public string Sql { get { return base.Item1; } } - public DbParameterCollection Parameters { get { return base.Item2; } } - } - } + internal static class DbCacheUtil + { + public static ObjectQuery GetObjectQuery(this IQueryable source) + { + var dbQuery = source as DbQuery; + + if (dbQuery != null) + { + var internalQueryProperty = FastProperty.GetProperty(source.GetType(), "InternalQuery"); + var internalQuery = internalQueryProperty.GetValue(source); + + var objectQueryProperty = FastProperty.GetProperty(internalQuery.GetType(), "ObjectQuery"); + var objectQuery = objectQueryProperty.GetValue(internalQuery); + + return objectQuery as ObjectQuery; + } + + return null; + } + + public static CommandInfo GetCommandInfo(this ObjectQuery objectQuery) + { + var stateField = objectQuery.GetType().BaseType.GetField("_state", BindingFlags.NonPublic | BindingFlags.Instance); + var state = stateField.GetValue(objectQuery); + var getExecutionPlanMethod = state.GetType().GetMethod("GetExecutionPlan", BindingFlags.NonPublic | BindingFlags.Instance); + var getExecutionPlan = getExecutionPlanMethod.Invoke(state, new object[] { null }); + var prepareEntityCommandMethod = getExecutionPlan.GetType().GetMethod("PrepareEntityCommand", BindingFlags.NonPublic | BindingFlags.Instance); + + var sql = ""; + + using (var entityCommand = (EntityCommand)prepareEntityCommandMethod.Invoke(getExecutionPlan, new object[] { objectQuery.Context, objectQuery.Parameters })) + { + var getCommandDefinitionMethod = entityCommand.GetType().GetMethod("GetCommandDefinition", BindingFlags.NonPublic | BindingFlags.Instance); + var getCommandDefinition = getCommandDefinitionMethod.Invoke(entityCommand, new object[0]); + + var prepareEntityCommandBeforeExecutionMethod = getCommandDefinition.GetType().GetMethod("PrepareEntityCommandBeforeExecution", BindingFlags.NonPublic | BindingFlags.Instance); + var prepareEntityCommandBeforeExecution = (DbCommand)prepareEntityCommandBeforeExecutionMethod.Invoke(getCommandDefinition, new object[] { entityCommand }); + + sql = prepareEntityCommandBeforeExecution.CommandText; + var parameters = prepareEntityCommandBeforeExecution.Parameters; + + return new CommandInfo(sql, parameters); + } + } + + public static string[] GetAffectedEntitySets(this ObjectQuery objectQuery) + { + var result = new HashSet(); + + var entityType = objectQuery.GetResultType().EdmType as EntityType; + + if (entityType != null) + { + result.Add(entityType.Name); + foreach (var nav in entityType.NavigationProperties) + { + var edmType = nav.TypeUsage.EdmType; + if (edmType is CollectionType) + { + result.Add(((CollectionType)edmType).TypeUsage.EdmType.Name); + } + else + { + result.Add(edmType.Name); + } + } + } + + return result.ToArray(); + } + + internal class CommandInfo : Tuple + { + public CommandInfo(string sql, DbParameterCollection parameters) + : base(sql, parameters) + { + } + + public string Sql => base.Item1; + public DbParameterCollection Parameters => base.Item2; + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/DbCachingPolicy.cs b/src/Libraries/SmartStore.Data/Caching/DbCachingPolicy.cs index 6977e00901..7223f8d33a 100644 --- a/src/Libraries/SmartStore.Data/Caching/DbCachingPolicy.cs +++ b/src/Libraries/SmartStore.Data/Caching/DbCachingPolicy.cs @@ -4,7 +4,6 @@ using System.Data.Entity.Core.Metadata.Edm; using System.Linq; using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Localization; @@ -21,7 +20,7 @@ namespace SmartStore.Data.Caching { - /* TODO: (mc) + /* TODO: (mc) * ======================== * 1. Let developers register custom caching policies for single entities (from plugins) * 2. Caching policies should contain expiration info and cacheable rows count @@ -29,80 +28,80 @@ namespace SmartStore.Data.Caching * 4. Backend: Let users purge the cache */ - public partial class DbCachingPolicy - { - private static readonly HashSet _cacheableSets = new HashSet - { - typeof(AclRecord).Name, - typeof(ActivityLogType).Name, - typeof(CategoryTemplate).Name, - typeof(CheckoutAttribute).Name, - typeof(CheckoutAttributeValue).Name, - typeof(Country).Name, - typeof(Currency).Name, - typeof(CustomerRole).Name, + public partial class DbCachingPolicy + { + private static readonly HashSet _cacheableSets = new HashSet + { + typeof(AclRecord).Name, + typeof(ActivityLogType).Name, + typeof(CategoryTemplate).Name, + typeof(CheckoutAttribute).Name, + typeof(CheckoutAttributeValue).Name, + typeof(Country).Name, + typeof(Currency).Name, + //typeof(CustomerRole).Name, typeof(DeliveryTime).Name, - typeof(Discount).Name, - typeof(EmailAccount).Name, - typeof(Language).Name, - typeof(ManufacturerTemplate).Name, - typeof(MeasureDimension).Name, - typeof(MeasureWeight).Name, - typeof(MessageTemplate).Name, - typeof(PaymentMethod).Name, - typeof(PermissionRecord).Name, - typeof(ProductTemplate).Name, - typeof(QuantityUnit).Name, - typeof(ShippingMethod).Name, - typeof(StateProvince).Name, - typeof(Store).Name, - typeof(TaxCategory).Name, - typeof(ThemeVariable).Name, - typeof(Topic).Name - }; + typeof(Discount).Name, + typeof(EmailAccount).Name, + typeof(Language).Name, + typeof(ManufacturerTemplate).Name, + typeof(MeasureDimension).Name, + typeof(MeasureWeight).Name, + typeof(MessageTemplate).Name, + typeof(PaymentMethod).Name, + typeof(PermissionRecord).Name, + typeof(ProductTemplate).Name, + typeof(QuantityUnit).Name, + typeof(ShippingMethod).Name, + typeof(StateProvince).Name, + typeof(Store).Name, + typeof(TaxCategory).Name, + typeof(ThemeVariable).Name, + typeof(Topic).Name + }; - /// - /// Determines whether the specified command definition can be cached. - /// - /// Entity sets affected by the command. - /// SQL statement for the command. - /// Command parameters. - /// - /// true when the specified command definition can be cached; otherwise, false. - /// - protected internal virtual bool CanBeCached( - ReadOnlyCollection affectedEntitySets, - string sql, - IEnumerable> parameters) - { - var entitySets = affectedEntitySets.Select(x => x.Name); - var result = entitySets.All(x => _cacheableSets.Contains(x)); - return result; - } + /// + /// Determines whether the specified command definition can be cached. + /// + /// Entity sets affected by the command. + /// SQL statement for the command. + /// Command parameters. + /// + /// true when the specified command definition can be cached; otherwise, false. + /// + protected internal virtual bool CanBeCached( + ReadOnlyCollection affectedEntitySets, + string sql, + IEnumerable> parameters) + { + var entitySets = affectedEntitySets.Select(x => x.Name); + var result = entitySets.All(x => _cacheableSets.Contains(x)); + return result; + } - /// - /// Gets the minimum and maximum number cacheable rows for a given command definition. - /// - /// Entity sets affected by the command. - /// The minimum number of cacheable rows. - /// The maximum number of cacheable rows. - protected internal virtual void GetCacheableRows( - ReadOnlyCollection affectedEntitySets, - out int minCacheableRows, - out int maxCacheableRows) - { - minCacheableRows = 0; - maxCacheableRows = 5000; // int.MaxValue; - } + /// + /// Gets the minimum and maximum number cacheable rows for a given command definition. + /// + /// Entity sets affected by the command. + /// The minimum number of cacheable rows. + /// The maximum number of cacheable rows. + protected internal virtual void GetCacheableRows( + ReadOnlyCollection affectedEntitySets, + out int minCacheableRows, + out int maxCacheableRows) + { + minCacheableRows = 0; + maxCacheableRows = 5000; // int.MaxValue; + } - /// - /// Gets the expiration timeout for a given command definition. - /// - /// Entity sets affected by the command. - /// The absolute TTL - protected internal virtual TimeSpan? GetExpirationTimeout(ReadOnlyCollection affectedEntitySets) - { - return TimeSpan.FromDays(1); - } - } + /// + /// Gets the expiration timeout for a given command definition. + /// + /// Entity sets affected by the command. + /// The absolute TTL + protected internal virtual TimeSpan? GetExpirationTimeout(ReadOnlyCollection affectedEntitySets) + { + return TimeSpan.FromDays(1); + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs b/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs index 67d4561746..adfb712a9a 100644 --- a/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs +++ b/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs @@ -12,365 +12,362 @@ namespace SmartStore.Data.Caching { - public partial class EfDbCache : IDbCache - { - // Entity sets to be never cached or invalidated - private static readonly HashSet _toxicSets = new HashSet - { - typeof(ScheduleTask).Name, - typeof(ScheduleTaskHistory).Name, - typeof(Log).Name, - typeof(ActivityLog).Name, - typeof(QueuedEmail).Name - }; - - private const string KEYPREFIX = "efcache:"; - private readonly object _lock = new object(); - - private bool _enabled; - - private readonly ICacheManager _cache; - private readonly Work _requestCache; - - public EfDbCache(ICacheManager innerCache, Work requestCache) - { - _cache = innerCache; - _requestCache = requestCache; - - _enabled = true; - } - - public bool Enabled - { - get - { - return _enabled; - } - set - { - if (_enabled == value) - return; - - lock (_lock) - { - if (_enabled == false && value == true) - { - // When cache was disabled previously and gets enabled, - // we should clear the cache, because no invalidation has been performed - // during disabled state. We would deal with stale data otherwise. - Clear(); - } - _enabled = value; - } - } - } - - private bool IsToxic(IEnumerable entitySets) - { - return entitySets.Any(x => _toxicSets.Contains(x)); - } - - #region Request Scoped - - public virtual bool RequestTryGet(string key, out DbCacheEntry value) - { - value = null; - - if (!Enabled) - { - return false; - } - - key = HashKey(key); - - value = _requestCache.Value.Get(key); - return value != null; - } - - public virtual DbCacheEntry RequestPut(string key, object value, string[] dependentEntitySets) - { - if (!Enabled || IsToxic(dependentEntitySets)) - { - return null; - } - - key = HashKey(key); - - var entry = new DbCacheEntry - { - Key = key, - Value = value, - EntitySets = dependentEntitySets, - CachedOnUtc = DateTime.UtcNow - }; - - _requestCache.Value.Put(key, entry); - - foreach (var entitySet in entry.EntitySets) - { - var lookup = RequestGetLookupSet(entitySet); - lookup.Add(key); - } - - return entry; - } - - public virtual void RequestInvalidateSets(IEnumerable entitySets) - { - Guard.NotNull(entitySets, nameof(entitySets)); - - if (!Enabled || !entitySets.Any() || IsToxic(entitySets)) - return; - - var sets = entitySets.Distinct().ToArray(); - - var itemsToInvalidate = new HashSet(); - - foreach (var entitySet in sets) - { - var lookup = RequestGetLookupSet(entitySet, false); - if (lookup != null) - { - itemsToInvalidate.UnionWith(lookup); - } - } - - foreach (var key in itemsToInvalidate) - { - RequestInvalidateItem(key); - } - } - - public virtual void RequestInvalidateItem(string key) - { - Guard.NotEmpty(key, nameof(key)); - - var cache = _requestCache.Value; - - var entry = cache.Get(key); - if (entry != null) - { - // remove item itself from cache - cache.Remove(key); - - // remove this key in all lookups - foreach (var set in entry.EntitySets) - { - var lookup = RequestGetLookupSet(set, false); - if (lookup != null) - { - lookup.Remove(entry.Key); - } - } - } - } - - private HashSet RequestGetLookupSet(string entitySet, bool create = true) - { - var key = GetLookupKeyFor(entitySet); - - if (create) - { - return _requestCache.Value.Get(key, () => new HashSet()); - } - else - { - return _requestCache.Value.Get>(key); - } - } - - #endregion - - public virtual bool TryGet(string key, out object value) - { - value = null; - - if (!Enabled) - { - return false; - } - - key = HashKey(key); - var now = DateTime.UtcNow; - - var entry = _cache.Get(key, independent: true); - - if (entry != null) - { - if (entry.HasExpired(now)) - { - using (KeyedLock.Lock(key)) - { - InvalidateItemUnlocked(entry); - } - } - else - { - value = entry.Value; - return true; - } - } - - return false; - } - - public virtual DbCacheEntry Put(string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration) - { - if (!Enabled || IsToxic(dependentEntitySets)) - { - return null; - } - - key = HashKey(key); - - using (KeyedLock.Lock(key)) - { - var entitySets = dependentEntitySets.Distinct().ToArray(); - var entry = new DbCacheEntry - { - Key = key, - Value = value, - EntitySets = entitySets, - CachedOnUtc = DateTime.UtcNow, - Duration = duration - }; - - _cache.Put(key, entry); - - foreach (var entitySet in entitySets) - { - var lookup = GetLookupSet(entitySet); - lookup.Add(key); - } - - return entry; - } - } - - public void Clear() - { - _cache.RemoveByPattern(KEYPREFIX + "*"); - _requestCache.Value.RemoveByPattern(KEYPREFIX + "*"); - } - - public virtual void InvalidateSets(IEnumerable entitySets) - { - Guard.NotNull(entitySets, nameof(entitySets)); - - if (!Enabled || !entitySets.Any() || IsToxic(entitySets)) - return; - - var sets = entitySets.Distinct().ToArray(); - - lock (_lock) - { - var itemsToInvalidate = new HashSet(); - - foreach (var entitySet in sets) - { - var lookup = GetLookupSet(entitySet, false); - if (lookup != null) - { - itemsToInvalidate.UnionWith(lookup); - } - } - - foreach (var key in itemsToInvalidate) - { - InvalidateItemUnlocked(key); - } - } - } - - public virtual void InvalidateItem(string key) - { - if (!Enabled) - { - return; - } - - Guard.NotEmpty(key, nameof(key)); - - using (KeyedLock.Lock(key)) - { - InvalidateItemUnlocked(key); - } - } - - private void InvalidateItemUnlocked(string key) - { - if (!Enabled) - { - return; - } - - if (_cache.Contains(key)) - { - var entry = _cache.Get(key, true); - if (entry != null) - { - InvalidateItemUnlocked(entry); - } - } - } - - protected void InvalidateItemUnlocked(DbCacheEntry entry) - { - // remove item itself from cache - _cache.Remove(entry.Key); - - // remove this key in all lookups - foreach (var set in entry.EntitySets) - { - var lookup = GetLookupSet(set, false); - if (lookup != null) - { - lookup.Remove(entry.Key); - } - } - } - - private ISet GetLookupSet(string entitySet, bool create = true) - { - var key = GetLookupKeyFor(entitySet); - - if (create) - { - return _cache.GetHashSet(key); - } - else - { - if (_cache.Contains(key)) - { - return _cache.GetHashSet(key); - } - } - - return null; - } - - private string GetLookupKeyFor(string entitySet) - { - return KEYPREFIX + "lookup:" + entitySet; - } - - private static string HashKey(string key) - { - // Looking up large Keys can be expensive (comparing Large Strings), so if keys are large, hash them, otherwise if keys are short just use as-is - if (key.Length <= 128) - return KEYPREFIX + "data:" + key; - - using (var sha = new SHA1CryptoServiceProvider()) - { - try - { - return KEYPREFIX + "data:" + Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(key))); - } - catch - { - return KEYPREFIX + "data:" + key; - } - } - } - } -} + public partial class EfDbCache : IDbCache + { + // Entity sets to be never cached or invalidated + private static readonly HashSet _toxicSets = new HashSet + { + typeof(ScheduleTask).Name, + typeof(ScheduleTaskHistory).Name, + typeof(Log).Name, + typeof(ActivityLog).Name, + typeof(QueuedEmail).Name + }; + + private const string KEYPREFIX = "efcache:"; + private readonly object _lock = new object(); + + private bool _enabled; + + private readonly ICacheManager _cache; + private readonly Work _requestCache; + + public EfDbCache(ICacheManager innerCache, Work requestCache) + { + _cache = innerCache; + _requestCache = requestCache; + + _enabled = true; + } + + public bool Enabled + { + get => _enabled; + set + { + if (_enabled == value) + return; + + lock (_lock) + { + if (_enabled == false && value == true) + { + // When cache was disabled previously and gets enabled, + // we should clear the cache, because no invalidation has been performed + // during disabled state. We would deal with stale data otherwise. + Clear(); + } + _enabled = value; + } + } + } + + private bool IsToxic(IEnumerable entitySets) + { + return entitySets.Any(x => _toxicSets.Contains(x)); + } + + #region Request Scoped + + public virtual bool RequestTryGet(string key, out DbCacheEntry value) + { + value = null; + + if (!Enabled) + { + return false; + } + + key = HashKey(key); + + value = _requestCache.Value.Get(key); + return value != null; + } + + public virtual DbCacheEntry RequestPut(string key, object value, string[] dependentEntitySets) + { + if (!Enabled || IsToxic(dependentEntitySets)) + { + return null; + } + + key = HashKey(key); + + var entry = new DbCacheEntry + { + Key = key, + Value = value, + EntitySets = dependentEntitySets, + CachedOnUtc = DateTime.UtcNow + }; + + _requestCache.Value.Put(key, entry); + + foreach (var entitySet in entry.EntitySets) + { + var lookup = RequestGetLookupSet(entitySet); + lookup.Add(key); + } + + return entry; + } + + public virtual void RequestInvalidateSets(IEnumerable entitySets) + { + Guard.NotNull(entitySets, nameof(entitySets)); + + if (!Enabled || !entitySets.Any() || IsToxic(entitySets)) + return; + + var sets = entitySets.Distinct().ToArray(); + + var itemsToInvalidate = new HashSet(); + + foreach (var entitySet in sets) + { + var lookup = RequestGetLookupSet(entitySet, false); + if (lookup != null) + { + itemsToInvalidate.UnionWith(lookup); + } + } + + foreach (var key in itemsToInvalidate) + { + RequestInvalidateItem(key); + } + } + + public virtual void RequestInvalidateItem(string key) + { + Guard.NotEmpty(key, nameof(key)); + + var cache = _requestCache.Value; + + var entry = cache.Get(key); + if (entry != null) + { + // remove item itself from cache + cache.Remove(key); + + // remove this key in all lookups + foreach (var set in entry.EntitySets) + { + var lookup = RequestGetLookupSet(set, false); + if (lookup != null) + { + lookup.Remove(entry.Key); + } + } + } + } + + private HashSet RequestGetLookupSet(string entitySet, bool create = true) + { + var key = GetLookupKeyFor(entitySet); + + if (create) + { + return _requestCache.Value.Get(key, () => new HashSet()); + } + else + { + return _requestCache.Value.Get>(key); + } + } + + #endregion + + public virtual bool TryGet(string key, out object value) + { + value = null; + + if (!Enabled) + { + return false; + } + + key = HashKey(key); + var now = DateTime.UtcNow; + + var entry = _cache.Get(key, independent: true); + + if (entry != null) + { + if (entry.HasExpired(now)) + { + using (KeyedLock.Lock(key)) + { + InvalidateItemUnlocked(entry); + } + } + else + { + value = entry.Value; + return true; + } + } + + return false; + } + + public virtual DbCacheEntry Put(string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration) + { + if (!Enabled || IsToxic(dependentEntitySets)) + { + return null; + } + + key = HashKey(key); + + using (KeyedLock.Lock(key)) + { + var entitySets = dependentEntitySets.Distinct().ToArray(); + var entry = new DbCacheEntry + { + Key = key, + Value = value, + EntitySets = entitySets, + CachedOnUtc = DateTime.UtcNow, + Duration = duration + }; + + _cache.Put(key, entry); + + foreach (var entitySet in entitySets) + { + var lookup = GetLookupSet(entitySet); + lookup.Add(key); + } + + return entry; + } + } + + public void Clear() + { + _cache.RemoveByPattern(KEYPREFIX + "*"); + _requestCache.Value.RemoveByPattern(KEYPREFIX + "*"); + } + + public virtual void InvalidateSets(IEnumerable entitySets) + { + Guard.NotNull(entitySets, nameof(entitySets)); + + if (!Enabled || !entitySets.Any() || IsToxic(entitySets)) + return; + + var sets = entitySets.Distinct().ToArray(); + + lock (_lock) + { + var itemsToInvalidate = new HashSet(); + + foreach (var entitySet in sets) + { + var lookup = GetLookupSet(entitySet, false); + if (lookup != null) + { + itemsToInvalidate.UnionWith(lookup); + } + } + + foreach (var key in itemsToInvalidate) + { + InvalidateItemUnlocked(key); + } + } + } + + public virtual void InvalidateItem(string key) + { + if (!Enabled) + { + return; + } + + Guard.NotEmpty(key, nameof(key)); + + using (KeyedLock.Lock(key)) + { + InvalidateItemUnlocked(key); + } + } + + private void InvalidateItemUnlocked(string key) + { + if (!Enabled) + { + return; + } + + if (_cache.Contains(key)) + { + var entry = _cache.Get(key, true); + if (entry != null) + { + InvalidateItemUnlocked(entry); + } + } + } + + protected void InvalidateItemUnlocked(DbCacheEntry entry) + { + // remove item itself from cache + _cache.Remove(entry.Key); + + // remove this key in all lookups + foreach (var set in entry.EntitySets) + { + var lookup = GetLookupSet(set, false); + if (lookup != null) + { + lookup.Remove(entry.Key); + } + } + } + + private ISet GetLookupSet(string entitySet, bool create = true) + { + var key = GetLookupKeyFor(entitySet); + + if (create) + { + return _cache.GetHashSet(key); + } + else + { + if (_cache.Contains(key)) + { + return _cache.GetHashSet(key); + } + } + + return null; + } + + private string GetLookupKeyFor(string entitySet) + { + return KEYPREFIX + "lookup:" + entitySet; + } + + private static string HashKey(string key) + { + // Looking up large Keys can be expensive (comparing Large Strings), so if keys are large, hash them, otherwise if keys are short just use as-is + if (key.Length <= 128) + return KEYPREFIX + "data:" + key; + + using (var sha = new SHA1CryptoServiceProvider()) + { + try + { + return KEYPREFIX + "data:" + Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(key))); + } + catch + { + return KEYPREFIX + "data:" + key; + } + } + } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs b/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs index 393becfbf3..83fe2f2d99 100644 --- a/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs +++ b/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data.Entity.Infrastructure; using System.Diagnostics; using System.IO; @@ -9,52 +8,52 @@ namespace SmartStore.Data.Caching { - public sealed class EfDbModelStore : DefaultDbModelStore - { - public EfDbModelStore(string location) - : base(location) - { - } - - public override DbCompiledModel TryLoad(Type contextType) - { - string path = GetFilePath(contextType); - - if (File.Exists(path)) - { - var cachedModelCreatedOn = File.GetLastWriteTimeUtc(path); - var localAssemblyFile = FindLocalAssemblyFile(contextType.Assembly); - if (localAssemblyFile == null || !localAssemblyFile.Exists || localAssemblyFile.LastWriteTimeUtc > cachedModelCreatedOn) - { - File.Delete(path); - Debug.WriteLine("Cached db model obsolete. Re-creating cached db model edmx."); - } - } - else - { - Debug.WriteLine("No cached db model found. Creating cached db model edmx."); - } - - return base.TryLoad(contextType); - } - - private FileInfo FindLocalAssemblyFile(Assembly assembly) - { - // ASP.NET loads assemblies from its dynamic temp folder, - // where DLLs get redeployed on every app startup, which - // makes it impossible for us to check for updated DLLs. - // Therefore we'll find passed assembly in the app folder for datetime checking. - var file = new FileInfo(assembly.Location); - - if (file.Name.IsCaseInsensitiveEqual("SmartStore.Data.dll")) - { - return new FileInfo(Path.Combine(CommonHelper.MapPath("~/bin"), file.Name)); - } - - var pluginSystemName = Path.GetFileNameWithoutExtension(file.Name); - var pluginDescriptor = PluginFinder.Current.GetPluginDescriptorBySystemName(pluginSystemName, false); - - return pluginDescriptor?.Assembly?.OriginalFile; - } - } + public sealed class EfDbModelStore : DefaultDbModelStore + { + public EfDbModelStore(string location) + : base(location) + { + } + + public override DbCompiledModel TryLoad(Type contextType) + { + string path = GetFilePath(contextType); + + if (File.Exists(path)) + { + var cachedModelCreatedOn = File.GetLastWriteTimeUtc(path); + var localAssemblyFile = FindLocalAssemblyFile(contextType.Assembly); + if (localAssemblyFile == null || !localAssemblyFile.Exists || localAssemblyFile.LastWriteTimeUtc > cachedModelCreatedOn) + { + File.Delete(path); + Debug.WriteLine("Cached db model obsolete. Re-creating cached db model edmx."); + } + } + else + { + Debug.WriteLine("No cached db model found. Creating cached db model edmx."); + } + + return base.TryLoad(contextType); + } + + private FileInfo FindLocalAssemblyFile(Assembly assembly) + { + // ASP.NET loads assemblies from its dynamic temp folder, + // where DLLs get redeployed on every app startup, which + // makes it impossible for us to check for updated DLLs. + // Therefore we'll find passed assembly in the app folder for datetime checking. + var file = new FileInfo(assembly.Location); + + if (file.Name.IsCaseInsensitiveEqual("SmartStore.Data.dll")) + { + return new FileInfo(Path.Combine(CommonHelper.MapPath("~/bin"), file.Name)); + } + + var pluginSystemName = Path.GetFileNameWithoutExtension(file.Name); + var pluginDescriptor = PluginFinder.Current.GetPluginDescriptorBySystemName(pluginSystemName, false); + + return pluginDescriptor?.Assembly?.OriginalFile; + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs index 80720306ed..02b105b920 100644 --- a/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs +++ b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs @@ -1,56 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Data.Entity.Infrastructure.MappingViews; +using System.Collections.Generic; using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Infrastructure.MappingViews; +using System.Linq; namespace SmartStore.Data.Caching { - public class EfMappingViewCache : DbMappingViewCache - { - private readonly string _hash; - private readonly Dictionary _views; - - public EfMappingViewCache(EfMappingView cachedView) - { - _hash = cachedView.Hash; - _views = cachedView.Views; - } - - public EfMappingViewCache(string hash, Dictionary views) - { - Guard.NotEmpty(hash, nameof(hash)); - Guard.NotNull(views, nameof(views)); - - _hash = hash; - _views = views.ToDictionary(kvp => GetExtentFullName(kvp.Key), kvp => kvp.Value); - } - - public override string MappingHashValue - { - get { return _hash; } - } - - public override DbMappingView GetView(EntitySetBase extent) - { - _views.TryGetValue(GetExtentFullName(extent), out var mappingView); - return mappingView; - } - - internal static string GetExtentFullName(EntitySetBase entitySet) - { - return entitySet.EntityContainer.Name + "." + entitySet.Name; - } - } - - /// - /// For JSON serialization - /// - public sealed class EfMappingView - { - public string Hash { get; set; } - public Dictionary Views { get; set; } - } + public class EfMappingViewCache : DbMappingViewCache + { + private readonly string _hash; + private readonly Dictionary _views; + + public EfMappingViewCache(EfMappingView cachedView) + { + _hash = cachedView.Hash; + _views = cachedView.Views; + } + + public EfMappingViewCache(string hash, Dictionary views) + { + Guard.NotEmpty(hash, nameof(hash)); + Guard.NotNull(views, nameof(views)); + + _hash = hash; + _views = views.ToDictionary(kvp => GetExtentFullName(kvp.Key), kvp => kvp.Value); + } + + public override string MappingHashValue => _hash; + + public override DbMappingView GetView(EntitySetBase extent) + { + _views.TryGetValue(GetExtentFullName(extent), out var mappingView); + return mappingView; + } + + internal static string GetExtentFullName(EntitySetBase entitySet) + { + return entitySet.EntityContainer.Name + "." + entitySet.Name; + } + } + + /// + /// For JSON serialization + /// + public sealed class EfMappingView + { + public string Hash { get; set; } + public Dictionary Views { get; set; } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs index 021da40be7..85c0d10238 100644 --- a/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs +++ b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs @@ -13,158 +13,158 @@ namespace SmartStore.Data.Caching { - public class EfMappingViewCacheFactory : DbMappingViewCacheFactory - { - private readonly static ConcurrentDictionary _lookup = - new ConcurrentDictionary(); - - private readonly string _viewFilePath; - private readonly static object _lock = new object(); - - private EfMappingViewCacheFactory(string viewFilePath) - { - _viewFilePath = viewFilePath; - } - - public static void SetContext() - where TContext : DbContext, new() - { - using (var ctx = new TContext()) - { - SetContext(ctx); - } - } - - /// - /// Sets a for a mapping represented - /// by a derived class. - /// - /// A derived class instance containing - /// mapping to set the view cache factory for. - /// View cache factory - /// - /// This method must be called before EntityFramework generates views for the mapping - /// (which typically happens on the first query). - /// cannot be set more than once for the same - /// . - /// - public static void SetContext(DbContext ctx) - { - Guard.NotNull(ctx, nameof(ctx)); - - var objectContext = ((IObjectContextAdapter)ctx).ObjectContext; - var itemCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace); - var type = ctx.GetType(); - var filePath = Path.Combine(CommonHelper.MapPath("~/App_Data/EfCache"), type.Namespace + "." + type.Name + ".views"); - var factory = new EfMappingViewCacheFactory(filePath); - - itemCollection.MappingViewCacheFactory = factory; - _lookup.TryAdd(factory, itemCollection); - } - - /// - /// Returns a instance for the . - /// - /// View cache factory to return for. - /// A instance for the . - public static StorageMappingItemCollection GetMappingItemCollection(DbMappingViewCacheFactory viewCacheFactory) - { - Guard.NotNull(viewCacheFactory, nameof(viewCacheFactory)); - - if (!_lookup.TryGetValue(viewCacheFactory, out var mappingItemCollection)) - { - throw new InvalidOperationException("No StorageMappingItemCollection instance found for the provided DbMappingViewCacheFactory."); - } - - return mappingItemCollection; - } - - /// - /// Creates a new instance of the class for - /// the given and . - /// - /// The name of the conceptual model container. - /// The name of the store model container. - /// A new instance of the class. - public override DbMappingViewCache Create(string conceptualModelContainerName, string storeModelContainerName) - { - var mappingItemCollection = GetMappingItemCollection(); - - if (mappingItemCollection == null) - { - throw new InvalidOperationException("View cache not set for this mapping item collection"); - } - - var hash = mappingItemCollection.ComputeMappingHashValue(conceptualModelContainerName, storeModelContainerName); - - var cachedViews = Load(); - if (cachedViews != null && cachedViews.Hash == hash) - { - return new EfMappingViewCache(cachedViews); - } - - var views = GenerateViews(mappingItemCollection, conceptualModelContainerName, storeModelContainerName); - - cachedViews = CreateCachedViews(hash, views); - Save(cachedViews); - - return new EfMappingViewCache(hash, views); - } - - // virtual for mocking - internal virtual StorageMappingItemCollection GetMappingItemCollection() - { - return GetMappingItemCollection(this); - } - - // virtual for mocking - internal virtual Dictionary GenerateViews( - StorageMappingItemCollection mappingItemCollection, - string conceptualModelContainerName, - string storeModelContainerName) - { - var errors = new List(); - return mappingItemCollection.GenerateViews(conceptualModelContainerName, storeModelContainerName, errors); - } - - private EfMappingView CreateCachedViews(string hash, Dictionary views) - { - return new EfMappingView - { - Hash = hash, - Views = views.ToDictionary(k => EfMappingViewCache.GetExtentFullName(k.Key), v => v.Value) - }; - } - - private EfMappingView Load() - { - try - { - lock (_lock) - { - if (File.Exists(_viewFilePath)) - { - var json = File.ReadAllText(_viewFilePath); - return JsonConvert.DeserializeObject(json); - } - } - } - catch { } - - return null; - } - - private void Save(EfMappingView view) - { - try - { - lock (_lock) - { - var json = JsonConvert.SerializeObject(view); - File.WriteAllText(_viewFilePath, json); - } - } - catch { } - } - } + public class EfMappingViewCacheFactory : DbMappingViewCacheFactory + { + private readonly static ConcurrentDictionary _lookup = + new ConcurrentDictionary(); + + private readonly string _viewFilePath; + private readonly static object _lock = new object(); + + private EfMappingViewCacheFactory(string viewFilePath) + { + _viewFilePath = viewFilePath; + } + + public static void SetContext() + where TContext : DbContext, new() + { + using (var ctx = new TContext()) + { + SetContext(ctx); + } + } + + /// + /// Sets a for a mapping represented + /// by a derived class. + /// + /// A derived class instance containing + /// mapping to set the view cache factory for. + /// View cache factory + /// + /// This method must be called before EntityFramework generates views for the mapping + /// (which typically happens on the first query). + /// cannot be set more than once for the same + /// . + /// + public static void SetContext(DbContext ctx) + { + Guard.NotNull(ctx, nameof(ctx)); + + var objectContext = ((IObjectContextAdapter)ctx).ObjectContext; + var itemCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace); + var type = ctx.GetType(); + var filePath = Path.Combine(CommonHelper.MapPath("~/App_Data/EfCache"), type.Namespace + "." + type.Name + ".views"); + var factory = new EfMappingViewCacheFactory(filePath); + + itemCollection.MappingViewCacheFactory = factory; + _lookup.TryAdd(factory, itemCollection); + } + + /// + /// Returns a instance for the . + /// + /// View cache factory to return for. + /// A instance for the . + public static StorageMappingItemCollection GetMappingItemCollection(DbMappingViewCacheFactory viewCacheFactory) + { + Guard.NotNull(viewCacheFactory, nameof(viewCacheFactory)); + + if (!_lookup.TryGetValue(viewCacheFactory, out var mappingItemCollection)) + { + throw new InvalidOperationException("No StorageMappingItemCollection instance found for the provided DbMappingViewCacheFactory."); + } + + return mappingItemCollection; + } + + /// + /// Creates a new instance of the class for + /// the given and . + /// + /// The name of the conceptual model container. + /// The name of the store model container. + /// A new instance of the class. + public override DbMappingViewCache Create(string conceptualModelContainerName, string storeModelContainerName) + { + var mappingItemCollection = GetMappingItemCollection(); + + if (mappingItemCollection == null) + { + throw new InvalidOperationException("View cache not set for this mapping item collection"); + } + + var hash = mappingItemCollection.ComputeMappingHashValue(conceptualModelContainerName, storeModelContainerName); + + var cachedViews = Load(); + if (cachedViews != null && cachedViews.Hash == hash) + { + return new EfMappingViewCache(cachedViews); + } + + var views = GenerateViews(mappingItemCollection, conceptualModelContainerName, storeModelContainerName); + + cachedViews = CreateCachedViews(hash, views); + Save(cachedViews); + + return new EfMappingViewCache(hash, views); + } + + // virtual for mocking + internal virtual StorageMappingItemCollection GetMappingItemCollection() + { + return GetMappingItemCollection(this); + } + + // virtual for mocking + internal virtual Dictionary GenerateViews( + StorageMappingItemCollection mappingItemCollection, + string conceptualModelContainerName, + string storeModelContainerName) + { + var errors = new List(); + return mappingItemCollection.GenerateViews(conceptualModelContainerName, storeModelContainerName, errors); + } + + private EfMappingView CreateCachedViews(string hash, Dictionary views) + { + return new EfMappingView + { + Hash = hash, + Views = views.ToDictionary(k => EfMappingViewCache.GetExtentFullName(k.Key), v => v.Value) + }; + } + + private EfMappingView Load() + { + try + { + lock (_lock) + { + if (File.Exists(_viewFilePath)) + { + var json = File.ReadAllText(_viewFilePath); + return JsonConvert.DeserializeObject(json); + } + } + } + catch { } + + return null; + } + + private void Save(EfMappingView view) + { + try + { + lock (_lock) + { + var json = JsonConvert.SerializeObject(view); + File.WriteAllText(_viewFilePath, json); + } + } + catch { } + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/IDbCache.cs b/src/Libraries/SmartStore.Data/Caching/IDbCache.cs index 086fdf447c..74aa51c1a7 100644 --- a/src/Libraries/SmartStore.Data/Caching/IDbCache.cs +++ b/src/Libraries/SmartStore.Data/Caching/IDbCache.cs @@ -4,127 +4,127 @@ namespace SmartStore.Data.Caching { - /// - /// Interface to be implemented by cache implementations. - /// - public interface IDbCache - { - /// - /// Controls whether query caching is enabled or idle - /// - bool Enabled { get; set; } - - /// - /// Tries to the get cached entry by key. - /// - /// The cache key. - /// The retrieved value. - /// A value of true if entry was found in the cache, false otherwise. - bool TryGet(string key, out object value); - - /// - /// Adds the specified entry to the cache. - /// - /// The entry key. - /// The entry value. - /// The list of dependent entity sets. - /// The absolute expiration. - DbCacheEntry Put(string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration); - - /// - /// Invalidates all cache entries which are dependent on any of the specified entity sets. - /// - /// The entity sets. - void InvalidateSets(IEnumerable entitySets); - - /// - /// Invalidates cache entry with a given key. - /// - /// The cache key. - void InvalidateItem(string key); - - /// - /// Deletes all items from static and request cache - /// - void Clear(); - - /// - /// Tries to the get cached entry by key from (HTTP) request scope - /// - /// The cache key. - /// The retrieved value. - /// A value of true if entry was found in the cache, false otherwise. - bool RequestTryGet(string key, out DbCacheEntry value); - - /// - /// Adds the specified entry to the request scoped cache. - /// - /// The entry key. - /// The entry value. - /// The list of dependent entity sets. - DbCacheEntry RequestPut(string key, object value, string[] dependentEntitySets); - - /// - /// Invalidates all request scoped cache entries which are dependent on any of the specified entity sets. - /// - /// The entity sets. - void RequestInvalidateSets(IEnumerable entitySets); - - /// - /// Invalidates request scoped cache entry with a given key. - /// - /// The cache key. - void RequestInvalidateItem(string key); - } - - public class NullDbCache : IDbCache - { - public bool Enabled - { - get; - set; - } - - public bool TryGet(string key, out object value) - { - value = null; - return false; - } - - public DbCacheEntry Put(string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration) - { - return new DbCacheEntry { Key = key, Value = value, CachedOnUtc = DateTime.UtcNow, EntitySets = dependentEntitySets.ToArray(), Duration = duration }; - } - - public void InvalidateSets(IEnumerable entitySets) - { - } - - public void InvalidateItem(string key) - { - } - - public void Clear() - { - } - - public bool RequestTryGet(string key, out DbCacheEntry value) - { - value = null; - return false; - } - - public DbCacheEntry RequestPut(string key, object value, string[] dependentEntitySets) - { - return new DbCacheEntry { Key = key, Value = value, CachedOnUtc = DateTime.UtcNow, EntitySets = dependentEntitySets.ToArray() }; - } - - public void RequestInvalidateSets(IEnumerable entitySets) - { - } - - public void RequestInvalidateItem(string key) - { - } - } + /// + /// Interface to be implemented by cache implementations. + /// + public interface IDbCache + { + /// + /// Controls whether query caching is enabled or idle + /// + bool Enabled { get; set; } + + /// + /// Tries to the get cached entry by key. + /// + /// The cache key. + /// The retrieved value. + /// A value of true if entry was found in the cache, false otherwise. + bool TryGet(string key, out object value); + + /// + /// Adds the specified entry to the cache. + /// + /// The entry key. + /// The entry value. + /// The list of dependent entity sets. + /// The absolute expiration. + DbCacheEntry Put(string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration); + + /// + /// Invalidates all cache entries which are dependent on any of the specified entity sets. + /// + /// The entity sets. + void InvalidateSets(IEnumerable entitySets); + + /// + /// Invalidates cache entry with a given key. + /// + /// The cache key. + void InvalidateItem(string key); + + /// + /// Deletes all items from static and request cache + /// + void Clear(); + + /// + /// Tries to the get cached entry by key from (HTTP) request scope + /// + /// The cache key. + /// The retrieved value. + /// A value of true if entry was found in the cache, false otherwise. + bool RequestTryGet(string key, out DbCacheEntry value); + + /// + /// Adds the specified entry to the request scoped cache. + /// + /// The entry key. + /// The entry value. + /// The list of dependent entity sets. + DbCacheEntry RequestPut(string key, object value, string[] dependentEntitySets); + + /// + /// Invalidates all request scoped cache entries which are dependent on any of the specified entity sets. + /// + /// The entity sets. + void RequestInvalidateSets(IEnumerable entitySets); + + /// + /// Invalidates request scoped cache entry with a given key. + /// + /// The cache key. + void RequestInvalidateItem(string key); + } + + public class NullDbCache : IDbCache + { + public bool Enabled + { + get; + set; + } + + public bool TryGet(string key, out object value) + { + value = null; + return false; + } + + public DbCacheEntry Put(string key, object value, IEnumerable dependentEntitySets, TimeSpan? duration) + { + return new DbCacheEntry { Key = key, Value = value, CachedOnUtc = DateTime.UtcNow, EntitySets = dependentEntitySets.ToArray(), Duration = duration }; + } + + public void InvalidateSets(IEnumerable entitySets) + { + } + + public void InvalidateItem(string key) + { + } + + public void Clear() + { + } + + public bool RequestTryGet(string key, out DbCacheEntry value) + { + value = null; + return false; + } + + public DbCacheEntry RequestPut(string key, object value, string[] dependentEntitySets) + { + return new DbCacheEntry { Key = key, Value = value, CachedOnUtc = DateTime.UtcNow, EntitySets = dependentEntitySets.ToArray() }; + } + + public void RequestInvalidateSets(IEnumerable entitySets) + { + } + + public void RequestInvalidateItem(string key) + { + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/QueryRegistrar.cs b/src/Libraries/SmartStore.Data/Caching/QueryRegistrar.cs index e094a0b521..3421c4651e 100644 --- a/src/Libraries/SmartStore.Data/Caching/QueryRegistrar.cs +++ b/src/Libraries/SmartStore.Data/Caching/QueryRegistrar.cs @@ -1,55 +1,54 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Entity.Core.Metadata.Edm; namespace SmartStore.Data.Caching { - internal class QueryRegistrar - { - private readonly ConcurrentDictionary> _queries = new ConcurrentDictionary>(); - - public void AddQuery(MetadataWorkspace workspace, string sql) - { - Guard.NotNull(workspace, nameof(workspace)); - Guard.NotEmpty(sql, nameof(sql)); - - var queries = _queries.GetOrAdd(workspace, new HashSet()); - lock (queries) - { - queries.Add(sql); - } - } - - public bool RemoveQuery(MetadataWorkspace workspace, string sql) - { - Guard.NotNull(workspace, nameof(workspace)); - Guard.NotEmpty(sql, nameof(sql)); - - HashSet queries; - if (_queries.TryGetValue(workspace, out queries)) - { - lock (queries) - { - return queries.Remove(sql); - } - } - - return false; - } - - public bool ContainsQuery(MetadataWorkspace workspace, string sql) - { - Guard.NotNull(workspace, nameof(workspace)); - Guard.NotEmpty(sql, nameof(sql)); - - HashSet queries; - if (_queries.TryGetValue(workspace, out queries)) - { - return queries.Contains(sql); - } - - return false; - } - } + internal class QueryRegistrar + { + private readonly ConcurrentDictionary> _queries = new ConcurrentDictionary>(); + + public void AddQuery(MetadataWorkspace workspace, string sql) + { + Guard.NotNull(workspace, nameof(workspace)); + Guard.NotEmpty(sql, nameof(sql)); + + var queries = _queries.GetOrAdd(workspace, new HashSet()); + lock (queries) + { + queries.Add(sql); + } + } + + public bool RemoveQuery(MetadataWorkspace workspace, string sql) + { + Guard.NotNull(workspace, nameof(workspace)); + Guard.NotEmpty(sql, nameof(sql)); + + HashSet queries; + if (_queries.TryGetValue(workspace, out queries)) + { + lock (queries) + { + return queries.Remove(sql); + } + } + + return false; + } + + public bool ContainsQuery(MetadataWorkspace workspace, string sql) + { + Guard.NotNull(workspace, nameof(workspace)); + Guard.NotEmpty(sql, nameof(sql)); + + HashSet queries; + if (_queries.TryGetValue(workspace, out queries)) + { + return queries.Contains(sql); + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Data/Caching/SingletonQueries.cs b/src/Libraries/SmartStore.Data/Caching/SingletonQueries.cs index 6f7ecb37e4..07c742e82d 100644 --- a/src/Libraries/SmartStore.Data/Caching/SingletonQueries.cs +++ b/src/Libraries/SmartStore.Data/Caching/SingletonQueries.cs @@ -2,29 +2,29 @@ namespace SmartStore.Data.Caching { - public sealed class SingletonQueries - { - public static readonly SingletonQueries Current = new SingletonQueries(); + public sealed class SingletonQueries + { + public static readonly SingletonQueries Current = new SingletonQueries(); - private readonly QueryRegistrar _cachedQueries = new QueryRegistrar(); + private readonly QueryRegistrar _cachedQueries = new QueryRegistrar(); - private SingletonQueries() - { - } + private SingletonQueries() + { + } - public void AddCachedQuery(MetadataWorkspace workspace, string sql) - { - _cachedQueries.AddQuery(workspace, sql); - } + public void AddCachedQuery(MetadataWorkspace workspace, string sql) + { + _cachedQueries.AddQuery(workspace, sql); + } - public bool RemoveCachedQuery(MetadataWorkspace workspace, string sql) - { - return _cachedQueries.RemoveQuery(workspace, sql); - } + public bool RemoveCachedQuery(MetadataWorkspace workspace, string sql) + { + return _cachedQueries.RemoveQuery(workspace, sql); + } - public bool IsQueryCached(MetadataWorkspace workspace, string sql) - { - return _cachedQueries.ContainsQuery(workspace, sql); - } - } + public bool IsQueryCached(MetadataWorkspace workspace, string sql) + { + return _cachedQueries.ContainsQuery(workspace, sql); + } + } } diff --git a/src/Libraries/SmartStore.Data/EfDataProviderFactory.cs b/src/Libraries/SmartStore.Data/EfDataProviderFactory.cs index 4f7652f4ab..e526f2cbec 100644 --- a/src/Libraries/SmartStore.Data/EfDataProviderFactory.cs +++ b/src/Libraries/SmartStore.Data/EfDataProviderFactory.cs @@ -1,28 +1,26 @@ -using System; -using SmartStore.Core; -using SmartStore.Core.Data; +using SmartStore.Core.Data; namespace SmartStore.Data { public partial class EfDataProviderFactory : DataProviderFactory { - public EfDataProviderFactory() - : this(DataSettings.Current) - { - } - - public EfDataProviderFactory(DataSettings settings) - : base(settings) + public EfDataProviderFactory() + : this(DataSettings.Current) + { + } + + public EfDataProviderFactory(DataSettings settings) + : base(settings) { } public override IDataProvider LoadDataProvider() { var providerName = Settings.DataProvider; - if (providerName.IsEmpty()) - { - throw new SmartException("Data Settings doesn't contain a providerName"); - } + if (providerName.IsEmpty()) + { + throw new SmartException("Data Settings doesn't contain a providerName"); + } switch (providerName.ToLowerInvariant()) { diff --git a/src/Libraries/SmartStore.Data/EfRepository.cs b/src/Libraries/SmartStore.Data/EfRepository.cs index 29dfd22f42..04934f60d3 100644 --- a/src/Libraries/SmartStore.Data/EfRepository.cs +++ b/src/Libraries/SmartStore.Data/EfRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Validation; @@ -9,7 +8,6 @@ using System.Threading.Tasks; using SmartStore.Core; using SmartStore.Core.Data; -using SmartStore.Data.Caching; using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data @@ -30,32 +28,26 @@ public virtual IQueryable Table { get { - if (_context.ForceNoTracking) - { - return this.Entities.AsNoTracking(); - } + if (_context.ForceNoTracking) + { + return this.Entities.AsNoTracking(); + } - return this.Entities; + return this.Entities; } } public virtual IQueryable TableUntracked { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return this.Entities.AsNoTracking(); - } + get => this.Entities.AsNoTracking(); } public virtual ICollection Local - { + { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return this.Entities.Local; - } - } + get => this.Entities.Local; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual T Create() @@ -77,17 +69,17 @@ public virtual Task GetByIdAsync(object id) [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual T Attach(T entity) - { - return this.Entities.Attach(entity); - } + { + return this.Entities.Attach(entity); + } - public virtual void Insert(T entity) + public virtual void Insert(T entity) { - Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(entity, nameof(entity)); - this.Entities.Add(entity); - if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + this.Entities.Add(entity); + if (this.AutoCommitEnabledInternal) + _context.SaveChanges(); } public virtual async Task InsertAsync(T entity) @@ -139,11 +131,11 @@ public virtual async Task InsertRangeAsync(IEnumerable entities, int batchSiz public virtual void Update(T entity) { - Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(entity, nameof(entity)); - ChangeStateToModifiedIfApplicable(entity); - if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + ChangeStateToModifiedIfApplicable(entity); + if (this.AutoCommitEnabledInternal) + _context.SaveChanges(); } public virtual async Task UpdateAsync(T entity) @@ -156,19 +148,19 @@ public virtual async Task UpdateAsync(T entity) } public virtual void UpdateRange(IEnumerable entities) - { - Guard.NotNull(entities, nameof(entities)); + { + Guard.NotNull(entities, nameof(entities)); - foreach (var entity in entities) - { - ChangeStateToModifiedIfApplicable(entity); - } + foreach (var entity in entities) + { + ChangeStateToModifiedIfApplicable(entity); + } - if (this.AutoCommitEnabledInternal) - { - _context.SaveChanges(); - } - } + if (this.AutoCommitEnabledInternal) + { + _context.SaveChanges(); + } + } public virtual async Task UpdateRangeAsync(IEnumerable entities) { @@ -186,37 +178,37 @@ public virtual async Task UpdateRangeAsync(IEnumerable entities) } private void ChangeStateToModifiedIfApplicable(T entity) - { - if (entity.IsTransientRecord()) - return; - - var entry = InternalContext.Entry(entity); - - if (entry.State == EfState.Detached) - { - // Entity was detached before or was explicitly constructed. - // This unfortunately sets all properties to modified. - entry.State = EfState.Modified; - } - else if (entry.State == EfState.Unchanged) - { - // We simply do nothing here, because it is ensured now that DetectChanges() - // gets implicitly called prior SaveChanges(). - - //if (this.AutoCommitEnabledInternal && !ctx.Configuration.AutoDetectChangesEnabled) - //{ - // _context.DetectChanges(); - //} - } - } - - public virtual void Delete(T entity) { - Guard.NotNull(entity, nameof(entity)); + if (entity.IsTransientRecord()) + return; + + var entry = InternalContext.Entry(entity); + + if (entry.State == EfState.Detached) + { + // Entity was detached before or was explicitly constructed. + // This unfortunately sets all properties to modified. + entry.State = EfState.Modified; + } + else if (entry.State == EfState.Unchanged) + { + // We simply do nothing here, because it is ensured now that DetectChanges() + // gets implicitly called prior SaveChanges(). + + //if (this.AutoCommitEnabledInternal && !ctx.Configuration.AutoDetectChangesEnabled) + //{ + // _context.DetectChanges(); + //} + } + } + + public virtual void Delete(T entity) + { + Guard.NotNull(entity, nameof(entity)); - InternalContext.Entry(entity).State = EfState.Deleted; - if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + InternalContext.Entry(entity).State = EfState.Deleted; + if (this.AutoCommitEnabledInternal) + _context.SaveChanges(); } public virtual async Task DeleteAsync(T entity) @@ -229,19 +221,19 @@ public virtual async Task DeleteAsync(T entity) } public virtual void DeleteRange(IEnumerable entities) - { - Guard.NotNull(entities, nameof(entities)); + { + Guard.NotNull(entities, nameof(entities)); - foreach (var entity in entities) - { - InternalContext.Entry(entity).State = EfState.Deleted; - } + foreach (var entity in entities) + { + InternalContext.Entry(entity).State = EfState.Deleted; + } - if (this.AutoCommitEnabledInternal) - { - _context.SaveChanges(); - } - } + if (this.AutoCommitEnabledInternal) + { + _context.SaveChanges(); + } + } public virtual async Task DeleteRangeAsync(IEnumerable entities) { @@ -267,7 +259,7 @@ public IQueryable Expand(IQueryable query, string path) return query.Include(path); } - [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] + [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] public IQueryable Expand(IQueryable query, Expression> path) { Guard.NotNull(query, "query"); @@ -276,29 +268,17 @@ public IQueryable Expand(IQueryable query, Expression _context; public bool? AutoCommitEnabled { get; set; } - private bool AutoCommitEnabledInternal - { - get - { - return this.AutoCommitEnabled ?? _context.AutoCommitEnabled; - } - } + private bool AutoCommitEnabledInternal => this.AutoCommitEnabled ?? _context.AutoCommitEnabled; #endregion #region Helpers - protected internal ObjectContextBase InternalContext - { - get { return _context as ObjectContextBase; } - } + protected internal ObjectContextBase InternalContext => _context as ObjectContextBase; private DbSet Entities { diff --git a/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs index a625b8bb1c..49d4d26460 100644 --- a/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs @@ -3,142 +3,141 @@ using System; using System.Collections.Generic; using System.Data; -using System.Reflection; -using System.Linq; using System.Dynamic; +using System.Linq; using SmartStore.ComponentModel; namespace SmartStore.Data { public static class DataReaderExtensions { - public static object GetValue(this IDataReader reader, string columnName) - { - try - { - if (reader != null && !reader.IsClosed && columnName.HasValue()) - { - int ordinal = reader.GetOrdinal(columnName); - return reader.GetValue(ordinal); - } - } - catch (Exception ex) - { - ex.Dump(); - } - - return null; - } - - public static IEnumerable MapSequence(this IDataReader reader, params string[] fieldsToSkip) - where T : new() - { - Guard.NotNull(reader, nameof(reader)); - - while (reader.Read()) - { - yield return reader.Map(fieldsToSkip); - } - } - - public static IEnumerable MapSequence(this IDataReader reader, params string[] fieldsToSkip) - { - Guard.NotNull(reader, nameof(reader)); - + public static object GetValue(this IDataReader reader, string columnName) + { + try + { + if (reader != null && !reader.IsClosed && columnName.HasValue()) + { + int ordinal = reader.GetOrdinal(columnName); + return reader.GetValue(ordinal); + } + } + catch (Exception ex) + { + ex.Dump(); + } + + return null; + } + + public static IEnumerable MapSequence(this IDataReader reader, params string[] fieldsToSkip) + where T : new() + { + Guard.NotNull(reader, nameof(reader)); + while (reader.Read()) - { - yield return reader.Map(fieldsToSkip); - } - } - - /// - /// Populates an object from a single DataReader row using - /// fast reflection routines by matching the DataReader fields to a public property on - /// the object passed in. Unmatched properties are left unchanged. - /// - /// You need to pass in a data reader located on the active row you want - /// to serialize. - /// - /// Instance of the DataReader to read data from. Should be located on the correct record (Read() should have been called on it before calling this method) - /// An array of reader field names to ignore - public static T Map(this IDataReader reader, params string[] fieldsToSkip) - where T : new() - { - if (reader.IsClosed) - throw new InvalidOperationException("Data reader cannot be used because it's already closed"); - - var instance = new T(); - MapObject(reader, instance, fieldsToSkip); - return instance; - } - - public static dynamic Map(this IDataReader reader, params string[] fieldsToSkip) - { - if (reader.IsClosed) - throw new InvalidOperationException("Data reader cannot be used because it's already closed"); - - dynamic instance = new ExpandoObject(); - MapDictionary(reader, instance, fieldsToSkip); - return instance; - } - - public static void Map(this IDataReader reader, object instance, params string[] fieldsToSkip) - { - Guard.NotNull(instance, "instance"); - - if (reader.IsClosed) - throw new InvalidOperationException("Data reader cannot be used because it's already closed"); - - var dict = instance as IDictionary; - - if (dict != null) - { - MapDictionary(reader, dict, fieldsToSkip); + { + yield return reader.Map(fieldsToSkip); } - else - { - MapObject(reader, instance, fieldsToSkip); - } - } - - private static void MapObject(IDataReader reader, object instance, params string[] fieldsToSkip) - { - var fastProperties = FastProperty.GetProperties(instance.GetType()); - - if (fastProperties.Count == 0) - return; - - for (int i = 0; i < reader.FieldCount; i++) - { - string name = reader.GetName(i); - if (fastProperties.ContainsKey(name)) - { - var fastProp = fastProperties[name]; - - if (fieldsToSkip.Contains(name)) - continue; - - if ((fastProp != null) && fastProp.Property.CanWrite) - { - var dbValue = reader.GetValue(i); - fastProp.SetValue(instance, (dbValue == DBNull.Value) ? null : dbValue); - } - } - } - } - - private static void MapDictionary(IDataReader reader, IDictionary instance, params string[] fieldsToSkip) - { - for (int i = 0; i < reader.FieldCount; i++) - { - string name = reader.GetName(i); - - if (fieldsToSkip.Contains(name)) - continue; - - var dbValue = reader.GetValue(i); - instance[name] = (dbValue == DBNull.Value) ? null : dbValue; + } + + public static IEnumerable MapSequence(this IDataReader reader, params string[] fieldsToSkip) + { + Guard.NotNull(reader, nameof(reader)); + + while (reader.Read()) + { + yield return reader.Map(fieldsToSkip); + } + } + + /// + /// Populates an object from a single DataReader row using + /// fast reflection routines by matching the DataReader fields to a public property on + /// the object passed in. Unmatched properties are left unchanged. + /// + /// You need to pass in a data reader located on the active row you want + /// to serialize. + /// + /// Instance of the DataReader to read data from. Should be located on the correct record (Read() should have been called on it before calling this method) + /// An array of reader field names to ignore + public static T Map(this IDataReader reader, params string[] fieldsToSkip) + where T : new() + { + if (reader.IsClosed) + throw new InvalidOperationException("Data reader cannot be used because it's already closed"); + + var instance = new T(); + MapObject(reader, instance, fieldsToSkip); + return instance; + } + + public static dynamic Map(this IDataReader reader, params string[] fieldsToSkip) + { + if (reader.IsClosed) + throw new InvalidOperationException("Data reader cannot be used because it's already closed"); + + dynamic instance = new ExpandoObject(); + MapDictionary(reader, instance, fieldsToSkip); + return instance; + } + + public static void Map(this IDataReader reader, object instance, params string[] fieldsToSkip) + { + Guard.NotNull(instance, "instance"); + + if (reader.IsClosed) + throw new InvalidOperationException("Data reader cannot be used because it's already closed"); + + var dict = instance as IDictionary; + + if (dict != null) + { + MapDictionary(reader, dict, fieldsToSkip); + } + else + { + MapObject(reader, instance, fieldsToSkip); + } + } + + private static void MapObject(IDataReader reader, object instance, params string[] fieldsToSkip) + { + var fastProperties = FastProperty.GetProperties(instance.GetType()); + + if (fastProperties.Count == 0) + return; + + for (int i = 0; i < reader.FieldCount; i++) + { + string name = reader.GetName(i); + if (fastProperties.ContainsKey(name)) + { + var fastProp = fastProperties[name]; + + if (fieldsToSkip.Contains(name)) + continue; + + if ((fastProp != null) && fastProp.Property.CanWrite) + { + var dbValue = reader.GetValue(i); + fastProp.SetValue(instance, (dbValue == DBNull.Value) ? null : dbValue); + } + } + } + } + + private static void MapDictionary(IDataReader reader, IDictionary instance, params string[] fieldsToSkip) + { + for (int i = 0; i < reader.FieldCount; i++) + { + string name = reader.GetName(i); + + if (fieldsToSkip.Contains(name)) + continue; + + var dbValue = reader.GetValue(i); + instance[name] = (dbValue == DBNull.Value) ? null : dbValue; } - } - } + } + } } diff --git a/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs index 6f68d459f7..1dace89f64 100644 --- a/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs @@ -1,60 +1,59 @@ -using System.Linq; -using System.Data.Entity; +using System.Data.Entity; using System.Data.Entity.Infrastructure; -using SmartStore.Core; using System.Diagnostics; +using System.Linq; +using SmartStore.Core; using SmartStore.Core.Data; namespace SmartStore { - public class SqlServerInfo - { - public string ProductVersion { get; set; } - public string PatchLevel { get; set; } - public string ProductEdition { get; set; } - public string ClrVersion { get; set; } - public string DefaultCollation { get; set; } - public string Instance { get; set; } - public int Lcid { get; set; } - public string ServerName { get; set; } - } - - public static class DbContextExtensions - { - - public static bool ColumnExists(this DbContext context, string tableName, string columnName) - { - if (context != null && tableName.HasValue() && columnName.HasValue()) - { - //string sql = @"Select column_name From INFORMATION_SCHEMA.COLUMNS Where table_name = @tableName And column_name = @columnName"; - - //string col = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, - // new SqlParameter("@tableName", tableName), new SqlParameter("@columnName", columnName)).FirstOrDefault(); - - string sql = @"Select column_name From INFORMATION_SCHEMA.COLUMNS Where table_name = {0} And column_name = {1}"; - string col = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, tableName, columnName).FirstOrDefault(); - return col.HasValue(); - } - - return false; - } - - public static void ColumnEnsure(this DbContext context, string tableName, string columnName, string columnDataType) - { - if (!context.ColumnExists(tableName, columnName)) - { - //context.Database.ExecuteSqlCommand("ALTER TABLE dbo.[{0}] ADD {1} {2}".FormatWith(tableName, columnName, columnDataType)); - context.Database.ExecuteSqlCommand("ALTER TABLE {0} ADD {1} {2}".FormatWith(tableName, columnName, columnDataType)); - } - } - - public static void ColumnDelete(this DbContext context, string tableName, string columnName) - { - if (context.ColumnExists(tableName, columnName)) - { - context.Database.ExecuteSqlCommand("ALTER TABLE {0} DROP COLUMN {1}".FormatWith(tableName, columnName)); - } - } + public class SqlServerInfo + { + public string ProductVersion { get; set; } + public string PatchLevel { get; set; } + public string ProductEdition { get; set; } + public string ClrVersion { get; set; } + public string DefaultCollation { get; set; } + public string Instance { get; set; } + public int Lcid { get; set; } + public string ServerName { get; set; } + } + + public static class DbContextExtensions + { + public static bool ColumnExists(this DbContext context, string tableName, string columnName) + { + if (context != null && tableName.HasValue() && columnName.HasValue()) + { + //string sql = @"Select column_name From INFORMATION_SCHEMA.COLUMNS Where table_name = @tableName And column_name = @columnName"; + + //string col = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, + // new SqlParameter("@tableName", tableName), new SqlParameter("@columnName", columnName)).FirstOrDefault(); + + string sql = @"Select column_name From INFORMATION_SCHEMA.COLUMNS Where table_name = {0} And column_name = {1}"; + string col = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, tableName, columnName).FirstOrDefault(); + return col.HasValue(); + } + + return false; + } + + public static void ColumnEnsure(this DbContext context, string tableName, string columnName, string columnDataType) + { + if (!context.ColumnExists(tableName, columnName)) + { + //context.Database.ExecuteSqlCommand("ALTER TABLE dbo.[{0}] ADD {1} {2}".FormatWith(tableName, columnName, columnDataType)); + context.Database.ExecuteSqlCommand("ALTER TABLE {0} ADD {1} {2}".FormatWith(tableName, columnName, columnDataType)); + } + } + + public static void ColumnDelete(this DbContext context, string tableName, string columnName) + { + if (context.ColumnExists(tableName, columnName)) + { + context.Database.ExecuteSqlCommand("ALTER TABLE {0} DROP COLUMN {1}".FormatWith(tableName, columnName)); + } + } public static bool TableExists(this DbContext context, string tableName) { @@ -69,41 +68,42 @@ public static bool TableExists(this DbContext context, string tableName) } public static bool DatabaseExists(this DbContext context, string databaseName) - { - if (context != null && databaseName.HasValue()) { - //string sql = @"Select database_id From sys.databases Where Name = @databaseName"; + { + if (context != null && databaseName.HasValue()) + { + //string sql = @"Select database_id From sys.databases Where Name = @databaseName"; - //int databaseID = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, new SqlParameter("@databaseName", databaseName)).FirstOrDefault(); + //int databaseID = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, new SqlParameter("@databaseName", databaseName)).FirstOrDefault(); - string sql = @"Select database_id From sys.databases Where Name = {0}"; + string sql = @"Select database_id From sys.databases Where Name = {0}"; - int databaseID = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, databaseName).FirstOrDefault(); + int databaseID = ((IObjectContextAdapter)context).ObjectContext.ExecuteStoreQuery(sql, databaseName).FirstOrDefault(); - return databaseID > 0; - } + return databaseID > 0; + } - return false; - } + return false; + } - public static int InsertInto(this DbContext context, string sql, params object[] parameters) - { - return (int)context.Database.SqlQuery(sql + "; SELECT @@IDENTITY;", parameters).FirstOrDefault(); - } + public static int InsertInto(this DbContext context, string sql, params object[] parameters) + { + return (int)context.Database.SqlQuery(sql + "; SELECT @@IDENTITY;", parameters).FirstOrDefault(); + } - public static int Execute(this DbContext context, string sql, params object[] parameters) - { - return context.Database.ExecuteSqlCommand(sql, parameters); - } + public static int Execute(this DbContext context, string sql, params object[] parameters) + { + return context.Database.ExecuteSqlCommand(sql, parameters); + } - [Conditional("DEBUG")] - [DebuggerStepThrough] - public static void DumpAttachedEntities(this DbContext context) - { - context.ChangeTracker.Entries() - .Where(x => x.State != System.Data.Entity.EntityState.Detached) - .ToList() - .ForEach(x => "{0} {1} {2}".FormatInvariant((x.Entity as BaseEntity).Id, x.State.ToString(), x.Entity.GetType().Name).Dump()); - } + [Conditional("DEBUG")] + [DebuggerStepThrough] + public static void DumpAttachedEntities(this DbContext context) + { + context.ChangeTracker.Entries() + .Where(x => x.State != System.Data.Entity.EntityState.Detached) + .ToList() + .ForEach(x => "{0} {1} {2}".FormatInvariant((x.Entity as BaseEntity).Id, x.State.ToString(), x.Entity.GetType().Name).Dump()); + } [Conditional("DEBUG")] [DebuggerStepThrough] @@ -135,8 +135,8 @@ public static void CreateSqlTimeout(this DbContext context) } internal static SqlServerInfo GetSqlServerInfo(this DbContext context) - { - string sql = @"SELECT + { + string sql = @"SELECT SERVERPROPERTY('productversion') as 'ProductVersion', SERVERPROPERTY('productlevel') as 'PatchLevel', SERVERPROPERTY('edition') as 'ProductEdition', @@ -146,8 +146,8 @@ internal static SqlServerInfo GetSqlServerInfo(this DbContext context) SERVERPROPERTY('lcid') as 'Lcid', SERVERPROPERTY('servername') as 'ServerName'"; - return context.Database.SqlQuery(sql).FirstOrDefault(); - } + return context.Database.SqlQuery(sql).FirstOrDefault(); + } - } + } } diff --git a/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs index 9d5688cac9..1543a8ba41 100644 --- a/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Entity.Infrastructure; using System.Linq; using SmartStore.Core.Data; @@ -7,185 +6,185 @@ namespace SmartStore.Data { - public static class DbEntityEntryExtensions - { - public static void ReloadEntity(this DbEntityEntry entry) - { - try - { - entry.Reload(); - } - catch - { - // Can occur when entity has been detached in the meantime (for whatever fucking reasons) - if (entry.State == EfState.Detached) - { - entry.State = EfState.Unchanged; - entry.Reload(); - } - } - } - - /// - /// Gets a dictionary with modified properties for the specified entity - /// - /// The entity entry instance for which to get modified properties for - /// - /// A dictionary, where the key is the name of the modified property - /// and the value is its ORIGINAL value (which was tracked when the entity - /// was attached to the context the first time) - /// Returns an empty dictionary if no modification could be detected. - /// - public static IDictionary GetModifiedProperties(this DbEntityEntry entry, IDbContext ctx) - { - var props = GetModifiedPropertyEntries(entry, ctx).ToDictionary(k => k.Name, v => v.OriginalValue); - - //System.Diagnostics.Debug.WriteLine("GetModifiedProperties: " + String.Join(", ", props.Select(x => x.Key))); - - return props; - } - - /// - /// Checks whether an entity entry has any modified property. - /// Only entities in state are scanned for changes. - /// Merged values provided by the are ignored. - /// - /// The entry instance - /// The data context - /// true if any property has changed, false otherwise - public static bool HasChanges(this DbEntityEntry entry, IDbContext ctx) - { - var hasChanges = GetModifiedPropertyEntries(entry, ctx).Any(); - return hasChanges; - } - - internal static IEnumerable GetModifiedPropertyEntries(this DbEntityEntry entry, IDbContext ctx) - { - // Be aware of the entity state. you cannot get modified properties for detached entities. - EnsureChangesDetected(entry, ctx); - - if (entry.State != EfState.Modified) - { - yield break; - } - - foreach (var name in entry.CurrentValues.PropertyNames) - { - var prop = entry.Property(name); - if (prop != null && PropIsModified(prop, ctx)) - { - // INFO: under certain conditions DbPropertyEntry.IsModified returns true, even when values are equal - yield return prop; - } - } - } - - public static bool IsPropertyModified(this DbEntityEntry entry, IDbContext ctx, string propertyName) - { - object originalValue; - return TryGetModifiedProperty(entry, ctx, propertyName, out originalValue); - } - - public static bool TryGetModifiedProperty(this DbEntityEntry entry, IDbContext ctx, string propertyName, out object originalValue) - { - Guard.NotEmpty(propertyName, nameof(propertyName)); - - EnsureChangesDetected(entry, ctx); - - originalValue = null; - - if (entry.State != EfState.Modified) - { - return false; - } - - var prop = entry.Property(propertyName); - if (prop != null && PropIsModified(prop, ctx)) - { - // INFO: under certain conditions DbPropertyEntry.IsModified returns true, even when values are equal - originalValue = prop.OriginalValue; - return true; - } - - return false; - } - - private static void EnsureChangesDetected(DbEntityEntry entry, IDbContext ctx) - { - var state = entry.State; - - if (ctx.AutoDetectChangesEnabled && state == EfState.Modified) - return; - - if ((ctx as ObjectContextBase)?.IsInSaveOperation == true) - return; - - if (state == EfState.Unchanged || state == EfState.Modified) - { - // When AutoDetectChanges is off we cannot be sure whether the entity is really unchanged, - // because no detection was performed to verify this. - DetectChangesInProperties(entry, ctx); - } - } - - public static void DetectChangesInProperties(this DbEntityEntry entry, IDbContext ctx) - { - ctx.DetectChanges(); - - #region Experimental - - //// ChangeDetection for single entity: calls DbEntityEntry.InternalEntry > ObjectStateEntry._stateEntry.DetectChangesInProperties(bool) - //var invoked = false; - - //try - //{ - // var internalEntry = FastProperty.GetProperty(entry.GetType(), "InternalEntry").GetValue(entry); - // if (internalEntry != null) - // { - // var objectStateEntry = FastProperty.GetProperty(internalEntry.GetType(), "ObjectStateEntry").GetValue(internalEntry); - // if (objectStateEntry != null) - // { - // var innerStateEntry = objectStateEntry.GetType().GetField("_stateEntry", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(objectStateEntry); - // if (innerStateEntry != null) - // { - // var dcMethod = innerStateEntry.GetType().GetMethod("DetectChangesInProperties", BindingFlags.NonPublic | BindingFlags.Instance); - // if (dcMethod != null) - // { - // //var requiresScalarChangeTracking = (bool)FastProperty.GetProperty(objectStateEntry.GetType(), "RequiresScalarChangeTracking").GetValue(objectStateEntry); - // //var requiresComplexChangeTracking = (bool)FastProperty.GetProperty(objectStateEntry.GetType(), "RequiresComplexChangeTracking").GetValue(objectStateEntry); - // dcMethod.Invoke(innerStateEntry, new object[] { false }); - // invoked = true; - // } - // } - // } - // } - //} - //finally - //{ - // if (!invoked) ctx.DetectChanges(); - //} - - #endregion - } - - private static bool PropIsModified(DbPropertyEntry prop, IDbContext ctx) - { - // INFO: "CurrentValue" cannot be used for entities in the Deleted state. - // INFO: "OriginalValues" cannot be used for entities in the Added state. - //return !AreEqual(prop.CurrentValue, prop.OriginalValue); - return ctx.AutoDetectChangesEnabled - ? prop.IsModified - : !AreEqual(prop.CurrentValue, prop.OriginalValue); - } - - private static bool AreEqual(object cur, object orig) - { - if (cur == null && orig == null) - return true; - - return orig != null - ? orig.Equals(cur) - : cur.Equals(orig); - } - } + public static class DbEntityEntryExtensions + { + public static void ReloadEntity(this DbEntityEntry entry) + { + try + { + entry.Reload(); + } + catch + { + // Can occur when entity has been detached in the meantime (for whatever fucking reasons) + if (entry.State == EfState.Detached) + { + entry.State = EfState.Unchanged; + entry.Reload(); + } + } + } + + /// + /// Gets a dictionary with modified properties for the specified entity + /// + /// The entity entry instance for which to get modified properties for + /// + /// A dictionary, where the key is the name of the modified property + /// and the value is its ORIGINAL value (which was tracked when the entity + /// was attached to the context the first time) + /// Returns an empty dictionary if no modification could be detected. + /// + public static IDictionary GetModifiedProperties(this DbEntityEntry entry, IDbContext ctx) + { + var props = GetModifiedPropertyEntries(entry, ctx).ToDictionary(k => k.Name, v => v.OriginalValue); + + //System.Diagnostics.Debug.WriteLine("GetModifiedProperties: " + String.Join(", ", props.Select(x => x.Key))); + + return props; + } + + /// + /// Checks whether an entity entry has any modified property. + /// Only entities in state are scanned for changes. + /// Merged values provided by the are ignored. + /// + /// The entry instance + /// The data context + /// true if any property has changed, false otherwise + public static bool HasChanges(this DbEntityEntry entry, IDbContext ctx) + { + var hasChanges = GetModifiedPropertyEntries(entry, ctx).Any(); + return hasChanges; + } + + internal static IEnumerable GetModifiedPropertyEntries(this DbEntityEntry entry, IDbContext ctx) + { + // Be aware of the entity state. you cannot get modified properties for detached entities. + EnsureChangesDetected(entry, ctx); + + if (entry.State != EfState.Modified) + { + yield break; + } + + foreach (var name in entry.CurrentValues.PropertyNames) + { + var prop = entry.Property(name); + if (prop != null && PropIsModified(prop, ctx)) + { + // INFO: under certain conditions DbPropertyEntry.IsModified returns true, even when values are equal + yield return prop; + } + } + } + + public static bool IsPropertyModified(this DbEntityEntry entry, IDbContext ctx, string propertyName) + { + object originalValue; + return TryGetModifiedProperty(entry, ctx, propertyName, out originalValue); + } + + public static bool TryGetModifiedProperty(this DbEntityEntry entry, IDbContext ctx, string propertyName, out object originalValue) + { + Guard.NotEmpty(propertyName, nameof(propertyName)); + + EnsureChangesDetected(entry, ctx); + + originalValue = null; + + if (entry.State != EfState.Modified) + { + return false; + } + + var prop = entry.Property(propertyName); + if (prop != null && PropIsModified(prop, ctx)) + { + // INFO: under certain conditions DbPropertyEntry.IsModified returns true, even when values are equal + originalValue = prop.OriginalValue; + return true; + } + + return false; + } + + private static void EnsureChangesDetected(DbEntityEntry entry, IDbContext ctx) + { + var state = entry.State; + + if (ctx.AutoDetectChangesEnabled && state == EfState.Modified) + return; + + if ((ctx as ObjectContextBase)?.IsInSaveOperation == true) + return; + + if (state == EfState.Unchanged || state == EfState.Modified) + { + // When AutoDetectChanges is off we cannot be sure whether the entity is really unchanged, + // because no detection was performed to verify this. + DetectChangesInProperties(entry, ctx); + } + } + + public static void DetectChangesInProperties(this DbEntityEntry entry, IDbContext ctx) + { + ctx.DetectChanges(); + + #region Experimental + + //// ChangeDetection for single entity: calls DbEntityEntry.InternalEntry > ObjectStateEntry._stateEntry.DetectChangesInProperties(bool) + //var invoked = false; + + //try + //{ + // var internalEntry = FastProperty.GetProperty(entry.GetType(), "InternalEntry").GetValue(entry); + // if (internalEntry != null) + // { + // var objectStateEntry = FastProperty.GetProperty(internalEntry.GetType(), "ObjectStateEntry").GetValue(internalEntry); + // if (objectStateEntry != null) + // { + // var innerStateEntry = objectStateEntry.GetType().GetField("_stateEntry", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(objectStateEntry); + // if (innerStateEntry != null) + // { + // var dcMethod = innerStateEntry.GetType().GetMethod("DetectChangesInProperties", BindingFlags.NonPublic | BindingFlags.Instance); + // if (dcMethod != null) + // { + // //var requiresScalarChangeTracking = (bool)FastProperty.GetProperty(objectStateEntry.GetType(), "RequiresScalarChangeTracking").GetValue(objectStateEntry); + // //var requiresComplexChangeTracking = (bool)FastProperty.GetProperty(objectStateEntry.GetType(), "RequiresComplexChangeTracking").GetValue(objectStateEntry); + // dcMethod.Invoke(innerStateEntry, new object[] { false }); + // invoked = true; + // } + // } + // } + // } + //} + //finally + //{ + // if (!invoked) ctx.DetectChanges(); + //} + + #endregion + } + + private static bool PropIsModified(DbPropertyEntry prop, IDbContext ctx) + { + // INFO: "CurrentValue" cannot be used for entities in the Deleted state. + // INFO: "OriginalValues" cannot be used for entities in the Added state. + //return !AreEqual(prop.CurrentValue, prop.OriginalValue); + return ctx.AutoDetectChangesEnabled + ? prop.IsModified + : !AreEqual(prop.CurrentValue, prop.OriginalValue); + } + + private static bool AreEqual(object cur, object orig) + { + if (cur == null && orig == null) + return true; + + return orig != null + ? orig.Equals(cur) + : cur.Equals(orig); + } + } } diff --git a/src/Libraries/SmartStore.Data/Extensions/ExceptionExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/ExceptionExtensions.cs index a0159326b1..6f685642a4 100644 --- a/src/Libraries/SmartStore.Data/Extensions/ExceptionExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/ExceptionExtensions.cs @@ -5,75 +5,75 @@ namespace SmartStore { - public static class ExceptionExtensions - { - /// - /// Checks whether the inner exception indicates uniqueness violation - /// (is 2627 = Unique constraint error, OR is 547 = Constraint check violation, OR is 2601 = Duplicated key row error) - /// - /// The exception wrapper - /// - public static bool IsUniquenessViolationException(this DbUpdateException exception) - { - var sqlException = exception?.InnerException as SqlException; + public static class ExceptionExtensions + { + /// + /// Checks whether the inner exception indicates uniqueness violation + /// (is 2627 = Unique constraint error, OR is 547 = Constraint check violation, OR is 2601 = Duplicated key row error) + /// + /// The exception wrapper + /// + public static bool IsUniquenessViolationException(this DbUpdateException exception) + { + var sqlException = exception?.InnerException as SqlException; - if (sqlException == null) - sqlException = exception?.InnerException?.InnerException as SqlException; + if (sqlException == null) + sqlException = exception?.InnerException?.InnerException as SqlException; - if (sqlException == null) - return false; + if (sqlException == null) + return false; - switch (sqlException.Number) - { - case 2627: // Unique constraint error - case 547: // Constraint check violation - case 2601: // Duplicated key row error - return true; - default: - return false; - } - } + switch (sqlException.Number) + { + case 2627: // Unique constraint error + case 547: // Constraint check violation + case 2601: // Duplicated key row error + return true; + default: + return false; + } + } - /// - /// Checks whether the inner exception indicates a deadlock error - /// - /// The exception wrapper - /// - public static bool IsDeadlockException(this EntityCommandExecutionException exception) - { - var sqlException = exception?.InnerException as SqlException; + /// + /// Checks whether the inner exception indicates a deadlock error + /// + /// The exception wrapper + /// + public static bool IsDeadlockException(this EntityCommandExecutionException exception) + { + var sqlException = exception?.InnerException as SqlException; - if (sqlException == null) - sqlException = exception?.InnerException?.InnerException as SqlException; + if (sqlException == null) + sqlException = exception?.InnerException?.InnerException as SqlException; - if (sqlException == null) - return false; + if (sqlException == null) + return false; - return sqlException.IsDeadlockException(); - } + return sqlException.IsDeadlockException(); + } - /// - /// Checks whether the exception indicates a deadlock error (1205) - /// - /// The exception wrapper - /// - public static bool IsDeadlockException(this SqlException exception) - { - if (exception == null) - return false; + /// + /// Checks whether the exception indicates a deadlock error (1205) + /// + /// The exception wrapper + /// + public static bool IsDeadlockException(this SqlException exception) + { + if (exception == null) + return false; - return exception.Number == 1205; - } + return exception.Number == 1205; + } - /// - /// Checks whether the exception indicates attaching an already attached entity (HResult -2146233079) - /// "Attaching an entity of type 'x' failed because another entity of the same type already has the same primary key value." - /// - /// Invalid operation exception - /// - public static bool IsAlreadyAttachedEntityException(this InvalidOperationException exception) - { - return exception != null && exception.HResult == -2146233079; - } - } + /// + /// Checks whether the exception indicates attaching an already attached entity (HResult -2146233079) + /// "Attaching an entity of type 'x' failed because another entity of the same type already has the same primary key value." + /// + /// Invalid operation exception + /// + public static bool IsAlreadyAttachedEntityException(this InvalidOperationException exception) + { + return exception != null && exception.HResult == -2146233079; + } + } } diff --git a/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs index 3cce9d4032..68b043a6a5 100644 --- a/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs @@ -10,21 +10,20 @@ using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Data; -using EfState = System.Data.Entity.EntityState; -namespace SmartStore +namespace SmartStore { - public static class IDbContextExtensions - { - /// + public static class IDbContextExtensions + { + /// /// Loads the database copy. /// /// /// The context. /// The current copy. /// - public static T LoadDatabaseCopy(this IDbContext context, T currentCopy) where T : BaseEntity - { + public static T LoadDatabaseCopy(this IDbContext context, T currentCopy) where T : BaseEntity + { return InnerGetCopy(context, currentCopy, e => e.GetDatabaseValues()); } @@ -35,152 +34,152 @@ public static T LoadDatabaseCopy(this IDbContext context, T currentCopy) wher /// The context. /// The current copy. /// - public static T LoadOriginalCopy(this IDbContext context, T currentCopy) where T : BaseEntity - { + public static T LoadOriginalCopy(this IDbContext context, T currentCopy) where T : BaseEntity + { return InnerGetCopy(context, currentCopy, e => e.OriginalValues); } - public static DbEntityEntry GetEntry(this IDbContext context, T entity) where T : BaseEntity - { - var entry = CastOrThrow(context).Entry(entity); - return entry; - } - - public static bool IsPropertyModified(this IDbContext ctx, T entity, Expression> propertySelector) - where T : BaseEntity - { - object originalValue; - return TryGetModifiedProperty(ctx, entity, propertySelector, out originalValue); - } - - public static bool TryGetModifiedProperty(this IDbContext ctx, T entity, Expression> propertySelector, out object originalValue) - where T : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(propertySelector, nameof(propertySelector)); - - var propertyName = propertySelector.ExtractMemberInfo().Name; - return ctx.TryGetModifiedProperty(entity, propertyName, out originalValue); - } - - /// - /// Executes the DBCC SHRINKDATABASE(0) command against the SQL Server (Express) database - /// - /// The context - /// true, when the operation completed successfully. - public static bool ShrinkDatabase(this IDbContext context) - { - if (DataSettings.Current.IsSqlServer) - { - try - { - context.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); - return true; - } - catch { } - } - - return false; - } - - /// - /// Executes sql by using SQL-Server Management Objects which supports GO statements. - /// - public static int ExecuteSqlThroughSmo(this IDbContext ctx, string sql) - { - Guard.NotEmpty(sql, "sql"); - - int result = 0; - - try - { - bool isSqlServer = DataSettings.Current.IsSqlServer; - - if (!isSqlServer) - { - result = ctx.ExecuteSqlCommand(sql); - } - else - { - using (var sqlConnection = new SqlConnection(ObjectContextBase.GetConnectionString())) - { - var serverConnection = new ServerConnection(sqlConnection); - var server = new Server(serverConnection); - - result = server.ConnectionContext.ExecuteNonQuery(sql); - } - } - } - catch (Exception) - { - // remove the GO statements - sql = Regex.Replace(sql, @"\r{0,1}\n[Gg][Oo]\r{0,1}\n", "\n"); - - result = ctx.ExecuteSqlCommand(sql); - } - return result; - } - - #region Utils - - private static bool IsInSaveOperation(this IDbContext context) - { - return (context as ObjectContextBase)?.IsInSaveOperation == true; - } - - private static T InnerGetCopy(IDbContext context, T currentCopy, Func, DbPropertyValues> func) where T : BaseEntity - { - // Get the database context - var dbContext = CastOrThrow(context); - - // Get the entity tracking object - DbEntityEntry entry = GetEntityOrDefault(currentCopy, dbContext); - - // The output - T output = null; - - // Try and get the values - if (entry != null) - { - DbPropertyValues dbPropertyValues = func(entry); - if (dbPropertyValues != null) - { - output = dbPropertyValues.ToObject() as T; - } - } - - return output; - } - - /// - /// Gets the entity or return null. - /// - /// - /// The current copy. - /// The db context. - /// - private static DbEntityEntry GetEntityOrDefault(T currentCopy, DbContext dbContext) where T : BaseEntity - { - return dbContext.ChangeTracker.Entries().Where(e => e.Entity == currentCopy).FirstOrDefault(); - } - - private static DbContext CastOrThrow(IDbContext context) - { - return CastOrThrow(context); - } - - private static T CastOrThrow(IDbContext context) where T : DbContext - { - var dbContext = (context as T); - - if (dbContext == null) - { - throw new InvalidOperationException("Context does not support operation."); - } - - return dbContext; - } - - #endregion - } + public static DbEntityEntry GetEntry(this IDbContext context, T entity) where T : BaseEntity + { + var entry = CastOrThrow(context).Entry(entity); + return entry; + } + + public static bool IsPropertyModified(this IDbContext ctx, T entity, Expression> propertySelector) + where T : BaseEntity + { + object originalValue; + return TryGetModifiedProperty(ctx, entity, propertySelector, out originalValue); + } + + public static bool TryGetModifiedProperty(this IDbContext ctx, T entity, Expression> propertySelector, out object originalValue) + where T : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(propertySelector, nameof(propertySelector)); + + var propertyName = propertySelector.ExtractMemberInfo().Name; + return ctx.TryGetModifiedProperty(entity, propertyName, out originalValue); + } + + /// + /// Executes the DBCC SHRINKDATABASE(0) command against the SQL Server (Express) database + /// + /// The context + /// true, when the operation completed successfully. + public static bool ShrinkDatabase(this IDbContext context) + { + if (DataSettings.Current.IsSqlServer) + { + try + { + context.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); + return true; + } + catch { } + } + + return false; + } + + /// + /// Executes sql by using SQL-Server Management Objects which supports GO statements. + /// + public static int ExecuteSqlThroughSmo(this IDbContext ctx, string sql) + { + Guard.NotEmpty(sql, "sql"); + + int result = 0; + + try + { + bool isSqlServer = DataSettings.Current.IsSqlServer; + + if (!isSqlServer) + { + result = ctx.ExecuteSqlCommand(sql); + } + else + { + using (var sqlConnection = new SqlConnection(ObjectContextBase.GetConnectionString())) + { + var serverConnection = new ServerConnection(sqlConnection); + var server = new Server(serverConnection); + + result = server.ConnectionContext.ExecuteNonQuery(sql); + } + } + } + catch (Exception) + { + // remove the GO statements + sql = Regex.Replace(sql, @"\r{0,1}\n[Gg][Oo]\r{0,1}\n", "\n"); + + result = ctx.ExecuteSqlCommand(sql); + } + return result; + } + + #region Utils + + private static bool IsInSaveOperation(this IDbContext context) + { + return (context as ObjectContextBase)?.IsInSaveOperation == true; + } + + private static T InnerGetCopy(IDbContext context, T currentCopy, Func, DbPropertyValues> func) where T : BaseEntity + { + // Get the database context + var dbContext = CastOrThrow(context); + + // Get the entity tracking object + DbEntityEntry entry = GetEntityOrDefault(currentCopy, dbContext); + + // The output + T output = null; + + // Try and get the values + if (entry != null) + { + DbPropertyValues dbPropertyValues = func(entry); + if (dbPropertyValues != null) + { + output = dbPropertyValues.ToObject() as T; + } + } + + return output; + } + + /// + /// Gets the entity or return null. + /// + /// + /// The current copy. + /// The db context. + /// + private static DbEntityEntry GetEntityOrDefault(T currentCopy, DbContext dbContext) where T : BaseEntity + { + return dbContext.ChangeTracker.Entries().Where(e => e.Entity == currentCopy).FirstOrDefault(); + } + + private static DbContext CastOrThrow(IDbContext context) + { + return CastOrThrow(context); + } + + private static T CastOrThrow(IDbContext context) where T : DbContext + { + var dbContext = (context as T); + + if (dbContext == null) + { + throw new InvalidOperationException("Context does not support operation."); + } + + return dbContext; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs index 5feec4be90..2756eb5a5e 100644 --- a/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs @@ -2,11 +2,11 @@ namespace SmartStore { - public static class MiscExtensions - { - public static bool IsEntityFrameworkProvider(this IQueryProvider provider) - { - return provider.GetType().FullName == "System.Data.Objects.ELinq.ObjectQueryProvider"; - } - } + public static class MiscExtensions + { + public static bool IsEntityFrameworkProvider(this IQueryProvider provider) + { + return provider.GetType().FullName == "System.Data.Objects.ELinq.ObjectQueryProvider"; + } + } } diff --git a/src/Libraries/SmartStore.Data/IEfDataProvider.cs b/src/Libraries/SmartStore.Data/IEfDataProvider.cs index c0ebf1a63b..f6fc6e8cc7 100644 --- a/src/Libraries/SmartStore.Data/IEfDataProvider.cs +++ b/src/Libraries/SmartStore.Data/IEfDataProvider.cs @@ -1,5 +1,4 @@ -using System.Data.Entity; -using System.Data.Entity.Infrastructure; +using System.Data.Entity.Infrastructure; using SmartStore.Core.Data; namespace SmartStore.Data diff --git a/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogCommentMap.cs b/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogCommentMap.cs index 1372394cee..99f73ff900 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogCommentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogCommentMap.cs @@ -11,7 +11,7 @@ public BlogCommentMap() //commented because it's already configured by CustomerContentMap class //this.HasKey(pr => pr.Id); - this.Property(bc => bc.CommentText).IsMaxLength(); + this.Property(bc => bc.CommentText).IsMaxLength(); this.HasRequired(bc => bc.BlogPost) .WithMany(bp => bp.BlogComments) .HasForeignKey(bc => bc.BlogPostId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogPostMap.cs b/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogPostMap.cs index 3b29445fbe..bca23be8d1 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogPostMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Blogs/BlogPostMap.cs @@ -16,10 +16,10 @@ public BlogPostMap() Property(bp => bp.MediaFileId).HasColumnName("MediaFileId"); Property(bp => bp.PreviewMediaFileId).HasColumnName("PreviewMediaFileId"); - HasRequired(bp => bp.Language) + HasOptional(bp => bp.Language) .WithMany() .HasForeignKey(bp => bp.LanguageId) - .WillCascadeOnDelete(true); + .WillCascadeOnDelete(false); HasOptional(bp => bp.MediaFile) .WithMany() diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/BackInStockSubscriptionMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/BackInStockSubscriptionMap.cs index bcf2e9e4dc..3767cac539 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/BackInStockSubscriptionMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/BackInStockSubscriptionMap.cs @@ -9,12 +9,12 @@ public BackInStockSubscriptionMap() { this.ToTable("BackInStockSubscription"); this.HasKey(x => x.Id); - + this.HasRequired(x => x.Product) .WithMany() .HasForeignKey(x => x.ProductId) .WillCascadeOnDelete(true); - + this.HasRequired(x => x.Customer) .WithMany() .HasForeignKey(x => x.CustomerId) diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs index dbb05cc470..21705b06ae 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs @@ -10,20 +10,20 @@ public CategoryMap() ToTable("Category"); HasKey(c => c.Id); Property(c => c.Name).IsRequired().HasMaxLength(400); - Property(c => c.FullName).HasMaxLength(400); - Property(c => c.BottomDescription).IsMaxLength(); + Property(c => c.FullName).HasMaxLength(400); + Property(c => c.BottomDescription).IsMaxLength(); Property(c => c.ExternalLink).HasMaxLength(255).IsOptional(); Property(c => c.Description).IsMaxLength(); Property(c => c.MetaKeywords).HasMaxLength(400); Property(c => c.MetaTitle).HasMaxLength(400); - Property(c => c.PageSizeOptions).HasMaxLength(200).IsOptional(); - Property(c => c.Alias).HasMaxLength(100); + Property(c => c.PageSizeOptions).HasMaxLength(200).IsOptional(); + Property(c => c.Alias).HasMaxLength(100); Property(c => c.MediaFileId).HasColumnName("MediaFileId"); - HasOptional(p => p.MediaFile) - .WithMany() - .HasForeignKey(p => p.MediaFileId) - .WillCascadeOnDelete(false); + HasOptional(p => p.MediaFile) + .WithMany() + .HasForeignKey(p => p.MediaFileId) + .WillCascadeOnDelete(false); HasMany(d => d.RuleSets) .WithMany(rs => rs.Categories) diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ManufacturerMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ManufacturerMap.cs index 9597ae2201..f74f5cd16d 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ManufacturerMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ManufacturerMap.cs @@ -10,7 +10,7 @@ public ManufacturerMap() ToTable("Manufacturer"); HasKey(m => m.Id); Property(m => m.Name).IsRequired().HasMaxLength(400); - Property(m => m.Description).IsMaxLength(); + Property(m => m.Description).IsMaxLength(); Property(c => c.BottomDescription).IsMaxLength(); Property(m => m.MetaKeywords).HasMaxLength(400); Property(m => m.MetaTitle).HasMaxLength(400); @@ -18,9 +18,9 @@ public ManufacturerMap() Property(m => m.MediaFileId).HasColumnName("MediaFileId"); HasOptional(p => p.MediaFile) - .WithMany() - .HasForeignKey(p => p.MediaFileId) - .WillCascadeOnDelete(false); + .WithMany() + .HasForeignKey(p => p.MediaFileId) + .WillCascadeOnDelete(false); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs index 466c56ebd7..fafd737864 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs @@ -11,7 +11,7 @@ public ProductAttributeMap() HasKey(pa => pa.Id); Property(pa => pa.Alias).HasMaxLength(100); Property(pa => pa.Name).IsRequired(); - Property(pa => pa.ExportMappings).IsMaxLength(); - } + Property(pa => pa.ExportMappings).IsMaxLength(); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionMap.cs index 56f1335650..5580f04f00 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionMap.cs @@ -3,25 +3,25 @@ namespace SmartStore.Data.Mapping.Catalog { - public partial class ProductAttributeOptionMap : EntityTypeConfiguration - { - public ProductAttributeOptionMap() - { - ToTable("ProductAttributeOption"); - HasKey(x => x.Id); - Property(x => x.Name).HasMaxLength(4000); - Property(x => x.Alias).HasMaxLength(100); - Property(x => x.Color).HasMaxLength(100); - Property(x => x.MediaFileId).HasColumnName("MediaFileId"); + public partial class ProductAttributeOptionMap : EntityTypeConfiguration + { + public ProductAttributeOptionMap() + { + ToTable("ProductAttributeOption"); + HasKey(x => x.Id); + Property(x => x.Name).HasMaxLength(4000); + Property(x => x.Alias).HasMaxLength(100); + Property(x => x.Color).HasMaxLength(100); + Property(x => x.MediaFileId).HasColumnName("MediaFileId"); - Property(x => x.PriceAdjustment).HasPrecision(18, 4); - Property(x => x.WeightAdjustment).HasPrecision(18, 4); + Property(x => x.PriceAdjustment).HasPrecision(18, 4); + Property(x => x.WeightAdjustment).HasPrecision(18, 4); - Ignore(x => x.ValueType); + Ignore(x => x.ValueType); - HasRequired(x => x.ProductAttributeOptionsSet) - .WithMany(x => x.ProductAttributeOptions) - .HasForeignKey(x => x.ProductAttributeOptionsSetId); - } - } + HasRequired(x => x.ProductAttributeOptionsSet) + .WithMany(x => x.ProductAttributeOptions) + .HasForeignKey(x => x.ProductAttributeOptionsSetId); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionsSetMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionsSetMap.cs index e01b2224dc..e33b6081a5 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionsSetMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeOptionsSetMap.cs @@ -3,17 +3,17 @@ namespace SmartStore.Data.Mapping.Catalog { - public partial class ProductAttributeOptionsSetMap : EntityTypeConfiguration - { - public ProductAttributeOptionsSetMap() - { - ToTable("ProductAttributeOptionsSet"); - HasKey(x => x.Id); - Property(x => x.Name).HasMaxLength(400); + public partial class ProductAttributeOptionsSetMap : EntityTypeConfiguration + { + public ProductAttributeOptionsSetMap() + { + ToTable("ProductAttributeOptionsSet"); + HasKey(x => x.Id); + Property(x => x.Name).HasMaxLength(400); - HasRequired(x => x.ProductAttribute) - .WithMany(x => x.ProductAttributeOptionsSets) - .HasForeignKey(x => x.ProductAttributeId); - } - } + HasRequired(x => x.ProductAttribute) + .WithMany(x => x.ProductAttributeOptionsSets) + .HasForeignKey(x => x.ProductAttributeId); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemAttributeFilterMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemAttributeFilterMap.cs index f7297412db..7da5ef6420 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemAttributeFilterMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemAttributeFilterMap.cs @@ -3,17 +3,17 @@ namespace SmartStore.Data.Mapping.Catalog { - public partial class ProductBundleItemAttributeFilterMap : EntityTypeConfiguration - { - public ProductBundleItemAttributeFilterMap() - { - this.ToTable("ProductBundleItemAttributeFilter"); - this.HasKey(biaf => biaf.Id); + public partial class ProductBundleItemAttributeFilterMap : EntityTypeConfiguration + { + public ProductBundleItemAttributeFilterMap() + { + this.ToTable("ProductBundleItemAttributeFilter"); + this.HasKey(biaf => biaf.Id); - this.HasRequired(biaf => biaf.BundleItem) - .WithMany(pbi => pbi.AttributeFilters) - .HasForeignKey(biaf => biaf.BundleItemId) - .WillCascadeOnDelete(true); - } - } + this.HasRequired(biaf => biaf.BundleItem) + .WithMany(pbi => pbi.AttributeFilters) + .HasForeignKey(biaf => biaf.BundleItemId) + .WillCascadeOnDelete(true); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemMap.cs index 262883235b..0e4022315c 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductBundleItemMap.cs @@ -3,26 +3,26 @@ namespace SmartStore.Data.Mapping.Catalog { - public partial class ProductBundleItemMap : EntityTypeConfiguration - { - public ProductBundleItemMap() - { - this.ToTable("ProductBundleItem"); - this.HasKey(pbi => pbi.Id); + public partial class ProductBundleItemMap : EntityTypeConfiguration + { + public ProductBundleItemMap() + { + this.ToTable("ProductBundleItem"); + this.HasKey(pbi => pbi.Id); - this.Property(pbi => pbi.Discount).HasPrecision(18, 4).IsOptional(); - this.Property(pbi => pbi.Name).HasMaxLength(400); - this.Property(pbi => pbi.ShortDescription).IsMaxLength(); - - this.HasRequired(pbi => pbi.Product) - .WithMany() - .HasForeignKey(pbi => pbi.ProductId) - .WillCascadeOnDelete(false); // SQL Server does not support multiple cascade deletes + this.Property(pbi => pbi.Discount).HasPrecision(18, 4).IsOptional(); + this.Property(pbi => pbi.Name).HasMaxLength(400); + this.Property(pbi => pbi.ShortDescription).IsMaxLength(); - this.HasRequired(pbi => pbi.BundleProduct) - .WithMany(p => p.ProductBundleItems) - .HasForeignKey(pbi => pbi.BundleProductId) - .WillCascadeOnDelete(true); - } - } + this.HasRequired(pbi => pbi.Product) + .WithMany() + .HasForeignKey(pbi => pbi.ProductId) + .WillCascadeOnDelete(false); // SQL Server does not support multiple cascade deletes + + this.HasRequired(pbi => pbi.BundleProduct) + .WithMany(p => p.ProductBundleItems) + .HasForeignKey(pbi => pbi.BundleProductId) + .WillCascadeOnDelete(true); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductCategoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductCategoryMap.cs index e152d7c5ca..d4d26e1ff8 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductCategoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductCategoryMap.cs @@ -9,7 +9,7 @@ public ProductCategoryMap() { this.ToTable("Product_Category_Mapping"); this.HasKey(pc => pc.Id); - + this.HasRequired(pc => pc.Category) .WithMany() .HasForeignKey(pc => pc.CategoryId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductManufacturerMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductManufacturerMap.cs index bb903e023e..a707bed719 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductManufacturerMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductManufacturerMap.cs @@ -9,7 +9,7 @@ public ProductManufacturerMap() { this.ToTable("Product_Manufacturer_Mapping"); this.HasKey(pm => pm.Id); - + this.HasRequired(pm => pm.Manufacturer) .WithMany() .HasForeignKey(pm => pm.ManufacturerId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMap.cs index 042db6e7f2..387c86a13a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMap.cs @@ -14,66 +14,66 @@ public ProductMap() this.Property(p => p.MetaTitle).HasMaxLength(400); this.Property(p => p.FullDescription).IsMaxLength(); - this.Property(p => p.Sku).HasMaxLength(400); - this.Property(p => p.ManufacturerPartNumber).HasMaxLength(400); - this.Property(p => p.Gtin).HasMaxLength(400); - this.Property(p => p.AdditionalShippingCharge).HasPrecision(18, 4); - this.Property(p => p.Price).HasPrecision(18, 4); - this.Property(p => p.OldPrice).HasPrecision(18, 4); - this.Property(p => p.ProductCost).HasPrecision(18, 4); - this.Property(p => p.SpecialPrice).HasPrecision(18, 4); - this.Property(p => p.MinimumCustomerEnteredPrice).HasPrecision(18, 4); - this.Property(p => p.MaximumCustomerEnteredPrice).HasPrecision(18, 4); - this.Property(p => p.Weight).HasPrecision(18, 4); - this.Property(p => p.Length).HasPrecision(18, 4); - this.Property(p => p.Width).HasPrecision(18, 4); - this.Property(p => p.Height).HasPrecision(18, 4); - this.Property(p => p.LowestAttributeCombinationPrice).HasPrecision(18, 4); - this.Property(p => p.RequiredProductIds).HasMaxLength(1000); - this.Property(p => p.AllowedQuantities).HasMaxLength(1000); - this.Property(p => p.CustomsTariffNumber).HasMaxLength(30); - this.Property(p => p.SystemName).HasMaxLength(400); + this.Property(p => p.Sku).HasMaxLength(400); + this.Property(p => p.ManufacturerPartNumber).HasMaxLength(400); + this.Property(p => p.Gtin).HasMaxLength(400); + this.Property(p => p.AdditionalShippingCharge).HasPrecision(18, 4); + this.Property(p => p.Price).HasPrecision(18, 4); + this.Property(p => p.OldPrice).HasPrecision(18, 4); + this.Property(p => p.ProductCost).HasPrecision(18, 4); + this.Property(p => p.SpecialPrice).HasPrecision(18, 4); + this.Property(p => p.MinimumCustomerEnteredPrice).HasPrecision(18, 4); + this.Property(p => p.MaximumCustomerEnteredPrice).HasPrecision(18, 4); + this.Property(p => p.Weight).HasPrecision(18, 4); + this.Property(p => p.Length).HasPrecision(18, 4); + this.Property(p => p.Width).HasPrecision(18, 4); + this.Property(p => p.Height).HasPrecision(18, 4); + this.Property(p => p.LowestAttributeCombinationPrice).HasPrecision(18, 4); + this.Property(p => p.RequiredProductIds).HasMaxLength(1000); + this.Property(p => p.AllowedQuantities).HasMaxLength(1000); + this.Property(p => p.CustomsTariffNumber).HasMaxLength(30); + this.Property(p => p.SystemName).HasMaxLength(400); - this.HasOptional(p => p.DeliveryTime) - .WithMany() - .HasForeignKey(p => p.DeliveryTimeId) - .WillCascadeOnDelete(false); + this.HasOptional(p => p.DeliveryTime) + .WithMany() + .HasForeignKey(p => p.DeliveryTimeId) + .WillCascadeOnDelete(false); this.HasOptional(p => p.QuantityUnit) .WithMany() .HasForeignKey(p => p.QuantityUnitId) .WillCascadeOnDelete(false); - this.HasOptional(p => p.SampleDownload) - .WithMany() - .HasForeignKey(p => p.SampleDownloadId) - .WillCascadeOnDelete(false); + this.HasOptional(p => p.SampleDownload) + .WithMany() + .HasForeignKey(p => p.SampleDownloadId) + .WillCascadeOnDelete(false); - this.HasOptional(p => p.CountryOfOrigin) - .WithMany() - .HasForeignKey(p => p.CountryOfOriginId) - .WillCascadeOnDelete(false); + this.HasOptional(p => p.CountryOfOrigin) + .WithMany() + .HasForeignKey(p => p.CountryOfOriginId) + .WillCascadeOnDelete(false); - this.Ignore(p => p.ProductType); - this.Ignore(p => p.ProductTypeLabelHint); - this.Ignore(p => p.BackorderMode); - this.Ignore(p => p.DownloadActivationType); - this.Ignore(p => p.GiftCardType); - this.Ignore(p => p.LowStockActivity); - this.Ignore(p => p.ManageInventoryMethod); - this.Ignore(p => p.RecurringCyclePeriod); - this.Ignore(p => p.MergedDataIgnore); - this.Ignore(p => p.MergedDataValues); + this.Ignore(p => p.ProductType); + this.Ignore(p => p.ProductTypeLabelHint); + this.Ignore(p => p.BackorderMode); + this.Ignore(p => p.DownloadActivationType); + this.Ignore(p => p.GiftCardType); + this.Ignore(p => p.LowStockActivity); + this.Ignore(p => p.ManageInventoryMethod); + this.Ignore(p => p.RecurringCyclePeriod); + this.Ignore(p => p.MergedDataIgnore); + this.Ignore(p => p.MergedDataValues); - this.Property(p => p.BasePriceMeasureUnit).HasMaxLength(50); - this.Property(p => p.BasePriceAmount).HasPrecision(18, 4).IsOptional(); - this.Ignore(p => p.BasePriceHasValue); + this.Property(p => p.BasePriceMeasureUnit).HasMaxLength(50); + this.Property(p => p.BasePriceAmount).HasPrecision(18, 4).IsOptional(); + this.Ignore(p => p.BasePriceHasValue); - this.Property(p => p.BundleTitleText).HasMaxLength(400); + this.Property(p => p.BundleTitleText).HasMaxLength(400); - this.HasMany(p => p.ProductTags) - .WithMany(pt => pt.Products) - .Map(m => m.ToTable("Product_ProductTag_Mapping")); + this.HasMany(p => p.ProductTags) + .WithMany(pt => pt.Products) + .Map(m => m.ToTable("Product_ProductTag_Mapping")); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMediaFileMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMediaFileMap.cs index 641606bc7f..7996718c6c 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMediaFileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductMediaFileMap.cs @@ -10,7 +10,7 @@ public ProductMediaFileMap() ToTable("Product_MediaFile_Mapping"); HasKey(pp => pp.Id); Property(pp => pp.MediaFileId).HasColumnName("MediaFileId"); - + HasRequired(pp => pp.MediaFile) .WithMany(p => p.ProductMediaFiles) .HasForeignKey(pp => pp.MediaFileId) diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductReviewMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductReviewMap.cs index 4376c5d6aa..ce61d806b7 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductReviewMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductReviewMap.cs @@ -10,7 +10,7 @@ public ProductReviewMap() this.ToTable("ProductReview"); //commented because it's already configured by CustomerContentMap class //this.HasKey(pr => pr.Id); - this.Property(m => m.ReviewText).IsMaxLength(); + this.Property(m => m.ReviewText).IsMaxLength(); this.HasRequired(pr => pr.Product) .WithMany(p => p.ProductReviews) .HasForeignKey(pr => pr.ProductId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeCombinationMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeCombinationMap.cs index 48e08ab084..7e91dbe2a0 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeCombinationMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeCombinationMap.cs @@ -13,7 +13,7 @@ public ProductVariantAttributeCombinationMap() Property(pvac => pvac.Sku).HasMaxLength(400); Property(pvac => pvac.ManufacturerPartNumber).HasMaxLength(400); Property(pvac => pvac.Gtin).HasMaxLength(400); - Property(pvac => pvac.Price).HasPrecision(18, 4); + Property(pvac => pvac.Price).HasPrecision(18, 4); Property(pvac => pvac.AssignedMediaFileIds).HasMaxLength(1000).HasColumnName("AssignedMediaFileIds"); Property(pvac => pvac.Length).HasPrecision(18, 4); Property(pvac => pvac.Width).HasPrecision(18, 4); diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeMap.cs index 6f8e7d9ba5..8bc170faa6 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeMap.cs @@ -7,14 +7,14 @@ public partial class ProductVariantAttributeMap : EntityTypeConfiguration pva.Id); - this.Ignore(pva => pva.AttributeControlType); + this.Ignore(pva => pva.AttributeControlType); this.HasRequired(pva => pva.Product) .WithMany(pva => pva.ProductVariantAttributes) .HasForeignKey(pva => pva.ProductId); - + this.HasRequired(pva => pva.ProductAttribute) .WithMany() .HasForeignKey(pva => pva.ProductAttributeId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeValueMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeValueMap.cs index 90ed9cb47c..5bc9a202ff 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeValueMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductVariantAttributeValueMap.cs @@ -16,7 +16,7 @@ public ProductVariantAttributeValueMap() Property(pvav => pvav.PriceAdjustment).HasPrecision(18, 4); Property(pvav => pvav.WeightAdjustment).HasPrecision(18, 4); - Ignore(pvav => pvav.ValueType); + Ignore(pvav => pvav.ValueType); HasRequired(pvav => pvav.ProductVariantAttribute) .WithMany(pva => pva.ProductVariantAttributeValues) diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeMap.cs index 21a0500f6e..ed57c5b86d 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeMap.cs @@ -10,7 +10,7 @@ public SpecificationAttributeMap() this.ToTable("SpecificationAttribute"); this.HasKey(sa => sa.Id); this.Property(sa => sa.Name).IsRequired(); - this.Property(sa => sa.Alias).HasMaxLength(30); - } + this.Property(sa => sa.Alias).HasMaxLength(30); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeOptionMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeOptionMap.cs index ff6bfae506..a7f7d5a2f5 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeOptionMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/SpecificationAttributeOptionMap.cs @@ -3,20 +3,20 @@ namespace SmartStore.Data.Mapping.Catalog { - public partial class SpecificationAttributeOptionMap : EntityTypeConfiguration + public partial class SpecificationAttributeOptionMap : EntityTypeConfiguration { public SpecificationAttributeOptionMap() { ToTable("SpecificationAttributeOption"); HasKey(sao => sao.Id); Property(sao => sao.Name).IsRequired(); - Property(sao => sao.Alias).HasMaxLength(30); + Property(sao => sao.Alias).HasMaxLength(30); Property(sao => sao.Color).HasMaxLength(100); Property(sao => sao.MediaFileId).HasColumnName("MediaFileId"); Property(soa => soa.NumberValue).HasPrecision(18, 4); - HasRequired(sao => sao.SpecificationAttribute) + HasRequired(sao => sao.SpecificationAttribute) .WithMany(sa => sa.SpecificationAttributeOptions) .HasForeignKey(sao => sao.SpecificationAttributeId); } diff --git a/src/Libraries/SmartStore.Data/Mapping/Customers/CustomerMap.cs b/src/Libraries/SmartStore.Data/Mapping/Customers/CustomerMap.cs index 56c8708098..817228436a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Customers/CustomerMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Customers/CustomerMap.cs @@ -12,20 +12,20 @@ public CustomerMap() HasKey(c => c.Id); Property(u => u.Username).HasMaxLength(500); Property(u => u.Email).HasMaxLength(500); - Property(u => u.SystemName).HasMaxLength(500); - Property(u => u.Password).HasMaxLength(500); - Property(u => u.PasswordSalt).HasMaxLength(500); - Property(u => u.LastIpAddress).HasMaxLength(100); + Property(u => u.SystemName).HasMaxLength(500); + Property(u => u.Password).HasMaxLength(500); + Property(u => u.PasswordSalt).HasMaxLength(500); + Property(u => u.LastIpAddress).HasMaxLength(100); - Property(u => u.Title).HasMaxLength(100); - Property(u => u.Salutation).HasMaxLength(50); - Property(u => u.FirstName).HasMaxLength(225); - Property(u => u.LastName).HasMaxLength(225); - Property(u => u.FullName).HasMaxLength(450); - Property(u => u.Company).HasMaxLength(255); - Property(u => u.CustomerNumber).HasMaxLength(100); + Property(u => u.Title).HasMaxLength(100); + Property(u => u.Salutation).HasMaxLength(50); + Property(u => u.FirstName).HasMaxLength(225); + Property(u => u.LastName).HasMaxLength(225); + Property(u => u.FullName).HasMaxLength(450); + Property(u => u.Company).HasMaxLength(255); + Property(u => u.CustomerNumber).HasMaxLength(100); - Ignore(u => u.PasswordFormat); + Ignore(u => u.PasswordFormat); HasMany
(c => c.Addresses) .WithMany() diff --git a/src/Libraries/SmartStore.Data/Mapping/Customers/WalletHistoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Customers/WalletHistoryMap.cs index 1db98316dc..336620879c 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Customers/WalletHistoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Customers/WalletHistoryMap.cs @@ -3,27 +3,27 @@ namespace SmartStore.Data.Mapping.Customers { - public partial class WalletHistoryMap : EntityTypeConfiguration - { - public WalletHistoryMap() - { - ToTable("WalletHistory"); - HasKey(x => x.Id); + public partial class WalletHistoryMap : EntityTypeConfiguration + { + public WalletHistoryMap() + { + ToTable("WalletHistory"); + HasKey(x => x.Id); - Property(x => x.Amount).HasPrecision(18, 4); - Property(x => x.AmountBalance).HasPrecision(18, 4); - Property(x => x.AmountBalancePerStore).HasPrecision(18, 4); - Property(x => x.Message).HasMaxLength(1000); - Property(x => x.AdminComment).HasMaxLength(4000); + Property(x => x.Amount).HasPrecision(18, 4); + Property(x => x.AmountBalance).HasPrecision(18, 4); + Property(x => x.AmountBalancePerStore).HasPrecision(18, 4); + Property(x => x.Message).HasMaxLength(1000); + Property(x => x.AdminComment).HasMaxLength(4000); - HasRequired(x => x.Customer) - .WithMany(x => x.WalletHistory) - .HasForeignKey(x => x.CustomerId); + HasRequired(x => x.Customer) + .WithMany(x => x.WalletHistory) + .HasForeignKey(x => x.CustomerId); - HasOptional(x => x.Order) - .WithMany(x => x.WalletHistory) - .HasForeignKey(x => x.OrderId) - .WillCascadeOnDelete(false); - } - } + HasOptional(x => x.Order) + .WithMany(x => x.WalletHistory) + .HasForeignKey(x => x.OrderId) + .WillCascadeOnDelete(false); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs index 2c49aa7d7a..a850cb8fc6 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs @@ -3,29 +3,29 @@ namespace SmartStore.Data.Mapping.DataExchange { - public class ExportDeploymentMap : EntityTypeConfiguration - { - public ExportDeploymentMap() - { - this.ToTable("ExportDeployment"); - this.HasKey(x => x.Id); + public class ExportDeploymentMap : EntityTypeConfiguration + { + public ExportDeploymentMap() + { + this.ToTable("ExportDeployment"); + this.HasKey(x => x.Id); - this.Property(x => x.Name).IsRequired().HasMaxLength(100); - this.Property(x => x.ResultInfo).IsMaxLength(); - this.Property(x => x.Username).HasMaxLength(400); - this.Property(x => x.Password).HasMaxLength(400); - this.Property(x => x.Url).HasMaxLength(4000); - this.Property(x => x.FileSystemPath).HasMaxLength(400); - this.Property(x => x.SubFolder).HasMaxLength(400); - this.Property(x => x.EmailAddresses).HasMaxLength(4000); - this.Property(x => x.EmailSubject).HasMaxLength(400); + this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.ResultInfo).IsMaxLength(); + this.Property(x => x.Username).HasMaxLength(400); + this.Property(x => x.Password).HasMaxLength(400); + this.Property(x => x.Url).HasMaxLength(4000); + this.Property(x => x.FileSystemPath).HasMaxLength(400); + this.Property(x => x.SubFolder).HasMaxLength(400); + this.Property(x => x.EmailAddresses).HasMaxLength(4000); + this.Property(x => x.EmailSubject).HasMaxLength(400); - this.Ignore(x => x.DeploymentType); + this.Ignore(x => x.DeploymentType); - this.HasRequired(x => x.Profile) - .WithMany(x => x.Deployments) - .HasForeignKey(x => x.ProfileId) - .WillCascadeOnDelete(true); - } - } + this.HasRequired(x => x.Profile) + .WithMany(x => x.Deployments) + .HasForeignKey(x => x.ProfileId) + .WillCascadeOnDelete(true); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs index adacc3c4c6..87574080f1 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs @@ -3,28 +3,28 @@ namespace SmartStore.Data.Mapping.DataExchange { - public partial class ExportProfileMap : EntityTypeConfiguration - { - public ExportProfileMap() - { - this.ToTable("ExportProfile"); - this.HasKey(x => x.Id); + public partial class ExportProfileMap : EntityTypeConfiguration + { + public ExportProfileMap() + { + this.ToTable("ExportProfile"); + this.HasKey(x => x.Id); - this.Property(x => x.Name).IsRequired().HasMaxLength(100); - this.Property(x => x.FolderName).IsRequired().HasMaxLength(400); - this.Property(x => x.SystemName).HasMaxLength(400); - this.Property(x => x.ProviderSystemName).IsRequired().HasMaxLength(4000); - this.Property(x => x.Filtering).IsMaxLength(); - this.Property(x => x.Projection).IsMaxLength(); - this.Property(x => x.ProviderConfigData).IsMaxLength(); - this.Property(x => x.ResultInfo).IsMaxLength(); - this.Property(x => x.CompletedEmailAddresses).HasMaxLength(400); - this.Property(x => x.FileNamePattern).HasMaxLength(400); + this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(400); + this.Property(x => x.SystemName).HasMaxLength(400); + this.Property(x => x.ProviderSystemName).IsRequired().HasMaxLength(4000); + this.Property(x => x.Filtering).IsMaxLength(); + this.Property(x => x.Projection).IsMaxLength(); + this.Property(x => x.ProviderConfigData).IsMaxLength(); + this.Property(x => x.ResultInfo).IsMaxLength(); + this.Property(x => x.CompletedEmailAddresses).HasMaxLength(400); + this.Property(x => x.FileNamePattern).HasMaxLength(400); - this.HasRequired(x => x.ScheduleTask) - .WithMany() - .HasForeignKey(x => x.SchedulingTaskId) - .WillCascadeOnDelete(false); - } - } + this.HasRequired(x => x.ScheduleTask) + .WithMany() + .HasForeignKey(x => x.SchedulingTaskId) + .WillCascadeOnDelete(false); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs index 02515fd268..d2402d6303 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs @@ -3,28 +3,28 @@ namespace SmartStore.Data.Mapping.DataExchange { - public partial class ImportProfileMap : EntityTypeConfiguration - { - public ImportProfileMap() - { - this.ToTable("ImportProfile"); - this.HasKey(x => x.Id); + public partial class ImportProfileMap : EntityTypeConfiguration + { + public ImportProfileMap() + { + this.ToTable("ImportProfile"); + this.HasKey(x => x.Id); - this.Property(x => x.Name).IsRequired().HasMaxLength(100); - this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); - this.Property(x => x.KeyFieldNames).HasMaxLength(1000); - this.Property(x => x.FileTypeConfiguration).IsMaxLength(); - this.Property(x => x.ExtraData).IsMaxLength(); - this.Property(x => x.ColumnMapping).IsMaxLength(); - this.Property(x => x.ResultInfo).IsMaxLength(); + this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); + this.Property(x => x.KeyFieldNames).HasMaxLength(1000); + this.Property(x => x.FileTypeConfiguration).IsMaxLength(); + this.Property(x => x.ExtraData).IsMaxLength(); + this.Property(x => x.ColumnMapping).IsMaxLength(); + this.Property(x => x.ResultInfo).IsMaxLength(); - this.Ignore(x => x.FileType); - this.Ignore(x => x.EntityType); + this.Ignore(x => x.FileType); + this.Ignore(x => x.EntityType); - this.HasRequired(x => x.ScheduleTask) - .WithMany() - .HasForeignKey(x => x.SchedulingTaskId) - .WillCascadeOnDelete(false); - } - } + this.HasRequired(x => x.ScheduleTask) + .WithMany() + .HasForeignKey(x => x.SchedulingTaskId) + .WillCascadeOnDelete(false); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/SyncMappingMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/SyncMappingMap.cs index c5430692f0..498a39be7d 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/SyncMappingMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/SyncMappingMap.cs @@ -3,18 +3,18 @@ namespace SmartStore.Data.Mapping.DataExchange { - public partial class SyncMappingMap : EntityTypeConfiguration - { - public SyncMappingMap() - { - this.ToTable("SyncMapping"); - this.HasKey(x => x.Id); + public partial class SyncMappingMap : EntityTypeConfiguration + { + public SyncMappingMap() + { + this.ToTable("SyncMapping"); + this.HasKey(x => x.Id); - this.Property(x => x.EntityName).IsRequired().HasMaxLength(100); - this.Property(x => x.ContextName).IsRequired().HasMaxLength(100); - this.Property(x => x.SourceKey).IsRequired().HasMaxLength(150); - this.Property(x => x.SourceHash).HasMaxLength(40); - this.Property(x => x.CustomString).IsMaxLength(); - } - } + this.Property(x => x.EntityName).IsRequired().HasMaxLength(100); + this.Property(x => x.ContextName).IsRequired().HasMaxLength(100); + this.Property(x => x.SourceKey).IsRequired().HasMaxLength(150); + this.Property(x => x.SourceHash).HasMaxLength(40); + this.Property(x => x.CustomString).IsMaxLength(); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Directory/CountryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Directory/CountryMap.cs index 01d6ec5720..ba4ce8536b 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Directory/CountryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Directory/CountryMap.cs @@ -7,11 +7,16 @@ public partial class CountryMap : EntityTypeConfiguration { public CountryMap() { - this.ToTable("Country"); - this.HasKey(c =>c.Id); - this.Property(c => c.Name).IsRequired().HasMaxLength(100); - this.Property(c =>c.TwoLetterIsoCode).HasMaxLength(2); - this.Property(c =>c.ThreeLetterIsoCode).HasMaxLength(3); + ToTable("Country"); + HasKey(c => c.Id); + Property(c => c.Name).IsRequired().HasMaxLength(100); + Property(c => c.TwoLetterIsoCode).HasMaxLength(2); + Property(c => c.ThreeLetterIsoCode).HasMaxLength(3); + + HasOptional(x => x.DefaultCurrency) + .WithMany() + .HasForeignKey(x => x.DefaultCurrencyId) + .WillCascadeOnDelete(false); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs b/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs index b5f2dabb34..32a9ef1701 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs @@ -8,13 +8,13 @@ public partial class CurrencyMap : EntityTypeConfiguration public CurrencyMap() { this.ToTable("Currency"); - this.HasKey(c =>c.Id); + this.HasKey(c => c.Id); this.Property(c => c.Name).IsRequired().HasMaxLength(50); this.Property(c => c.CurrencyCode).IsRequired().HasMaxLength(5); this.Property(c => c.DisplayLocale).HasMaxLength(50); this.Property(c => c.CustomFormatting).HasMaxLength(50); - this.Property(c => c.Rate).HasPrecision(18, 8); // // With virtual currencies (e.g. BitCoin) being so precise, we need to store rates up to 8 decimal places - this.Property(c => c.DomainEndings).HasMaxLength(1000); + this.Property(c => c.Rate).HasPrecision(18, 8); // // With virtual currencies (e.g. BitCoin) being so precise, we need to store rates up to 8 decimal places + this.Property(c => c.DomainEndings).HasMaxLength(1000); this.Property(o => o.RoundOrderTotalDenominator).HasPrecision(18, 4); } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountMap.cs b/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountMap.cs index 58cf79999c..0136595340 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountMap.cs @@ -25,13 +25,13 @@ public DiscountMap() .WithMany(c => c.AppliedDiscounts) .Map(m => m.ToTable("Discount_AppliedToCategories")); - HasMany(dr => dr.AppliedToManufacturers) - .WithMany(x => x.AppliedDiscounts) - .Map(m => m.ToTable("Discount_AppliedToManufacturers")); + HasMany(dr => dr.AppliedToManufacturers) + .WithMany(x => x.AppliedDiscounts) + .Map(m => m.ToTable("Discount_AppliedToManufacturers")); - HasMany(dr => dr.AppliedToProducts) + HasMany(dr => dr.AppliedToProducts) .WithMany(p => p.AppliedDiscounts) - .Map(m => m.ToTable("Discount_AppliedToProducts")); + .Map(m => m.ToTable("Discount_AppliedToProducts")); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountUsageHistoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountUsageHistoryMap.cs index 82028ea8e9..2ef6b8b368 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountUsageHistoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Discounts/DiscountUsageHistoryMap.cs @@ -9,7 +9,7 @@ public DiscountUsageHistoryMap() { this.ToTable("DiscountUsageHistory"); this.HasKey(duh => duh.Id); - + this.HasRequired(duh => duh.Discount) .WithMany() .HasForeignKey(duh => duh.DiscountId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Forums/ForumGroupMap.cs b/src/Libraries/SmartStore.Data/Mapping/Forums/ForumGroupMap.cs index 6296b387d1..c838c6d893 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Forums/ForumGroupMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Forums/ForumGroupMap.cs @@ -10,7 +10,7 @@ public ForumGroupMap() this.ToTable("Forums_Group"); this.HasKey(fg => fg.Id); this.Property(fg => fg.Name).IsRequired().HasMaxLength(200); - this.Property(fg => fg.Description).IsMaxLength(); + this.Property(fg => fg.Description).IsMaxLength(); } } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Forums/ForumMap.cs b/src/Libraries/SmartStore.Data/Mapping/Forums/ForumMap.cs index 8d2f0d5db4..92ed155ac8 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Forums/ForumMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Forums/ForumMap.cs @@ -10,8 +10,8 @@ public ForumMap() this.ToTable("Forums_Forum"); this.HasKey(f => f.Id); this.Property(f => f.Name).IsRequired().HasMaxLength(200); - this.Property(f => f.Description).IsMaxLength(); - + this.Property(f => f.Description).IsMaxLength(); + this.HasRequired(f => f.ForumGroup) .WithMany(fg => fg.Forums) .HasForeignKey(f => f.ForumGroupId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Localization/LanguageMap.cs b/src/Libraries/SmartStore.Data/Mapping/Localization/LanguageMap.cs index 966095b639..ee9b04ec9a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Localization/LanguageMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Localization/LanguageMap.cs @@ -13,7 +13,7 @@ public LanguageMap() this.Property(l => l.LanguageCulture).IsRequired().HasMaxLength(20); this.Property(l => l.UniqueSeoCode).HasMaxLength(2); this.Property(l => l.FlagImageFileName).HasMaxLength(50); - + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Localization/LocalizedPropertyMap.cs b/src/Libraries/SmartStore.Data/Mapping/Localization/LocalizedPropertyMap.cs index bd223a711e..0caaa0f2cd 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Localization/LocalizedPropertyMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Localization/LocalizedPropertyMap.cs @@ -13,7 +13,7 @@ public LocalizedPropertyMap() this.Property(lp => lp.LocaleKeyGroup).IsRequired().HasMaxLength(400); this.Property(lp => lp.LocaleKey).IsRequired().HasMaxLength(400); this.Property(lp => lp.LocaleValue).IsRequired().IsMaxLength(); - + this.HasRequired(lp => lp.Language) .WithMany() .HasForeignKey(lp => lp.LanguageId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Logging/LogMap.cs b/src/Libraries/SmartStore.Data/Mapping/Logging/LogMap.cs index ec282bd4db..54c139fe45 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Logging/LogMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Logging/LogMap.cs @@ -10,16 +10,16 @@ public LogMap() this.ToTable("Log"); this.HasKey(l => l.Id); this.Property(l => l.ShortMessage).IsRequired().HasMaxLength(4000); - this.Property(l => l.Logger).IsRequired().HasMaxLength(400); - this.Property(l => l.FullMessage).IsMaxLength(); + this.Property(l => l.Logger).IsRequired().HasMaxLength(400); + this.Property(l => l.FullMessage).IsMaxLength(); this.Property(l => l.IpAddress).HasMaxLength(200); - this.Property(l => l.UserName).HasMaxLength(100); - this.Property(l => l.HttpMethod).HasMaxLength(10); - this.Property(l => l.PageUrl).HasMaxLength(1500); - this.Property(l => l.ReferrerUrl).HasMaxLength(1500); + this.Property(l => l.UserName).HasMaxLength(100); + this.Property(l => l.HttpMethod).HasMaxLength(10); + this.Property(l => l.PageUrl).HasMaxLength(1500); + this.Property(l => l.ReferrerUrl).HasMaxLength(1500); + + this.Ignore(l => l.LogLevel); - this.Ignore(l => l.LogLevel); - this.HasOptional(l => l.Customer) .WithMany() .HasForeignKey(l => l.CustomerId) diff --git a/src/Libraries/SmartStore.Data/Mapping/Media/DownloadMap.cs b/src/Libraries/SmartStore.Data/Mapping/Media/DownloadMap.cs index a939bb913e..2b94e64c0c 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Media/DownloadMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Media/DownloadMap.cs @@ -14,6 +14,6 @@ public DownloadMap() .WithMany() .HasForeignKey(x => x.MediaFileId) .WillCascadeOnDelete(false); - } + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Media/MediaFileMap.cs b/src/Libraries/SmartStore.Data/Mapping/Media/MediaFileMap.cs index 52f0885448..e95f99ddeb 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Media/MediaFileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Media/MediaFileMap.cs @@ -18,24 +18,15 @@ public MediaFileMap() Property(x => x.Title).HasMaxLength(400); Property(x => x.Metadata).IsMaxLength(); - HasIndex(x => x.Name).HasName("IX_Name"); - HasIndex(x => x.Alt).HasName("IX_Alt"); - HasIndex(x => x.Extension).HasName("IX_Extension"); - HasIndex(x => x.MimeType).HasName("IX_MimeType"); - HasIndex(x => x.MediaType).HasName("IX_MediaType"); - HasIndex(x => x.Size).HasName("IX_Size"); - HasIndex(x => x.PixelSize).HasName("IX_PixelSize"); - HasIndex(x => x.Deleted).HasName("IX_Deleted"); - HasOptional(x => x.Folder) .WithMany(x => x.Files) .HasForeignKey(x => x.FolderId) .WillCascadeOnDelete(false); HasOptional(x => x.MediaStorage) - .WithMany() - .HasForeignKey(x => x.MediaStorageId) - .WillCascadeOnDelete(false); + .WithMany() + .HasForeignKey(x => x.MediaStorageId) + .WillCascadeOnDelete(false); HasMany(x => x.Tags) .WithMany(t => t.MediaFiles) diff --git a/src/Libraries/SmartStore.Data/Mapping/Media/MediaFolderMap.cs b/src/Libraries/SmartStore.Data/Mapping/Media/MediaFolderMap.cs index 7b0b12b6cf..3873f15a54 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Media/MediaFolderMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Media/MediaFolderMap.cs @@ -1,5 +1,4 @@ -using System; -using System.Data.Entity.ModelConfiguration; +using System.Data.Entity.ModelConfiguration; using SmartStore.Core.Domain.Media; namespace SmartStore.Data.Mapping.Media diff --git a/src/Libraries/SmartStore.Data/Mapping/Media/MediaStorageMap.cs b/src/Libraries/SmartStore.Data/Mapping/Media/MediaStorageMap.cs index a1d68963d3..eaa13b5aac 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Media/MediaStorageMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Media/MediaStorageMap.cs @@ -3,13 +3,13 @@ namespace SmartStore.Data.Mapping.Media { - public partial class MediaStorageMap : EntityTypeConfiguration - { - public MediaStorageMap() - { - ToTable("MediaStorage"); - HasKey(x => x.Id); - Property(x => x.Data).IsRequired().IsMaxLength(); - } - } + public partial class MediaStorageMap : EntityTypeConfiguration + { + public MediaStorageMap() + { + ToTable("MediaStorage"); + HasKey(x => x.Id); + Property(x => x.Data).IsRequired().IsMaxLength(); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Media/MediaTagMap.cs b/src/Libraries/SmartStore.Data/Mapping/Media/MediaTagMap.cs index b6c478df11..8a3faf1072 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Media/MediaTagMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Media/MediaTagMap.cs @@ -1,5 +1,4 @@ -using System; -using System.Data.Entity.ModelConfiguration; +using System.Data.Entity.ModelConfiguration; using SmartStore.Core.Domain.Media; namespace SmartStore.Data.Mapping.Media diff --git a/src/Libraries/SmartStore.Data/Mapping/Media/MediaTrackMap.cs b/src/Libraries/SmartStore.Data/Mapping/Media/MediaTrackMap.cs index 6ab7ddf2ab..ec2f551d08 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Media/MediaTrackMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Media/MediaTrackMap.cs @@ -1,5 +1,4 @@ -using System; -using System.Data.Entity.ModelConfiguration; +using System.Data.Entity.ModelConfiguration; using SmartStore.Core.Domain.Media; namespace SmartStore.Data.Mapping.Media diff --git a/src/Libraries/SmartStore.Data/Mapping/Messages/MessageTemplateMap.cs b/src/Libraries/SmartStore.Data/Mapping/Messages/MessageTemplateMap.cs index aa12ec200f..67d301bc0c 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Messages/MessageTemplateMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Messages/MessageTemplateMap.cs @@ -11,7 +11,7 @@ public MessageTemplateMap() this.HasKey(mt => mt.Id); this.Property(mt => mt.Name).IsRequired().HasMaxLength(200); - this.Property(mt => mt.Body).IsMaxLength(); + this.Property(mt => mt.Body).IsMaxLength(); this.Property(mt => mt.BccEmailAddresses).HasMaxLength(200); this.Property(mt => mt.Subject).HasMaxLength(1000); this.Property(mt => mt.EmailAccountId).IsRequired(); diff --git a/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailAttachmentMap.cs b/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailAttachmentMap.cs index f8f9389a5a..8ac9b15950 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailAttachmentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailAttachmentMap.cs @@ -3,32 +3,32 @@ namespace SmartStore.Data.Mapping.Messages { - public class QueuedEmailAttachmentMap : EntityTypeConfiguration - { - public QueuedEmailAttachmentMap() - { - ToTable("QueuedEmailAttachment"); - HasKey(x => x.Id); + public class QueuedEmailAttachmentMap : EntityTypeConfiguration + { + public QueuedEmailAttachmentMap() + { + ToTable("QueuedEmailAttachment"); + HasKey(x => x.Id); - Property(x => x.Path).IsOptional().HasMaxLength(1000); + Property(x => x.Path).IsOptional().HasMaxLength(1000); - Property(x => x.Name).IsRequired().HasMaxLength(200); - Property(x => x.MimeType).IsRequired().HasMaxLength(200); + Property(x => x.Name).IsRequired().HasMaxLength(200); + Property(x => x.MimeType).IsRequired().HasMaxLength(200); - HasRequired(x => x.QueuedEmail) - .WithMany(qe => qe.Attachments) - .HasForeignKey(x => x.QueuedEmailId) - .WillCascadeOnDelete(true); + HasRequired(x => x.QueuedEmail) + .WithMany(qe => qe.Attachments) + .HasForeignKey(x => x.QueuedEmailId) + .WillCascadeOnDelete(true); - HasOptional(x => x.MediaFile) - .WithMany() - .HasForeignKey(x => x.MediaFileId) - .WillCascadeOnDelete(false); + HasOptional(x => x.MediaFile) + .WithMany() + .HasForeignKey(x => x.MediaFileId) + .WillCascadeOnDelete(false); - HasOptional(x => x.MediaStorage) - .WithMany() - .HasForeignKey(x => x.MediaStorageId) - .WillCascadeOnDelete(true); - } - } + HasOptional(x => x.MediaStorage) + .WithMany() + .HasForeignKey(x => x.MediaStorageId) + .WillCascadeOnDelete(true); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs b/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs index a7cfe9ea32..9ef6a7ebe2 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs @@ -12,7 +12,7 @@ public QueuedEmailMap() this.Property(qe => qe.From).IsRequired().HasMaxLength(500); this.Property(qe => qe.To).IsRequired().HasMaxLength(500); - this.Property(qe => qe.ReplyTo).HasMaxLength(500); + this.Property(qe => qe.ReplyTo).HasMaxLength(500); this.Property(qe => qe.CC).HasMaxLength(500); this.Property(qe => qe.Bcc).HasMaxLength(500); this.Property(qe => qe.Subject).HasMaxLength(1000); diff --git a/src/Libraries/SmartStore.Data/Mapping/News/NewsCommentMap.cs b/src/Libraries/SmartStore.Data/Mapping/News/NewsCommentMap.cs index a0296203ae..13317df3ff 100644 --- a/src/Libraries/SmartStore.Data/Mapping/News/NewsCommentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/News/NewsCommentMap.cs @@ -10,7 +10,7 @@ public NewsCommentMap() this.ToTable("NewsComment"); //commented because it's already configured by CustomerContentMap class //this.HasKey(pr => pr.Id); - this.Property(nc => nc.CommentText).IsMaxLength(); + this.Property(nc => nc.CommentText).IsMaxLength(); this.HasRequired(nc => nc.NewsItem) .WithMany(n => n.NewsComments) .HasForeignKey(nc => nc.NewsItemId); diff --git a/src/Libraries/SmartStore.Data/Mapping/News/NewsItemMap.cs b/src/Libraries/SmartStore.Data/Mapping/News/NewsItemMap.cs index 745cebddad..5e99db7272 100644 --- a/src/Libraries/SmartStore.Data/Mapping/News/NewsItemMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/News/NewsItemMap.cs @@ -17,9 +17,10 @@ public NewsItemMap() Property(x => x.MediaFileId).HasColumnName("MediaFileId"); Property(x => x.PreviewMediaFileId).HasColumnName("PreviewMediaFileId"); - HasRequired(x => x.Language) + HasOptional(x => x.Language) .WithMany() - .HasForeignKey(x => x.LanguageId).WillCascadeOnDelete(true); + .HasForeignKey(x => x.LanguageId) + .WillCascadeOnDelete(false); HasOptional(bp => bp.MediaFile) .WithMany() diff --git a/src/Libraries/SmartStore.Data/Mapping/Orders/CheckoutAttributeValueMap.cs b/src/Libraries/SmartStore.Data/Mapping/Orders/CheckoutAttributeValueMap.cs index ea65bca068..bab207962c 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Orders/CheckoutAttributeValueMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Orders/CheckoutAttributeValueMap.cs @@ -7,13 +7,18 @@ public partial class CheckoutAttributeValueMap : EntityTypeConfiguration cav.Id); - this.Property(cav => cav.Name).IsRequired().HasMaxLength(400); - this.Property(cav => cav.PriceAdjustment).HasPrecision(18, 4); - this.Property(cav => cav.WeightAdjustment).HasPrecision(18, 4); + ToTable("CheckoutAttributeValue"); + HasKey(cav => cav.Id); + Property(cav => cav.Name).IsRequired().HasMaxLength(400); + Property(cav => cav.PriceAdjustment).HasPrecision(18, 4); + Property(cav => cav.WeightAdjustment).HasPrecision(18, 4); - this.HasRequired(cav => cav.CheckoutAttribute) + HasOptional(x => x.MediaFile) + .WithMany() + .HasForeignKey(x => x.MediaFileId) + .WillCascadeOnDelete(false); + + HasRequired(cav => cav.CheckoutAttribute) .WithMany(ca => ca.CheckoutAttributeValues) .HasForeignKey(cav => cav.CheckoutAttributeId); } diff --git a/src/Libraries/SmartStore.Data/Mapping/Orders/OrderItemMap.cs b/src/Libraries/SmartStore.Data/Mapping/Orders/OrderItemMap.cs index ea68047512..f07ec73c2b 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Orders/OrderItemMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Orders/OrderItemMap.cs @@ -7,21 +7,21 @@ public partial class OrderItemMap : EntityTypeConfiguration { public OrderItemMap() { - this.ToTable("OrderItem"); + this.ToTable("OrderItem"); this.HasKey(orderItem => orderItem.Id); this.Property(orderItem => orderItem.AttributeDescription); this.Property(orderItem => orderItem.AttributesXml).IsMaxLength(); - this.Property(orderItem => orderItem.BundleData).IsMaxLength(); + this.Property(orderItem => orderItem.BundleData).IsMaxLength(); this.Property(orderItem => orderItem.UnitPriceInclTax).HasPrecision(18, 4); this.Property(orderItem => orderItem.UnitPriceExclTax).HasPrecision(18, 4); this.Property(orderItem => orderItem.PriceInclTax).HasPrecision(18, 4); this.Property(orderItem => orderItem.PriceExclTax).HasPrecision(18, 4); - this.Property(orderItem => orderItem.TaxRate).HasPrecision(18, 4); + this.Property(orderItem => orderItem.TaxRate).HasPrecision(18, 4); this.Property(orderItem => orderItem.DiscountAmountInclTax).HasPrecision(18, 4); this.Property(orderItem => orderItem.DiscountAmountExclTax).HasPrecision(18, 4); this.Property(orderItem => orderItem.ItemWeight).HasPrecision(18, 4); - this.Property(orderItem => orderItem.ProductCost).HasPrecision(18, 4); + this.Property(orderItem => orderItem.ProductCost).HasPrecision(18, 4); this.HasRequired(orderItem => orderItem.Order) .WithMany(o => o.OrderItems) diff --git a/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs b/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs index a0378d4303..43d7722631 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs @@ -16,24 +16,24 @@ public OrderMap() this.Property(o => o.OrderSubTotalDiscountExclTax).HasPrecision(18, 4); this.Property(o => o.OrderShippingInclTax).HasPrecision(18, 4); this.Property(o => o.OrderShippingExclTax).HasPrecision(18, 4); - this.Property(o => o.OrderShippingTaxRate).HasPrecision(18, 4); + this.Property(o => o.OrderShippingTaxRate).HasPrecision(18, 4); this.Property(o => o.PaymentMethodAdditionalFeeInclTax).HasPrecision(18, 4); this.Property(o => o.PaymentMethodAdditionalFeeExclTax).HasPrecision(18, 4); - this.Property(o => o.PaymentMethodAdditionalFeeTaxRate).HasPrecision(18, 4); + this.Property(o => o.PaymentMethodAdditionalFeeTaxRate).HasPrecision(18, 4); this.Property(o => o.OrderTax).HasPrecision(18, 4); this.Property(o => o.OrderDiscount).HasPrecision(18, 4); - this.Property(o => o.CreditBalance).HasPrecision(18, 4); - this.Property(o => o.OrderTotalRounding).HasPrecision(18, 4); + this.Property(o => o.CreditBalance).HasPrecision(18, 4); + this.Property(o => o.OrderTotalRounding).HasPrecision(18, 4); this.Property(o => o.OrderTotal).HasPrecision(18, 4); this.Property(o => o.RefundedAmount).HasPrecision(18, 4); - this.Property(o => o.OrderNumber).IsOptional(); + this.Property(o => o.OrderNumber).IsOptional(); this.Ignore(o => o.OrderStatus); this.Ignore(o => o.PaymentStatus); this.Ignore(o => o.ShippingStatus); this.Ignore(o => o.CustomerTaxDisplayType); this.Ignore(o => o.TaxRatesDictionary); - + this.HasRequired(o => o.Customer) .WithMany(c => c.Orders) .HasForeignKey(o => o.CustomerId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Orders/ReturnRequestMap.cs b/src/Libraries/SmartStore.Data/Mapping/Orders/ReturnRequestMap.cs index 5f963c41b0..67776065ba 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Orders/ReturnRequestMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Orders/ReturnRequestMap.cs @@ -11,7 +11,7 @@ public ReturnRequestMap() this.HasKey(rr => rr.Id); this.Property(rr => rr.ReasonForReturn).IsRequired(); this.Property(rr => rr.RequestedAction).IsRequired(); - this.Property(rr => rr.AdminComment).HasMaxLength(4000); + this.Property(rr => rr.AdminComment).HasMaxLength(4000); this.Ignore(rr => rr.ReturnRequestStatus); diff --git a/src/Libraries/SmartStore.Data/Mapping/Orders/ShoppingCartItemMap.cs b/src/Libraries/SmartStore.Data/Mapping/Orders/ShoppingCartItemMap.cs index 2fb5426404..661c658e1d 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Orders/ShoppingCartItemMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Orders/ShoppingCartItemMap.cs @@ -11,7 +11,7 @@ public ShoppingCartItemMap() this.HasKey(sci => sci.Id); this.Property(sci => sci.CustomerEnteredPrice).HasPrecision(18, 4); - this.Property(sci => sci.AttributesXml).IsMaxLength(); + this.Property(sci => sci.AttributesXml).IsMaxLength(); this.Ignore(sci => sci.ShoppingCartType); this.Ignore(sci => sci.IsFreeShipping); @@ -26,10 +26,10 @@ public ShoppingCartItemMap() .WithMany() .HasForeignKey(sci => sci.ProductId); - this.HasOptional(sci => sci.BundleItem) - .WithMany() - .HasForeignKey(sci => sci.BundleItemId) - .WillCascadeOnDelete(false); + this.HasOptional(sci => sci.BundleItem) + .WithMany() + .HasForeignKey(sci => sci.BundleItemId) + .WillCascadeOnDelete(false); } } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs b/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs index 279027c86d..01675e8f87 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs @@ -4,19 +4,19 @@ namespace SmartStore.Data.Mapping.Payments { public partial class PaymentMethodMap : EntityTypeConfiguration - { - public PaymentMethodMap() - { - ToTable("PaymentMethod"); - HasKey(x => x.Id); + { + public PaymentMethodMap() + { + ToTable("PaymentMethod"); + HasKey(x => x.Id); - Property(x => x.PaymentMethodSystemName).IsRequired().HasMaxLength(4000); + Property(x => x.PaymentMethodSystemName).IsRequired().HasMaxLength(4000); - Property(x => x.FullDescription).HasMaxLength(4000); + Property(x => x.FullDescription).HasMaxLength(4000); HasMany(pm => pm.RuleSets) .WithMany(rs => rs.PaymentMethods) .Map(m => m.ToTable("RuleSet_PaymentMethod_Mapping")); } - } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Polls/PollMap.cs b/src/Libraries/SmartStore.Data/Mapping/Polls/PollMap.cs index f647b338d3..1f2eaf2e54 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Polls/PollMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Polls/PollMap.cs @@ -10,7 +10,7 @@ public PollMap() this.ToTable("Poll"); this.HasKey(p => p.Id); this.Property(p => p.Name).IsRequired(); - + this.HasRequired(p => p.Language) .WithMany() .HasForeignKey(p => p.LanguageId).WillCascadeOnDelete(true); diff --git a/src/Libraries/SmartStore.Data/Mapping/Security/AclRecordMap.cs b/src/Libraries/SmartStore.Data/Mapping/Security/AclRecordMap.cs index 6604508ab4..7c3f705d9a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Security/AclRecordMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Security/AclRecordMap.cs @@ -11,12 +11,12 @@ public AclRecordMap() this.HasKey(lp => lp.Id); this.Property(lp => lp.EntityName).IsRequired().HasMaxLength(400); - this.Property(x => x.IsIdle).IsRequired(); + this.Property(x => x.IsIdle).IsRequired(); - this.HasRequired(tp => tp.CustomerRole) - .WithMany() - .HasForeignKey(tp => tp.CustomerRoleId) - .WillCascadeOnDelete(true); + this.HasRequired(tp => tp.CustomerRole) + .WithMany() + .HasForeignKey(tp => tp.CustomerRoleId) + .WillCascadeOnDelete(true); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentItemMap.cs b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentItemMap.cs index 8a32be4a89..e8108ae002 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentItemMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentItemMap.cs @@ -7,7 +7,7 @@ public partial class ShipmentItemMap : EntityTypeConfiguration { public ShipmentItemMap() { - this.ToTable("ShipmentItem"); + this.ToTable("ShipmentItem"); this.HasKey(si => si.Id); this.HasRequired(si => si.Shipment) diff --git a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentMap.cs b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentMap.cs index 6ce4aeb5d7..056cd938de 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShipmentMap.cs @@ -11,7 +11,7 @@ public ShipmentMap() this.HasKey(s => s.Id); this.Property(s => s.TotalWeight).HasPrecision(18, 4); - + this.HasRequired(s => s.Order) .WithMany(o => o.Shipments) .HasForeignKey(s => s.OrderId); diff --git a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs index 61bf4ddd67..457c769e63 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs @@ -3,7 +3,7 @@ namespace SmartStore.Data.Mapping.Shipping { - public class ShippingMethodMap : EntityTypeConfiguration + public class ShippingMethodMap : EntityTypeConfiguration { public ShippingMethodMap() { diff --git a/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMap.cs b/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMap.cs index 3cb0da6402..8691c9755d 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMap.cs @@ -3,28 +3,28 @@ namespace SmartStore.Data.Mapping.Stores { - public partial class StoreMap : EntityTypeConfiguration - { - public StoreMap() - { - ToTable("Store"); - HasKey(s => s.Id); - Property(s => s.Name).IsRequired().HasMaxLength(400); - Property(s => s.Url).IsRequired().HasMaxLength(400); - Property(s => s.ContentDeliveryNetwork).HasMaxLength(400); - Property(s => s.SecureUrl).HasMaxLength(400); - Property(s => s.Hosts).HasMaxLength(1000); - Property(x => x.LogoMediaFileId).HasColumnName("LogoMediaFileId"); + public partial class StoreMap : EntityTypeConfiguration + { + public StoreMap() + { + ToTable("Store"); + HasKey(s => s.Id); + Property(s => s.Name).IsRequired().HasMaxLength(400); + Property(s => s.Url).IsRequired().HasMaxLength(400); + Property(s => s.ContentDeliveryNetwork).HasMaxLength(400); + Property(s => s.SecureUrl).HasMaxLength(400); + Property(s => s.Hosts).HasMaxLength(1000); + Property(x => x.LogoMediaFileId).HasColumnName("LogoMediaFileId"); - HasRequired(s => s.PrimaryStoreCurrency) - .WithMany() - .HasForeignKey(s => s.PrimaryStoreCurrencyId) - .WillCascadeOnDelete(false); + HasRequired(s => s.PrimaryStoreCurrency) + .WithMany() + .HasForeignKey(s => s.PrimaryStoreCurrencyId) + .WillCascadeOnDelete(false); - HasRequired(s => s.PrimaryExchangeRateCurrency) - .WithMany() - .HasForeignKey(s => s.PrimaryExchangeRateCurrencyId) - .WillCascadeOnDelete(false); - } - } + HasRequired(s => s.PrimaryExchangeRateCurrency) + .WithMany() + .HasForeignKey(s => s.PrimaryExchangeRateCurrencyId) + .WillCascadeOnDelete(false); + } + } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMappingMap.cs b/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMappingMap.cs index bea098ab0c..9d98b89f75 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMappingMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Stores/StoreMappingMap.cs @@ -3,14 +3,14 @@ namespace SmartStore.Data.Mapping.Stores { - public partial class StoreMappingMap : EntityTypeConfiguration - { - public StoreMappingMap() - { - this.ToTable("StoreMapping"); - this.HasKey(sm => sm.Id); + public partial class StoreMappingMap : EntityTypeConfiguration + { + public StoreMappingMap() + { + this.ToTable("StoreMapping"); + this.HasKey(sm => sm.Id); - this.Property(sm => sm.EntityName).IsRequired().HasMaxLength(400); - } - } + this.Property(sm => sm.EntityName).IsRequired().HasMaxLength(400); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs index 817cabd4e6..c582f53566 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskHistoryMap.cs @@ -11,7 +11,7 @@ public ScheduleTaskHistoryMap() HasKey(x => x.Id); Property(x => x.MachineName).IsRequired().HasMaxLength(400); Property(x => x.Error); - Property(x => x.ProgressMessage).HasMaxLength(1000); + Property(x => x.ProgressMessage).HasMaxLength(1000); HasRequired(x => x.ScheduleTask) .WithMany(x => x.ScheduleTaskHistory) diff --git a/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskMap.cs b/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskMap.cs index c3854ec515..1e3c8f8818 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Tasks/ScheduleTaskMap.cs @@ -11,10 +11,10 @@ public ScheduleTaskMap() HasKey(t => t.Id); Property(t => t.Name).HasMaxLength(500).IsRequired(); Property(t => t.Type).HasMaxLength(800).IsRequired(); - Property(t => t.Alias).HasMaxLength(500); - Property(t => t.CronExpression).HasMaxLength(1000); + Property(t => t.Alias).HasMaxLength(500); + Property(t => t.CronExpression).HasMaxLength(1000); - Ignore(t => t.IsPending); + Ignore(t => t.IsPending); Ignore(t => t.LastHistoryEntry); } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Themes/ThemeVariableMap.cs b/src/Libraries/SmartStore.Data/Mapping/Themes/ThemeVariableMap.cs index 03be4cf827..bab6575777 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Themes/ThemeVariableMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Themes/ThemeVariableMap.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; - + using System.Data.Entity.ModelConfiguration; using SmartStore.Core.Domain.Themes; namespace SmartStore.Data.Mapping.Themes { - + public class ThemeVariableMap : EntityTypeConfiguration { public ThemeVariableMap() - { + { this.ToTable("ThemeVariable"); this.HasKey(t => t.Id); this.Property(t => t.Theme).HasMaxLength(400); diff --git a/src/Libraries/SmartStore.Data/Mapping/Topics/TopicMap.cs b/src/Libraries/SmartStore.Data/Mapping/Topics/TopicMap.cs index 4e2af6ab58..acc15e4e16 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Topics/TopicMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Topics/TopicMap.cs @@ -9,10 +9,10 @@ public TopicMap() { this.ToTable("Topic"); this.HasKey(t => t.Id); - this.Property(t => t.ShortTitle).HasMaxLength(50); - this.Property(t => t.Intro).HasMaxLength(255); - this.Property(t => t.Body).IsMaxLength(); - //this.Property(t => t.IsPublished).HasColumnAnnotation("defaultValue", true); - } + this.Property(t => t.ShortTitle).HasMaxLength(50); + this.Property(t => t.Intro).HasMaxLength(255); + this.Property(t => t.Body).IsMaxLength(); + //this.Property(t => t.IsPublished).HasColumnAnnotation("defaultValue", true); + } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201705281903241_MoreIndexes.cs b/src/Libraries/SmartStore.Data/Migrations/201705281903241_MoreIndexes.cs index b487861c48..5f2db2e10f 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201705281903241_MoreIndexes.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201705281903241_MoreIndexes.cs @@ -1,58 +1,58 @@ namespace SmartStore.Data.Migrations { - using System.Data.Entity.Migrations; - using System.Web.Hosting; - using Core.Data; + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using Core.Data; using SmartStore.Data.Setup; public partial class MoreIndexes : DbMigration - { - public override void Up() - { - if (!DbMigrationContext.Current.SuppressInitialCreate()) + { + public override void Up() + { + if (!DbMigrationContext.Current.SuppressInitialCreate()) { UpInitial(); } - + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) - { - // Avoid "Column 'Name' in table 'dbo.ProductVariantAttributeValue' is of a type that is invalid for use as a key column in an index". - Sql("If -1 = (SELECT CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'ProductVariantAttributeValue' AND COLUMN_NAME = 'Name') ALTER TABLE [dbo].[ProductVariantAttributeValue] ALTER COLUMN [Name] nvarchar(4000) NULL;"); - } - - CreateIndex("dbo.Product_Category_Mapping", "IsFeaturedProduct"); - CreateIndex("dbo.Product_Manufacturer_Mapping", "IsFeaturedProduct"); - CreateIndex("dbo.SpecificationAttribute", "AllowFiltering"); - CreateIndex("dbo.Product_ProductAttribute_Mapping", "AttributeControlTypeId"); - CreateIndex("dbo.ProductAttribute", "AllowFiltering"); - CreateIndex("dbo.ProductVariantAttributeValue", "ValueTypeId"); + { + // Avoid "Column 'Name' in table 'dbo.ProductVariantAttributeValue' is of a type that is invalid for use as a key column in an index". + Sql("If -1 = (SELECT CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'ProductVariantAttributeValue' AND COLUMN_NAME = 'Name') ALTER TABLE [dbo].[ProductVariantAttributeValue] ALTER COLUMN [Name] nvarchar(4000) NULL;"); + } + + CreateIndex("dbo.Product_Category_Mapping", "IsFeaturedProduct"); + CreateIndex("dbo.Product_Manufacturer_Mapping", "IsFeaturedProduct"); + CreateIndex("dbo.SpecificationAttribute", "AllowFiltering"); + CreateIndex("dbo.Product_ProductAttribute_Mapping", "AttributeControlTypeId"); + CreateIndex("dbo.ProductAttribute", "AllowFiltering"); + CreateIndex("dbo.ProductVariantAttributeValue", "ValueTypeId"); // Useless index. The name field with 4000 characters is too long for an index. if (DataSettings.Current.IsSqlServer) { CreateIndex("dbo.ProductVariantAttributeValue", "Name"); } - } + } - public override void Down() - { + public override void Down() + { if (DataSettings.Current.IsSqlServer) { DropIndex("dbo.ProductVariantAttributeValue", new[] { "Name" }); } DropIndex("dbo.ProductVariantAttributeValue", new[] { "ValueTypeId" }); - DropIndex("dbo.ProductAttribute", new[] { "AllowFiltering" }); - DropIndex("dbo.Product_ProductAttribute_Mapping", new[] { "AttributeControlTypeId" }); - DropIndex("dbo.SpecificationAttribute", new[] { "AllowFiltering" }); - DropIndex("dbo.Product_Manufacturer_Mapping", new[] { "IsFeaturedProduct" }); - DropIndex("dbo.Product_Category_Mapping", new[] { "IsFeaturedProduct" }); + DropIndex("dbo.ProductAttribute", new[] { "AllowFiltering" }); + DropIndex("dbo.Product_ProductAttribute_Mapping", new[] { "AttributeControlTypeId" }); + DropIndex("dbo.SpecificationAttribute", new[] { "AllowFiltering" }); + DropIndex("dbo.Product_Manufacturer_Mapping", new[] { "IsFeaturedProduct" }); + DropIndex("dbo.Product_Category_Mapping", new[] { "IsFeaturedProduct" }); DownInitial(); - } + } - private void UpInitial() - { + private void UpInitial() + { #region auto-generated CreateTable( @@ -2141,7 +2141,7 @@ private void UpInitial() } private void DownInitial() - { + { DropForeignKey("dbo.ProductReview", "ProductId", "dbo.Product"); DropForeignKey("dbo.ProductReview", "Id", "dbo.CustomerContent"); DropForeignKey("dbo.ProductReviewHelpfulness", "ProductReviewId", "dbo.ProductReview"); @@ -2508,5 +2508,5 @@ private void DownInitial() DropTable("dbo.TaxCategory"); DropTable("dbo.Topic"); } - } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201706020759565_UpdateMediaPath.cs b/src/Libraries/SmartStore.Data/Migrations/201706020759565_UpdateMediaPath.cs index b4f74fd90f..0cbb97ef49 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201706020759565_UpdateMediaPath.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201706020759565_UpdateMediaPath.cs @@ -1,37 +1,37 @@ namespace SmartStore.Data.Migrations { - using System.Data.Entity.Migrations; - using System.Web.Hosting; - using Core.Data; - using Setup; + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using Core.Data; + using Setup; - public partial class UpdateMediaPath : DbMigration, ILocaleResourcesProvider, IDataSeeder - { + public partial class UpdateMediaPath : DbMigration, ILocaleResourcesProvider, IDataSeeder + { public override void Up() { - if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) - { - var tenantName = DataSettings.Current.TenantName.NullEmpty() ?? "Default"; - var uploadedPath = $"/Media/{tenantName}/Uploaded/"; - var thumbsPath = $"/Media/{tenantName}/Thumbs/"; - - Sql($"UPDATE [dbo].[Category] SET [Description] = REPLACE([Description],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[Category] SET [BottomDescription] = REPLACE([BottomDescription],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[Manufacturer] SET [Description] = REPLACE([Description],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[ShippingMethod] SET [Description] = REPLACE([Description],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[PaymentMethod] SET [FullDescription] = REPLACE([FullDescription],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[BlogPost] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[News] SET [Full] = REPLACE([Full],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[Campaign] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[QueuedEmail] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[QueuedEmail] SET [Body] = REPLACE([Body],'/Media/Thumbs/','{thumbsPath}')"); - Sql($"UPDATE [dbo].[Topic] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[MessageTemplate] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); - Sql($"UPDATE [dbo].[Product] SET [FullDescription] = REPLACE([FullDescription],'/Media/Uploaded/','{uploadedPath}')"); - - // LocalizedProperty - Sql($"UPDATE [dbo].[LocalizedProperty] SET [LocaleValue] = REPLACE([LocaleValue],'/Media/Thumbs/','{thumbsPath}') WHERE [LocaleKey] = 'Body' AND [LocaleKeyGroup] = 'QueuedEmail'"); - Sql($@" + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + var tenantName = DataSettings.Current.TenantName.NullEmpty() ?? "Default"; + var uploadedPath = $"/Media/{tenantName}/Uploaded/"; + var thumbsPath = $"/Media/{tenantName}/Thumbs/"; + + Sql($"UPDATE [dbo].[Category] SET [Description] = REPLACE([Description],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[Category] SET [BottomDescription] = REPLACE([BottomDescription],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[Manufacturer] SET [Description] = REPLACE([Description],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[ShippingMethod] SET [Description] = REPLACE([Description],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[PaymentMethod] SET [FullDescription] = REPLACE([FullDescription],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[BlogPost] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[News] SET [Full] = REPLACE([Full],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[Campaign] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[QueuedEmail] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[QueuedEmail] SET [Body] = REPLACE([Body],'/Media/Thumbs/','{thumbsPath}')"); + Sql($"UPDATE [dbo].[Topic] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[MessageTemplate] SET [Body] = REPLACE([Body],'/Media/Uploaded/','{uploadedPath}')"); + Sql($"UPDATE [dbo].[Product] SET [FullDescription] = REPLACE([FullDescription],'/Media/Uploaded/','{uploadedPath}')"); + + // LocalizedProperty + Sql($"UPDATE [dbo].[LocalizedProperty] SET [LocaleValue] = REPLACE([LocaleValue],'/Media/Thumbs/','{thumbsPath}') WHERE [LocaleKey] = 'Body' AND [LocaleKeyGroup] = 'QueuedEmail'"); + Sql($@" UPDATE [dbo].[LocalizedProperty] SET [LocaleValue] = REPLACE([LocaleValue],'/Media/Uploaded/','{uploadedPath}') WHERE ([LocaleKey] = 'BottomDescription' AND [LocaleKeyGroup] = 'Category') OR @@ -39,33 +39,30 @@ public override void Up() ([LocaleKey] = 'Description' AND [LocaleKeyGroup] IN ('Category', 'Manufacturer', 'ShippingMethod')) OR ([LocaleKey] = 'FullDescription' AND [LocaleKeyGroup] IN ('PaymentMethod', 'Product')) OR ([LocaleKey] = 'Body' AND [LocaleKeyGroup] IN ('BlogPost', 'Campaign', 'QueuedEmail', 'MessageTemplate', 'Topic'))"); - } - } - + } + } + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; - public void Seed(SmartObjectContext context) - { - context.MigrateLocaleResources(MigrateLocaleResources); + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); - context.SaveChanges(); - } + context.SaveChanges(); + } - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - builder.AddOrUpdate("Common.For", "For: {0}", "Für: {0}"); - builder.AddOrUpdate("Products.Sorting.Featured", "Featured", "Empfehlung"); + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.For", "For: {0}", "Für: {0}"); + builder.AddOrUpdate("Products.Sorting.Featured", "Featured", "Empfehlung"); - builder.AddOrUpdate("Common.AdditionalShippingSurcharge", - "Plus {0} shipping surcharge", - "zzgl. {0} zusätzlicher Versandgebühr"); + builder.AddOrUpdate("Common.AdditionalShippingSurcharge", + "Plus {0} shipping surcharge", + "zzgl. {0} zusätzlicher Versandgebühr"); builder.AddOrUpdate("Address.Fields.Salutation", "Salutation", "Anrede"); builder.AddOrUpdate("Address.Fields.Title", "Title", "Titel"); @@ -107,8 +104,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Specifies whether to only export individually visible associated products.", "Legt fest, ob nur individuell sichtbare, verknüpfte Produkte exportiert werden sollen."); - builder.AddOrUpdate("Product.ThumbTitle", "{0}, Picture {1} large", "{0}, Bild {1} groß"); - builder.AddOrUpdate("Product.ThumbAlternateText", "{0}, Picture {1}", "{0}, Bild {1}"); - } - } + builder.AddOrUpdate("Product.ThumbTitle", "{0}, Picture {1} large", "{0}, Bild {1} groß"); + builder.AddOrUpdate("Product.ThumbAlternateText", "{0}, Picture {1}", "{0}, Bild {1}"); + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201707190940318_V302Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201707190940318_V302Resources.cs index d2f4b57b0b..c858380a1f 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201707190940318_V302Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201707190940318_V302Resources.cs @@ -1,23 +1,19 @@ namespace SmartStore.Data.Migrations { - using Setup; - using System; using System.Data.Entity.Migrations; + using Setup; public partial class V302Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201707281452589_TierPriceCalcMethod.cs b/src/Libraries/SmartStore.Data/Migrations/201707281452589_TierPriceCalcMethod.cs index 3662816352..c4c696c686 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201707281452589_TierPriceCalcMethod.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201707281452589_TierPriceCalcMethod.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using Setup; - using System; using System.Data.Entity.Migrations; + using Setup; public partial class TierPriceCalcMethod : DbMigration, ILocaleResourcesProvider, IDataSeeder { @@ -10,17 +9,14 @@ public override void Up() { AddColumn("dbo.TierPrice", "CalculationMethod", c => c.Int(nullable: false)); } - + public override void Down() { DropColumn("dbo.TierPrice", "CalculationMethod"); } - public bool RollbackOnFailure - { - get { return false; } - } - + public bool RollbackOnFailure => false; + public void Seed(SmartObjectContext context) { context.MigrateLocaleResources(MigrateLocaleResources); @@ -41,7 +37,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Specifies whether to apply tierprice percentage to attribute price adjustments", "Bestimmt ob prozentuale Ermigungen von Staffelpreisen auf Auf- & Abpreise von Attributen angewendet werden sollen"); - builder.AddOrUpdate("Admin.Header.Account", "Account", "Account"); + builder.AddOrUpdate("Admin.Header.Account", "Account", "Account"); } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201708251628482_SystemTopics.cs b/src/Libraries/SmartStore.Data/Migrations/201708251628482_SystemTopics.cs index 70488ff6a7..fae5395bdf 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201708251628482_SystemTopics.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201708251628482_SystemTopics.cs @@ -1,10 +1,9 @@ namespace SmartStore.Data.Migrations { + using System.Data.Entity.Migrations; + using System.Linq; using Core.Domain.Topics; using Setup; - using System; - using System.Linq; - using System.Data.Entity.Migrations; public partial class SystemTopics : DbMigration, ILocaleResourcesProvider, IDataSeeder { @@ -12,16 +11,13 @@ public override void Up() { AddColumn("dbo.Topic", "IsSystemTopic", c => c.Boolean(nullable: false, defaultValue: false)); } - + public override void Down() { DropColumn("dbo.Topic", "IsSystemTopic"); } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { @@ -43,15 +39,15 @@ public void Seed(SmartObjectContext context) var topics = context.Set().Where(x => systemTopics.Contains(x.SystemName)).ToList(); topics.Each(x => x.IsSystemTopic = true); - context.MigrateLocaleResources(MigrateLocaleResources); + context.MigrateLocaleResources(MigrateLocaleResources); - context.SaveChanges(); + context.SaveChanges(); } public void MigrateLocaleResources(LocaleResourcesBuilder builder) { - builder.AddOrUpdate("Admin.ContentManagement.Topics.CannotBeDeleted", - "This topic is required by your Shop and can therefore not be deleted.", + builder.AddOrUpdate("Admin.ContentManagement.Topics.CannotBeDeleted", + "This topic is required by your Shop and can therefore not be deleted.", "Diese Seite wird von Ihrem Shop bentigt und kann daher nicht gelscht werden."); } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201709141000226_V303Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201709141000226_V303Resources.cs index 12b8d62ab1..2142dca719 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201709141000226_V303Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201709141000226_V303Resources.cs @@ -1,23 +1,19 @@ namespace SmartStore.Data.Migrations { - using Setup; - using System; using System.Data.Entity.Migrations; + using Setup; public partial class V303Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201709251538312_UpdateTrustedShopsTask.cs b/src/Libraries/SmartStore.Data/Migrations/201709251538312_UpdateTrustedShopsTask.cs index ac54451906..b7984d1b2b 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201709251538312_UpdateTrustedShopsTask.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201709251538312_UpdateTrustedShopsTask.cs @@ -1,25 +1,21 @@ namespace SmartStore.Data.Migrations { - using Core.Domain.Tasks; - using Setup; - using System; using System.Data.Entity.Migrations; using System.Linq; + using Core.Domain.Tasks; + using Setup; public partial class UpdateTrustedShopsTask : DbMigration, IDataSeeder { public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { @@ -30,7 +26,7 @@ public void Seed(SmartObjectContext context) { // remove old task table.Remove(taskToDelete); - + // if task to remnove is not null we assume the plugin is currently installed and so we add the new task without any further request var taskToAdd = new ScheduleTask { diff --git a/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs index 7cfdcbb63b..cf6805e1cf 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs @@ -15,7 +15,7 @@ public override void Up() AddColumn("dbo.Currency", "RoundOrderTotalRule", c => c.Int(nullable: false)); AddColumn("dbo.PaymentMethod", "RoundOrderTotalEnabled", c => c.Boolean(nullable: false)); } - + public override void Down() { DropColumn("dbo.PaymentMethod", "RoundOrderTotalEnabled"); @@ -27,10 +27,7 @@ public override void Down() DropColumn("dbo.Order", "OrderTotalRounding"); } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { @@ -105,11 +102,11 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "The number of decimal digits must be between 0 and 8.", "Die Anzahl der Dezimalstellen muss zwischen 0 und 8 liegen."); - builder.Delete( - "Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation", - "Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation.Hint"); + builder.Delete( + "Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation", + "Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation.Hint"); - builder.AddOrUpdate("Admin.Orders.Fields.OrderTotalRounding", + builder.AddOrUpdate("Admin.Orders.Fields.OrderTotalRounding", "Rounding", "Rundung", "The amount by which the order total was rounded up or down.", diff --git a/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs index d7f9caa6a4..5020d69b3d 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs @@ -10,17 +10,14 @@ public override void Up() AddColumn("dbo.SpecificationAttribute", "IndexOptionNames", c => c.Boolean(nullable: false)); AddColumn("dbo.ProductAttribute", "IndexOptionNames", c => c.Boolean(nullable: false)); } - + public override void Down() { DropColumn("dbo.ProductAttribute", "IndexOptionNames"); DropColumn("dbo.SpecificationAttribute", "IndexOptionNames"); } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs index a3073c4e43..8b6f013735 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs @@ -1,30 +1,26 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Data.Entity.Migrations; - using Setup; - using Utilities; + using System.Data.Entity.Migrations; + using Setup; + using Utilities; - public partial class ProductMainPictureId : DbMigration, IDataSeeder - { - public override void Up() + public partial class ProductMainPictureId : DbMigration, IDataSeeder + { + public override void Up() { AddColumn("dbo.Product", "MainPictureId", c => c.Int()); } - + public override void Down() { DropColumn("dbo.Product", "MainPictureId"); } - public bool RollbackOnFailure - { - get { return true; } - } + public bool RollbackOnFailure => true; - public void Seed(SmartObjectContext context) - { - DataMigrator.FixProductMainPictureIds(context, true); - } - } + public void Seed(SmartObjectContext context) + { + DataMigrator.FixProductMainPictureIds(context, true); + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs index 547f68afe8..cd4527c919 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs @@ -1,64 +1,60 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Data.Entity.Migrations; - using System.Web.Hosting; - using SmartStore.Data.Setup; - using SmartStore.Utilities; - using System.IO; - using Core.Data; - using SmartStore.Data.Utilities; - - public partial class MoveFsMedia : DbMigration, IDataSeeder - { - const int DirMaxLength = 4; - - public override void Up() + using System.Data.Entity.Migrations; + using System.IO; + using System.Web.Hosting; + using Core.Data; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; + using SmartStore.Utilities; + + public partial class MoveFsMedia : DbMigration, IDataSeeder + { + const int DirMaxLength = 4; + + public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return true; } - } + public bool RollbackOnFailure => true; - public void Seed(SmartObjectContext context) - { - if (!HostingEnvironment.IsHosted) - return; + public void Seed(SmartObjectContext context) + { + if (!HostingEnvironment.IsHosted) + return; - // Move the whole media folder to new location at first - MoveMediaFolder(); + // Move the whole media folder to new location at first + MoveMediaFolder(); - // Reorganize files (root > Storage/{subfolder}) - DataMigrator.MoveFsMedia(context); - } + // Reorganize files (root > Storage/{subfolder}) + DataMigrator.MoveFsMedia(context); + } - private void MoveMediaFolder() - { - // Moves "~/Media/{Tenant}" to "~/App_Data/Tenants/{Tenant}/Media" - // This is relevant for local file system only. For cloud storages (like Azure) - // we don't need to move the main folder. + private void MoveMediaFolder() + { + // Moves "~/Media/{Tenant}" to "~/App_Data/Tenants/{Tenant}/Media" + // This is relevant for local file system only. For cloud storages (like Azure) + // we don't need to move the main folder. - var sourceDir = new DirectoryInfo(CommonHelper.MapPath("~/Media/" + DataSettings.Current.TenantName, false)); - var destinationDir = new DirectoryInfo(CommonHelper.MapPath("~/App_Data/Tenants/" + DataSettings.Current.TenantName + "/Media", false)); + var sourceDir = new DirectoryInfo(CommonHelper.MapPath("~/Media/" + DataSettings.Current.TenantName, false)); + var destinationDir = new DirectoryInfo(CommonHelper.MapPath("~/App_Data/Tenants/" + DataSettings.Current.TenantName + "/Media", false)); - if (!sourceDir.Exists) - { - // Source (legacy media folder) does not exist, for whatever reasons. Nothing to move here. - return; - } + if (!sourceDir.Exists) + { + // Source (legacy media folder) does not exist, for whatever reasons. Nothing to move here. + return; + } - //if (!destinationDir.Exists) - //{ - // destinationDir.Create(); - //} + //if (!destinationDir.Exists) + //{ + // destinationDir.Create(); + //} - sourceDir.MoveTo(destinationDir.FullName); - } - } + sourceDir.MoveTo(destinationDir.FullName); + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs index 072c5aeac3..7613251157 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs @@ -1,174 +1,171 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Collections.Generic; - using System.Data.Entity.Migrations; - using System.Linq; - using SmartStore.Collections; + using System; + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Collections; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; - using SmartStore.Data.Setup; - - public partial class SyncStringResources : DbMigration, IDataSeeder - { - private static HashSet _unusedNames = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "Enums.SmartStore.Core.Domain.Catalog.AttributeControlType.ColorSquares", - "Plugins.Payments.PayPal.IpnLogInfo", - "Plugins.Widgets.TrustedShopsCustomerProtection.Prepared", - "Plugins.Shipping.ByWeight.SmallQuantityThresholdNotReached", - "Products.ProductNotAddedToTheCart.Link" - }; - - private static Dictionary _missingEnglishResources = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.BusinessServiceCenter", "Business service center" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.DropBox", "Drop box" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.RegularPickup", "Regular pickup" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.RequestCourier", "Request courier" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.Station", "Station" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByDimensions", "Pack by dimensions" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByOneItemPerPackage", "Pack by one item per package" }, - { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByVolume", "Pack by volume" }, - { "Order.OrderDetails", "Order Details" }, - { "Plugins.SmartStore.AccardaKar.Amount", "Amount" }, - { "Plugins.SmartStore.Exports.BMEcatProductXml.ExportProfileInfo", "Please click here to view existing BMEcat export profiles or to create a new one." }, - { "Plugins.Description.SmartStore.DiscountRules", "Provides standard discount rules, e.g. \"Country is\", \"Customer group is\", \"Has amount x spent\" etc." }, - { "Plugins.FriendlyName.DiscountRequirement.BillingCountryIs", "Country of invoice address is" }, - { "Plugins.FriendlyName.DiscountRequirement.HadSpentAmount", "Customer has spent amount x" }, - { "Plugins.FriendlyName.DiscountRequirement.HasAllProducts", "Customer has the following products in his shopping cart" }, - { "Plugins.FriendlyName.DiscountRequirement.HasOneProduct", "Customer has one of the following products in his shopping cart" }, - { "Plugins.FriendlyName.DiscountRequirement.MustBeAssignedToCustomerRole", "The customer must be assigned to a customer group" }, - { "Plugins.FriendlyName.DiscountRequirement.ShippingCountryIs", "Country of delivery address is" }, - { "Plugins.FriendlyName.DiscountRequirement.Store", "Limited to specific shop" }, - { "Plugins.FriendlyName.SmartStore.DiscountRules", "Standard discount rules" }, - { "Plugins.FriendlyName.DiscountRequirement.HasPaymentMethod", "Customer selected specific payment method" }, - { "Plugins.FriendlyName.SmartStore.DiscountRules.HasPaymentMethod", "\"Customer has selected certain payment method\" discount rule" }, - { "Plugins.FriendlyName.DiscountRequirement.HasShippingOption", "Customer selected specific shipping method" }, - { "Plugins.FriendlyName.SmartStore.DiscountRules.HasShippingOption", "\"Customer has selected certain shipping method\" discount rule" }, - { "Plugins.FriendlyName.DiscountRequirement.PurchasedAllProducts", "Customer has already purchased the following products" }, - { "Plugins.FriendlyName.DiscountRequirement.PurchasedOneProduct", "The customer has already purchased one of the following products" }, - { "Plugins.FriendlyName.SmartStore.DiscountRules.PurchasedProducts", "\"Customer has already purchased the following products\" discount rule" }, - { "Plugins.SmartStore.GoogleRemarketing.ConversionId.Hint", "You'll find your conversion id in the admin area of google adwords" }, - { "Plugins.Description.SmartStore.OfflinePayment", "Provides offline payment methods, e.g. direct debit, credit card, invoice, prepayment, cash on delivery etc." }, - { "Plugins.FriendlyName.SmartStore.OfflinePayment", "Offline payment methods" }, - { "Plugins.FriendlyName.SmartStore.OutputCache", "Output Cache" }, - { "Plugins.Description.SmartStore.OutputCache", "Allows the temporary storage of entire shop pages and thus contributes to a considerable increase in performance." }, - { "Plugins.FriendlyName.OutputCacheProvider.Memory", "Local memory" }, - { "Plugins.FriendlyName.OutputCacheProvider.Database", "Database" }, - { "Plugins.FriendlyName.SmartStore.PostFinanceECommerce", "PostFinance" }, - { "Plugins.Description.SmartStore.PostFinanceECommerce", "Enables payment with the swiss PostFinance." }, - { "Plugins.SmartStore.ShopConnector.FinalResult", "{0}... {1} of {2} processed. {3} success, {4} failed, {5} skipped. {6} added, {7} updated." }, - { "Plugins.Widgets.TrustedShopsCustomerProtection.Password", "Trusted Shops password" }, - { "Plugins.Widgets.TrustedShopsCustomerProtection.Password.Hint", "Please enter the password provided by Trusted Shops here." }, - { "Plugins.Widgets.TrustedShopsCustomerProtection.UserName", "Trusted Shops user name" }, - { "Plugins.Widgets.TrustedShopsCustomerProtection.UserName.Hint", "Please enter the user name provided by Trusted Shops here." }, - }; - - public override void Up() - { - } - - public override void Down() - { - } - - public bool RollbackOnFailure - { - get { return true; } - } - - public void Seed(SmartObjectContext context) - { - using (var scope = new DbContextScope(context, autoDetectChanges: false, validateOnSave: false)) - { - var resourceSet = context.Set(); - var allLanguages = context.Set().AsNoTracking().ToList(); - - // Accidents. - var accidents = resourceSet.Where(x => x.ResourceName == "Admin.Configuration.ActivityLog.ActivityLogType").ToList(); - if (accidents.Any()) - { - accidents.Each(x => x.ResourceName = "Admin.Configuration.ActivityLog.ActivityLogType"); - context.SaveChanges(); - } - - // Remove unused resources that could be included in the German set. - var unusedResources = resourceSet.Where(x => _unusedNames.Contains(x.ResourceName)).ToList(); - if (unusedResources.Any()) - { - resourceSet.RemoveRange(unusedResources); - context.SaveChanges(); - unusedResources.Clear(); - } - - context.DetachEntities(); - - // Remove duplicate resources. - foreach (var language in allLanguages) - { - var resources = resourceSet.Where(x => x.LanguageId == language.Id).ToList(); - var deleteResources = new List(); - var resourcesMap = new Multimap(StringComparer.OrdinalIgnoreCase); - resources.Each(x => resourcesMap.Add(x.ResourceName, x)); - - foreach (var item in resourcesMap) - { - if (item.Value.Count > 1) - { - // First is ok, rest is bad. - foreach (var resource in item.Value.OrderByDescending(x => x.IsTouched).Skip(1)) - { - deleteResources.Add(resource); - } - } - } - - if (deleteResources.Any()) - { - resourceSet.RemoveRange(deleteResources); - context.SaveChanges(); - deleteResources.Clear(); - context.DetachEntities(); - } - } - - // Remove resources that are not included in the German set. - // Unfortunately we cannot do that. We have no information about the origin of a resource. We would delete resources of other developers. - - // Add resources included in the German set but missing in the English set. - var deLanguage = allLanguages.FirstOrDefault(x => x.LanguageCulture.IsCaseInsensitiveEqual("de-DE")); - var enLanguage = allLanguages.FirstOrDefault(x => x.LanguageCulture.IsCaseInsensitiveEqual("en-US")); - if (deLanguage != null && enLanguage != null) - { - var deResources = resourceSet.AsNoTracking().Where(x => x.LanguageId == deLanguage.Id).ToList(); - - var enNames = resourceSet - .Where(x => x.LanguageId == enLanguage.Id) - .Select(x => x.ResourceName) - .Distinct() - .ToList(); - var enNamesSet = new HashSet(enNames, StringComparer.OrdinalIgnoreCase); - - foreach (var resource in deResources) - { - if (!enNames.Contains(resource.ResourceName) && _missingEnglishResources.TryGetValue(resource.ResourceName, out string value)) - { - resourceSet.Add(new LocaleStringResource - { - LanguageId = enLanguage.Id, - ResourceName = resource.ResourceName, - ResourceValue = value, - IsFromPlugin = resource.IsFromPlugin - }); - } - } - - context.SaveChanges(); - deResources.Clear(); - context.DetachEntities(); - } - } - } - } + using SmartStore.Data.Setup; + + public partial class SyncStringResources : DbMigration, IDataSeeder + { + private static HashSet _unusedNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Enums.SmartStore.Core.Domain.Catalog.AttributeControlType.ColorSquares", + "Plugins.Payments.PayPal.IpnLogInfo", + "Plugins.Widgets.TrustedShopsCustomerProtection.Prepared", + "Plugins.Shipping.ByWeight.SmallQuantityThresholdNotReached", + "Products.ProductNotAddedToTheCart.Link" + }; + + private static Dictionary _missingEnglishResources = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.BusinessServiceCenter", "Business service center" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.DropBox", "Drop box" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.RegularPickup", "Regular pickup" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.RequestCourier", "Request courier" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.Station", "Station" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByDimensions", "Pack by dimensions" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByOneItemPerPackage", "Pack by one item per package" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByVolume", "Pack by volume" }, + { "Order.OrderDetails", "Order Details" }, + { "Plugins.SmartStore.AccardaKar.Amount", "Amount" }, + { "Plugins.SmartStore.Exports.BMEcatProductXml.ExportProfileInfo", "Please click here to view existing BMEcat export profiles or to create a new one." }, + { "Plugins.Description.SmartStore.DiscountRules", "Provides standard discount rules, e.g. \"Country is\", \"Customer group is\", \"Has amount x spent\" etc." }, + { "Plugins.FriendlyName.DiscountRequirement.BillingCountryIs", "Country of invoice address is" }, + { "Plugins.FriendlyName.DiscountRequirement.HadSpentAmount", "Customer has spent amount x" }, + { "Plugins.FriendlyName.DiscountRequirement.HasAllProducts", "Customer has the following products in his shopping cart" }, + { "Plugins.FriendlyName.DiscountRequirement.HasOneProduct", "Customer has one of the following products in his shopping cart" }, + { "Plugins.FriendlyName.DiscountRequirement.MustBeAssignedToCustomerRole", "The customer must be assigned to a customer group" }, + { "Plugins.FriendlyName.DiscountRequirement.ShippingCountryIs", "Country of delivery address is" }, + { "Plugins.FriendlyName.DiscountRequirement.Store", "Limited to specific shop" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules", "Standard discount rules" }, + { "Plugins.FriendlyName.DiscountRequirement.HasPaymentMethod", "Customer selected specific payment method" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules.HasPaymentMethod", "\"Customer has selected certain payment method\" discount rule" }, + { "Plugins.FriendlyName.DiscountRequirement.HasShippingOption", "Customer selected specific shipping method" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules.HasShippingOption", "\"Customer has selected certain shipping method\" discount rule" }, + { "Plugins.FriendlyName.DiscountRequirement.PurchasedAllProducts", "Customer has already purchased the following products" }, + { "Plugins.FriendlyName.DiscountRequirement.PurchasedOneProduct", "The customer has already purchased one of the following products" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules.PurchasedProducts", "\"Customer has already purchased the following products\" discount rule" }, + { "Plugins.SmartStore.GoogleRemarketing.ConversionId.Hint", "You'll find your conversion id in the admin area of google adwords" }, + { "Plugins.Description.SmartStore.OfflinePayment", "Provides offline payment methods, e.g. direct debit, credit card, invoice, prepayment, cash on delivery etc." }, + { "Plugins.FriendlyName.SmartStore.OfflinePayment", "Offline payment methods" }, + { "Plugins.FriendlyName.SmartStore.OutputCache", "Output Cache" }, + { "Plugins.Description.SmartStore.OutputCache", "Allows the temporary storage of entire shop pages and thus contributes to a considerable increase in performance." }, + { "Plugins.FriendlyName.OutputCacheProvider.Memory", "Local memory" }, + { "Plugins.FriendlyName.OutputCacheProvider.Database", "Database" }, + { "Plugins.FriendlyName.SmartStore.PostFinanceECommerce", "PostFinance" }, + { "Plugins.Description.SmartStore.PostFinanceECommerce", "Enables payment with the swiss PostFinance." }, + { "Plugins.SmartStore.ShopConnector.FinalResult", "{0}... {1} of {2} processed. {3} success, {4} failed, {5} skipped. {6} added, {7} updated." }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.Password", "Trusted Shops password" }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.Password.Hint", "Please enter the password provided by Trusted Shops here." }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.UserName", "Trusted Shops user name" }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.UserName.Hint", "Please enter the user name provided by Trusted Shops here." }, + }; + + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + using (var scope = new DbContextScope(context, autoDetectChanges: false, validateOnSave: false)) + { + var resourceSet = context.Set(); + var allLanguages = context.Set().AsNoTracking().ToList(); + + // Accidents. + var accidents = resourceSet.Where(x => x.ResourceName == "Admin.Configuration.ActivityLog.ActivityLogType").ToList(); + if (accidents.Any()) + { + accidents.Each(x => x.ResourceName = "Admin.Configuration.ActivityLog.ActivityLogType"); + context.SaveChanges(); + } + + // Remove unused resources that could be included in the German set. + var unusedResources = resourceSet.Where(x => _unusedNames.Contains(x.ResourceName)).ToList(); + if (unusedResources.Any()) + { + resourceSet.RemoveRange(unusedResources); + context.SaveChanges(); + unusedResources.Clear(); + } + + context.DetachEntities(); + + // Remove duplicate resources. + foreach (var language in allLanguages) + { + var resources = resourceSet.Where(x => x.LanguageId == language.Id).ToList(); + var deleteResources = new List(); + var resourcesMap = new Multimap(StringComparer.OrdinalIgnoreCase); + resources.Each(x => resourcesMap.Add(x.ResourceName, x)); + + foreach (var item in resourcesMap) + { + if (item.Value.Count > 1) + { + // First is ok, rest is bad. + foreach (var resource in item.Value.OrderByDescending(x => x.IsTouched).Skip(1)) + { + deleteResources.Add(resource); + } + } + } + + if (deleteResources.Any()) + { + resourceSet.RemoveRange(deleteResources); + context.SaveChanges(); + deleteResources.Clear(); + context.DetachEntities(); + } + } + + // Remove resources that are not included in the German set. + // Unfortunately we cannot do that. We have no information about the origin of a resource. We would delete resources of other developers. + + // Add resources included in the German set but missing in the English set. + var deLanguage = allLanguages.FirstOrDefault(x => x.LanguageCulture.IsCaseInsensitiveEqual("de-DE")); + var enLanguage = allLanguages.FirstOrDefault(x => x.LanguageCulture.IsCaseInsensitiveEqual("en-US")); + if (deLanguage != null && enLanguage != null) + { + var deResources = resourceSet.AsNoTracking().Where(x => x.LanguageId == deLanguage.Id).ToList(); + + var enNames = resourceSet + .Where(x => x.LanguageId == enLanguage.Id) + .Select(x => x.ResourceName) + .Distinct() + .ToList(); + var enNamesSet = new HashSet(enNames, StringComparer.OrdinalIgnoreCase); + + foreach (var resource in deResources) + { + if (!enNames.Contains(resource.ResourceName) && _missingEnglishResources.TryGetValue(resource.ResourceName, out string value)) + { + resourceSet.Add(new LocaleStringResource + { + LanguageId = enLanguage.Id, + ResourceName = resource.ResourceName, + ResourceValue = value, + IsFromPlugin = resource.IsFromPlugin + }); + } + } + + context.SaveChanges(); + deResources.Clear(); + context.DetachEntities(); + } + } + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs index 450133d11a..cb8df2f1fb 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs @@ -1,16 +1,15 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Data.Entity.Migrations; - using System.Web.Hosting; - using System.Linq; - using SmartStore.Core.Data; - using SmartStore.Core.Domain.Localization; - using SmartStore.Data.Setup; - using SmartStore.Data.Utilities; - - public partial class Liquid : DbMigration, ILocaleResourcesProvider, IDataSeeder - { + using System.Data.Entity.Migrations; + using System.Linq; + using System.Web.Hosting; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Localization; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; + + public partial class Liquid : DbMigration, ILocaleResourcesProvider, IDataSeeder + { public override void Up() { AddColumn("dbo.MessageTemplate", "To", c => c.String(nullable: false, maxLength: 500, defaultValue: " ")); @@ -21,7 +20,7 @@ public override void Up() DropColumn("dbo.QueuedEmail", "ToName"); DropColumn("dbo.QueuedEmail", "ReplyToName"); } - + public override void Down() { AddColumn("dbo.QueuedEmail", "ReplyToName", c => c.String(maxLength: 500)); @@ -33,83 +32,83 @@ public override void Down() DropColumn("dbo.MessageTemplate", "To"); } - public void Seed(SmartObjectContext context) - { - context.MigrateLocaleResources(MigrateLocaleResources); - context.SaveChanges(); - - if (HostingEnvironment.IsHosted && DataSettings.DatabaseIsInstalled()) - { - // Import all xml templates on disk - var converter = new MessageTemplateConverter(context); - var language = ResolveMasterLanguage(context); - converter.ImportAll(language); - - DropDefaultValueConstraint(context); - } - } - - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - builder.Delete( - "Admin.System.QueuedEmails.Fields.FromName", - "Admin.System.QueuedEmails.Fields.FromName.Hint", - "Admin.System.QueuedEmails.Fields.ToName", - "Admin.System.QueuedEmails.Fields.ToName.Hint"); - - builder.AddOrUpdate("Admin.System.QueuedEmails.Fields.ReplyTo", - "Reply to", - "Antwort an", - "Reply-To address of the email.", - "Antwortadresse der E-Mail."); - - builder.AddOrUpdate("Common.Error.NoMessageTemplate", - "The message template '{0}' does not exist.", - "Die Nachrichtenvorlage '{0}' existiert nicht."); - - builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.NoModelTree", - "Variables are unknown until at least one message of the current type has either been sent or previewed.", - "Variablen sind erst bekannt, wenn mind. eine Nachricht vom aktuellen Typ entweder gesendet oder getestet wurde."); - - builder.AddOrUpdate("Admin.Promotions.Campaigns.Fields.AllowedTokens", - "Allowed template variables", - "Erlaubte Template Variablen", - "Inserts the selected variable in the HTML document.", - "Fgt die gewhlte Variable in das HTML-Dokument ein."); - } - - private Language ResolveMasterLanguage(SmartObjectContext context) - { - var query = context.Set().OrderBy(x => x.DisplayOrder); - - var language = query - .Where(x => (x.UniqueSeoCode == "de" || x.UniqueSeoCode == "en") && x.Published) - .FirstOrDefault(); - - if (language == null) - { - language = query.Where(x => x.Published).FirstOrDefault(); - } - - return language; - } - - private void DropDefaultValueConstraint(SmartObjectContext context) - { - // During migration we created a new NON-Nullable column ("To") - // with a default value contraint of ' ', otherwise column creation - // would have failed. Now we need to get rid of this constraint. - - if (DataSettings.Current.IsSqlServer) - { - string sql = @"DECLARE @name nvarchar(100) + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + if (HostingEnvironment.IsHosted && DataSettings.DatabaseIsInstalled()) + { + // Import all xml templates on disk + var converter = new MessageTemplateConverter(context); + var language = ResolveMasterLanguage(context); + converter.ImportAll(language); + + DropDefaultValueConstraint(context); + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete( + "Admin.System.QueuedEmails.Fields.FromName", + "Admin.System.QueuedEmails.Fields.FromName.Hint", + "Admin.System.QueuedEmails.Fields.ToName", + "Admin.System.QueuedEmails.Fields.ToName.Hint"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.Fields.ReplyTo", + "Reply to", + "Antwort an", + "Reply-To address of the email.", + "Antwortadresse der E-Mail."); + + builder.AddOrUpdate("Common.Error.NoMessageTemplate", + "The message template '{0}' does not exist.", + "Die Nachrichtenvorlage '{0}' existiert nicht."); + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.NoModelTree", + "Variables are unknown until at least one message of the current type has either been sent or previewed.", + "Variablen sind erst bekannt, wenn mind. eine Nachricht vom aktuellen Typ entweder gesendet oder getestet wurde."); + + builder.AddOrUpdate("Admin.Promotions.Campaigns.Fields.AllowedTokens", + "Allowed template variables", + "Erlaubte Template Variablen", + "Inserts the selected variable in the HTML document.", + "Fgt die gewhlte Variable in das HTML-Dokument ein."); + } + + private Language ResolveMasterLanguage(SmartObjectContext context) + { + var query = context.Set().OrderBy(x => x.DisplayOrder); + + var language = query + .Where(x => (x.UniqueSeoCode == "de" || x.UniqueSeoCode == "en") && x.Published) + .FirstOrDefault(); + + if (language == null) + { + language = query.Where(x => x.Published).FirstOrDefault(); + } + + return language; + } + + private void DropDefaultValueConstraint(SmartObjectContext context) + { + // During migration we created a new NON-Nullable column ("To") + // with a default value contraint of ' ', otherwise column creation + // would have failed. Now we need to get rid of this constraint. + + if (DataSettings.Current.IsSqlServer) + { + string sql = @"DECLARE @name nvarchar(100) SELECT @name = [name] from sys.objects WHERE type = 'D' and parent_object_id = object_id('MessageTemplate') IF (@name is not null) BEGIN EXEC ('ALTER TABLE [MessageTemplate] Drop Constraint [' + @name +']') END"; - context.ExecuteSqlCommand(sql); - } - } + context.ExecuteSqlCommand(sql); + } + } - public bool RollbackOnFailure => true; - } + public bool RollbackOnFailure => true; + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs index 038f51ae45..8dd8239740 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs @@ -1,39 +1,38 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - using SmartStore.Data.Setup; - using SmartStore.Data.Utilities; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; - public partial class AddressFormat : DbMigration, ILocaleResourcesProvider, IDataSeeder - { + public partial class AddressFormat : DbMigration, ILocaleResourcesProvider, IDataSeeder + { public override void Up() { AddColumn("dbo.Country", "AddressFormat", c => c.String()); } - + public override void Down() { DropColumn("dbo.Country", "AddressFormat"); } - public void Seed(SmartObjectContext context) - { - context.MigrateLocaleResources(MigrateLocaleResources); - DataMigrator.ImportAddressFormats(context); + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + DataMigrator.ImportAddressFormats(context); - context.SaveChanges(); - } + context.SaveChanges(); + } - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - builder.AddOrUpdate("Admin.Configuration.Countries.Fields.AddressFormat", - "Address format", - "Adressenformat", - "The address format according to the countries mailing address format rules.", - "Das Addressenformat gem der Landes-Richtlinien."); - } + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Countries.Fields.AddressFormat", + "Address format", + "Adressenformat", + "The address format according to the countries mailing address format rules.", + "Das Addressenformat gem der Landes-Richtlinien."); + } - public bool RollbackOnFailure => false; - } + public bool RollbackOnFailure => false; + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs index 92e74e27ac..fa8929783e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ShippingMethodMultistore : DbMigration { public override void Up() @@ -15,18 +14,18 @@ public override void Up() AddColumn("dbo.PaymentMethod", "LimitedToStores", c => c.Boolean(nullable: false)); DropTable("dbo.ShippingMethodRestrictions"); } - + public override void Down() { CreateTable( "dbo.ShippingMethodRestrictions", c => new - { - ShippingMethod_Id = c.Int(nullable: false), - Country_Id = c.Int(nullable: false), - }) + { + ShippingMethod_Id = c.Int(nullable: false), + Country_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.ShippingMethod_Id, t.Country_Id }); - + DropColumn("dbo.PaymentMethod", "LimitedToStores"); DropColumn("dbo.ShippingMethod", "LimitedToStores"); CreateIndex("dbo.ShippingMethodRestrictions", "Country_Id"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs index d99106df13..38f873aac7 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ExportAttributeMappings : DbMigration { public override void Up() { AddColumn("dbo.ProductAttribute", "ExportMappings", c => c.String()); } - + public override void Down() { DropColumn("dbo.ProductAttribute", "ExportMappings"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201804060721031_Wallet.cs b/src/Libraries/SmartStore.Data/Migrations/201804060721031_Wallet.cs index 634e0f249f..5b15b5ed48 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201804060721031_Wallet.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201804060721031_Wallet.cs @@ -1,8 +1,6 @@ namespace SmartStore.Data.Migrations { using System.Data.Entity.Migrations; - using System.Web.Hosting; - using Core.Data; using Setup; public partial class Wallet : DbMigration, ILocaleResourcesProvider, IDataSeeder @@ -12,33 +10,33 @@ public override void Up() CreateTable( "dbo.WalletHistory", c => new - { - Id = c.Int(nullable: false, identity: true), - StoreId = c.Int(nullable: false), - CustomerId = c.Int(nullable: false), - OrderId = c.Int(), - Amount = c.Decimal(nullable: false, precision: 18, scale: 4), - AmountBalance = c.Decimal(nullable: false, precision: 18, scale: 4), - AmountBalancePerStore = c.Decimal(nullable: false, precision: 18, scale: 4), - CreatedOnUtc = c.DateTime(nullable: false), - Reason = c.Int(), - Message = c.String(maxLength: 1000), - AdminComment = c.String(maxLength: 4000), - }) + { + Id = c.Int(nullable: false, identity: true), + StoreId = c.Int(nullable: false), + CustomerId = c.Int(nullable: false), + OrderId = c.Int(), + Amount = c.Decimal(nullable: false, precision: 18, scale: 4), + AmountBalance = c.Decimal(nullable: false, precision: 18, scale: 4), + AmountBalancePerStore = c.Decimal(nullable: false, precision: 18, scale: 4), + CreatedOnUtc = c.DateTime(nullable: false), + Reason = c.Int(), + Message = c.String(maxLength: 1000), + AdminComment = c.String(maxLength: 4000), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.Customer", t => t.CustomerId, cascadeDelete: true) .ForeignKey("dbo.Order", t => t.OrderId) .Index(t => new { t.StoreId, t.CreatedOnUtc }, name: "IX_StoreId_CreatedOn") .Index(t => t.CustomerId) .Index(t => t.OrderId); - + AddColumn("dbo.Product", "IsSystemProduct", c => c.Boolean(nullable: false)); AddColumn("dbo.Product", "SystemName", c => c.String(maxLength: 500)); AddColumn("dbo.Order", "CreditBalance", c => c.Decimal(nullable: false, precision: 18, scale: 4)); AddColumn("dbo.Order", "RefundedCreditBalance", c => c.Decimal(nullable: false, precision: 18, scale: 4)); CreateIndex("dbo.Product", new[] { "SystemName", "IsSystemProduct" }, name: "Product_SystemName_IsSystemProduct"); } - + public override void Down() { DropForeignKey("dbo.WalletHistory", "OrderId", "dbo.Order"); @@ -54,10 +52,7 @@ public override void Down() DropTable("dbo.WalletHistory"); } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201804090744324_ForceSslForAllPages.cs b/src/Libraries/SmartStore.Data/Migrations/201804090744324_ForceSslForAllPages.cs index d0835dc7e8..9d98c90284 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201804090744324_ForceSslForAllPages.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201804090744324_ForceSslForAllPages.cs @@ -16,7 +16,7 @@ public override void Up() CreateIndex("dbo.Product", new[] { "SystemName", "IsSystemProduct" }, name: "Product_SystemName_IsSystemProduct"); DropColumn("dbo.Order", "RefundedCreditBalance"); } - + public override void Down() { AddColumn("dbo.Order", "RefundedCreditBalance", c => c.Decimal(nullable: false, precision: 18, scale: 4)); @@ -26,10 +26,7 @@ public override void Down() CreateIndex("dbo.Product", new[] { "SystemName", "IsSystemProduct" }, name: "Product_SystemName_IsSystemProduct"); } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201804200835273_V310Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201804200835273_V310Resources.cs index ff3a2d3b7b..346a3c8e6c 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201804200835273_V310Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201804200835273_V310Resources.cs @@ -1,394 +1,390 @@ namespace SmartStore.Data.Migrations { - using SmartStore.Core.Domain.Configuration; - using SmartStore.Core.Domain.Customers; - using SmartStore.Core.Domain.Media; - using SmartStore.Data.Setup; - using SmartStore.Utilities; - using System; using System.Data.Entity.Migrations; - using System.Linq; - - public partial class V310Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder - { + using System.Linq; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Core.Domain.Customers; + using SmartStore.Core.Domain.Media; + using SmartStore.Data.Setup; + using SmartStore.Utilities; + + public partial class V310Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { public override void Up() { } - + public override void Down() { } - - public bool RollbackOnFailure - { - get { return false; } - } - - public void Seed(SmartObjectContext context) - { - context.MigrateLocaleResources(MigrateLocaleResources); - - MigrateSettings(context); - - context.SaveChanges(); - } - - public void MigrateSettings(SmartObjectContext context) - { - // Change MediaSettings.MaximumImageSize to 2048 - var name = TypeHelper.NameOf(y => y.MaximumImageSize, true); - var setting = context.Set().FirstOrDefault(x => x.Name == name); - if (setting != null && setting.Value.Convert() < 2048) - { - setting.Value = "2048"; - } - - // Change MediaSettings.AvatarPictureSize to 250 - name = TypeHelper.NameOf(y => y.AvatarPictureSize, true); - setting = context.Set().FirstOrDefault(x => x.Name == name); - if (setting != null && setting.Value.Convert() < 250) - { - setting.Value = "250"; - } - - // Change MediaSettings.AvatarMaximumSizeBytes to 512000 (500 KB) - name = TypeHelper.NameOf(y => y.AvatarMaximumSizeBytes, true); - setting = context.Set().FirstOrDefault(x => x.Name == name); - if (setting != null && setting.Value.Convert() < 512000) - { - setting.Value = "512000"; - } - - // Delete MessageTemplatesSettings - var settings = context.Set(); - var caseInvariantReplacementSetting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.CaseInvariantReplacement"); - var color1Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color1"); - var color2Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color2"); - var color3Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color3"); - - if (caseInvariantReplacementSetting != null) settings.Remove(caseInvariantReplacementSetting); - if (color1Setting != null) settings.Remove(color1Setting); - if (color2Setting != null) settings.Remove(color2Setting); - if (color3Setting != null) settings.Remove(color3Setting); - } - - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - builder.AddOrUpdate("Admin.Orders.Shipment", "Shipment", "Lieferung"); - builder.AddOrUpdate("Admin.Order", "Order", "Auftrag"); - - builder.AddOrUpdate("Admin.Order.ViaShippingMethod", "via {0}", "via {0}"); - builder.AddOrUpdate("Admin.Order.WithPaymentMethod", "with {0}", "per {0}"); - builder.AddOrUpdate("Admin.Order.FromStore", "from {0}", "von {0}"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.MaxItemsToDisplayInCatalogMenu", - "Max items to display in catalog menu", - "Maximale Anzahl von Elementen im Katalogmen", - "Defines the maximum number of top level items to be displayed in the main catalog menu. All menu items which are exceeding this limit will be placed in a new dropdown menu item.", - "Legt die maximale Anzahl von Menu-Eintrgen der obersten Hierarchie fest, die im Katalogmen angezeigt werden. Alle weiteren Menu-Eintrge werden innerhalb eines neuen Dropdownmenus ausgegeben."); - - builder.AddOrUpdate("CatalogMenu.MoreLink", "More", "Mehr"); - - builder.AddOrUpdate("Admin.CatalogSettings.Homepage", "Homepage", "Homepage"); - builder.AddOrUpdate("Admin.CatalogSettings.ProductDisplay", "Product display", "Produktdarstellung"); - builder.AddOrUpdate("Admin.CatalogSettings.Prices", "Prices", "Preise"); - builder.AddOrUpdate("Admin.CatalogSettings.CompareProducts", "Compare products", "Produktvergleich"); - - builder.AddOrUpdate("Footer.Service.Mobile", "Service", "Service, Versand & Zahlung"); - builder.AddOrUpdate("Footer.Company.Mobile", "Company", "Firma, Impressum & Datenschutz"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Search.Facets.FacetSorting.LabelAsc", - "Displayed Name: A to Z", - "Angezeigter Name: A bis Z"); - - builder.AddOrUpdate("Admin.Catalog.Products.Copy.NumberOfCopies", - "Number of copies", - "Anzahl an Kopien", - "Defines the number of copies to be created.", - "Legt die Anzahl der anzulegenden Kopien fest."); - - builder.AddOrUpdate("Admin.Configuration.Languages.OfType", - "of type \"{0}\"", - "vom Typ \"{0}\""); - - builder.AddOrUpdate("Admin.Configuration.Languages.CheckAvailableLanguagesFailed", - "An error occurred while checking for other available languages.", - "Bei der Suche nach weiteren verfgbaren Sprachen trat ein Fehler auf."); - - builder.AddOrUpdate("Admin.Configuration.Languages.NoAvailableLanguagesFound", - "There were no other available languages found for version {0}. On translate.smartstore.com you will find more details about available resources.", - "Es wurden keine weiteren verfgbaren Sprachen fr Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); - - builder.AddOrUpdate("Admin.Configuration.Languages.InstalledLanguages", - "Installed Languages", - "Installierte Sprachen"); - builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages", - "Available Languages", - "Verfgbare Sprachen"); - - builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages.Note", - "Click Download to install a new language including all localized resources. On translate.smartstore.com you will find more details about available resources.", - "Klicken Sie auf Download, um eine neue Sprache mit allen lokalisierten Ressourcen zu installieren. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); - - builder.AddOrUpdate("Common.Translated", - "Translated", - "bersetzt"); - builder.AddOrUpdate("Admin.Configuration.Languages.TranslatedPercentage", - "{0}% translated", - "{0}% bersetzt"); - builder.AddOrUpdate("Admin.Configuration.Languages.TranslatedPercentageAtLastImport", - "{0}% at the last import", - "{0}% beim letzten Import"); - - builder.AddOrUpdate("Admin.Configuration.Languages.NumberOfTranslatedResources", - "{0} of {1}", - "{0} von {1}"); - - builder.AddOrUpdate("Admin.Configuration.Languages.DownloadingResources", - "Loading ressources", - "Lade Ressourcen"); - builder.AddOrUpdate("Admin.Configuration.Languages.ImportResources", - "Import resources", - "Importiere Ressourcen"); - - builder.AddOrUpdate("Admin.Configuration.Languages.OnePublishedLanguageRequired", - "At least one published language is required.", - "Mindestens eine verffentlichte Sprache ist erforderlich."); - - builder.AddOrUpdate("Admin.Configuration.Languages.Fields.AvailableLanguageSetId", - "Available Languages", - "Verfgbare Sprachen", - "Specifies the available language whose localized resources are to be imported.", - "Legt die verfgbare Sprache fest, deren lokalisierte Ressourcen importiert werden sollen."); - - builder.AddOrUpdate("Admin.Configuration.Languages.UploadFileOrSelectLanguage", - "Please upload an import file or select an available language whose resources are to be imported.", - "Bitte laden Sie eine Importdatei hoch oder whlen Sie eine verfgbare Sprache, deren Ressourcen importiert werden sollen."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Shipping.ChargeOnlyHighestProductShippingSurcharge", - "Charge the highest shipping surcharge only", - "Nur den hchsten Transportzuschlag berechnen", - "Specifies whether to charge only the highest additional shipping surcharge of products.", - "Bestimmt ob bei der Berechnung der Versandkosten nur der hchste Transportzuschlag von Produkten bercksichtigt wird."); - - builder.AddOrUpdate("Order.OrderDetails") - .Value("en", "Order Details"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Media.AutoGenerateAbsoluteUrls", - "Generate absolute URLs", - "Absolute URLs erzeugen", - "Generates absolute URLs for media files by prepending the current host name (e.g. http://myshop.com/media/image/1.jpg instead of /media/image/1.jpg). Has no effect if a CDN URL has been applied to the store.", - "Erzeugt absolute URLs fr Mediendateien, indem der aktuelle Hostname vorangestellt wird (z.B. http://meinshop.de/media/image/1.jpg statt /media/image/1.jpg). Hat keine Auswirkung, wenn fr den Store eine CDN-URL eingerichtet wurde."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Search.SearchFieldsNote", - "The Name, SKU and Short Description fields can be searched in the standard search. Other fields require a search plugin such as the MegaSearch plugin from Premium Edition.", - "In der Standardsuche knnen die Felder Name, SKU und Kurzbeschreibung durchsucht werden. Fr weitere Felder ist ein Such-Plugin wie etwa das MegaSearch-Plugin aus der Premium Edition notwendig."); - - builder.AddOrUpdate("Admin.DataExchange.Import.FolderName", "Folder path", "Ordnerpfad"); - - builder.AddOrUpdate("Admin.MessageTemplate.Preview.From", "From", "Von"); - builder.AddOrUpdate("Admin.MessageTemplate.Preview.To", "To", "An"); - builder.AddOrUpdate("Admin.MessageTemplate.Preview.ReplyTo", "Reply To", "Antwort an"); - builder.AddOrUpdate("Admin.MessageTemplate.Preview.SendTestMail", "Test-E-mail to...", "Test E-Mail an..."); - builder.AddOrUpdate("Admin.MessageTemplate.Preview.TestMailSent", "E-mail has been sent.", "E-Mail gesendet."); - builder.AddOrUpdate("Admin.MessageTemplate.Preview.NoBody", - "The generated preview file seems to have expired. Please reload the page.", - "Die generierte Vorschaudatei scheint abgelaufen zu sein. Laden Sie die Seite bitte neu."); - - builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.Preview.SuccessfullySent", - "The email has been sent successfully.", - "Die E-Mail wurde erfolgreich versendet."); - - builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.SuccessfullyCopied", - "The message template has been copied successfully.", - "Die Nachrichtenvorlage wurde erfolgreich kopiert."); - - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.ShoppingCartItem", "Shopping Cart", "Warenkorb"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.ShoppingCartType.ShoppingCart", "Shopping Cart", "Warenkorb"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.ShoppingCartType.Wishlist", "Wishlist", "Wunschliste"); - - builder.AddOrUpdate("Admin.DataExchange.Export.Projection.NoBundleProducts", - "Do not export bundled products", - "Keine Produkt-Bundle exportieren", - "Specifies whether to export bundled products. If this option is activated, then the associated bundle items will be exported.", - "Legt fest, ob Produkt-Bundle exportiert werden sollen. Ist diese Option aktiviert, so werden die zum Bundle gehrenden Produkte (Bundle-Bestandteile) exportiert."); - - builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ShoppingCartTypeId", - "Shopping cart type", - "Warenkorbtyp", - "Filter by shopping cart type.", - "Nach Warenkorbtyp filtern."); - - builder.AddOrUpdate("Common.CustomerId", "Customer ID", "Kunden ID"); - - builder.AddOrUpdate("Account.AccountActivation.InvalidEmailOrToken", - "Unknown email or token. Please register again.", - "Unbekannte E-Mail oder Token. Bitte fhren Sie die Registrierung erneut durch."); - - builder.AddOrUpdate("Account.PasswordRecoveryConfirm.InvalidEmailOrToken", - "Unknown email or token. Please click \"Forgot password\" again, if you want to renew your password.", - "Unbekannte E-Mail oder Token. Klicken Sie bitte erneut \"Passwort vergessen\", falls Sie Ihr Passwort erneuern mchten."); - - builder.Delete("Account.PasswordRecoveryConfirm.InvalidEmail"); - builder.Delete("Account.PasswordRecoveryConfirm.InvalidToken"); - - builder.AddOrUpdate("Admin.Common.Acl.SubjectTo", - "Restrict access", - "Zugriff einschrnken", - "Determines whether this entity is subject to access restrictions (no = no restriction, yes = accessible only for selected customer groups)", - "Legt fest, ob dieser Datensatz Zugriffsbeschrnkungen unterliegt (Nein = keine Beschrnkung, Ja = zugnglich nur fr gewhlte Kundengruppen)"); - - builder.AddOrUpdate("Admin.Common.Acl.AvailableFor", - "Customer roles", - "Kundengruppen", - "Select customer roles who can access the entity. For all inactive roles, this record is hidden.", - "Whlen Sie Kundengruppen, die auf den Datensatz zugreifen knnen. Bei allen nicht aktivierten Gruppen wird dieser Datensatz ausgeblendet."); - - builder.Delete( - "Admin.Catalog.Categories.Fields.SubjectToAcl", - "Admin.Catalog.Categories.Fields.AclCustomerRoles", - "Admin.Catalog.Products.Fields.SubjectToAcl", - "Admin.Catalog.Products.Fields.AclCustomerRoles", - "Common.Options.Count"); - - builder.AddOrUpdate("Admin.Common.ApplyFilter", "Apply filter", "Filter anwenden"); - builder.AddOrUpdate("Time.Milliseconds", "Milliseconds", "Millisekunden"); - builder.AddOrUpdate("Common.Pixel", "Pixel", "Pixel"); - builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ShowPlaceholder", "Show placeholder", "Zeige Platzhalter"); - builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.HidePlaceholder", "Hide placeholder", "Verberge Platzhalter"); - builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.UpdateExampleFileName", "Update example", "Aktualisiere Beispiel"); - - builder.AddOrUpdate("Admin.Configuration.Themes.AvailableDesktopThemes", "Installed themes", "Installierte Themes"); - - builder.AddOrUpdate("Admin.Catalog.Products.List.GoDirectlyToSku", "Find by SKU", "Nach SKU suchen"); - builder.AddOrUpdate("Admin.Orders.List.GoDirectlyToNumber", "Find by order id", "Nach Auftragsnummer suchen"); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress", - "Store IP address", - "IP-Adresse speichern", - "Specifies whether to store the IP address in the customer data set.", - "Legt fest, ob die IP-Adresse im Kundendatensatz gespeichert werden soll."); - - builder.AddOrUpdate("Admin.Orders.Info", "General", "Allgemein"); - builder.AddOrUpdate("Admin.Orders.BillingAndShipment", "Billing & Shipping", "Rechnung & Versand"); - builder.AddOrUpdate("Admin.Orders.Fields.ShippingAddress.ViewOnGoogleMaps", "View on Google Maps", "Auf Google Maps ansehen"); - - builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SocialSettings.InstagramLink", - "Instagram Link", - "Instagram Link", - "Leave this field empty if the Instagram link should not be shown", - "Lassen Sie dieses Feld leer, wenn der Instagram Link nicht angezeigt werden soll"); - - builder.AddOrUpdate("Common.License", "License", "Lizenz"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Payments.CapturePaymentReason.OrderShipped", - "The order has been marked as shipped", - "Der Auftrag wurde als versendet markiert"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Payments.CapturePaymentReason.OrderDelivered", - "The order has been marked as delivered", - "Der Auftrag wurde als ausgeliefert markiert"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Payment.CapturePaymentReason", - "Capture payment amount when", - "Zahlungsbetrag einziehen, wenn", - "Specifies the event when the payment amount is automatically captured. The selected payment method must support capturing for this.", - "Legt das Ereignis fest, zu dem der Zahlunsgbetrag automatisch eingezogen wird. Die gewhlte Zahlart muss hierfr Buchungen untersttzen."); - - #region taken from V22Final, because they were never added yet - - builder.AddOrUpdate("Common.Next", - "Next", - "Weiter"); - builder.AddOrUpdate("Admin.Common.BackToConfiguration", - "Back to configuration", - "Zurck zur Konfiguration"); - builder.AddOrUpdate("Admin.Common.UploadFileSucceeded", - "The file has been successfully uploaded.", - "Die Datei wurde erfolgreich hochgeladen."); - builder.AddOrUpdate("Admin.Common.UploadFileFailed", - "The upload has failed.", - "Der Upload ist leider fehlgeschlagen."); - builder.AddOrUpdate("Admin.Common.ImportAll", - "Import all", - "Alle importieren"); - builder.AddOrUpdate("Admin.Common.ImportSelected", - "Import selected", - "Ausgewhlte importieren"); - builder.AddOrUpdate("Admin.Common.UnknownError", - "An unknown error has occurred.", - "Es ist ein unbekannter Fehler aufgetreten."); - builder.AddOrUpdate("Plugins.Feed.FreeShippingThreshold", - "Free shipping threshold", - "Kostenloser Versand ab", - "Amount as from shipping is free.", - "Betrag, ab dem keine Versandkosten anfallen."); - - #endregion - - builder.AddOrUpdate("Admin.Product.Picture.Added", - "The picture has successfully been added", - "Das Bild wurde erfolgreich zugefgt"); - - builder.AddOrUpdate("HtmlEditor.ClickToEdit", "Click to edit HTML...", "Hier klicken, um HTML zu editieren..."); - - builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings.Note", - "Define mappings of attribute values to export fields according to the pattern <Format prefix>:<Export field name>. Example: gmc:color exports the attribute values for colors to the field color during the Google Merchant Center Export. The mappings are only effective when exporting attribute combinations.", - "Legen Sie Zuordnungen von Attributwerten zu Exportfeldern nach dem Muster <Formatprfix>:<Export-Feldname> fest. Beispiel: gmc:color exportiert beim Google Merchant Center Export die Attributwerte fr Farben in das Feld color. Die Zuordnungen sind nur beim Export von Attributkombinationen wirksam."); - - builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings", - "Mappings to export fields", - "Zuordnungen zu Exportfeldern", - "Allows to map attribute values to export fields. Each entry has to be entered in a new line.", - "Ermglicht die Zuordnung von Attributwerten zu Exportfeldern. Jeder Eintrag muss in einer neuen Zeile erfolgen."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.AdditionalFee", - "Additional fee", - "Zustzliche Gebhr", - "Specifies an additional fee to be charged to the customer for using the payment method.", - "Legt eine zustzliche Gebhr fest, die dem Kunden fr die Inanspruchnahme der Zahlart berechnet wird."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.AdditionalFeePercentage", - "Additional fee percentage", - "Zustzliche Gebhr prozentual", - "Specifies whether the additional fee should be calculated as a percentage. A fixed value is used if this option is disabled.", - "Legt fest, ob die zustzliche Gebhr prozentual berechnet werden soll. Es wird ein fester Wert verwendet, falls diese Option deaktiviert ist."); - - builder.Delete("Common.Buttons.Default"); - builder.AddOrUpdate("Common.Buttons.Secondary", "Secondary", "Secondary"); - builder.AddOrUpdate("Common.Buttons.Light", "Light", "Light"); - builder.AddOrUpdate("Common.Buttons.Dark", "Dark", "Dark"); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryRequired", - "'Country' required", - "Die Eingabe eines Landes ist erforderlich", - "Check the box if 'Country' is required.", - "Legt fest, ob die Eingabe eines Landes erforderlich ist."); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceRequired", - "'State/province' required", - "Die Eingabe eines Bundeslandes ist erforderlich", - "Check the box if 'State/province' is required.", - "Legt fest, ob die Eingabe eines Bundeslandes erforderlich ist."); - - builder.AddOrUpdate("Address.Fields.StateProvince.Required", "State is required.", "Bundesland wird bentigt"); - - builder.AddOrUpdate("Common.Columns", "Columns", "Spalten"); - builder.AddOrUpdate("Common.Mru", "Recently", "Zuletzt"); - - builder.AddOrUpdate("Admin.WidgetZones.UserDefined", "User-defined", "Benutzerdefiniert"); - - builder.AddOrUpdate("Admin.Configuration.ManageLanguages", "Manage languages", "Sprachen verwalten"); - - builder.AddOrUpdate("Admin.Customers.Customers.Info", "General", "Allgemein"); - builder.AddOrUpdate("Admin.Customers.Customers.Impersonate", "Impersonate", "Imitieren"); - builder.AddOrUpdate("Admin.Customers.Customers.CurrentCart", "Current cart", "Aktueller Warenkorb"); - - builder.AddOrUpdate("Admin.ContentManagement.Topics.CannotBeDeleted", - "This topic is needed by your Shop and can therefore not be deleted.", - "Diese Seite wird von Ihrem Shop bentigt und kann daher nicht gelscht werden."); - } - } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + MigrateSettings(context); + + context.SaveChanges(); + } + + public void MigrateSettings(SmartObjectContext context) + { + // Change MediaSettings.MaximumImageSize to 2048 + var name = TypeHelper.NameOf(y => y.MaximumImageSize, true); + var setting = context.Set().FirstOrDefault(x => x.Name == name); + if (setting != null && setting.Value.Convert() < 2048) + { + setting.Value = "2048"; + } + + // Change MediaSettings.AvatarPictureSize to 250 + name = TypeHelper.NameOf(y => y.AvatarPictureSize, true); + setting = context.Set().FirstOrDefault(x => x.Name == name); + if (setting != null && setting.Value.Convert() < 250) + { + setting.Value = "250"; + } + + // Change MediaSettings.AvatarMaximumSizeBytes to 512000 (500 KB) + name = TypeHelper.NameOf(y => y.AvatarMaximumSizeBytes, true); + setting = context.Set().FirstOrDefault(x => x.Name == name); + if (setting != null && setting.Value.Convert() < 512000) + { + setting.Value = "512000"; + } + + // Delete MessageTemplatesSettings + var settings = context.Set(); + var caseInvariantReplacementSetting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.CaseInvariantReplacement"); + var color1Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color1"); + var color2Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color2"); + var color3Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color3"); + + if (caseInvariantReplacementSetting != null) settings.Remove(caseInvariantReplacementSetting); + if (color1Setting != null) settings.Remove(color1Setting); + if (color2Setting != null) settings.Remove(color2Setting); + if (color3Setting != null) settings.Remove(color3Setting); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Orders.Shipment", "Shipment", "Lieferung"); + builder.AddOrUpdate("Admin.Order", "Order", "Auftrag"); + + builder.AddOrUpdate("Admin.Order.ViaShippingMethod", "via {0}", "via {0}"); + builder.AddOrUpdate("Admin.Order.WithPaymentMethod", "with {0}", "per {0}"); + builder.AddOrUpdate("Admin.Order.FromStore", "from {0}", "von {0}"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.MaxItemsToDisplayInCatalogMenu", + "Max items to display in catalog menu", + "Maximale Anzahl von Elementen im Katalogmen", + "Defines the maximum number of top level items to be displayed in the main catalog menu. All menu items which are exceeding this limit will be placed in a new dropdown menu item.", + "Legt die maximale Anzahl von Menu-Eintrgen der obersten Hierarchie fest, die im Katalogmen angezeigt werden. Alle weiteren Menu-Eintrge werden innerhalb eines neuen Dropdownmenus ausgegeben."); + + builder.AddOrUpdate("CatalogMenu.MoreLink", "More", "Mehr"); + + builder.AddOrUpdate("Admin.CatalogSettings.Homepage", "Homepage", "Homepage"); + builder.AddOrUpdate("Admin.CatalogSettings.ProductDisplay", "Product display", "Produktdarstellung"); + builder.AddOrUpdate("Admin.CatalogSettings.Prices", "Prices", "Preise"); + builder.AddOrUpdate("Admin.CatalogSettings.CompareProducts", "Compare products", "Produktvergleich"); + + builder.AddOrUpdate("Footer.Service.Mobile", "Service", "Service, Versand & Zahlung"); + builder.AddOrUpdate("Footer.Company.Mobile", "Company", "Firma, Impressum & Datenschutz"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Search.Facets.FacetSorting.LabelAsc", + "Displayed Name: A to Z", + "Angezeigter Name: A bis Z"); + + builder.AddOrUpdate("Admin.Catalog.Products.Copy.NumberOfCopies", + "Number of copies", + "Anzahl an Kopien", + "Defines the number of copies to be created.", + "Legt die Anzahl der anzulegenden Kopien fest."); + + builder.AddOrUpdate("Admin.Configuration.Languages.OfType", + "of type \"{0}\"", + "vom Typ \"{0}\""); + + builder.AddOrUpdate("Admin.Configuration.Languages.CheckAvailableLanguagesFailed", + "An error occurred while checking for other available languages.", + "Bei der Suche nach weiteren verfgbaren Sprachen trat ein Fehler auf."); + + builder.AddOrUpdate("Admin.Configuration.Languages.NoAvailableLanguagesFound", + "There were no other available languages found for version {0}. On translate.smartstore.com you will find more details about available resources.", + "Es wurden keine weiteren verfgbaren Sprachen fr Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); + + builder.AddOrUpdate("Admin.Configuration.Languages.InstalledLanguages", + "Installed Languages", + "Installierte Sprachen"); + builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages", + "Available Languages", + "Verfgbare Sprachen"); + + builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages.Note", + "Click Download to install a new language including all localized resources. On translate.smartstore.com you will find more details about available resources.", + "Klicken Sie auf Download, um eine neue Sprache mit allen lokalisierten Ressourcen zu installieren. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); + + builder.AddOrUpdate("Common.Translated", + "Translated", + "bersetzt"); + builder.AddOrUpdate("Admin.Configuration.Languages.TranslatedPercentage", + "{0}% translated", + "{0}% bersetzt"); + builder.AddOrUpdate("Admin.Configuration.Languages.TranslatedPercentageAtLastImport", + "{0}% at the last import", + "{0}% beim letzten Import"); + + builder.AddOrUpdate("Admin.Configuration.Languages.NumberOfTranslatedResources", + "{0} of {1}", + "{0} von {1}"); + + builder.AddOrUpdate("Admin.Configuration.Languages.DownloadingResources", + "Loading ressources", + "Lade Ressourcen"); + builder.AddOrUpdate("Admin.Configuration.Languages.ImportResources", + "Import resources", + "Importiere Ressourcen"); + + builder.AddOrUpdate("Admin.Configuration.Languages.OnePublishedLanguageRequired", + "At least one published language is required.", + "Mindestens eine verffentlichte Sprache ist erforderlich."); + + builder.AddOrUpdate("Admin.Configuration.Languages.Fields.AvailableLanguageSetId", + "Available Languages", + "Verfgbare Sprachen", + "Specifies the available language whose localized resources are to be imported.", + "Legt die verfgbare Sprache fest, deren lokalisierte Ressourcen importiert werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Languages.UploadFileOrSelectLanguage", + "Please upload an import file or select an available language whose resources are to be imported.", + "Bitte laden Sie eine Importdatei hoch oder whlen Sie eine verfgbare Sprache, deren Ressourcen importiert werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Shipping.ChargeOnlyHighestProductShippingSurcharge", + "Charge the highest shipping surcharge only", + "Nur den hchsten Transportzuschlag berechnen", + "Specifies whether to charge only the highest additional shipping surcharge of products.", + "Bestimmt ob bei der Berechnung der Versandkosten nur der hchste Transportzuschlag von Produkten bercksichtigt wird."); + + builder.AddOrUpdate("Order.OrderDetails") + .Value("en", "Order Details"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.AutoGenerateAbsoluteUrls", + "Generate absolute URLs", + "Absolute URLs erzeugen", + "Generates absolute URLs for media files by prepending the current host name (e.g. http://myshop.com/media/image/1.jpg instead of /media/image/1.jpg). Has no effect if a CDN URL has been applied to the store.", + "Erzeugt absolute URLs fr Mediendateien, indem der aktuelle Hostname vorangestellt wird (z.B. http://meinshop.de/media/image/1.jpg statt /media/image/1.jpg). Hat keine Auswirkung, wenn fr den Store eine CDN-URL eingerichtet wurde."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.SearchFieldsNote", + "The Name, SKU and Short Description fields can be searched in the standard search. Other fields require a search plugin such as the MegaSearch plugin from Premium Edition.", + "In der Standardsuche knnen die Felder Name, SKU und Kurzbeschreibung durchsucht werden. Fr weitere Felder ist ein Such-Plugin wie etwa das MegaSearch-Plugin aus der Premium Edition notwendig."); + + builder.AddOrUpdate("Admin.DataExchange.Import.FolderName", "Folder path", "Ordnerpfad"); + + builder.AddOrUpdate("Admin.MessageTemplate.Preview.From", "From", "Von"); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.To", "To", "An"); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.ReplyTo", "Reply To", "Antwort an"); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.SendTestMail", "Test-E-mail to...", "Test E-Mail an..."); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.TestMailSent", "E-mail has been sent.", "E-Mail gesendet."); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.NoBody", + "The generated preview file seems to have expired. Please reload the page.", + "Die generierte Vorschaudatei scheint abgelaufen zu sein. Laden Sie die Seite bitte neu."); + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.Preview.SuccessfullySent", + "The email has been sent successfully.", + "Die E-Mail wurde erfolgreich versendet."); + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.SuccessfullyCopied", + "The message template has been copied successfully.", + "Die Nachrichtenvorlage wurde erfolgreich kopiert."); + + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.ShoppingCartItem", "Shopping Cart", "Warenkorb"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.ShoppingCartType.ShoppingCart", "Shopping Cart", "Warenkorb"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.ShoppingCartType.Wishlist", "Wishlist", "Wunschliste"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.NoBundleProducts", + "Do not export bundled products", + "Keine Produkt-Bundle exportieren", + "Specifies whether to export bundled products. If this option is activated, then the associated bundle items will be exported.", + "Legt fest, ob Produkt-Bundle exportiert werden sollen. Ist diese Option aktiviert, so werden die zum Bundle gehrenden Produkte (Bundle-Bestandteile) exportiert."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ShoppingCartTypeId", + "Shopping cart type", + "Warenkorbtyp", + "Filter by shopping cart type.", + "Nach Warenkorbtyp filtern."); + + builder.AddOrUpdate("Common.CustomerId", "Customer ID", "Kunden ID"); + + builder.AddOrUpdate("Account.AccountActivation.InvalidEmailOrToken", + "Unknown email or token. Please register again.", + "Unbekannte E-Mail oder Token. Bitte fhren Sie die Registrierung erneut durch."); + + builder.AddOrUpdate("Account.PasswordRecoveryConfirm.InvalidEmailOrToken", + "Unknown email or token. Please click \"Forgot password\" again, if you want to renew your password.", + "Unbekannte E-Mail oder Token. Klicken Sie bitte erneut \"Passwort vergessen\", falls Sie Ihr Passwort erneuern mchten."); + + builder.Delete("Account.PasswordRecoveryConfirm.InvalidEmail"); + builder.Delete("Account.PasswordRecoveryConfirm.InvalidToken"); + + builder.AddOrUpdate("Admin.Common.Acl.SubjectTo", + "Restrict access", + "Zugriff einschrnken", + "Determines whether this entity is subject to access restrictions (no = no restriction, yes = accessible only for selected customer groups)", + "Legt fest, ob dieser Datensatz Zugriffsbeschrnkungen unterliegt (Nein = keine Beschrnkung, Ja = zugnglich nur fr gewhlte Kundengruppen)"); + + builder.AddOrUpdate("Admin.Common.Acl.AvailableFor", + "Customer roles", + "Kundengruppen", + "Select customer roles who can access the entity. For all inactive roles, this record is hidden.", + "Whlen Sie Kundengruppen, die auf den Datensatz zugreifen knnen. Bei allen nicht aktivierten Gruppen wird dieser Datensatz ausgeblendet."); + + builder.Delete( + "Admin.Catalog.Categories.Fields.SubjectToAcl", + "Admin.Catalog.Categories.Fields.AclCustomerRoles", + "Admin.Catalog.Products.Fields.SubjectToAcl", + "Admin.Catalog.Products.Fields.AclCustomerRoles", + "Common.Options.Count"); + + builder.AddOrUpdate("Admin.Common.ApplyFilter", "Apply filter", "Filter anwenden"); + builder.AddOrUpdate("Time.Milliseconds", "Milliseconds", "Millisekunden"); + builder.AddOrUpdate("Common.Pixel", "Pixel", "Pixel"); + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ShowPlaceholder", "Show placeholder", "Zeige Platzhalter"); + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.HidePlaceholder", "Hide placeholder", "Verberge Platzhalter"); + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.UpdateExampleFileName", "Update example", "Aktualisiere Beispiel"); + + builder.AddOrUpdate("Admin.Configuration.Themes.AvailableDesktopThemes", "Installed themes", "Installierte Themes"); + + builder.AddOrUpdate("Admin.Catalog.Products.List.GoDirectlyToSku", "Find by SKU", "Nach SKU suchen"); + builder.AddOrUpdate("Admin.Orders.List.GoDirectlyToNumber", "Find by order id", "Nach Auftragsnummer suchen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress", + "Store IP address", + "IP-Adresse speichern", + "Specifies whether to store the IP address in the customer data set.", + "Legt fest, ob die IP-Adresse im Kundendatensatz gespeichert werden soll."); + + builder.AddOrUpdate("Admin.Orders.Info", "General", "Allgemein"); + builder.AddOrUpdate("Admin.Orders.BillingAndShipment", "Billing & Shipping", "Rechnung & Versand"); + builder.AddOrUpdate("Admin.Orders.Fields.ShippingAddress.ViewOnGoogleMaps", "View on Google Maps", "Auf Google Maps ansehen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SocialSettings.InstagramLink", + "Instagram Link", + "Instagram Link", + "Leave this field empty if the Instagram link should not be shown", + "Lassen Sie dieses Feld leer, wenn der Instagram Link nicht angezeigt werden soll"); + + builder.AddOrUpdate("Common.License", "License", "Lizenz"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Payments.CapturePaymentReason.OrderShipped", + "The order has been marked as shipped", + "Der Auftrag wurde als versendet markiert"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Payments.CapturePaymentReason.OrderDelivered", + "The order has been marked as delivered", + "Der Auftrag wurde als ausgeliefert markiert"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Payment.CapturePaymentReason", + "Capture payment amount when", + "Zahlungsbetrag einziehen, wenn", + "Specifies the event when the payment amount is automatically captured. The selected payment method must support capturing for this.", + "Legt das Ereignis fest, zu dem der Zahlunsgbetrag automatisch eingezogen wird. Die gewhlte Zahlart muss hierfr Buchungen untersttzen."); + + #region taken from V22Final, because they were never added yet + + builder.AddOrUpdate("Common.Next", + "Next", + "Weiter"); + builder.AddOrUpdate("Admin.Common.BackToConfiguration", + "Back to configuration", + "Zurck zur Konfiguration"); + builder.AddOrUpdate("Admin.Common.UploadFileSucceeded", + "The file has been successfully uploaded.", + "Die Datei wurde erfolgreich hochgeladen."); + builder.AddOrUpdate("Admin.Common.UploadFileFailed", + "The upload has failed.", + "Der Upload ist leider fehlgeschlagen."); + builder.AddOrUpdate("Admin.Common.ImportAll", + "Import all", + "Alle importieren"); + builder.AddOrUpdate("Admin.Common.ImportSelected", + "Import selected", + "Ausgewhlte importieren"); + builder.AddOrUpdate("Admin.Common.UnknownError", + "An unknown error has occurred.", + "Es ist ein unbekannter Fehler aufgetreten."); + builder.AddOrUpdate("Plugins.Feed.FreeShippingThreshold", + "Free shipping threshold", + "Kostenloser Versand ab", + "Amount as from shipping is free.", + "Betrag, ab dem keine Versandkosten anfallen."); + + #endregion + + builder.AddOrUpdate("Admin.Product.Picture.Added", + "The picture has successfully been added", + "Das Bild wurde erfolgreich zugefgt"); + + builder.AddOrUpdate("HtmlEditor.ClickToEdit", "Click to edit HTML...", "Hier klicken, um HTML zu editieren..."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings.Note", + "Define mappings of attribute values to export fields according to the pattern <Format prefix>:<Export field name>. Example: gmc:color exports the attribute values for colors to the field color during the Google Merchant Center Export. The mappings are only effective when exporting attribute combinations.", + "Legen Sie Zuordnungen von Attributwerten zu Exportfeldern nach dem Muster <Formatprfix>:<Export-Feldname> fest. Beispiel: gmc:color exportiert beim Google Merchant Center Export die Attributwerte fr Farben in das Feld color. Die Zuordnungen sind nur beim Export von Attributkombinationen wirksam."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings", + "Mappings to export fields", + "Zuordnungen zu Exportfeldern", + "Allows to map attribute values to export fields. Each entry has to be entered in a new line.", + "Ermglicht die Zuordnung von Attributwerten zu Exportfeldern. Jeder Eintrag muss in einer neuen Zeile erfolgen."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.AdditionalFee", + "Additional fee", + "Zustzliche Gebhr", + "Specifies an additional fee to be charged to the customer for using the payment method.", + "Legt eine zustzliche Gebhr fest, die dem Kunden fr die Inanspruchnahme der Zahlart berechnet wird."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.AdditionalFeePercentage", + "Additional fee percentage", + "Zustzliche Gebhr prozentual", + "Specifies whether the additional fee should be calculated as a percentage. A fixed value is used if this option is disabled.", + "Legt fest, ob die zustzliche Gebhr prozentual berechnet werden soll. Es wird ein fester Wert verwendet, falls diese Option deaktiviert ist."); + + builder.Delete("Common.Buttons.Default"); + builder.AddOrUpdate("Common.Buttons.Secondary", "Secondary", "Secondary"); + builder.AddOrUpdate("Common.Buttons.Light", "Light", "Light"); + builder.AddOrUpdate("Common.Buttons.Dark", "Dark", "Dark"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryRequired", + "'Country' required", + "Die Eingabe eines Landes ist erforderlich", + "Check the box if 'Country' is required.", + "Legt fest, ob die Eingabe eines Landes erforderlich ist."); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceRequired", + "'State/province' required", + "Die Eingabe eines Bundeslandes ist erforderlich", + "Check the box if 'State/province' is required.", + "Legt fest, ob die Eingabe eines Bundeslandes erforderlich ist."); + + builder.AddOrUpdate("Address.Fields.StateProvince.Required", "State is required.", "Bundesland wird bentigt"); + + builder.AddOrUpdate("Common.Columns", "Columns", "Spalten"); + builder.AddOrUpdate("Common.Mru", "Recently", "Zuletzt"); + + builder.AddOrUpdate("Admin.WidgetZones.UserDefined", "User-defined", "Benutzerdefiniert"); + + builder.AddOrUpdate("Admin.Configuration.ManageLanguages", "Manage languages", "Sprachen verwalten"); + + builder.AddOrUpdate("Admin.Customers.Customers.Info", "General", "Allgemein"); + builder.AddOrUpdate("Admin.Customers.Customers.Impersonate", "Impersonate", "Imitieren"); + builder.AddOrUpdate("Admin.Customers.Customers.CurrentCart", "Current cart", "Aktueller Warenkorb"); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.CannotBeDeleted", + "This topic is needed by your Shop and can therefore not be deleted.", + "Diese Seite wird von Ihrem Shop bentigt und kann daher nicht gelscht werden."); + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201804252356096_TopicSlugs.cs b/src/Libraries/SmartStore.Data/Migrations/201804252356096_TopicSlugs.cs index 3eefe926f1..dfb772e099 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201804252356096_TopicSlugs.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201804252356096_TopicSlugs.cs @@ -1,70 +1,66 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Linq; - using System.Data.Entity.Migrations; - using SmartStore.Core.Domain.Topics; - using SmartStore.Data.Setup; - using SmartStore.Core.Domain.Seo; - using SmartStore.Utilities; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Domain.Seo; + using SmartStore.Core.Domain.Topics; + using SmartStore.Data.Setup; + using SmartStore.Utilities; - public partial class TopicSlugs : DbMigration, IDataSeeder - { + public partial class TopicSlugs : DbMigration, IDataSeeder + { public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; - public void Seed(SmartObjectContext context) - { - var allTopics = context.Set() - .AsNoTracking() - .Select(x => new { x.Id, x.SystemName, x.Title }) - .ToList(); + public void Seed(SmartObjectContext context) + { + var allTopics = context.Set() + .AsNoTracking() + .Select(x => new { x.Id, x.SystemName, x.Title }) + .ToList(); - var urlRecords = context.Set(); + var urlRecords = context.Set(); - foreach (var topic in allTopics) - { - var slug = SeoHelper.GetSeName(topic.SystemName, true, false).Truncate(400); - int i = 2; - var tempSlug = slug; + foreach (var topic in allTopics) + { + var slug = SeoHelper.GetSeName(topic.SystemName, true, false).Truncate(400); + int i = 2; + var tempSlug = slug; - while (urlRecords.Any(x => x.Slug == tempSlug)) - { - tempSlug = string.Format("{0}-{1}", slug, i); - i++; - } + while (urlRecords.Any(x => x.Slug == tempSlug)) + { + tempSlug = string.Format("{0}-{1}", slug, i); + i++; + } - slug = tempSlug; + slug = tempSlug; - var ur = urlRecords.FirstOrDefault(x => x.LanguageId == 0 && x.EntityName == "Topic" && x.EntityId == topic.Id); - if (ur != null) - { - ur.Slug = slug; - } - else - { - urlRecords.Add(new UrlRecord - { - EntityId = topic.Id, - EntityName = "Topic", - IsActive = true, - LanguageId = 0, - Slug = slug - }); - } + var ur = urlRecords.FirstOrDefault(x => x.LanguageId == 0 && x.EntityName == "Topic" && x.EntityId == topic.Id); + if (ur != null) + { + ur.Slug = slug; + } + else + { + urlRecords.Add(new UrlRecord + { + EntityId = topic.Id, + EntityName = "Topic", + IsActive = true, + LanguageId = 0, + Slug = slug + }); + } - context.SaveChanges(); - } - } - } + context.SaveChanges(); + } + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201805250724399_V315Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201805250724399_V315Resources.cs index e1a02d62ce..f42a572047 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201805250724399_V315Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201805250724399_V315Resources.cs @@ -1,195 +1,192 @@ namespace SmartStore.Data.Migrations { - using SmartStore.Core.Domain.Catalog; - using SmartStore.Core.Domain.Configuration; - using SmartStore.Core.Domain.Seo; - using SmartStore.Data.Setup; - using SmartStore.Utilities; - using System.Data.Entity.Migrations; - using System.Linq; - - public partial class V315Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder - { + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Domain.Catalog; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Core.Domain.Seo; + using SmartStore.Data.Setup; + using SmartStore.Utilities; + + public partial class V315Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { public override void Up() { - } - + } + public override void Down() { } - - public bool RollbackOnFailure - { - get { return false; } - } - - public void Seed(SmartObjectContext context) - { - context.MigrateLocaleResources(MigrateLocaleResources); - context.SaveChanges(); - - MigrateSettings(context); - context.SaveChanges(); - } - - public void MigrateSettings(SmartObjectContext context) - { - // SeoSettings.RedirectLegacyTopicUrls should be true when migrating (it is false by default after fresh install) - var name = TypeHelper.NameOf(y => y.RedirectLegacyTopicUrls, true); - context.MigrateSettings(x => x.Add(name, true)); - - // remove setting which were moved from customer settings to privacy settings and have new default values which should be applied immediately - var settings = context.Set(); - var storeLastIpAddressSetting = settings.FirstOrDefault(x => x.Name == "CustomerSettings.StoreLastIpAddress"); - if (storeLastIpAddressSetting != null) settings.Remove(storeLastIpAddressSetting); - - var displayPrivacyAgreementOnContactUs = settings.FirstOrDefault(x => x.Name == "CustomerSettings.DisplayPrivacyAgreementOnContactUs"); - if (displayPrivacyAgreementOnContactUs != null) settings.Remove(displayPrivacyAgreementOnContactUs); - - - var showShareButtonName = TypeHelper.NameOf(y => y.ShowShareButton, true); - var showShareButtonSetting = context.Set().FirstOrDefault(x => x.Name == showShareButtonName); - if (showShareButtonSetting != null) - { - showShareButtonSetting.Value = "False"; - } - - var allowAnonymousUsersToEmailAFriendSetting = context.Set().FirstOrDefault(x => x.Name == "CatalogSettings.AllowAnonymousUsersToEmailAFriend"); - if (allowAnonymousUsersToEmailAFriendSetting != null) - { - allowAnonymousUsersToEmailAFriendSetting.Value = "False"; - } - - var allowAnonymousUsersToReviewProductSetting = context.Set().FirstOrDefault(x => x.Name == "CatalogSettings.AllowAnonymousUsersToReviewProduct"); - if (allowAnonymousUsersToReviewProductSetting != null) - { - allowAnonymousUsersToReviewProductSetting.Value = "False"; - } - - var allowAnonymousUsersToEmailWishlistSetting = context.Set().FirstOrDefault(x => x.Name == "ShoppingCartSettings.AllowAnonymousUsersToEmailWishlist"); - if (allowAnonymousUsersToEmailWishlistSetting != null) - { - allowAnonymousUsersToEmailWishlistSetting.Value = "False"; - } - } - - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOver.Hint", - "Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout. Please note that the 'Show activated' option isn't legally compliant in line with the GDPR.", - "Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen knnen und ob die Checkbox dafr standardmig aktiviert ist. Bitte beachten Sie, dass die Option 'Aktiviert anzeigen' im Rahmen der DSVGO nicht rechtskonform ist."); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy", "Privacy", "Datenschutz"); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.EnableCookieConsent", - "Enable cookie consent", - "Cookie-Hinweis aktivieren", - "Specifies whether the cookie consent box will be displayed in the frontend.", - "Legt fest, ob ein Element fr die Zustimmung zur Nutzung von Cookies im Frontend angezeigt wird."); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.CookieConsentBadgetext", - "Cookie consent display text", - "Cookie-Hinweistext", - "Specifies the text, that will be displayed to your customers if they havn't agreed to the usage of cookis yet.", - "Bestimmt den Text, der Ihren Kunden beim Besuch der Seite angezeigt wird, sofern Sie ihre Zustimmung zur Nutzung von Cookies noch nicht gegeben haben."); - - builder.AddOrUpdate("CookieConsent.BadgeText", - "{0} is using cookies, to guarantee the best shopping experience. Partially cookies will be set by third parties. Privacy Info", - "{0} benutzt Cookies, um Ihnen das beste Einkaufserlebnis zu ermglichen. Zum Teil werden Cookies auch von Drittanbietern gesetzt. Datenschutzerklrung"); - - builder.AddOrUpdate("CookieConsent.Button", "Okay, got it", "Ok, verstanden"); - - builder.Delete("ContactUs.PrivacyAgreement"); - - builder.Delete("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress"); - builder.Delete("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress.Hint"); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.StoreLastIpAddress", - "Store IP address", - "IP-Adresse speichern", - "Specifies whether to store the IP address in the customer data set.", - "Legt fest, ob die IP-Adresse im Kundendatensatz gespeichert werden soll."); - - builder.Delete("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs"); - builder.Delete("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs.Hint"); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.DisplayGdprConsentOnForms", - "Get privacy consent for form submissions", - "Einwilligungserklrung in Formularen fordern", - "Specifies whether a checkbox is displayed in forms that prompts the user to agree to the processing of his data.", - "Bestimmt ob in Formularen eine Checkbox angezeigt wird, die den Benutzer auffordert der Verarbeitung seiner Daten zuzustimmen."); - - builder.AddOrUpdate("Gdpr.Consent.ValidationMessage", - "Please agree to the processing of your data.", - "Bitte stimmen Sie der Verarbeitung Ihrer Daten zu."); - - builder.Delete("ContactUs.PrivacyAgreement.MustBeAccepted"); - builder.Delete("ContactUs.PrivacyAgreement.DetailText"); - builder.AddOrUpdate("Gdpr.Consent.DetailText", - "Yes I've read the privacy terms and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry.", - "Ja, ich habe die Datenschutzerklrung zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt."); - - builder.AddOrUpdate("Gdpr.Anonymous", "Anonymous", "Anonym"); - builder.AddOrUpdate("Gdpr.Anonymize", "Anonymize", "Anonymisieren"); - builder.AddOrUpdate("Gdpr.DeletedText", "Deleted", "Gelscht"); - builder.AddOrUpdate("Gdpr.DeletedLongText", - "This content was deleted by the author.", - "Dieser Inhalt wurde vom Autor gelscht."); - builder.AddOrUpdate("Gdpr.Anonymize.Success", - "The customer record '{0}' has been anonymized.", - "Der Kundendatensatz '{0}' wurde anonymisiert."); - - builder.AddOrUpdate("Admin.Configuration.Languages.Fields.LastResourcesImportOn", - "Last import", - "Letzter Import", - "The date on which resources were last downloaded and imported.", - "Das Datum, an dem zuletzt Ressourcen heruntergeladen und importiert worden sind."); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerFormFields", "Registration", "Registrierung"); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields", "Addresses", "Adressen"); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.FirstNameRequired", - "First name required", - "Vorname ist erforderlich", - "Check the box if 'First name' is required.", - "Legt fest, ob die Angabe des Vornamens erforderlich ist."); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.LastNameRequired", - "Last name required", - "Nachname ist erforderlich", - "Check the box if 'Last name' is required.", - "Legt fest, ob die Angabe des Nachnamens erforderlich ist."); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.FullNameOnContactUsRequired", - "Name in the contact form is required", - "Name im Kontaktformular ist erforderlich", - "Specifies whether the name is required in the contact form.", - "Legt fest, ob die Angabe des Namens im Kontaktformular erforderlich ist."); - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.FullNameOnProductRequestRequired", - "Name in the product request is required", - "Name im Produktanfrage-Formular ist erforderlich", - "Specifies whether the name is required in the product request form.", - "Legt fest, ob die Angabe des Namens im Produktanfrage-Formular erforderlich ist."); - - builder.AddOrUpdate("Checkout.TermsOfService.IAccept", - "I agree with the {0}terms of service{1} and I adhere to them unconditionally. I've read the {3}privacy terms{1} and agree that my data given by me can be stored electronically.", - "Ich habe {0}die AGB{1} und {2}das Widerrufsrecht{1} gelesen und bin mit der Geltung einverstanden. Ich habe die {3}Datenschutzerklrung{1} zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden."); - - builder.AddOrUpdate("Admin.Customers.Customers.List.SearchDeletedOnly", "Only deactivated customers", "Nur deaktivierte Kunden"); - - builder.AddOrUpdate("Admin.Common.Global", "Global", "Global"); - builder.AddOrUpdate("Admin.Common.News", "News", "News"); - builder.AddOrUpdate("Admin.Common.Navigation", "Navigation", "Navigation"); - builder.AddOrUpdate("Admin.Common.PDF", "PDF", "PDF"); - builder.AddOrUpdate("Admin.Common.Footer", "Footer", "Footer"); - - builder.AddOrUpdate("Gdpr.Consent.DetailText.Small", "I agree to the Privacy policy.", "Mit den Bestimmungen zum Datenschutz bin ich einverstanden"); - - builder.AddOrUpdate("Account.Fields.Newsletter", - "I would like to subscribe to the newsletter. I agree to the . Unsubscription is possible at any time.", - "Ich mchte den Newsletter abonnieren. Mit den Bestimmungen zum Datenschutz bin ich einverstanden. Eine Abmeldung ist jederzeit mglich."); - - builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.EnableHoneypotProtection", - "Enable Honeypot protection", - "Honeypot aktivieren", - "Honeypot is a simple but reliable bot detection method that does not require any captcha. If active, registration and contact forms are protected against bots and attackers.", - "Honeypot ist eine simple aber zuverlssige Bot-Erkennungsmethode, die ganz ohne Captcha auskommt. Wenn aktiv, werden Registrierungs- und Kontaktformular vor Bots und Angreifern geschtzt."); - } - } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + MigrateSettings(context); + context.SaveChanges(); + } + + public void MigrateSettings(SmartObjectContext context) + { + // SeoSettings.RedirectLegacyTopicUrls should be true when migrating (it is false by default after fresh install) + var name = TypeHelper.NameOf(y => y.RedirectLegacyTopicUrls, true); + context.MigrateSettings(x => x.Add(name, true)); + + // remove setting which were moved from customer settings to privacy settings and have new default values which should be applied immediately + var settings = context.Set(); + var storeLastIpAddressSetting = settings.FirstOrDefault(x => x.Name == "CustomerSettings.StoreLastIpAddress"); + if (storeLastIpAddressSetting != null) settings.Remove(storeLastIpAddressSetting); + + var displayPrivacyAgreementOnContactUs = settings.FirstOrDefault(x => x.Name == "CustomerSettings.DisplayPrivacyAgreementOnContactUs"); + if (displayPrivacyAgreementOnContactUs != null) settings.Remove(displayPrivacyAgreementOnContactUs); + + + var showShareButtonName = TypeHelper.NameOf(y => y.ShowShareButton, true); + var showShareButtonSetting = context.Set().FirstOrDefault(x => x.Name == showShareButtonName); + if (showShareButtonSetting != null) + { + showShareButtonSetting.Value = "False"; + } + + var allowAnonymousUsersToEmailAFriendSetting = context.Set().FirstOrDefault(x => x.Name == "CatalogSettings.AllowAnonymousUsersToEmailAFriend"); + if (allowAnonymousUsersToEmailAFriendSetting != null) + { + allowAnonymousUsersToEmailAFriendSetting.Value = "False"; + } + + var allowAnonymousUsersToReviewProductSetting = context.Set().FirstOrDefault(x => x.Name == "CatalogSettings.AllowAnonymousUsersToReviewProduct"); + if (allowAnonymousUsersToReviewProductSetting != null) + { + allowAnonymousUsersToReviewProductSetting.Value = "False"; + } + + var allowAnonymousUsersToEmailWishlistSetting = context.Set().FirstOrDefault(x => x.Name == "ShoppingCartSettings.AllowAnonymousUsersToEmailWishlist"); + if (allowAnonymousUsersToEmailWishlistSetting != null) + { + allowAnonymousUsersToEmailWishlistSetting.Value = "False"; + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOver.Hint", + "Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout. Please note that the 'Show activated' option isn't legally compliant in line with the GDPR.", + "Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen knnen und ob die Checkbox dafr standardmig aktiviert ist. Bitte beachten Sie, dass die Option 'Aktiviert anzeigen' im Rahmen der DSVGO nicht rechtskonform ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy", "Privacy", "Datenschutz"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.EnableCookieConsent", + "Enable cookie consent", + "Cookie-Hinweis aktivieren", + "Specifies whether the cookie consent box will be displayed in the frontend.", + "Legt fest, ob ein Element fr die Zustimmung zur Nutzung von Cookies im Frontend angezeigt wird."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.CookieConsentBadgetext", + "Cookie consent display text", + "Cookie-Hinweistext", + "Specifies the text, that will be displayed to your customers if they havn't agreed to the usage of cookis yet.", + "Bestimmt den Text, der Ihren Kunden beim Besuch der Seite angezeigt wird, sofern Sie ihre Zustimmung zur Nutzung von Cookies noch nicht gegeben haben."); + + builder.AddOrUpdate("CookieConsent.BadgeText", + "{0} is using cookies, to guarantee the best shopping experience. Partially cookies will be set by third parties. Privacy Info", + "{0} benutzt Cookies, um Ihnen das beste Einkaufserlebnis zu ermglichen. Zum Teil werden Cookies auch von Drittanbietern gesetzt. Datenschutzerklrung"); + + builder.AddOrUpdate("CookieConsent.Button", "Okay, got it", "Ok, verstanden"); + + builder.Delete("ContactUs.PrivacyAgreement"); + + builder.Delete("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress"); + builder.Delete("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress.Hint"); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.StoreLastIpAddress", + "Store IP address", + "IP-Adresse speichern", + "Specifies whether to store the IP address in the customer data set.", + "Legt fest, ob die IP-Adresse im Kundendatensatz gespeichert werden soll."); + + builder.Delete("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs"); + builder.Delete("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs.Hint"); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.DisplayGdprConsentOnForms", + "Get privacy consent for form submissions", + "Einwilligungserklrung in Formularen fordern", + "Specifies whether a checkbox is displayed in forms that prompts the user to agree to the processing of his data.", + "Bestimmt ob in Formularen eine Checkbox angezeigt wird, die den Benutzer auffordert der Verarbeitung seiner Daten zuzustimmen."); + + builder.AddOrUpdate("Gdpr.Consent.ValidationMessage", + "Please agree to the processing of your data.", + "Bitte stimmen Sie der Verarbeitung Ihrer Daten zu."); + + builder.Delete("ContactUs.PrivacyAgreement.MustBeAccepted"); + builder.Delete("ContactUs.PrivacyAgreement.DetailText"); + builder.AddOrUpdate("Gdpr.Consent.DetailText", + "Yes I've read the privacy terms and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry.", + "Ja, ich habe die Datenschutzerklrung zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt."); + + builder.AddOrUpdate("Gdpr.Anonymous", "Anonymous", "Anonym"); + builder.AddOrUpdate("Gdpr.Anonymize", "Anonymize", "Anonymisieren"); + builder.AddOrUpdate("Gdpr.DeletedText", "Deleted", "Gelscht"); + builder.AddOrUpdate("Gdpr.DeletedLongText", + "This content was deleted by the author.", + "Dieser Inhalt wurde vom Autor gelscht."); + builder.AddOrUpdate("Gdpr.Anonymize.Success", + "The customer record '{0}' has been anonymized.", + "Der Kundendatensatz '{0}' wurde anonymisiert."); + + builder.AddOrUpdate("Admin.Configuration.Languages.Fields.LastResourcesImportOn", + "Last import", + "Letzter Import", + "The date on which resources were last downloaded and imported.", + "Das Datum, an dem zuletzt Ressourcen heruntergeladen und importiert worden sind."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerFormFields", "Registration", "Registrierung"); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields", "Addresses", "Adressen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.FirstNameRequired", + "First name required", + "Vorname ist erforderlich", + "Check the box if 'First name' is required.", + "Legt fest, ob die Angabe des Vornamens erforderlich ist."); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.LastNameRequired", + "Last name required", + "Nachname ist erforderlich", + "Check the box if 'Last name' is required.", + "Legt fest, ob die Angabe des Nachnamens erforderlich ist."); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.FullNameOnContactUsRequired", + "Name in the contact form is required", + "Name im Kontaktformular ist erforderlich", + "Specifies whether the name is required in the contact form.", + "Legt fest, ob die Angabe des Namens im Kontaktformular erforderlich ist."); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.FullNameOnProductRequestRequired", + "Name in the product request is required", + "Name im Produktanfrage-Formular ist erforderlich", + "Specifies whether the name is required in the product request form.", + "Legt fest, ob die Angabe des Namens im Produktanfrage-Formular erforderlich ist."); + + builder.AddOrUpdate("Checkout.TermsOfService.IAccept", + "I agree with the {0}terms of service{1} and I adhere to them unconditionally. I've read the {3}privacy terms{1} and agree that my data given by me can be stored electronically.", + "Ich habe {0}die AGB{1} und {2}das Widerrufsrecht{1} gelesen und bin mit der Geltung einverstanden. Ich habe die {3}Datenschutzerklrung{1} zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden."); + + builder.AddOrUpdate("Admin.Customers.Customers.List.SearchDeletedOnly", "Only deactivated customers", "Nur deaktivierte Kunden"); + + builder.AddOrUpdate("Admin.Common.Global", "Global", "Global"); + builder.AddOrUpdate("Admin.Common.News", "News", "News"); + builder.AddOrUpdate("Admin.Common.Navigation", "Navigation", "Navigation"); + builder.AddOrUpdate("Admin.Common.PDF", "PDF", "PDF"); + builder.AddOrUpdate("Admin.Common.Footer", "Footer", "Footer"); + + builder.AddOrUpdate("Gdpr.Consent.DetailText.Small", "I agree to the Privacy policy.", "Mit den Bestimmungen zum Datenschutz bin ich einverstanden"); + + builder.AddOrUpdate("Account.Fields.Newsletter", + "I would like to subscribe to the newsletter. I agree to the . Unsubscription is possible at any time.", + "Ich mchte den Newsletter abonnieren. Mit den Bestimmungen zum Datenschutz bin ich einverstanden. Eine Abmeldung ist jederzeit mglich."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.EnableHoneypotProtection", + "Enable Honeypot protection", + "Honeypot aktivieren", + "Honeypot is a simple but reliable bot detection method that does not require any captcha. If active, registration and contact forms are protected against bots and attackers.", + "Honeypot ist eine simple aber zuverlssige Bot-Erkennungsmethode, die ganz ohne Captcha auskommt. Wenn aktiv, werden Registrierungs- und Kontaktformular vor Bots und Angreifern geschtzt."); + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201806051221399_RefundReturnRequests.cs b/src/Libraries/SmartStore.Data/Migrations/201806051221399_RefundReturnRequests.cs index c887156aea..e74f179a68 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201806051221399_RefundReturnRequests.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201806051221399_RefundReturnRequests.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class RefundReturnRequests : DbMigration { public override void Up() { AddColumn("dbo.ReturnRequest", "RefundToWallet", c => c.Boolean()); } - + public override void Down() { DropColumn("dbo.ReturnRequest", "RefundToWallet"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201807051830375_MoveCustomerFields.cs b/src/Libraries/SmartStore.Data/Migrations/201807051830375_MoveCustomerFields.cs index 3def6ef8f3..0b211b6e2c 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201807051830375_MoveCustomerFields.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201807051830375_MoveCustomerFields.cs @@ -1,12 +1,14 @@ namespace SmartStore.Data.Migrations { using System; - using System.Data.Entity.Migrations; - using SmartStore.Data.Setup; - using SmartStore.Data.Utilities; + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; - public partial class MoveCustomerFields : DbMigration, ILocaleResourcesProvider, IDataSeeder - { + public partial class MoveCustomerFields : DbMigration, ILocaleResourcesProvider, IDataSeeder + { public override void Up() { AddColumn("dbo.Customer", "Salutation", c => c.String(maxLength: 50)); @@ -22,7 +24,7 @@ public override void Up() CreateIndex("dbo.Customer", "CustomerNumber", name: "IX_Customer_CustomerNumber", unique: false); CreateIndex("dbo.Customer", "BirthDate", name: "IX_Customer_BirthDate"); } - + public override void Down() { DropIndex("dbo.Customer", "IX_Customer_BirthDate"); @@ -39,30 +41,64 @@ public override void Down() DropColumn("dbo.Customer", "Salutation"); } - public bool RollbackOnFailure => true; + public bool RollbackOnFailure => true; - public void Seed(SmartObjectContext context) - { + public void Seed(SmartObjectContext context) + { context.MigrateLocaleResources(MigrateLocaleResources); - DataMigrator.MoveCustomerFields(context); - } + // Perf + var numDeletedAttrs = DataMigrator.DeleteGuestCustomerGenericAttributes(context, TimeSpan.FromDays(30)); + var numDeletedCustomers = DataMigrator.DeleteGuestCustomers(context, TimeSpan.FromDays(30)); + + var candidates = new[] { "Title", "FirstName", "LastName", "Company", "CustomerNumber", "DateOfBirth" }; + var numUpdatedCustomers = DataMigrator.MoveCustomerFields(context, UpdateCustomer, candidates); + } + + private static void UpdateCustomer(IDictionary columns, string key, string value) + { + switch (key) + { + case "Title": + columns[key] = value?.Truncate(50); + break; + case "FirstName": + columns[key] = value?.Truncate(199); + break; + case "LastName": + columns[key] = value?.Truncate(199); + break; + case "Company": + columns[key] = value?.Truncate(255); + break; + case "CustomerNumber": + columns[key] = value?.Truncate(100); + break; + case "DateOfBirth": + columns["BirthDate"] = value?.Convert(); + break; + } - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - builder.Delete( - "Admin.Customers.Customers.List.SearchFirstName", - "Admin.Customers.Customers.List.SearchFirstName.Hint", - "Admin.Customers.Customers.List.SearchLastName", - "Admin.Customers.Customers.List.SearchLastName.Hint", - "Admin.Customers.Customers.List.SearchCompany", - "Admin.Customers.Customers.List.SearchCompany.Hint"); + // Update FullName + var parts = new string[] { (string)columns.Get("Title"), (string)columns.Get("FirstName"), (string)columns.Get("LastName") }; + columns["FullName"] = string.Join(" ", parts.Where(x => x.HasValue())).NullEmpty(); + } - builder.AddOrUpdate("Admin.Customers.Customers.List.SearchTerm", - "Search term", - "Suchbegriff", - "Name or company", - "Name oder Firma"); - } - } + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete( + "Admin.Customers.Customers.List.SearchFirstName", + "Admin.Customers.Customers.List.SearchFirstName.Hint", + "Admin.Customers.Customers.List.SearchLastName", + "Admin.Customers.Customers.List.SearchLastName.Hint", + "Admin.Customers.Customers.List.SearchCompany", + "Admin.Customers.Customers.List.SearchCompany.Hint"); + + builder.AddOrUpdate("Admin.Customers.Customers.List.SearchTerm", + "Search term", + "Suchbegriff", + "Name or company", + "Name oder Firma"); + } + } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201807122120062_TopicAcl.cs b/src/Libraries/SmartStore.Data/Migrations/201807122120062_TopicAcl.cs index 639c810790..a525ea429a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201807122120062_TopicAcl.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201807122120062_TopicAcl.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class TopicAcl : DbMigration { public override void Up() @@ -14,7 +13,7 @@ public override void Up() AddColumn("dbo.Topic", "IsPublished", c => c.Boolean(nullable: false, defaultValue: true)); CreateIndex("dbo.Customer", "CustomerNumber", name: "IX_Customer_CustomerNumber"); } - + public override void Down() { DropIndex("dbo.Customer", "IX_Customer_CustomerNumber"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201807191020207_OrderItemDeliveryTime.cs b/src/Libraries/SmartStore.Data/Migrations/201807191020207_OrderItemDeliveryTime.cs index 39805ce878..341a2322b7 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201807191020207_OrderItemDeliveryTime.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201807191020207_OrderItemDeliveryTime.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class OrderItemDeliveryTime : DbMigration { public override void Up() @@ -10,7 +9,7 @@ public override void Up() AddColumn("dbo.OrderItem", "DeliveryTimeId", c => c.Int()); AddColumn("dbo.OrderItem", "DisplayDeliveryTime", c => c.Boolean(nullable: false)); } - + public override void Down() { DropColumn("dbo.OrderItem", "DisplayDeliveryTime"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201807201157391_DownloadVersions.cs b/src/Libraries/SmartStore.Data/Migrations/201807201157391_DownloadVersions.cs index 6210c475fc..caadf9da72 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201807201157391_DownloadVersions.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201807201157391_DownloadVersions.cs @@ -1,13 +1,12 @@ namespace SmartStore.Data.Migrations { + using System.Data.Entity.Migrations; + using System.Linq; + using System.Web.Hosting; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Data.Setup; using SmartStore.Data.Utilities; - using System; - using System.Data.Entity.Migrations; - using System.Linq; - using System.Web.Hosting; public partial class DownloadVersions : DbMigration, IDataSeeder { @@ -37,10 +36,7 @@ public override void Down() DropColumn("dbo.Download", "EntityId"); } - public bool RollbackOnFailure - { - get { return true; } - } + public bool RollbackOnFailure => true; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201807311708428_ProductPreviewPicture.cs b/src/Libraries/SmartStore.Data/Migrations/201807311708428_ProductPreviewPicture.cs index 91c06bfb44..9b7aa16571 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201807311708428_ProductPreviewPicture.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201807311708428_ProductPreviewPicture.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ProductPreviewPicture : DbMigration { public override void Up() { AddColumn("dbo.Product", "HasPreviewPicture", c => c.Boolean(nullable: false)); } - + public override void Down() { DropColumn("dbo.Product", "HasPreviewPicture"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.resx b/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.resx deleted file mode 100644 index 198ea117b6..0000000000 --- a/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 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 - - - dbo - - \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201808051818238_Merge4.cs b/src/Libraries/SmartStore.Data/Migrations/201808051818238_Merge4.cs index 0777552123..cf6adcbfff 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201808051818238_Merge4.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201808051818238_Merge4.cs @@ -1,14 +1,13 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class Merge4 : DbMigration { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/201809171309522_NewsletterSubscriptionLanguage.cs b/src/Libraries/SmartStore.Data/Migrations/201809171309522_NewsletterSubscriptionLanguage.cs index f1eccaf9e5..3304f86e1a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201809171309522_NewsletterSubscriptionLanguage.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201809171309522_NewsletterSubscriptionLanguage.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class NewsletterSubscriptionLanguage : DbMigration { public override void Up() { AddColumn("dbo.NewsLetterSubscription", "WorkingLanguageId", c => c.Int(nullable: false)); } - + public override void Down() { DropColumn("dbo.NewsLetterSubscription", "WorkingLanguageId"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201809261026134_ForumGroupAcl.cs b/src/Libraries/SmartStore.Data/Migrations/201809261026134_ForumGroupAcl.cs index 4abd203c4a..0c7e1af33d 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201809261026134_ForumGroupAcl.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201809261026134_ForumGroupAcl.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ForumGroupAcl : DbMigration { public override void Up() @@ -24,7 +23,7 @@ public override void Up() CreateIndex("dbo.Forums_Group", "LimitedToStores"); CreateIndex("dbo.Forums_Group", "SubjectToAcl"); } - + public override void Down() { DropIndex("dbo.Forums_Group", new[] { "SubjectToAcl" }); diff --git a/src/Libraries/SmartStore.Data/Migrations/201810011954195_ForumPostVote.cs b/src/Libraries/SmartStore.Data/Migrations/201810011954195_ForumPostVote.cs index 843ad1b291..37d44c2b2e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201810011954195_ForumPostVote.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201810011954195_ForumPostVote.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ForumPostVote : DbMigration { public override void Up() @@ -10,19 +9,19 @@ public override void Up() CreateTable( "dbo.ForumPostVote", c => new - { - Id = c.Int(nullable: false), - ForumPostId = c.Int(nullable: false), - Vote = c.Boolean(nullable: false), - }) + { + Id = c.Int(nullable: false), + ForumPostId = c.Int(nullable: false), + Vote = c.Boolean(nullable: false), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.CustomerContent", t => t.Id) .ForeignKey("dbo.Forums_Post", t => t.ForumPostId, cascadeDelete: true) .Index(t => t.Id) .Index(t => t.ForumPostId); - + } - + public override void Down() { DropForeignKey("dbo.ForumPostVote", "ForumPostId", "dbo.Forums_Post"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201810231214068_DataExchangeEnhancements.cs b/src/Libraries/SmartStore.Data/Migrations/201810231214068_DataExchangeEnhancements.cs index 1bdcda90d3..910446ec89 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201810231214068_DataExchangeEnhancements.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201810231214068_DataExchangeEnhancements.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Data.Setup; @@ -14,7 +13,7 @@ public override void Up() AlterColumn("dbo.ScheduleTaskHistory", "MachineName", c => c.String(nullable: false, maxLength: 400)); CreateIndex("dbo.ScheduleTaskHistory", new[] { "MachineName", "IsRunning" }); } - + public override void Down() { DropIndex("dbo.ScheduleTaskHistory", new[] { "MachineName", "IsRunning" }); diff --git a/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs index a51d10dc8f..964d170410 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201811061745204_IsSystemProductIndex.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Core.Data; @@ -15,7 +14,7 @@ public override void Up() } CreateIndex("dbo.Product", new[] { "Published", "Deleted", "IsSystemProduct" }, name: "IX_Product_Published_Deleted_IsSystemProduct"); } - + public override void Down() { DropIndex("dbo.Product", "IX_Product_Published_Deleted_IsSystemProduct"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201811082148279_LocalizedPropertyKeyGroupIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201811082148279_LocalizedPropertyKeyGroupIndex.cs index b5aef74649..847bea3807 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201811082148279_LocalizedPropertyKeyGroupIndex.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201811082148279_LocalizedPropertyKeyGroupIndex.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class LocalizedPropertyKeyGroupIndex : DbMigration { public override void Up() { CreateIndex("dbo.LocalizedProperty", "LocaleKeyGroup", name: "IX_LocalizedProperty_LocaleKeyGroup"); } - + public override void Down() { DropIndex("dbo.LocalizedProperty", "IX_LocalizedProperty_LocaleKeyGroup"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs index 6a1180c68d..6c6ba2e7a5 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201811161142587_TaskHistoryErrorLength.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class TaskHistoryErrorLength : DbMigration { public override void Up() { AlterColumn("dbo.ScheduleTaskHistory", "Error", c => c.String()); } - + public override void Down() { AlterColumn("dbo.ScheduleTaskHistory", "Error", c => c.String(maxLength: 1000)); diff --git a/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs index e78d62dc3f..90640d3991 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201811202204501_ProductIndexSeekExport.cs @@ -1,16 +1,15 @@ namespace SmartStore.Data.Migrations { - using SmartStore.Core.Data; - using System; using System.Data.Entity.Migrations; - + using SmartStore.Core.Data; + public partial class ProductIndexSeekExport : DbMigration { public override void Up() { - if (DataSettings.Current.IsSqlServer) - { - Sql(@"CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] + if (DataSettings.Current.IsSqlServer) + { + Sql(@"CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] ( [Published] ASC, [Id] ASC, @@ -21,15 +20,15 @@ public override void Up() [AvailableEndDateTimeUtc] ASC ) INCLUDE ([UpdatedOnUtc]) WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]"); - } + } } - public override void Down() - { - if (DataSettings.Current.IsSqlServer) - { - DropIndex("dbo.Product", "IX_SeekExport1"); - } - } + public override void Down() + { + if (DataSettings.Current.IsSqlServer) + { + DropIndex("dbo.Product", "IX_SeekExport1"); + } + } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201902211855242_TopicHtmlIdAndBodyCss.cs b/src/Libraries/SmartStore.Data/Migrations/201902211855242_TopicHtmlIdAndBodyCss.cs index 83f8e64ce5..c0a44dcdc9 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201902211855242_TopicHtmlIdAndBodyCss.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201902211855242_TopicHtmlIdAndBodyCss.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class TopicHtmlIdAndBodyCss : DbMigration { public override void Up() @@ -10,7 +9,7 @@ public override void Up() AddColumn("dbo.Topic", "HtmlId", c => c.String(maxLength: 128)); AddColumn("dbo.Topic", "BodyCssClass", c => c.String(maxLength: 512)); } - + public override void Down() { DropColumn("dbo.Topic", "BodyCssClass"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs index 645fdd9314..ae8714e7d7 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs @@ -12,56 +12,56 @@ public override void Up() CreateTable( "dbo.MenuRecord", c => new - { - Id = c.Int(nullable: false, identity: true), - SystemName = c.String(nullable: false, maxLength: 400), - IsSystemMenu = c.Boolean(nullable: false), - Template = c.String(maxLength: 400), - WidgetZone = c.String(maxLength: 4000), - Title = c.String(maxLength: 400), - Published = c.Boolean(nullable: false), - DisplayOrder = c.Int(nullable: false), - LimitedToStores = c.Boolean(nullable: false), - SubjectToAcl = c.Boolean(nullable: false), - }) + { + Id = c.Int(nullable: false, identity: true), + SystemName = c.String(nullable: false, maxLength: 400), + IsSystemMenu = c.Boolean(nullable: false), + Template = c.String(maxLength: 400), + WidgetZone = c.String(maxLength: 4000), + Title = c.String(maxLength: 400), + Published = c.Boolean(nullable: false), + DisplayOrder = c.Int(nullable: false), + LimitedToStores = c.Boolean(nullable: false), + SubjectToAcl = c.Boolean(nullable: false), + }) .PrimaryKey(t => t.Id) .Index(t => new { t.SystemName, t.IsSystemMenu }, name: "IX_Menu_SystemName_IsSystemMenu") .Index(t => t.Published, name: "IX_Menu_Published") .Index(t => t.LimitedToStores, name: "IX_Menu_LimitedToStores") .Index(t => t.SubjectToAcl, name: "IX_Menu_SubjectToAcl"); - + CreateTable( "dbo.MenuItemRecord", c => new - { - Id = c.Int(nullable: false, identity: true), - MenuId = c.Int(nullable: false), - ParentItemId = c.Int(nullable: false), - ProviderName = c.String(maxLength: 100), - Model = c.String(), - Title = c.String(maxLength: 400), - ShortDescription = c.String(maxLength: 400), - PermissionNames = c.String(), - Published = c.Boolean(nullable: false), - DisplayOrder = c.Int(nullable: false), - BeginGroup = c.Boolean(nullable: false), - ShowExpanded = c.Boolean(nullable: false), - NoFollow = c.Boolean(nullable: false), - NewWindow = c.Boolean(nullable: false), - Icon = c.String(maxLength: 100), - Style = c.String(maxLength: 10), - HtmlId = c.String(maxLength: 100), - CssClass = c.String(maxLength: 100), - }) + { + Id = c.Int(nullable: false, identity: true), + MenuId = c.Int(nullable: false), + ParentItemId = c.Int(nullable: false), + ProviderName = c.String(maxLength: 100), + Model = c.String(), + Title = c.String(maxLength: 400), + ShortDescription = c.String(maxLength: 400), + PermissionNames = c.String(), + Published = c.Boolean(nullable: false), + DisplayOrder = c.Int(nullable: false), + BeginGroup = c.Boolean(nullable: false), + ShowExpanded = c.Boolean(nullable: false), + NoFollow = c.Boolean(nullable: false), + NewWindow = c.Boolean(nullable: false), + Icon = c.String(maxLength: 100), + Style = c.String(maxLength: 10), + HtmlId = c.String(maxLength: 100), + CssClass = c.String(maxLength: 100), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.MenuRecord", t => t.MenuId, cascadeDelete: true) .Index(t => t.MenuId) .Index(t => t.ParentItemId, name: "IX_MenuItem_ParentItemId") .Index(t => t.Published, name: "IX_MenuItem_Published") .Index(t => t.DisplayOrder, name: "IX_MenuItem_DisplayOrder"); - + } - + public override void Down() { DropForeignKey("dbo.MenuItemRecord", "MenuId", "dbo.MenuRecord"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs index 304479485c..d26335d830 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Linq; using SmartStore.Core.Data; @@ -12,21 +11,18 @@ public partial class WidgetTopics : DbMigration, IDataSeeder public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { - using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false)) - { - var widgetTopics = new[] + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false)) + { + var widgetTopics = new[] { "CheckoutAsGuestOrRegister", "ContactUs", @@ -36,14 +32,14 @@ public void Seed(SmartObjectContext context) }; var topics = context.Set().Where(x => widgetTopics.Contains(x.SystemName)).ToList(); - topics.Each(x => - { + topics.Each(x => + { x.RenderAsWidget = true; x.WidgetWrapContent = false; }); context.SaveChanges(); - } - } + } + } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs index 387d1a9290..88c8d19d1b 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs @@ -2,24 +2,21 @@ namespace SmartStore.Data.Migrations { using System.Data.Entity.Migrations; using SmartStore.Core.Domain.Catalog; + using SmartStore.Core.Domain.Common; using SmartStore.Data.Setup; using SmartStore.Utilities; - using SmartStore.Core.Domain.Common; public partial class V320Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs index 89ade79f68..4bd176f7f9 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Data.Setup; @@ -9,15 +8,12 @@ public partial class V321Resources : DbMigration, ILocaleResourcesProvider, IDat public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs index 313530a024..5f5ee9c386 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs @@ -8,11 +8,11 @@ namespace SmartStore.Data.Migrations using SmartStore.Core.Domain.Localization; using SmartStore.Data.Setup; - public partial class QuantityUnitNamePlural : DbMigration, IDataSeeder + public partial class QuantityUnitNamePlural : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { - AddColumn("dbo.QuantityUnit", "NamePlural", c => c.String(nullable: false, maxLength: 50, defaultValue: "" )); + AddColumn("dbo.QuantityUnit", "NamePlural", c => c.String(nullable: false, maxLength: 50, defaultValue: "")); } public override void Down() diff --git a/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs index 708ca38b83..f856a343e3 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class CategoryExternalLink : DbMigration { public override void Up() { AddColumn("dbo.Category", "ExternalLink", c => c.String(maxLength: 255)); } - + public override void Down() { DropColumn("dbo.Category", "ExternalLink"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201907221803421_GranularPermissions.cs b/src/Libraries/SmartStore.Data/Migrations/201907221803421_GranularPermissions.cs index 42b01be189..689a322ec2 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201907221803421_GranularPermissions.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201907221803421_GranularPermissions.cs @@ -12,20 +12,20 @@ public override void Up() CreateTable( "dbo.PermissionRoleMapping", c => new - { - Id = c.Int(nullable: false, identity: true), - Allow = c.Boolean(nullable: false), - PermissionRecordId = c.Int(nullable: false), - CustomerRoleId = c.Int(nullable: false), - }) + { + Id = c.Int(nullable: false, identity: true), + Allow = c.Boolean(nullable: false), + PermissionRecordId = c.Int(nullable: false), + CustomerRoleId = c.Int(nullable: false), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.CustomerRole", t => t.CustomerRoleId, cascadeDelete: true) .ForeignKey("dbo.PermissionRecord", t => t.PermissionRecordId, cascadeDelete: true) .Index(t => t.PermissionRecordId) .Index(t => t.CustomerRoleId); - + } - + public override void Down() { DropForeignKey("dbo.PermissionRoleMapping", "PermissionRecordId", "dbo.PermissionRecord"); @@ -35,7 +35,7 @@ public override void Down() DropTable("dbo.PermissionRoleMapping"); } - public bool RollbackOnFailure => false; + public bool RollbackOnFailure => true; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/201907250103367_RuleSystem.cs b/src/Libraries/SmartStore.Data/Migrations/201907250103367_RuleSystem.cs index 62c412bd1d..e51e96adb5 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201907250103367_RuleSystem.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201907250103367_RuleSystem.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class RuleSystem : DbMigration { public override void Up() @@ -10,40 +9,40 @@ public override void Up() CreateTable( "dbo.RuleSet", c => new - { - Id = c.Int(nullable: false, identity: true), - Name = c.String(maxLength: 200), - Description = c.String(maxLength: 400), - IsActive = c.Boolean(nullable: false), - Scope = c.Int(nullable: false), - IsSubGroup = c.Boolean(nullable: false), - LogicalOperator = c.Int(nullable: false), - CreatedOnUtc = c.DateTime(nullable: false), - UpdatedOnUtc = c.DateTime(nullable: false), - LastProcessedOnUtc = c.DateTime(), - }) + { + Id = c.Int(nullable: false, identity: true), + Name = c.String(maxLength: 200), + Description = c.String(maxLength: 400), + IsActive = c.Boolean(nullable: false), + Scope = c.Int(nullable: false), + IsSubGroup = c.Boolean(nullable: false), + LogicalOperator = c.Int(nullable: false), + CreatedOnUtc = c.DateTime(nullable: false), + UpdatedOnUtc = c.DateTime(nullable: false), + LastProcessedOnUtc = c.DateTime(), + }) .PrimaryKey(t => t.Id) .Index(t => new { t.IsActive, t.Scope }, name: "IX_RuleSetEntity_Scope"); - + CreateTable( "dbo.Rule", c => new - { - Id = c.Int(nullable: false, identity: true), - RuleSetId = c.Int(nullable: false), - RuleType = c.String(nullable: false, maxLength: 100), - Operator = c.String(nullable: false, maxLength: 10), - Value = c.String(), - DisplayOrder = c.Int(nullable: false), - }) + { + Id = c.Int(nullable: false, identity: true), + RuleSetId = c.Int(nullable: false), + RuleType = c.String(nullable: false, maxLength: 100), + Operator = c.String(nullable: false, maxLength: 10), + Value = c.String(), + DisplayOrder = c.Int(nullable: false), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.RuleSet", t => t.RuleSetId, cascadeDelete: true) .Index(t => t.RuleSetId) .Index(t => t.RuleType, name: "IX_PageBuilder_RuleType") .Index(t => t.DisplayOrder, name: "IX_PageBuilder_DisplayOrder"); - + } - + public override void Down() { DropForeignKey("dbo.Rule", "RuleSetId", "dbo.RuleSet"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201908050758298_MoveFurtherCustomerFields.cs b/src/Libraries/SmartStore.Data/Migrations/201908050758298_MoveFurtherCustomerFields.cs index b95b01129b..020c785b60 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201908050758298_MoveFurtherCustomerFields.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201908050758298_MoveFurtherCustomerFields.cs @@ -1,10 +1,11 @@ namespace SmartStore.Data.Migrations { - using SmartStore.Data.Setup; - using SmartStore.Data.Utilities; using System; + using System.Collections.Generic; using System.Data.Entity.Migrations; - + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; + public partial class MoveFurtherCustomerFields : DbMigration, IDataSeeder { public override void Up() @@ -21,7 +22,7 @@ public override void Up() AddColumn("dbo.Customer", "LastUserAgent", c => c.String()); AddColumn("dbo.Customer", "LastUserDeviceType", c => c.String()); } - + public override void Down() { DropColumn("dbo.Customer", "LastUserDeviceType"); @@ -41,7 +42,38 @@ public override void Down() public void Seed(SmartObjectContext context) { - DataMigrator.MoveFurtherCustomerFields(context); + // Perf + var numDeletedAttrs = DataMigrator.DeleteGuestCustomerGenericAttributes(context, TimeSpan.FromDays(30)); + var numDeletedCustomers = DataMigrator.DeleteGuestCustomers(context, TimeSpan.FromDays(30)); + + var candidates = new[] { "Gender", "VatNumberStatusId", "TimeZoneId", "TaxDisplayTypeId", "LastForumVisit", "LastUserAgent", "LastUserDeviceType" }; + var numUpdatedCustomers = DataMigrator.MoveCustomerFields(context, UpdateCustomer, candidates); + } + + private static void UpdateCustomer(IDictionary columns, string key, string value) + { + switch (key) + { + case "Gender": + columns[key] = value?.Truncate(100); + break; + case "VatNumberStatusId": + columns[key] = value.Convert(); + break; + case "TimeZoneId": + columns[key] = value?.Truncate(255); + break; + case "TaxDisplayTypeId": + columns[key] = value.Convert(); + break; + case "LastForumVisit": + columns[key] = value.Convert(); + break; + case "LastUserAgent": + case "LastUserDeviceType": + columns[key] = value; + break; + } } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs index 12a38b031e..81056149ca 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Data.Setup; @@ -14,10 +13,7 @@ public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { @@ -62,8 +58,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Bitte berprfen Sie die Link-Daten. Auf Basis Ihrer Eingabe kann kein Link erzeugt werden."); builder.AddOrUpdate("Admin.Packaging.IsIncompatible", - "The package is not compatible the current app version {0}. Please update Smartstore.NET or install another version of this package.", - "Das Paket ist nicht kompatibel mit der aktuellen Programmversion {0}. Bitte aktualisieren Sie Smartstore.NET oder nutzen Sie eine andere, kompatible Paket-Version."); + "The package is not compatible the current app version {0}. Please update Smartstore or install another version of this package.", + "Das Paket ist nicht kompatibel mit der aktuellen Programmversion {0}. Bitte aktualisieren Sie Smartstore oder nutzen Sie eine andere, kompatible Paket-Version."); } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201908211821559_Merge5.cs b/src/Libraries/SmartStore.Data/Migrations/201908211821559_Merge5.cs index 2d7060fa00..ee1db0c32e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201908211821559_Merge5.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201908211821559_Merge5.cs @@ -1,14 +1,13 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class Merge5 : DbMigration { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/201908211825244_ProductTagPublished.cs b/src/Libraries/SmartStore.Data/Migrations/201908211825244_ProductTagPublished.cs index 8a60eb2088..b3d720ca95 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201908211825244_ProductTagPublished.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201908211825244_ProductTagPublished.cs @@ -5,7 +5,7 @@ namespace SmartStore.Data.Migrations using SmartStore.Core.Data; using SmartStore.Data.Setup; - public partial class ProductTagPublished : DbMigration, IDataSeeder + public partial class ProductTagPublished : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { diff --git a/src/Libraries/SmartStore.Data/Migrations/201908261226350_MenuItemMultistore.cs b/src/Libraries/SmartStore.Data/Migrations/201908261226350_MenuItemMultistore.cs index b18a0af699..c5948be73d 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201908261226350_MenuItemMultistore.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201908261226350_MenuItemMultistore.cs @@ -1,10 +1,9 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Data.Setup; - public partial class MenuItemMultistore : DbMigration, IDataSeeder + public partial class MenuItemMultistore : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { @@ -14,7 +13,7 @@ public override void Up() CreateIndex("dbo.MenuItemRecord", "LimitedToStores", name: "IX_MenuItem_LimitedToStores"); CreateIndex("dbo.MenuItemRecord", "SubjectToAcl", name: "IX_MenuItem_SubjectToAcl"); } - + public override void Down() { DropIndex("dbo.MenuItemRecord", "IX_MenuItem_SubjectToAcl"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201909291043284_BlogAndNewsItemPictures.cs b/src/Libraries/SmartStore.Data/Migrations/201909291043284_BlogAndNewsItemPictures.cs index 4ffefefc2d..8dc2ea6c66 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201909291043284_BlogAndNewsItemPictures.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201909291043284_BlogAndNewsItemPictures.cs @@ -1,24 +1,23 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Data.Setup; - public partial class BlogAndNewsItemPictures : DbMigration, IDataSeeder + public partial class BlogAndNewsItemPictures : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() - { + { AddColumn("dbo.BlogPost", "PictureId", c => c.Int()); AddColumn("dbo.BlogPost", "PreviewPictureId", c => c.Int()); AddColumn("dbo.BlogPost", "SectionBg", c => c.String(maxLength: 100)); AddColumn("dbo.BlogPost", "Intro", c => c.String()); AddColumn("dbo.BlogPost", "DisplayTagsInPreview", c => c.Boolean(nullable: false)); - AddColumn("dbo.BlogPost", "IsPublished", c => c.Boolean(nullable: false)); + AddColumn("dbo.BlogPost", "IsPublished", c => c.Boolean(nullable: false, defaultValue: true)); AddColumn("dbo.BlogPost", "PreviewDisplayType", c => c.Int(nullable: false)); AddColumn("dbo.News", "PictureId", c => c.Int()); AddColumn("dbo.News", "PreviewPictureId", c => c.Int()); } - + public override void Down() { DropColumn("dbo.News", "PreviewPictureId"); @@ -95,7 +94,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Verffentlicht", "Specifies whether the blog post is published and thus will be displayed in the frontend", "Bestimmt, ob der Blog-Eintrag verffentlicht ist und somit im Frontend dargestellt wird."); - + builder.AddOrUpdate("Common.Cms.EditBlogPost", "Edit blog post", "Blog-Post bearbeiten"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201910021805242_RemoveOldPermissions.cs b/src/Libraries/SmartStore.Data/Migrations/201910021805242_RemoveOldPermissions.cs index 18ce81a9a2..c9b18aac65 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201910021805242_RemoveOldPermissions.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201910021805242_RemoveOldPermissions.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Web.Hosting; using SmartStore.Core.Data; @@ -24,7 +23,7 @@ public override void Up() //DropColumn("dbo.PermissionRecord", "Category"); //DropTable("dbo.PermissionRecord_Role_Mapping"); } - + public override void Down() { //CreateTable( @@ -50,6 +49,7 @@ public void Seed(SmartObjectContext context) { try { + // Note, core scheduled tasks must always be added to the installation as well! context.Set().AddOrUpdate(x => x.Type, new ScheduleTask { diff --git a/src/Libraries/SmartStore.Data/Migrations/201910162004581_Merge6.cs b/src/Libraries/SmartStore.Data/Migrations/201910162004581_Merge6.cs index 2d3713900a..f33fea2e5e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201910162004581_Merge6.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201910162004581_Merge6.cs @@ -1,14 +1,13 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class Merge6 : DbMigration { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/201911090805330_ProductVisibility.cs b/src/Libraries/SmartStore.Data/Migrations/201911090805330_ProductVisibility.cs index d76a3231a2..dd167ee0d9 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201911090805330_ProductVisibility.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201911090805330_ProductVisibility.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Web.Hosting; using SmartStore.Core.Data; @@ -25,7 +24,7 @@ public override void Up() Sql("IF EXISTS (SELECT * FROM sys.indexes WHERE name='IX_SeekExport1' AND object_id = OBJECT_ID('[dbo].[Product]')) DROP INDEX [IX_SeekExport1] ON [dbo].[Product];"); - Sql(@"CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] + Sql(@"CREATE NONCLUSTERED INDEX [IX_SeekExport1] ON [dbo].[Product] ( [Published] ASC, [Id] ASC, @@ -63,7 +62,7 @@ [AvailableEndDateTimeUtc] ASC "END\r\n"); } } - + public override void Down() { if (DataSettings.Current.IsSqlServer) diff --git a/src/Libraries/SmartStore.Data/Migrations/201911120909434_Merge7.cs b/src/Libraries/SmartStore.Data/Migrations/201911120909434_Merge7.cs index ef20736467..514a30a8f3 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201911120909434_Merge7.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201911120909434_Merge7.cs @@ -1,14 +1,13 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class Merge7 : DbMigration { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/201911141820264_ScheduleTaskPriority.cs b/src/Libraries/SmartStore.Data/Migrations/201911141820264_ScheduleTaskPriority.cs index e7cdd48bab..96ddfb67fa 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201911141820264_ScheduleTaskPriority.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201911141820264_ScheduleTaskPriority.cs @@ -1,12 +1,11 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Linq; using System.Data.Entity.Migrations; + using System.Linq; using SmartStore.Core.Domain.Tasks; using SmartStore.Data.Setup; - public partial class ScheduleTaskPriority : DbMigration, IDataSeeder + public partial class ScheduleTaskPriority : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { @@ -22,13 +21,13 @@ public override void Down() public void Seed(SmartObjectContext context) { - var highPriorityTaskTypes = new[] + var highPriorityTaskTypes = new[] { "SmartStore.Services.Messages.QueuedMessagesSendTask, SmartStore.Services", "SmartStore.Services.Directory.UpdateExchangeRateTask, SmartStore.Services", //"SmartStore.MegaSearch.IndexingTask, SmartStore.MegaSearch", }; - + context.MigrateLocaleResources(MigrateLocaleResources); var tasks = context.Set().Where(x => highPriorityTaskTypes.Contains(x.Type)).ToList(); diff --git a/src/Libraries/SmartStore.Data/Migrations/201912051209105_MessageTemplateEmailAddress.cs b/src/Libraries/SmartStore.Data/Migrations/201912051209105_MessageTemplateEmailAddress.cs index bec9f8df85..78d3aa6849 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201912051209105_MessageTemplateEmailAddress.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201912051209105_MessageTemplateEmailAddress.cs @@ -1,26 +1,22 @@ namespace SmartStore.Data.Migrations { + using System.Data.Entity.Migrations; + using System.Linq; using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; using SmartStore.Data.Setup; - using System; - using System.Data.Entity.Migrations; - using System.Linq; public partial class MessageTemplateEmailAddress : DbMigration, IDataSeeder { public override void Up() { } - + public override void Down() { } - public bool RollbackOnFailure - { - get { return false; } - } + public bool RollbackOnFailure => false; public void Seed(SmartObjectContext context) { @@ -31,7 +27,7 @@ public void Seed(SmartObjectContext context) { gdprAdminEmail.To = "{{ Email.DisplayName }} <{{ Email.Email }}>"; } - + context.SaveChanges(); } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201912111821362_AddNewPermissions.cs b/src/Libraries/SmartStore.Data/Migrations/201912111821362_AddNewPermissions.cs index 4addffb4b3..27839d3937 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201912111821362_AddNewPermissions.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201912111821362_AddNewPermissions.cs @@ -8,7 +8,7 @@ public partial class AddNewPermissions : DbMigration, IDataSeeder c.String(maxLength: 100)); AlterColumn("dbo.Rule", "Operator", c => c.String(nullable: false, maxLength: 20)); } - + public override void Down() { AlterColumn("dbo.Rule", "Operator", c => c.String(nullable: false, maxLength: 10)); diff --git a/src/Libraries/SmartStore.Data/Migrations/202001221054109_ManufacturerBottomDescription.cs b/src/Libraries/SmartStore.Data/Migrations/202001221054109_ManufacturerBottomDescription.cs index 2b0319d892..a3603f70c5 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202001221054109_ManufacturerBottomDescription.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202001221054109_ManufacturerBottomDescription.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ManufacturerBottomDescription : DbMigration { public override void Up() @@ -10,7 +9,7 @@ public override void Up() AddColumn("dbo.Manufacturer", "BottomDescription", c => c.String()); CreateIndex("dbo.PermissionRecord", "SystemName"); } - + public override void Down() { DropIndex("dbo.PermissionRecord", new[] { "SystemName" }); diff --git a/src/Libraries/SmartStore.Data/Migrations/202001301039020_DiscountRuleSets.cs b/src/Libraries/SmartStore.Data/Migrations/202001301039020_DiscountRuleSets.cs index 59780dadc5..d0391353e4 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202001301039020_DiscountRuleSets.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202001301039020_DiscountRuleSets.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Core.Data; using SmartStore.Data.Setup; @@ -13,45 +12,45 @@ public override void Up() CreateTable( "dbo.RuleSet_PaymentMethod_Mapping", c => new - { - PaymentMethod_Id = c.Int(nullable: false), - RuleSetEntity_Id = c.Int(nullable: false), - }) + { + PaymentMethod_Id = c.Int(nullable: false), + RuleSetEntity_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.PaymentMethod_Id, t.RuleSetEntity_Id }) .ForeignKey("dbo.PaymentMethod", t => t.PaymentMethod_Id, cascadeDelete: true) .ForeignKey("dbo.RuleSet", t => t.RuleSetEntity_Id, cascadeDelete: true) .Index(t => t.PaymentMethod_Id) .Index(t => t.RuleSetEntity_Id); - + CreateTable( "dbo.RuleSet_ShippingMethod_Mapping", c => new - { - ShippingMethod_Id = c.Int(nullable: false), - RuleSetEntity_Id = c.Int(nullable: false), - }) + { + ShippingMethod_Id = c.Int(nullable: false), + RuleSetEntity_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.ShippingMethod_Id, t.RuleSetEntity_Id }) .ForeignKey("dbo.ShippingMethod", t => t.ShippingMethod_Id, cascadeDelete: true) .ForeignKey("dbo.RuleSet", t => t.RuleSetEntity_Id, cascadeDelete: true) .Index(t => t.ShippingMethod_Id) .Index(t => t.RuleSetEntity_Id); - + CreateTable( "dbo.RuleSet_Discount_Mapping", c => new - { - Discount_Id = c.Int(nullable: false), - RuleSetEntity_Id = c.Int(nullable: false), - }) + { + Discount_Id = c.Int(nullable: false), + RuleSetEntity_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.Discount_Id, t.RuleSetEntity_Id }) .ForeignKey("dbo.Discount", t => t.Discount_Id, cascadeDelete: true) .ForeignKey("dbo.RuleSet", t => t.RuleSetEntity_Id, cascadeDelete: true) .Index(t => t.Discount_Id) .Index(t => t.RuleSetEntity_Id); - + CreateIndex("dbo.RuleSet", "IsSubGroup"); } - + public override void Down() { DropForeignKey("dbo.RuleSet_Discount_Mapping", "RuleSetEntity_Id", "dbo.RuleSet"); @@ -72,7 +71,7 @@ public override void Down() DropTable("dbo.RuleSet_PaymentMethod_Mapping"); } - public bool RollbackOnFailure => false; + public bool RollbackOnFailure => true; public void Seed(SmartObjectContext context) { diff --git a/src/Libraries/SmartStore.Data/Migrations/202002172101120_PictureMediaRename.cs b/src/Libraries/SmartStore.Data/Migrations/202002172101120_PictureMediaRename.cs index 9bf02df384..e0d19a569e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002172101120_PictureMediaRename.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002172101120_PictureMediaRename.cs @@ -112,7 +112,7 @@ public override void Up() new[] { "dbo.MediaFile", "Id", "false", "FK_dbo.Category_dbo.Picture_PictureId" }, true, true); - + RenameColumnCe("dbo.MediaFile", "SeoFilename", "Name", c => c.String(maxLength: 300)); RenameColumnCe("dbo.Product_MediaFile_Mapping", "PictureId", "MediaFileId", c => c.Int(nullable: false), @@ -120,7 +120,7 @@ public override void Up() new[] { "dbo.MediaFile", "Id", "true", "FK_dbo.Product_Picture_Mapping_dbo.Picture_PictureId" }, true, true); - + RenameColumnCe("dbo.Manufacturer", "PictureId", "MediaFileId", c => c.Int(), new[] { "PictureId", "dbo.Picture" }, new[] { "dbo.MediaFile", "Id", "false", "FK_dbo.Manufacturer_dbo.Picture_PictureId" }, @@ -136,7 +136,7 @@ public override void Up() RenameColumnCe("dbo.Store", "LogoPictureId", "LogoMediaFileId", c => c.Int(nullable: false)); } } - + public override void Down() { if (DataSettings.Current.IsSqlServer) diff --git a/src/Libraries/SmartStore.Data/Migrations/202002180228163_PictureMediaRename1.cs b/src/Libraries/SmartStore.Data/Migrations/202002180228163_PictureMediaRename1.cs index 16b9c98047..fb3fc9139d 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002180228163_PictureMediaRename1.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002180228163_PictureMediaRename1.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class PictureMediaRename1 : DbMigration { public override void Up() { // A cumbersome but necessary step to prevent EF from dropping renamed tables. } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/202002191252074_RemoveDiscountRequirements.cs b/src/Libraries/SmartStore.Data/Migrations/202002191252074_RemoveDiscountRequirements.cs index 280d3b7f7f..72b39d7843 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002191252074_RemoveDiscountRequirements.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002191252074_RemoveDiscountRequirements.cs @@ -16,7 +16,7 @@ public override void Up() //DropIndex("dbo.DiscountRequirement", new[] { "DiscountId" }); //DropTable("dbo.DiscountRequirement"); } - + public override void Down() { //CreateTable( @@ -37,7 +37,7 @@ public override void Down() // ExtraData = c.String(), // }) // .PrimaryKey(t => t.Id); - + //CreateIndex("dbo.DiscountRequirement", "DiscountId"); //AddForeignKey("dbo.DiscountRequirement", "DiscountId", "dbo.Discount", "Id", cascadeDelete: true); } diff --git a/src/Libraries/SmartStore.Data/Migrations/202002211011108_GenericMessageTemplate.cs b/src/Libraries/SmartStore.Data/Migrations/202002211011108_GenericMessageTemplate.cs index 909ec87d5b..b3e70a5953 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002211011108_GenericMessageTemplate.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002211011108_GenericMessageTemplate.cs @@ -6,12 +6,12 @@ namespace SmartStore.Data.Migrations using SmartStore.Core.Domain.Messages; using SmartStore.Data.Setup; - public partial class GenericMessageTemplate : DbMigration, IDataSeeder + public partial class GenericMessageTemplate : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { } - + public override void Down() { } @@ -25,7 +25,7 @@ public void Seed(SmartObjectContext context) return; } - context.MigrateLocaleResources(MigrateLocaleResources); + context.MigrateLocaleResources(MigrateLocaleResources); var defaultEmailAccount = context.Set().FirstOrDefault(x => x.Email != null); var table = context.Set(); @@ -49,8 +49,8 @@ public void Seed(SmartObjectContext context) public void MigrateLocaleResources(LocaleResourcesBuilder builder) { - builder.AddOrUpdate("Admin.Customers.Customers.SendEmail.EmailNotValid", - "No valid e-mail address is stored for the customer.", + builder.AddOrUpdate("Admin.Customers.Customers.SendEmail.EmailNotValid", + "No valid e-mail address is stored for the customer.", "Fr den Kunden ist keine gltige E-Mail-Adresse hinterlegt."); builder.Delete("Admin.Customers.Customers.SendPM.Message.Hint"); diff --git a/src/Libraries/SmartStore.Data/Migrations/202002241354543_ProductCondition.cs b/src/Libraries/SmartStore.Data/Migrations/202002241354543_ProductCondition.cs index 6a4dc2f7ca..e5ee0c0891 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002241354543_ProductCondition.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002241354543_ProductCondition.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ProductCondition : DbMigration { public override void Up() @@ -10,7 +9,7 @@ public override void Up() AddColumn("dbo.Product", "Condition", c => c.Int(nullable: false)); CreateIndex("dbo.ProductAttribute", "DisplayOrder"); } - + public override void Down() { DropIndex("dbo.ProductAttribute", new[] { "DisplayOrder" }); diff --git a/src/Libraries/SmartStore.Data/Migrations/202002251510114_ManufacturerSubjectToAcl.cs b/src/Libraries/SmartStore.Data/Migrations/202002251510114_ManufacturerSubjectToAcl.cs index 117ca62911..e7a616ff0a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002251510114_ManufacturerSubjectToAcl.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002251510114_ManufacturerSubjectToAcl.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ManufacturerSubjectToAcl : DbMigration { public override void Up() @@ -10,7 +9,7 @@ public override void Up() AddColumn("dbo.Manufacturer", "SubjectToAcl", c => c.Boolean(nullable: false)); CreateIndex("dbo.Manufacturer", "SubjectToAcl"); } - + public override void Down() { DropIndex("dbo.Manufacturer", new[] { "SubjectToAcl" }); diff --git a/src/Libraries/SmartStore.Data/Migrations/202002271206204_ShipmentTrackingUrl.cs b/src/Libraries/SmartStore.Data/Migrations/202002271206204_ShipmentTrackingUrl.cs index 4d355535ce..7d752fe47f 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002271206204_ShipmentTrackingUrl.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002271206204_ShipmentTrackingUrl.cs @@ -1,15 +1,14 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class ShipmentTrackingUrl : DbMigration { public override void Up() { AddColumn("dbo.Shipment", "TrackingUrl", c => c.String(maxLength: 2000)); } - + public override void Down() { DropColumn("dbo.Shipment", "TrackingUrl"); diff --git a/src/Libraries/SmartStore.Data/Migrations/202002280005340_MediaManager.cs b/src/Libraries/SmartStore.Data/Migrations/202002280005340_MediaManager.cs index 9f9d8302ab..b210c5ef59 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202002280005340_MediaManager.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202002280005340_MediaManager.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Data.Setup; @@ -19,61 +18,61 @@ public override void Up() CreateTable( "dbo.MediaFolder", c => new - { - Id = c.Int(nullable: false, identity: true), - ParentId = c.Int(), - Name = c.String(nullable: false, maxLength: 255), - Slug = c.String(maxLength: 255), - CanDetectTracks = c.Boolean(nullable: false), - Metadata = c.String(), - FilesCount = c.Int(nullable: false), - ResKey = c.String(maxLength: 255), - IncludePath = c.Boolean(), - Order = c.Int(), - Discriminator = c.String(nullable: false, maxLength: 128), - }) + { + Id = c.Int(nullable: false, identity: true), + ParentId = c.Int(), + Name = c.String(nullable: false, maxLength: 255), + Slug = c.String(maxLength: 255), + CanDetectTracks = c.Boolean(nullable: false), + Metadata = c.String(), + FilesCount = c.Int(nullable: false), + ResKey = c.String(maxLength: 255), + IncludePath = c.Boolean(), + Order = c.Int(), + Discriminator = c.String(nullable: false, maxLength: 128), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.MediaFolder", t => t.ParentId) .Index(t => new { t.ParentId, t.Name }, unique: true, name: "IX_NameParentId"); - + CreateTable( "dbo.MediaTag", c => new - { - Id = c.Int(nullable: false, identity: true), - Name = c.String(nullable: false, maxLength: 100), - }) + { + Id = c.Int(nullable: false, identity: true), + Name = c.String(nullable: false, maxLength: 100), + }) .PrimaryKey(t => t.Id) .Index(t => t.Name, name: "IX_MediaTag_Name"); - + CreateTable( "dbo.MediaTrack", c => new - { - Id = c.Int(nullable: false, identity: true), - MediaFileId = c.Int(nullable: false), - Album = c.String(nullable: false, maxLength: 50), - EntityId = c.Int(nullable: false), - EntityName = c.String(nullable: false, maxLength: 255), - }) + { + Id = c.Int(nullable: false, identity: true), + MediaFileId = c.Int(nullable: false), + Album = c.String(nullable: false, maxLength: 50), + EntityId = c.Int(nullable: false), + EntityName = c.String(nullable: false, maxLength: 255), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.MediaFile", t => t.MediaFileId, cascadeDelete: true) .Index(t => new { t.MediaFileId, t.EntityId, t.EntityName }, unique: true, name: "IX_MediaTrack_Composite") .Index(t => t.Album); - + CreateTable( "dbo.MediaFile_Tag_Mapping", c => new - { - MediaFile_Id = c.Int(nullable: false), - MediaTag_Id = c.Int(nullable: false), - }) + { + MediaFile_Id = c.Int(nullable: false), + MediaTag_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.MediaFile_Id, t.MediaTag_Id }) .ForeignKey("dbo.MediaFile", t => t.MediaFile_Id, cascadeDelete: true) .ForeignKey("dbo.MediaTag", t => t.MediaTag_Id, cascadeDelete: true) .Index(t => t.MediaFile_Id) .Index(t => t.MediaTag_Id); - + AddColumn("dbo.MediaFile", "FolderId", c => c.Int()); AddColumn("dbo.MediaFile", "Alt", c => c.String(maxLength: 400)); AddColumn("dbo.MediaFile", "Title", c => c.String(maxLength: 400)); @@ -111,7 +110,7 @@ public override void Up() AddForeignKey("dbo.News", "MediaFileId", "dbo.MediaFile", "Id"); AddForeignKey("dbo.News", "PreviewMediaFileId", "dbo.MediaFile", "Id"); } - + public override void Down() { DropForeignKey("dbo.News", "PreviewMediaFileId", "dbo.MediaFile"); diff --git a/src/Libraries/SmartStore.Data/Migrations/202003022018038_Merge8.cs b/src/Libraries/SmartStore.Data/Migrations/202003022018038_Merge8.cs index ec8c19e9e6..d1d37ebb47 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202003022018038_Merge8.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202003022018038_Merge8.cs @@ -1,14 +1,13 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class Merge8 : DbMigration { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/202003052100521_CustomerRoleMappings.cs b/src/Libraries/SmartStore.Data/Migrations/202003052100521_CustomerRoleMappings.cs index 5f0f4a9dbe..24bd074a26 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202003052100521_CustomerRoleMappings.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202003052100521_CustomerRoleMappings.cs @@ -1,8 +1,8 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Linq; + using System.Web.Hosting; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Tasks; @@ -12,88 +12,55 @@ public partial class CustomerRoleMappings : DbMigration, IDataSeeder new - { - Id = c.Int(nullable: false, identity: true), - CustomerId = c.Int(nullable: false), - CustomerRoleId = c.Int(nullable: false), - IsSystemMapping = c.Boolean(nullable: false), - }) + { + Id = c.Int(nullable: false, identity: true), + CustomerId = c.Int(nullable: false), + CustomerRoleId = c.Int(nullable: false), + IsSystemMapping = c.Boolean(nullable: false), + }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.Customer", t => t.CustomerId, cascadeDelete: true) .ForeignKey("dbo.CustomerRole", t => t.CustomerRoleId, cascadeDelete: true) .Index(t => t.CustomerId) .Index(t => t.CustomerRoleId) .Index(t => t.IsSystemMapping); - + CreateTable( "dbo.RuleSet_CustomerRole_Mapping", c => new - { - CustomerRole_Id = c.Int(nullable: false), - RuleSetEntity_Id = c.Int(nullable: false), - }) + { + CustomerRole_Id = c.Int(nullable: false), + RuleSetEntity_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.CustomerRole_Id, t.RuleSetEntity_Id }) .ForeignKey("dbo.CustomerRole", t => t.CustomerRole_Id, cascadeDelete: true) .ForeignKey("dbo.RuleSet", t => t.RuleSetEntity_Id, cascadeDelete: true) .Index(t => t.CustomerRole_Id) .Index(t => t.RuleSetEntity_Id); - - AddColumn("dbo.QueuedEmailAttachment", "MediaFileId", c => c.Int()); - CreateIndex("dbo.QueuedEmailAttachment", "MediaFileId"); - AddForeignKey("dbo.QueuedEmailAttachment", "MediaFileId", "dbo.MediaFile", "Id"); - AddForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage", "Id", cascadeDelete: true); - DropColumn("dbo.MediaFile", "IsNew"); - DropColumn("dbo.Download", "DownloadBinary"); - DropColumn("dbo.Download", "ContentType"); - DropColumn("dbo.Download", "Filename"); - DropColumn("dbo.Download", "Extension"); - DropColumn("dbo.Download", "IsNew"); - DropColumn("dbo.Download", "MediaStorageId"); - DropColumn("dbo.QueuedEmailAttachment", "FileId"); - DropColumn("dbo.QueuedEmailAttachment", "Data"); + + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + // Copy customer role mappings. + Sql("IF (EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Customer_CustomerRole_Mapping')) BEGIN Insert Into [dbo].[CustomerRoleMapping] (CustomerId, CustomerRoleId, IsSystemMapping) Select Customer_Id As Customer_Id, CustomerRole_Id As CustomerRole_Id, 0 As IsSystemMapping From [dbo].[Customer_CustomerRole_Mapping] END"); + } } - + public override void Down() { - AddColumn("dbo.QueuedEmailAttachment", "Data", c => c.Binary()); - AddColumn("dbo.QueuedEmailAttachment", "FileId", c => c.Int()); - AddColumn("dbo.Download", "MediaStorageId", c => c.Int()); - AddColumn("dbo.Download", "IsNew", c => c.Boolean(nullable: false)); - AddColumn("dbo.Download", "Extension", c => c.String()); - AddColumn("dbo.Download", "Filename", c => c.String()); - AddColumn("dbo.Download", "ContentType", c => c.String()); - AddColumn("dbo.Download", "DownloadBinary", c => c.Binary()); - AddColumn("dbo.MediaFile", "IsNew", c => c.Boolean(nullable: false)); - DropForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage"); - DropForeignKey("dbo.QueuedEmailAttachment", "MediaFileId", "dbo.MediaFile"); DropForeignKey("dbo.RuleSet_CustomerRole_Mapping", "RuleSetEntity_Id", "dbo.RuleSet"); DropForeignKey("dbo.RuleSet_CustomerRole_Mapping", "CustomerRole_Id", "dbo.CustomerRole"); DropForeignKey("dbo.CustomerRoleMapping", "CustomerRoleId", "dbo.CustomerRole"); DropForeignKey("dbo.CustomerRoleMapping", "CustomerId", "dbo.Customer"); DropIndex("dbo.RuleSet_CustomerRole_Mapping", new[] { "RuleSetEntity_Id" }); DropIndex("dbo.RuleSet_CustomerRole_Mapping", new[] { "CustomerRole_Id" }); - DropIndex("dbo.QueuedEmailAttachment", new[] { "MediaFileId" }); DropIndex("dbo.CustomerRoleMapping", new[] { "IsSystemMapping" }); DropIndex("dbo.CustomerRoleMapping", new[] { "CustomerRoleId" }); DropIndex("dbo.CustomerRoleMapping", new[] { "CustomerId" }); - DropColumn("dbo.QueuedEmailAttachment", "MediaFileId"); DropTable("dbo.RuleSet_CustomerRole_Mapping"); DropTable("dbo.CustomerRoleMapping"); - CreateIndex("dbo.QueuedEmailAttachment", "FileId"); - CreateIndex("dbo.Download", "MediaStorageId"); - CreateIndex("dbo.MediaFile", "IsNew"); - AddForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage", "Id"); - AddForeignKey("dbo.QueuedEmailAttachment", "FileId", "dbo.Download", "Id", cascadeDelete: true); - AddForeignKey("dbo.Download", "MediaStorageId", "dbo.MediaStorage", "Id"); } public bool RollbackOnFailure => true; @@ -101,18 +68,14 @@ public override void Down() public void Seed(SmartObjectContext context) { if (!DataSettings.DatabaseIsInstalled()) - return; - - try { - // Copy role mappings. - context.ExecuteSqlCommand("Insert Into [dbo].[CustomerRoleMapping] (CustomerId, CustomerRoleId, IsSystemMapping) Select Customer_Id As Customer_Id, CustomerRole_Id As CustomerRole_Id, 0 As IsSystemMapping From [dbo].[Customer_CustomerRole_Mapping]"); + return; } - catch { } var defaultLang = context.Set().AsNoTracking().OrderBy(x => x.DisplayOrder).First(); var isGerman = defaultLang.UniqueSeoCode.IsCaseInsensitiveEqual("de"); + // Note, core scheduled tasks must always be added to the installation as well! context.Set().AddOrUpdate(x => x.Type, new ScheduleTask { diff --git a/src/Libraries/SmartStore.Data/Migrations/202003112359492_MediaManager2.cs b/src/Libraries/SmartStore.Data/Migrations/202003112359492_MediaManager2.cs index ddd952a719..bcf8c00d5f 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202003112359492_MediaManager2.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202003112359492_MediaManager2.cs @@ -1,12 +1,26 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - - public partial class MediaManager2 : DbMigration + using SmartStore.Data.Setup; + + public partial class MediaManager2 : DbMigration, IDataSeeder { public override void Up() { + #region Moved from "202003052100521_CustomerRoleMappings" to here + + DropIndex("dbo.MediaFile", new[] { "IsNew" }); + AddColumn("dbo.QueuedEmailAttachment", "MediaFileId", c => c.Int()); + CreateIndex("dbo.QueuedEmailAttachment", "MediaFileId"); + AddForeignKey("dbo.QueuedEmailAttachment", "MediaFileId", "dbo.MediaFile", "Id"); + DropForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage"); + AddForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage", "Id", cascadeDelete: true); + DropColumn("dbo.MediaFile", "IsNew"); + DropColumn("dbo.Download", "DownloadBinary"); + DropColumn("dbo.Download", "IsNew"); + + #endregion + DropIndex("dbo.MediaTrack", "IX_MediaTrack_Composite"); AddColumn("dbo.MediaTrack", "Property", c => c.String(maxLength: 255)); AlterColumn("dbo.Setting", "Name", c => c.String(nullable: false, maxLength: 400)); @@ -15,9 +29,33 @@ public override void Up() CreateIndex("dbo.MediaTrack", new[] { "MediaFileId", "EntityId", "EntityName", "Property" }, unique: true, name: "IX_MediaTrack_Composite"); DropColumn("dbo.MediaFile", "PictureBinary"); } - + public override void Down() { + #region Moved from "202003052100521_CustomerRoleMappings" to here + + AddColumn("dbo.QueuedEmailAttachment", "Data", c => c.Binary()); + AddColumn("dbo.QueuedEmailAttachment", "FileId", c => c.Int()); + AddColumn("dbo.Download", "MediaStorageId", c => c.Int()); + AddColumn("dbo.Download", "IsNew", c => c.Boolean(nullable: false)); + AddColumn("dbo.Download", "Extension", c => c.String()); + AddColumn("dbo.Download", "Filename", c => c.String()); + AddColumn("dbo.Download", "ContentType", c => c.String()); + AddColumn("dbo.Download", "DownloadBinary", c => c.Binary()); + AddColumn("dbo.MediaFile", "IsNew", c => c.Boolean(nullable: false)); + DropForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage"); + DropForeignKey("dbo.QueuedEmailAttachment", "MediaFileId", "dbo.MediaFile"); + DropIndex("dbo.QueuedEmailAttachment", new[] { "MediaFileId" }); + DropColumn("dbo.QueuedEmailAttachment", "MediaFileId"); + CreateIndex("dbo.QueuedEmailAttachment", "FileId"); + CreateIndex("dbo.Download", "MediaStorageId"); + CreateIndex("dbo.MediaFile", "IsNew"); + AddForeignKey("dbo.QueuedEmailAttachment", "MediaStorageId", "dbo.MediaStorage", "Id"); + AddForeignKey("dbo.QueuedEmailAttachment", "FileId", "dbo.Download", "Id", cascadeDelete: true); + AddForeignKey("dbo.Download", "MediaStorageId", "dbo.MediaStorage", "Id"); + + #endregion + AddColumn("dbo.MediaFile", "PictureBinary", c => c.Binary()); DropIndex("dbo.MediaTrack", "IX_MediaTrack_Composite"); DropIndex("dbo.Setting", "IX_Setting_StoreId"); @@ -26,5 +64,26 @@ public override void Down() DropColumn("dbo.MediaTrack", "Property"); CreateIndex("dbo.MediaTrack", new[] { "MediaFileId", "EntityId", "EntityName" }, unique: true, name: "IX_MediaTrack_Composite"); } + + public void Seed(SmartObjectContext context) + { + // We cannot tear down obsolete DB structure during regular migration, but only + // AFTER a successfull data seed, 'cause during the seed we need the data. + var sql = @" +ALTER TABLE [dbo].[Download] DROP CONSTRAINT [FK_dbo.Download_dbo.MediaStorage_MediaStorageId] +ALTER TABLE [dbo].[QueuedEmailAttachment] DROP CONSTRAINT [FK_dbo.QueuedEmailAttachment_dbo.Download_FileId] +DROP INDEX [IX_MediaStorageId] ON [dbo].[Download] +DROP INDEX [IX_FileId] ON [dbo].[QueuedEmailAttachment] +ALTER TABLE [dbo].[Download] DROP COLUMN [ContentType] +ALTER TABLE [dbo].[Download] DROP COLUMN [Filename] +ALTER TABLE [dbo].[Download] DROP COLUMN [Extension] +ALTER TABLE [dbo].[Download] DROP COLUMN [MediaStorageId] +ALTER TABLE [dbo].[QueuedEmailAttachment] DROP COLUMN [FileId] +ALTER TABLE [dbo].[QueuedEmailAttachment] DROP COLUMN [Data] +"; + context.ExecuteSqlCommand(sql); + } + + public bool RollbackOnFailure => false; } } diff --git a/src/Libraries/SmartStore.Data/Migrations/202003171812584_CookieManager.cs b/src/Libraries/SmartStore.Data/Migrations/202003171812584_CookieManager.cs index 5fe20c4aa5..799ce565aa 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202003171812584_CookieManager.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202003171812584_CookieManager.cs @@ -1,19 +1,17 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Linq; using SmartStore.Core.Data; using SmartStore.Core.Domain.Cms; - using SmartStore.Core.Domain.Localization; using SmartStore.Data.Setup; - public partial class CookieManager : DbMigration, IDataSeeder + public partial class CookieManager : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/202003251118391_CategoryRuleSets.cs b/src/Libraries/SmartStore.Data/Migrations/202003251118391_CategoryRuleSets.cs index 3d2fc2d198..5069c892c2 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202003251118391_CategoryRuleSets.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202003251118391_CategoryRuleSets.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Linq; using SmartStore.Core.Domain.Localization; @@ -14,20 +13,20 @@ public override void Up() CreateTable( "dbo.RuleSet_Category_Mapping", c => new - { - Category_Id = c.Int(nullable: false), - RuleSetEntity_Id = c.Int(nullable: false), - }) + { + Category_Id = c.Int(nullable: false), + RuleSetEntity_Id = c.Int(nullable: false), + }) .PrimaryKey(t => new { t.Category_Id, t.RuleSetEntity_Id }) .ForeignKey("dbo.Category", t => t.Category_Id, cascadeDelete: true) .ForeignKey("dbo.RuleSet", t => t.RuleSetEntity_Id, cascadeDelete: true) .Index(t => t.Category_Id) .Index(t => t.RuleSetEntity_Id); - + AddColumn("dbo.Product_Category_Mapping", "IsSystemMapping", c => c.Boolean(nullable: false)); CreateIndex("dbo.Product_Category_Mapping", "IsSystemMapping"); } - + public override void Down() { DropForeignKey("dbo.RuleSet_Category_Mapping", "RuleSetEntity_Id", "dbo.RuleSet"); @@ -46,6 +45,7 @@ public void Seed(SmartObjectContext context) var defaultLang = context.Set().AsNoTracking().OrderBy(x => x.DisplayOrder).First(); var isGerman = defaultLang.UniqueSeoCode.IsCaseInsensitiveEqual("de"); + // Note, core scheduled tasks must always be added to the installation as well! context.Set().AddOrUpdate(x => x.Type, new ScheduleTask { diff --git a/src/Libraries/SmartStore.Data/Migrations/202003311314082_CookieManagerCountries.cs b/src/Libraries/SmartStore.Data/Migrations/202003311314082_CookieManagerCountries.cs index 643e252b00..bfcb56642b 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202003311314082_CookieManagerCountries.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202003311314082_CookieManagerCountries.cs @@ -1,23 +1,21 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using SmartStore.Core.Data; using SmartStore.Data.Setup; - public partial class CookieManagerCountries : DbMigration, IDataSeeder + public partial class CookieManagerCountries : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { - AddColumn("dbo.Country", "DisplayCookieManager", c => c.Boolean(nullable: false)); + AddColumn("dbo.Country", "DisplayCookieManager", c => c.Boolean(nullable: false, defaultValue: true)); } - + public override void Down() { DropColumn("dbo.Country", "DisplayCookieManager"); } - public bool RollbackOnFailure => true; public void Seed(SmartObjectContext context) @@ -33,8 +31,8 @@ public void Seed(SmartObjectContext context) public void MigrateLocaleResources(LocaleResourcesBuilder builder) { - builder.AddOrUpdate("Admin.Configuration.Countries.Fields.DisplayCookieManager", - "Display Cookie Manager", + builder.AddOrUpdate("Admin.Configuration.Countries.Fields.DisplayCookieManager", + "Display Cookie Manager", "Cookie-Manager anzeigen", "Specifies whether the Cookie Manager will be displayed to shop visitors from this country.", "Bestimmt, ob der Cookie-Manager Shop-Besuchern aus diesem Land angezeigt wird."); diff --git a/src/Libraries/SmartStore.Data/Migrations/202004301922188_RemoveCustomerCustomerRoles.cs b/src/Libraries/SmartStore.Data/Migrations/202004301922188_RemoveCustomerCustomerRoles.cs index 57b01b4f4d..99ac87c6de 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202004301922188_RemoveCustomerCustomerRoles.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202004301922188_RemoveCustomerCustomerRoles.cs @@ -1,6 +1,5 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; using System.Web.Hosting; using SmartStore.Core.Data; @@ -25,7 +24,7 @@ public override void Up() //DropIndex("dbo.Customer_CustomerRole_Mapping", new[] { "CustomerRole_Id" }); //DropTable("dbo.Customer_CustomerRole_Mapping"); } - + public override void Down() { AlterColumn("dbo.Rule", "Value", c => c.String(maxLength: 400)); @@ -38,7 +37,7 @@ public override void Down() // CustomerRole_Id = c.Int(nullable: false), // }) // .PrimaryKey(t => new { t.Customer_Id, t.CustomerRole_Id }); - + //CreateIndex("dbo.Customer_CustomerRole_Mapping", "CustomerRole_Id"); //CreateIndex("dbo.Customer_CustomerRole_Mapping", "Customer_Id"); //AddForeignKey("dbo.Customer_CustomerRole_Mapping", "CustomerRole_Id", "dbo.CustomerRole", "Id", cascadeDelete: true); diff --git a/src/Libraries/SmartStore.Data/Migrations/202005111006305_CampaignSubjectToAcl.cs b/src/Libraries/SmartStore.Data/Migrations/202005111006305_CampaignSubjectToAcl.cs index 28e7874474..e6d328c13a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/202005111006305_CampaignSubjectToAcl.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202005111006305_CampaignSubjectToAcl.cs @@ -1,8 +1,7 @@ namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class CampaignSubjectToAcl : DbMigration { public override void Up() @@ -10,7 +9,7 @@ public override void Up() AddColumn("dbo.Campaign", "SubjectToAcl", c => c.Boolean(nullable: false)); CreateIndex("dbo.NewsLetterSubscription", "Active"); } - + public override void Down() { DropIndex("dbo.NewsLetterSubscription", new[] { "Active" }); diff --git a/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.Designer.cs similarity index 80% rename from src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.Designer.cs rename to src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.Designer.cs index 9002b8b1aa..dd34b95167 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.Designer.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.Designer.cs @@ -7,13 +7,13 @@ namespace SmartStore.Data.Migrations using System.Resources; [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] - public sealed partial class Merge4 : IMigrationMetadata + public sealed partial class MediaManager3 : IMigrationMetadata { - private readonly ResourceManager Resources = new ResourceManager(typeof(Merge4)); + private readonly ResourceManager Resources = new ResourceManager(typeof(MediaManager3)); string IMigrationMetadata.Id { - get { return "201808051119578_Merge4"; } + get { return "202005201826512_MediaManager3"; } } string IMigrationMetadata.Source diff --git a/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.cs b/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.cs new file mode 100644 index 0000000000..d80781eac3 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.cs @@ -0,0 +1,23 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class MediaManager3 : DbMigration, IDataSeeder + { + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + // Empty, but we need the events in SmartStore.Services + } + + public override void Up() + { + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.resx b/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.resx new file mode 100644 index 0000000000..8bb9fa2f2d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202005201826512_MediaManager3.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.Designer.cs new file mode 100644 index 0000000000..395c46d11f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class V400Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V400Resources)); + + string IMigrationMetadata.Id + { + get { return "202006250801086_V400Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.cs b/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.cs new file mode 100644 index 0000000000..6ec22fa5c1 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.cs @@ -0,0 +1,901 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using System.Linq; + using System.Web.Hosting; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Core.Domain.Media; + using SmartStore.Data.Setup; + + public partial class V400Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + // MaxUploadFileSize setting is stored with an empty string in some databases, which leads to type conversion problems. + Sql("UPDATE [dbo].[Setting] SET [Value] = '102400' WHERE [Name] = 'MediaSettings.MaxUploadFileSize' And [Value] = ''"); + } + } + + public override void Down() + { + } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + MigrateSettings(context); + context.SaveChanges(); + } + + public void MigrateSettings(SmartObjectContext context) + { + var prefix = nameof(MediaSettings) + "."; + + ChangeMediaSetting(nameof(MediaSettings.AvatarPictureSize), "256", x => x == 250); + ChangeMediaSetting(nameof(MediaSettings.ProductThumbPictureSize), "256", x => x == 250); + ChangeMediaSetting(nameof(MediaSettings.CategoryThumbPictureSize), "256", x => x == 250); + ChangeMediaSetting(nameof(MediaSettings.ManufacturerThumbPictureSize), "256", x => x == 250); + ChangeMediaSetting(nameof(MediaSettings.CartThumbPictureSize), "256", x => x == 250); + ChangeMediaSetting(nameof(MediaSettings.MiniCartThumbPictureSize), "256", x => x == 250); + ChangeMediaSetting(nameof(MediaSettings.ProductThumbPictureSizeOnProductDetailsPage), "72", x => x == 70); + ChangeMediaSetting(nameof(MediaSettings.MessageProductThumbPictureSize), "72", x => x == 70); + ChangeMediaSetting(nameof(MediaSettings.BundledProductPictureSize), "72", x => x == 70); + ChangeMediaSetting(nameof(MediaSettings.VariantValueThumbPictureSize), "72", x => x == 70); + ChangeMediaSetting(nameof(MediaSettings.AttributeOptionThumbPictureSize), "72", x => x == 70); + + void ChangeMediaSetting(string propName, string newVal, Func predicate) + { + var name = prefix + propName; + var settings = context.Set().Where(x => x.Name == name).ToList(); + foreach (var setting in settings) + { + if (predicate(setting.Value.Convert())) + { + setting.Value = newVal; + } + } + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + // FluentValidation splits messages by dot for client validation. Don't use abbrevs. + builder.AddOrUpdate("Validation.MinimumLengthValidator") + .Value("de", "'{PropertyName}' muss mindestens {MinLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); + builder.AddOrUpdate("Validation.MaximumLengthValidator") + .Value("de", "'{PropertyName}' darf maximal {MaxLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); + + builder.AddOrUpdate("Admin.Configuration.Measures.Weights.AddWeight", "Add weight", "Gewicht hinzufgen"); + builder.AddOrUpdate("Admin.Configuration.Measures.Weights.EditWeight", "Edit weight", "Gewicht bearbeiten"); + + builder.AddOrUpdate("Admin.Configuration.Measures.Dimensions.AddDimension", "Add dimension", "Abmessung hinzufgen"); + builder.AddOrUpdate("Admin.Configuration.Measures.Dimensions.EditDimension", "Edit dimension", "Abmessung bearbeiten"); + + builder.AddOrUpdate("Admin.Configuration.QuantityUnit.AddQuantityUnit", "Add quantity unit", "Verpackungseinheit hinzufgen"); + builder.AddOrUpdate("Admin.Configuration.QuantityUnit.EditQuantityUnit", "Edit quantity unit", "Verpackungseinheit bearbeiten"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ApplyPercentageDiscountOnTierPrice", + "Apply percentage discounts on tier prices", + "Prozentuale Rabatte auf Staffelpreise anwenden", + "Specifies whether to apply percentage discounts also on tier prices.", + "Legt fest, ob prozentuale Rabatte auch auf Staffelpreise angewendet werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ShowProductBundleImagesOnShoppingCart", + "Show product images of bundle items", + "Produktbilder von Bundle-Bestandteilen anzeigen", + "Specifies whether to show product images of bundle items.", + "Legt fest, ob Produktbilder von Bundle-Bestandteilen angezeigt werden sollen."); + + builder.AddOrUpdate("Admin.Common.CustomerRole.LimitedTo", + "Limited to customer roles", + "Auf Kundengruppen begrenzt", + "Specifies whether the object is only available to certain customer groups.", + "Legt fest, ob das Objekt nur fr bestimmte Kundengruppen verfgbar ist."); + + builder.AddOrUpdate("Admin.Permissions.AllowInherited", "Allow (inherited)", "Erlaubt (geerbt)"); + builder.AddOrUpdate("Admin.Permissions.DenyInherited", "Deny (inherited)", "Verweigert (geerbt)"); + + builder.AddOrUpdate("Admin.AccessDenied.DetailedDescription", + "
You do not have permission to perform the selected operation.
Access right: {0}
System name: {1}
", + "
Sie haben keine Berechtigung, diesen Vorgang durchzufhren.
Zugriffsrecht: {0}
Systemname: {1}
"); + + builder.Delete( + "Admin.Configuration.Measures.Weights.Fields.MarkAsPrimaryWeight", + "Admin.Configuration.Measures.Dimensions.Fields.MarkAsPrimaryDimension", + "Admin.Customers.Customers.Addresses.AddButton", + "Admin.Address", + "Admin.Catalog.Products.Acl", + "Admin.Configuration.ACL.Updated", + "Admin.Configuration.Stores.NoStoresDefined", + "Admin.Configuration.Acl.NoRolesDefined", + "Admin.Common.Acl.AvailableFor", + "Admin.Catalog.Attributes.SpecificationAttributes.OptionsCount"); + + builder.AddOrUpdate("Admin.ContentManagement.Blog.BlogPosts.Fields.Tags.Hint", + "Tags are keywords that this blog post can also be identified by. Enter a comma separated list of the tags to be associated with this blog post.", + "Dieser Blog-Eintrag kann durch die Verwendung von Tags (Stichwrter) gekennzeichnet und kategorisiert werden. Mehrere Tags knnen als kommagetrennte Liste eingegeben werden."); + + // Granular permission. + builder.AddOrUpdate("Common.Read", "Read", "Lesen"); + builder.AddOrUpdate("Common.Create", "Create", "Erstellen"); + builder.AddOrUpdate("Common.Notify", "Notify", "Benachrichtigen"); + builder.AddOrUpdate("Common.Approve", "Approve", "Genehmigen"); + builder.AddOrUpdate("Common.Rules", "Rules", "Regeln"); + builder.AddOrUpdate("Common.Allow", "Allow", "Erlaubt"); + builder.AddOrUpdate("Common.Deny", "Deny", "Verweigert"); + builder.AddOrUpdate("Common.ExpandCollapseAll", @"Expand\collapse all", @"Alle auf-\zuklappen"); + builder.AddOrUpdate("Common.Trash", "Trash", "Papierkorb"); + builder.AddOrUpdate("Common.Cut", "Cut", "Ausschneiden"); + builder.AddOrUpdate("Common.Copy", "Copy", "Kopieren"); + builder.AddOrUpdate("Common.Paste", "Paste", "Einfgen"); + builder.AddOrUpdate("Common.SelectAll", "Select all", "Alles auswhlen"); + builder.AddOrUpdate("Common.Rename", "Rename", "Umbenennen"); + + builder.AddOrUpdate("Common.CtrlKey", "Ctrl", "Strg"); + builder.AddOrUpdate("Common.ShiftKey", "Shift", "Umschalt"); + builder.AddOrUpdate("Common.AltKey", "Alt", "Alt"); + builder.AddOrUpdate("Common.DelKey", "Del", "Entf"); + builder.AddOrUpdate("Common.EnterKey", "Enter", "Eingabe"); + builder.AddOrUpdate("Common.EscKey", "Esc", "Esc"); + + builder.AddOrUpdate("Admin.Customers.PermissionViewNote", + "The view shows the permissions that apply to this customer based on the customer roles assigned to him. To change permissions, switch to the relevant customer role.", + "Die Ansicht zeigt die Rechte, die fr diesen Kunden auf Basis der ihm zugeordneten Kundengruppen gelten. Um Rechte zu ndern, wechseln Sie bitte zur betreffenden Kundengruppe."); + + builder.AddOrUpdate("Admin.Permissions.AddedPermissions", + "Added permissions: {0}", + "Hinzugefgte Zugriffsrechte: {0}"); + + builder.AddOrUpdate("Admin.Permissions.RemovedPermissions", + "Permissions have been removed: {0}", + "Zugriffsrechte wurden entfernt: {0}"); + + builder.AddOrUpdate("Permissions.DisplayName.DisplayPrice", "Display prices", "Preise anzeigen"); + builder.AddOrUpdate("Permissions.DisplayName.AccessShop", "Access shop", "Zugang zum Shop"); + builder.AddOrUpdate("Permissions.DisplayName.AccessShoppingCart", "Access shoppping cart", "Auf Warenkorb zugreifen"); + builder.AddOrUpdate("Permissions.DisplayName.AccessWishlist", "Access wishlist", "Auf Wunschliste zugreifen"); + builder.AddOrUpdate("Permissions.DisplayName.AccessBackend", "Access backend", "Auf Backend zugreifen"); + builder.AddOrUpdate("Permissions.DisplayName.EditOrderItem", "Edit order items", "Auftragspositionen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditShipment", "Edit shipment", "Sendungen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditAnswer", "Edit answers", "Antworten bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditOptionSet", "Edit options sets", "Options-Sets bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditCategory", "Edit categories", "Warengruppen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditManufacturer", "Edit manufacturers", "Hersteller bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditAssociatedProduct", "Edit associated products", "Verknpfte Produkte bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditBundle", "Edit bundles", "Bundles bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditPromotion", "Edit promotion", "Promotion bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditPicture", "Edit pictures", "Bilder bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditTag", "Edit tags", "Tags bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditAttribute", "Edit attributes", "Attribute bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditVariant", "Edit variants", "Varianten bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditTierPrice", "Edit tier prices", "Staffelpreise bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditRecurringPayment", "Edit recurring payment", "Wiederkehrende Zahlungen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditOption", "Edit options", "Optionen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditAddress", "Edit addresses", "Adressen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditCustomerRole", "Edit customer roles", "Kundengruppen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.SendPM", "Send private messages", "Private Nachrichten senden"); + builder.AddOrUpdate("Permissions.DisplayName.EditProduct", "Edit products", "Produkte bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditComment", "Edit comments", "Kommentare bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.EditResource", "Edit resources", "Ressourcen bearbeiten"); + builder.AddOrUpdate("Permissions.DisplayName.ReadStats", "Display dashboard", "bersicht anzeigen"); + + builder.AddOrUpdate("Admin.ContentManagement.Blog.Heading.Publish", "Publishing", "Verffentlichung"); + builder.AddOrUpdate("Admin.ContentManagement.Blog.Heading.Display", "Display", "Darstellung"); + builder.AddOrUpdate("Admin.ContentManagement.News.Heading.Publish", "Publishing", "Verffentlichung"); + + builder.AddOrUpdate("Admin.Validation.Url", "Please enter a valid URL.", "Bitte geben Sie eine gltige URL ein."); + + builder.AddOrUpdate("Common.WrongInvisibleCaptcha", + "The reCAPTCHA failed. Please try it again.", + "reCAPTCHA ist fehlgeschlagen. Bitte versuchen Sie es erneut."); + + builder.AddOrUpdate("Common.CannotDisplayView", + "The view \"{0}\" could not be displayed.", + "Die Ansicht \"{0}\" konnte nicht angezeigt werden."); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.Visibility", + "Visibility", + "Sichtbarkeit", + "Limits the visibility of the product. In the case of \"Not visible\", the product only appears as an associated product on the parent product detail page, but without a link to an individual page.", + "Schrnkt die Sichtbarkeit des Produktes ein. Bei \"Nicht sichtbar\" erscheint das Produkt nur noch als verknpftes Produkt auf der bergeordneten Produktdetailseite, jedoch ohne Verlinkung auf eine eigenstndige Seite."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.Visibility", + "Visibility", + "Sichtbarkeit", + "Filter by visibility. \"In search results\" includes fully visible products.", + "Nach Sichtbarkeit filter. \"In Suchergebnissen\" schliet berall sichtbare Produkte mit ein."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.Full", "Fully visible", "berall sichtbar"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.SearchResults", "In search results", "In Suchergebnissen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.ProductPage", "On product detail pages", "Auf Produktdetailseiten"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.Hidden", "Not visible", "Nicht sichtbar"); + + builder.Delete( + "Admin.Catalog.Products.Fields.VisibleIndividually", + "Admin.Catalog.Products.Fields.VisibleIndividually.Hint", + "Admin.Promotions.Discounts.NoDiscountsAvailable", + "Admin.Orders.Shipments.TrackingNumber.Button", + "Admin.Catalog.Products.Copy.CopyImages"); + + // Rule + builder.AddOrUpdate("Admin.Rules.SystemName", "System name", "Systemname"); + builder.AddOrUpdate("Admin.Rules.Title", "Title", "Titel"); + builder.AddOrUpdate("Admin.Rules.TestConditions", "{0} Test {1} Rules", "Bedingungen {0} Testen {1}"); + builder.AddOrUpdate("Admin.Rules.AddGroup", "Add group", "Gruppe hinzufgen"); + builder.AddOrUpdate("Admin.Rules.DeleteGroup", "Delete group", "Gruppe lschen"); + builder.AddOrUpdate("Admin.Rules.AddCondition", "Add condition", "Bedingung hinzufgen"); + builder.AddOrUpdate("Admin.Rules.SaveConditions", "Save all conditions", "Alle Bedingungen speichern"); + builder.AddOrUpdate("Admin.Rules.OpenRule", "Open rule", "Regel ffnen"); + + builder.AddOrUpdate("Admin.Rules.EditRule", "Edit rule", "Regel bearbeiten"); + builder.AddOrUpdate("Admin.Rules.AddRule", "Add rule", "Regel hinzufgen"); + builder.AddOrUpdate("Admin.Rules.RuleSet.Added", "The rule has been successfully added.", "Die Regel wurde erfolgreich hinzugefgt."); + builder.AddOrUpdate("Admin.Rules.RuleSet.Deleted", "The rule has been successfully deleted.", "Die Regel wurde erfolgreich gelscht."); + builder.AddOrUpdate("Admin.Rules.Operator.All", "ALL", "ALLE"); + builder.AddOrUpdate("Admin.Rules.Operator.One", "ONE", "EINE"); + + builder.AddOrUpdate("Admin.Rules.OneCondition", + "If at least {0} of the following conditions is true.", + "Wenn mindestens {0} der folgenden Bedingungen zutrifft."); + + builder.AddOrUpdate("Admin.Rules.AllConditions", + "If {0} of the following conditions are true.", + "Wenn {0} der folgenden Bedingungen erfllt sind."); + + builder.AddOrUpdate("Admin.Rules.NotFound", "The rule with ID {0} was not found.", "Die Regel mit der ID {0} wurde nicht gefunden."); + builder.AddOrUpdate("Admin.Rules.GroupNotFound", "The group with ID {0} was not found.", "Die Gruppe mit der ID {0} wurde nicht gefunden."); + builder.AddOrUpdate("Admin.Rules.NumberOfRules", "Number of rules", "Anzahl an Regeln"); + builder.AddOrUpdate("Admin.Rules.OperatorNotSupported", "The rule scope does not support this operator.", "Die Regelart untersttzt diesen Operator nicht."); + + builder.AddOrUpdate("Admin.Rules.InvalidDescriptor", + "Invalid rule. This rule is no longer supported and should be deleted.", + "Ungltige Regel. Diese Regel wird nicht mehr untersttzt und sollte gelscht werden."); + + builder.AddOrUpdate("Admin.Rules.SaveToCreateConditions", + "Conditions can only be created after saving the rule.", + "Bedingungen knnen erst nach Speichern der Regel festgelegt werden."); + + builder.AddOrUpdate("Admin.Rules.Execute.MatchCustomers", + "{0} customers match the rule conditions.", + "{0} Kunden entsprechen den Regelbedingungen."); + builder.AddOrUpdate("Admin.Rules.Execute.MatchProducts", + "{0} products match the rule conditions.", + "{0} Produkte entsprechen den Regelbedingungen."); + builder.AddOrUpdate("Admin.Rules.Execute.MatchCart", + "The rule conditions are true for the current customer {0}.", + "Die Regelbedingungen sind fr den aktuellen Kunden {0} wahr."); + builder.AddOrUpdate("Admin.Rules.Execute.DoesNotMatchCart", + "The rule conditions are false for the current customer {0}.", + "Die Regelbedingungen sind fr den aktuellen Kunden {0} falsch."); + + // Rule fields + builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.Name", "Name", "Name"); + builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.Description", "Description", "Beschreibung"); + builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.IsActive", "Is active", "Ist aktiv"); + builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.Scope", "Scope", "Art"); + builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.IsSubGroup", "Is sub group", "Ist Untergruppe"); + builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.LogicalOperator", "Logical operator", "Logischer Operator"); + + // Rule descriptors + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.TaxExempt", "Tax exempt", "Steuerbefreit"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BillingCountry", "Billing country", "Rechnungsland"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingCountry", "Shipping country", "Lieferland"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastActivityDays", "Days since last activity", "Tage seit letztem Besuch"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CompletedOrderCount", "Completed order count", "Anzahl abgeschlossener Bestellungen"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CancelledOrderCount", "Cancelled order count", "Anzahl stornierter Bestellungen"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.NewOrderCount", "New order count", "Anzahl neuer Bestellungen"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderInStore", "Orders in store", "Bestellungen im Shop"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PurchasedProduct", "Purchased product", "Gekauftes Produkt"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PurchasedFromManufacturer", "Purchased from manufacturer", "Gekauft von Hersteller"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.HasPurchasedProduct", "Has purchased product", "Hat eines der folgenden Produkte gekauft"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.HasPurchasedAllProducts", "Has purchased all products", "Hat alle folgenden Produkte gekauft"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.RuleSet", "Other rule set", "Anderer Regelsatz"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Active", "Is active", "Ist aktiv"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastLoginDays", "Days since last login", "Tage seit letztem Login"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CreatedDays", "Days since registration", "Tage seit Registrierung"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Salutation", "Salutation", "Anrede"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Title", "Title", "Titel"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Company", "Company", "Firma"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CustomerNumber", "Customer number", "Kundennummer"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BirthDate", "Days since date of birth", "Tage seit dem Geburtsdatum"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Gender", "Gender", "Geschlecht"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ZipPostalCode", "Zip postal code", "PLZ"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.VatNumberStatus", "Vat number status", "Steuernummerstatus"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.TimeZone", "Time zone", "Zeitzone"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.TaxDisplayType", "Tax display type", "Steueranzeigetyp"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.IPCountry", "IP associated with country", "IP gehrt zu Land"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Currency", "Currency", "Whrung"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.MobileDevice", "Mobile device", "Mobiles Endgert"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.DeviceFamily", "Device family", "Endgertefamilie"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OperatingSystem", "Operating system", "Betriebssystem"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BrowserName", "Browser name", "Browser Name"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BrowserMajorVersion", "Browser major version", "Browser Hauptversionsnummer"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BrowserMinorVersion", "Browser minor version", "Browser Nebenversionsnummer"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Language", "Language", "Sprache"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastForumVisit", "Days since last forum visit", "Tage seit letztem Forenbesuch"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastUserAgent", "Last user agent", "Zuletzt genutzter User-Agent"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.IsInCustomerRole", "In customer role", "In Kundengruppe"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Store", "Store", "Shop"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastOrderDateDays", "Days since last order", "Tage seit letzter Bestellung"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.AcceptThirdPartyEmailHandOver", "Accept third party email handover", "Akzeptiert Weitergabe der E-Mail Adresse an Dritte"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CartTotal", "Total amount of cart", "Gesamtbetrag des Warenkorbes"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CartSubtotal", "Subtotal amount of cart", "Zwischensumme des Warenkorbes"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductInCart", "Product in cart", "Produkt im Warenkorb"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductFromCategoryInCart", "Product from category in cart", "Produkt aus Kategorie im Warenkorb"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductFromManufacturerInCart", "Product from manufacturer in cart", "Produkt von Hersteller im Warenkorb"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductOnWishlist", "Product on wishlist", "Produkt auf der Wunschliste"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductReviewCount", "Number of product reviews", "Anzahl der Produkt Rezensionen"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.RewardPointsBalance", "Number of reward points", "Anzahl der Bonuspunkte"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderTotal", "Order total", "Gesamtbetrag der Bestellung"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderSubtotalInclTax", "Order subtotal incl. tax", "Gesamtbetrag der Bestellung (Brutto)"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderSubtotalExclTax", "Order subtotal excl tax", "Gesamtbetrag der Bestellung (Netto)"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderCount", "Number of orders", "Anzahl der Auftrge"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.SpentAmount", "Amount spent", "Ausgegebener Betrag"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaymentMethod", "Selected payment method", "Gewhlte Zahlart"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaymentStatus", "Payment status", "Zahlungsstatus"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaidBy", "Paid by", "Bezahlt mit"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingRateComputationMethod", "Shipping rate computation method", "Berechnungsmethode fr Versandkosten"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingMethod", "Shipping method", "Versandart"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingStatus", "Shipping status", "Lieferstatus"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ReturnRequestCount", "Number of return requests", "Anzahl Rcksendeauftrge"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.AvailableByDate", "Available by date", "Nach Datum verfgbar"); + + // Rule operators + builder.AddOrUpdate("Admin.Rules.RuleOperator.ContainsOperator", "Contains", "Enthlt"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.EndsWithOperator", "Ends with", "Endet auf"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.GreaterThanOperator", "Greater than", "Grer als"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.GreaterThanOrEqualOperator", "Greater than or equal to", "Grer oder gleich"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.IsEmptyOperator", "Is empty", "Ist leer"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.EqualOperator", "Is equal to", "Gleich"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.IsNotEmptyOperator", "Is not empty", "Ist nicht leer"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.NotEqualOperator", "Is not equal to", "Ungleich"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.IsNotNullOperator", "Is not null", "Ist nicht NULL"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.IsNullOperator", "Is null", "Ist NULL"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.LessThanOperator", "Less than", "Kleiner als"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.LessThanOrEqualOperator", "Less than or equal to", "Kleiner oder gleich"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.NotContainsOperator", "Not contains", "Enthlt nicht"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.StartsWithOperator", "Starts with", "Beginnt mit"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.InOperator", "In", "Ist eine von"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.NotInOperator", "Not in", "Ist KEINE von"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.AllInOperator", "All in", "Sind alle von"); + builder.AddOrUpdate("Admin.Rules.RuleOperator.NotAllInOperator", "Not all in", "Sind nicht alle von"); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.InOperator", + "At least one value left is included right", + "Mind. ein Wert links ist rechts enthalten", + "True for left {1,2,3} and right {5,4,3}. False for left {1,2,3} and right {6,5,4}.", + "Wahr fr links {1,2,3} und rechts {5,4,3}. Falsch fr links {1,2,3} und rechts {6,5,4}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotInOperator", + "At least one value left is missing right", + "Mind. ein Wert links fehlt rechts", + "True for left {1,2,3} and right {3,4,5,6}. False for left {1,2,3} and right {5,4,3,2,1}.", + "Wahr fr links {1,2,3} und rechts {3,4,5,6}. Falsch fr links {1,2,3} und rechts {5,4,3,2,1}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.AllInOperator", + "Right contains ALL values of left", + "Rechts enthlt ALLE Werte von links", + "True for left {3,2,1} and right {0,1,2,3}. False for left {1,2,9} and right {9,8,2}.", + "Wahr fr links {3,2,1} und rechts {0,1,2,3}. Falsch fr links {1,2,9} und rechts {9,8,2}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotAllInOperator", + "Right contains NO value of left", + "Rechts enthlt KEINEN Wert von links", + "True for left {1,2,3} and right {4,5}. False for left {1,2,3} and right {3,4,5}.", + "Wahr fr links {1,2,3} und rechts {4,5}. Falsch fr links {1,2,3} und rechts {3,4,5}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.ContainsOperator", + "Left contains ALL values of right", + "Links enthlt ALLE Werte von rechts", + "True for left {3,2,1,0} and right {2,3}. False for left {3,2,1} and right {0,1,2,3}.", + "Wahr fr links {3,2,1,0} und rechts {2,3}. Falsch fr links {3,2,1} und rechts {0,1,2,3}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotContainsOperator", + "Left contains NO value of right", + "Links enthlt KEINEN Wert von rechts", + "True for left {1,2,3} and right {9,8}. False for left {1,2,3} and right {9,8,2}.", + "Wahr fr links {1,2,3} und rechts {9,8}. Falsch fr links {1,2,3} und rechts {9,8,2}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.EqualOperator", + "Left and right contain the same values", + "Links und rechts enthalten dieselben Werte", + "True for left {1,2,3} and right {3,1,2}. False for left {1,2,3} and right {1,2,3,4}.", + "Wahr fr links {1,2,3} und rechts {3,1,2}. Falsch fr links {1,2,3} und rechts {1,2,3,4}."); + + builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotEqualOperator", + "Left and right differ in at least one value", + "Links und rechts unterscheiden sich in mind. einem Wert", + "True for left {1,2,3} and right {1,2,3,4}. False for left {1,2,3} and right {3,1,2}.", + "Wahr fr links {1,2,3} und rechts {1,2,3,4}. Falsch fr links {1,2,3} und rechts {3,1,2}."); + + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Cart", "Cart", "Warenkorb"); + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.OrderItem", "Order item", "Bestellposition"); + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Customer", "Customer", "Kunde"); + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Product", "Product", "Produkt"); + + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Cart.Hint", + "Rule to grant discounts to the customer or offer shipping and payment methods.", + "Regel, um dem Kunden Rabatte zu gewhren oder Versand- und Zahlarten anzubieten."); + + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Customer.Hint", + "Rule to automatically assign customers to customer roles per scheduled task.", + "Regel, um Kunden automatisch per geplanter Aufgabe Kundengruppen zuzuordnen."); + + builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Product.Hint", + "Rule to automatically assign products to categories per scheduled task.", + "Regel, um Produkte automatisch per geplanter Aufgabe Warengruppen zuzuordnen."); + + builder.AddOrUpdate("SmartStore.Blog.Button", "Visit our blog", "Zum Blog"); + builder.AddOrUpdate("SmartStore.Blog.Button.Hint", "Click here to be forwarded to our blog", "Klicken Sie hier um zu unserem Blog weitergeleitet zu werden"); + + builder.AddOrUpdate("Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.ColorSquaresRgb", + "RGB color", + "RGB-Farbe", + "Specifies a color for the color squares control.", + "Legt eine Farbe fr das Farbquadrat-Steuerelement fest."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.Picture", + "Picture", + "Bild", + "Specifies an image as the selector element.", + "Legt ein Bild als Auswahlelement fest."); + + builder.AddOrUpdate("Admin.Catalog.Manufacturers.Fields.BottomDescription", + "Bottom description", + "Untere Beschreibung", + "Optional second description displayed below products on the category page.", + "Optionale zweite Beschreibung, die auf der Herstellerseite unterhalb der Produkte angezeigt wird."); + + builder.AddOrUpdate("Admin.Promotions.Discounts.AppliedDiscounts", + "Discounts", + "Rabatte", + "Specifies discounts to be applied to the object.", + "Legt auf das Objekt anzuwendende Rabatte fest."); + + builder.AddOrUpdate("Admin.Promotions.Discounts.RuleSetRequirements", + "Requirements", + "Voraussetzungen", + "Specifies requirements for the applying of the discount. The discount is applied when one of the selected rules is fulfilled.", + "Legt Voraussetzungen fr die Anwendung des Rabatts fest. Der Rabatt wird gewhrt, wenn eine der ausgewhlten Regeln erfllt ist."); + + builder.AddOrUpdate("Admin.Rules.RuleSet.AssignedObjects", + "Assigned {0}", + "Zugeordnete {0}", + "A list of objects to which the rule is assigned. The assignment can be made on the details page of the object.", + "Eine Liste von Objekten, denen die Regel zugeordnet ist. Die Zuordnung kann auf der Detailseite des Objektes vorgenommen werden."); + + builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.Fields.Requirements", + "Requirements", + "Voraussetzungen", + "Specifies requirements for the availability of the shipping method. The shipping method is offered if one of the selected rules is fulfilled.", + "Legt Voraussetzungen fr die Verfgbarkeit der Versandart fest. Die Versandart wird angeboten, wenn eine der ausgewhlten Regeln erfllt ist."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.Requirements", + "Requirements", + "Voraussetzungen", + "Specifies requirements for the availability of the payment method. The payment method is offered if one of the selected rules is fulfilled.", + "Legt Voraussetzungen fr die Verfgbarkeit der Zahlart fest. Die Zahlart wird angeboten, wenn eine der ausgewhlten Regeln erfllt ist."); + + builder.AddOrUpdate("Admin.Common.IsPublished", "Published", "Verffentlicht"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToOrderTotal") + .Value("de", "Bezogen auf Gesamtsumme"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToSkus") + .Value("de", "Bezogen auf Produkte"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToCategories") + .Value("de", "Bezogen auf Warengruppen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToManufacturers") + .Value("de", "Bezogen auf Hersteller"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToShipping") + .Value("de", "Bezogen auf Versandkosten"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToOrderSubTotal") + .Value("de", "Bezogen auf Zwischensumme"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Blog.NotifyAboutNewBlogComments.Hint") + .Value("de", "Der Administrator erhlt eine Benachrichtigungen bei neuen Blogkommentaren."); + + builder.AddOrUpdate("ActivityLog.EditSettings", + "The setting {0} has been changed. The new value is {1}.", + "Die Einstellung {0} wurde gendert. Der neue Wert ist {1}."); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.Condition", + "Product condition", + "Artikelzustand", + "Specifies the product condition.", + "Legt den Artikelzustand fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowProductCondition", + "Show product condition", + "Artikelzustand anzeigen", + "Specifies whether to show the product condition.", + "Legt fest, ob der Artikelzustand im Shop angezeigt werden soll."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.New", "New", "Neu"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.Refurbished", "Refurbished", "Generalberholt"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.Used", "Used", "Gebraucht"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.Damaged", "Damaged", "Defekt"); + + builder.AddOrUpdate("Products.Condition", "Product condition", "Artikelzustand"); + builder.AddOrUpdate("Common.OpenUrl", "Open URL", "URL ffnen"); + builder.AddOrUpdate("Common.NoPreview", "A preview is not available.", "Eine Vorschau ist nicht verfgbar."); + + builder.AddOrUpdate("Admin.Orders.Shipments.TrackingNumber", + "Tracking number", + "Tracking-Nummer", + "Specifies the tracking number for tracking the shipment.", + "Legt die Tracking-Nummer zur Sendungsverfolgung fest."); + + builder.AddOrUpdate("Admin.Orders.Shipments.TrackingUrl", + "Tracking URL", + "Tracking-URL", + "Specifies the URL for tracking the shipment.", + "Legt die URL zur Sendungsverfolgung fest."); + + builder.AddOrUpdate("Admin.Media.Album.Message", "Messages", "Nachrichten"); + builder.AddOrUpdate("Admin.Media.Album.File", "Files", "Dateien"); + builder.AddOrUpdate("Admin.Media.Album.Content", "Content", "Inhalte"); + + builder.AddOrUpdate("Admin.SalesReport.LatestOrders", "Latest orders", "Neueste Bestellungen"); + builder.AddOrUpdate("Admin.SalesReport.TopCustomers", "Top customers", "Top Kunden"); + + builder.AddOrUpdate("Admin.Customers.CustomerRoles.AutomatedAssignmentRules", + "Rules for automated assignment", + "Regeln fr automatische Zuordnung", + "Customers are automatically assigned to this customer group by scheduled task if they fulfill one of the selected rules.", + "Kunden werden automatisch per geplanter Aufgabe dieser Kundengruppe zugeordnet, wenn sie eine der gewhlten Regeln erfllen."); + + builder.AddOrUpdate("Admin.Catalog.Categories.AutomatedAssignmentRules", + "Rules for automated assignment", + "Regeln fr automatische Zuordnung", + "Products are automatically assigned to this category by scheduled task if they fulfill one of the selected rules.", + "Produkte werden automatisch per geplanter Aufgabe dieser Warengruppe zugeordnet, wenn sie eine der gewhlten Regeln erfllen."); + + builder.AddOrUpdate("Admin.Rules.AddedByRule", "Added by rule", "Durch Regel hinzugefgt"); + builder.AddOrUpdate("Admin.Rules.ReapplyRules", "Reapply rules", "Regeln neu anwenden"); + + builder.AddOrUpdate("Admin.CustomerRoleMapping.RoleMappingListDescription", + "The list shows customers who are assigned to this customer role. Customers are automatically assigned by scheduled task as long as the group is active and active rules are specified for it. You can make a manual assignment using the customer role selection at the respective customer.", + "Die Liste zeigt Kunden, die dieser Kundengruppe zugeordnet sind. Kunden werden automatisch per geplanter Aufgabe zugeordnet, sofern die Gruppe aktiv ist und fr sie aktive Regeln festgelegt sind. Eine manuelle Zuordnung knnen Sie ber die Kundengruppenauswahl beim jeweiligen Kunden vornehmen."); + + builder.AddOrUpdate("Admin.Catalog.Categories.ProductListDescription", + "The list shows products that are assigned to this category. Products are automatically assigned by scheduled task as long as the category is published and active rules are specified for it.", + "Die Liste zeigt Produkte, die dieser Warengruppe zugeordnet sind. Produkte werden automatisch per geplanter Aufgabe zugeordnet, sofern die Warengruppe verffentlicht ist und fr sie aktive Regeln festgelegt sind."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.TaskNotFound", + "The scheduled task \"{0}\" was not found.", + "Die geplante Aufgabe \"{0}\" wurde nicht gefunden."); + + builder.AddOrUpdate("Admin.SalesReport.ByQuantity", "By quantity", "Nach Anzahl"); + builder.AddOrUpdate("Admin.SalesReport.ByAmount", "By amount", "Nach Betrag"); + builder.AddOrUpdate("Admin.SalesReport.Attribute", "Attributes", "Attribute"); + builder.AddOrUpdate("Admin.SalesReport.Value", "Values", "Werte"); + builder.AddOrUpdate("Admin.SalesReport.NewOrders", "Total new orders", "Neue Auftrge"); + builder.AddOrUpdate("Admin.SalesReport.NoIncompleteOrders", "No incomplete orders", "Keine unvollstndigen Auftrge"); + builder.AddOrUpdate("Admin.Report.StoreStatistics", "Statistics", "Statistiken"); + builder.AddOrUpdate("Admin.Report.CustomerRegistrations", "Customer registrations", "Kundenregistrierungen"); + builder.AddOrUpdate("Admin.Report.Today", "Today", "Heute"); + builder.AddOrUpdate("Admin.Report.Yesterday", "Yesterday", "Gestern"); + builder.AddOrUpdate("Admin.Report.LastWeek", "Last 7 days", "Letzte 7 Tage"); + builder.AddOrUpdate("Admin.Report.LastMonth", "Last 28 days", "Letzte 28 Tage"); + builder.AddOrUpdate("Admin.Report.ThisYear", "This year", "Dieses Jahr"); + builder.AddOrUpdate("Admin.Report.Overall", "Overall", "Insgesamt"); + builder.AddOrUpdate("Admin.Promotions.NewsLetterSubscriptions.Short", "Newsletter abos", "Newsletter Abos"); + builder.AddOrUpdate("Admin.Promotions.SalesReport.Sales", "Sales", "Verkufe"); + builder.AddOrUpdate("Common.Amount", "Amount", "Betrag"); + builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.AttributeCombinations.Short", "Combinations", "Kombinationen"); + builder.AddOrUpdate("Admin.CurrentWishlists.Short", "Whishlists", "Wunschlisten"); + builder.AddOrUpdate("Admin.CurrentCarts.Short", "Shopping carts", "Warenkrbe"); + builder.AddOrUpdate("Admin.SalesReport.Sales", "Sales", "Umsatz"); + builder.AddOrUpdate("Admin.SalesReport.Sales.Hint", "Total value of all orders", "Gesamtwert aller Auftrge"); + builder.AddOrUpdate("Admin.Report.OnlineCustomers", "Customers online within last 15 minutes", "Kunden in den letzten 15 Minuten online"); + builder.AddOrUpdate("Admin.Orders.Overall", "Orders overall", "Auftrge insgesamt"); + builder.AddOrUpdate("Admin.Report.Registrations", "Registrations", "Registrierungen"); + + builder.AddOrUpdate("Common.FileUploader.Upload", "To upload files drop them here or click.", "Zum Hochladen Dateien hier ablegen oder klicken."); + builder.AddOrUpdate("FileUploader.Dropzone.Message", "To upload files drop them here or click", "Zum Hochladen Dateien hier ablegen oder klicken"); + builder.AddOrUpdate("FileUploader.MultiFiles.MainMediaFile", "Main media file", "Hauptbild"); + builder.AddOrUpdate("FileUploader.Preview.SetMainMedia.Title", "Set as main picture", "Zum Hauptbild machen"); + builder.AddOrUpdate("FileUploader.Preview.DeleteEntityMedia.Title", "Remove assignment", "Zuordnung entfernen"); + + builder.AddOrUpdate("FileUploader.StatusWindow.Uploading.File", "file is uploading", "Datei wird hochgeladen"); + builder.AddOrUpdate("FileUploader.StatusWindow.Uploading.Files", "files are uploading", "Dateien werden hochgeladen"); + builder.AddOrUpdate("FileUploader.StatusWindow.Complete.File", "upload complete", "Upload abgeschlossen"); + builder.AddOrUpdate("FileUploader.StatusWindow.Complete.Files", "uploads complete", "Uploads abgeschlossen"); + builder.AddOrUpdate("FileUploader.StatusWindow.Canceled.File", "upload canceled", "Upload abgebrochen"); + builder.AddOrUpdate("FileUploader.StatusWindow.Canceled.Files", "uploads canceled", "Uploads abgebrochen"); + builder.AddOrUpdate("FileUploader.StatusWindow.Collapse.Title", "Minimize", "Minimieren"); + + builder.AddOrUpdate("FileUploader.DuplicateDialog.Title", "Replace or skip", "Ersetzen oder berspringen"); + builder.AddOrUpdate("FileUploader.DuplicateDialog.Intro", "A file with the name already exists in the target.", "Im Ziel ist bereits eine Datei mit dem Namen vorhanden."); + builder.AddOrUpdate("FileUploader.DuplicateDialog.DupeFile.Title", "Source file", "Quelldatei"); + builder.AddOrUpdate("FileUploader.DuplicateDialog.ExistingFile.Title", "Destination file", "Zieldatei"); + builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.Skip", "Skip this file", "Diese Datei berspringen"); + builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.Replace", "Replace file in target", "Datei im Ziel ersetzen"); + builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.Rename", "Rename file", "Datei umbenennen"); + builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.SaveSelection", "Remember selection and apply to remaing conflicts", "Auswahl merken und auf verbleibende Konflikte anwenden"); + + builder.AddOrUpdate("FileUploader.Dropzone.DictDefaultMessage", "Drop files here to upload", "Dateien zum Hochladen hier ablegen"); + builder.AddOrUpdate("FileUploader.Dropzone.DictFallbackMessage", "Your browser does not support drag'n'drop file uploads.", "Ihr Browser untersttzt keine Datei-Uploads per Drag'n'Drop."); + builder.AddOrUpdate("FileUploader.Dropzone.DictFallbackText", "Please use the fallback form below to upload your files like in the olden days.", "Bitte benutzen Sie das untenstehende Formular, um Ihre Dateien wie in lngst vergangenen Zeiten hochzuladen."); + builder.AddOrUpdate("FileUploader.Dropzone.DictFileTooBig", "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", "Die Datei ist zu gro ({{filesize}}MB). Maximale Dateigre: {{maxFilesize}}MB."); + builder.AddOrUpdate("FileUploader.Dropzone.DictInvalidFileType", "You can't upload files of this type.", "Dateien dieses Typs knnen nicht hochgeladen werden."); + builder.AddOrUpdate("FileUploader.Dropzone.DictResponseError", "Server responded with {{statusCode}} code.", "Der Server gab die Antwort {{statusCode}} zurck."); + builder.AddOrUpdate("FileUploader.Dropzone.DictCancelUpload", "Cancel upload", "Upload abbrechen"); + builder.AddOrUpdate("FileUploader.Dropzone.DictUploadCanceled", "Upload canceled.", "Upload abgebrochen."); + builder.AddOrUpdate("FileUploader.Dropzone.DictCancelUploadConfirmation", "Are you sure you want to cancel this upload?", "Sind Sie sicher, dass Sie den Upload abbrechen wollen?"); + builder.AddOrUpdate("FileUploader.Dropzone.DictRemoveFile", "Remove file", "Datei entfernen"); + builder.AddOrUpdate("FileUploader.Dropzone.DictMaxFilesExceeded", "You can not upload any more files.", "Sie knnen keine weiteren Dateien hochladen."); + + builder.AddOrUpdate("Admin.Catalog.Products.ProductPictures.Delete.Success", "The assignment was successfully removed.", "Die Zuordnung wurde erfolgreich entfernt."); + + builder.AddOrUpdate("Common.Entity.Customer", "Customer", "Kunde"); + builder.AddOrUpdate("Common.Entity.ProductAttributeOption", "Product attribute options set", "Produktattribut-Options-Set"); + builder.AddOrUpdate("Common.Entity.ProductVariantAttributeValue", "Product attribute option", "Produktattribut-Option"); + builder.AddOrUpdate("Common.Entity.SpecificationAttributeOption", "Specification attribute option", "Spezifikationsattribut-Option"); + builder.AddOrUpdate("Common.Entity.BlogPost", "Blog post", "Blog-Eintrag"); + builder.AddOrUpdate("Common.Entity.NewsItem", "News item", "News"); + builder.AddOrUpdate("Common.Entity.Download", "Download", "Download"); + builder.AddOrUpdate("Common.Entity.MessageTemplate", "Message template", "Nachrichtenvorlage"); + + builder.AddOrUpdate("Common.Download.Version.Hint", "Enter the version number in the correct format (e.g.: 1.0.0.0, 2.0 or 3.1.5).", "Geben Sie die Versionsnummer in korrektem Format an (z.B.: 1.0.0.0, 2.0 oder 3.1.5)."); + builder.AddOrUpdate("Common.Download.Version.Placeholder", "e.g.: 1.0.0.0, 2.0 or 3.1.5", "z.B.: 1.0.0.0, 2.0 oder 3.1.5"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.MaxUploadFileSize", + "Maximum file size", + "Maximale Dateigre", + "Specifies the maximum file size of an upload (in KB). The default is 102,400 (100 MB).", + "Legt die maximale Dateigre eines Uploads in KB fest. Der Standardwert ist 102.400 (100 MB)."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapEnabled", + "Enable XML Sitemap", + "XML-Sitemap aktivieren", + "The XML sitemap contains URLs to store pages which can be automatically read and indexed by search engines like Google or Bing.", + "Die XML-Sitemap enthlt URLs zu Shop-Seiten, welche von Suchmaschinen wie Google oder Bing automatisch gelesen und indiziert werden knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesBlog", + "Include blog posts", + "Blog-Eintrge einbeziehen", + "Adds blog pages to sitemap.", + "Fgt Blog-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesCategories", + "Include categories", + "Warengruppen einbeziehen", + "Adds category pages to sitemap.", + "Fgt Warengruppen-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesForum", + "Include forums", + "Foren einbeziehen", + "Adds forum pages to sitemap.", + "Fgt Forum-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesManufacturers", + "Include brands", + "Hersteller einbeziehen", + "Adds brand pages to sitemap.", + "Fgt Hersteller-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesNews", + "Include news", + "News einbeziehen", + "Adds news pages to sitemap.", + "Fgt News-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesProducts", + "Include products", + "Produkte einbeziehen", + "Adds product pages to sitemap.", + "Fgt Produkt-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.XmlSitemapIncludesTopics", + "Include topics", + "Seiten einbeziehen", + "Adds topic pages to sitemap.", + "Fgt Inhalts-Seiten zur Sitemap hinzu."); + + builder.AddOrUpdate("Admin.System.XMLSitemap", "XML Sitemap", "XML-Sitemap"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.MakeFilesTransientWhenOrphaned", + "Automatically delete orphaned files", + "Verwaiste Dateien automatisch lschen", + "Specifies whether orphaned media files should be automatically deleted during the next cleanup operation.", + "Legt fest, ob verwaiste Mediendateien beim nchsten Aufrumvorgang automatisch gelscht werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.MediaTypes", "Media types", "Medientypen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.MediaTypesNotes", + "Media types control which files can be uploaded. All other media types are generally rejected. Please enter types separated by spaces and without dots.", + "Medientypen steuern, welche Dateien hochgeladen werden knnen. Alle anderen Medientypen werden grundstzlich abgelehnt. Die Typen bitte Leerzeichen getrennt und ohne Punkt angeben."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.Type.Image", "Image", "Bild"); + builder.AddOrUpdate("Admin.Configuration.Settings.Media.Type.Video", "Video", "Video"); + builder.AddOrUpdate("Admin.Configuration.Settings.Media.Type.Audio", "Audio", "Audio"); + builder.AddOrUpdate("Admin.Configuration.Settings.Media.Type.Document", "Document", "Dokument"); + builder.AddOrUpdate("Admin.Configuration.Settings.Media.Type.Text", "Text", "Text"); + builder.AddOrUpdate("Admin.Configuration.Settings.Media.Type.Bin", "Other", "Sonstige"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowDiscountSign", + "Show discount sign", + "Specifies whether a discount sign should be displayed on product pictures when discounts were applied", + "Rabattzeichen anzeigen", + "Legt fest, ob ein Rabattzeichen auf dem Produktbild angezeigt werden soll, wenn Rabatte angewendet wurden."); + + builder.AddOrUpdate("Admin.Common.IsPublished", "Published", "Verffentlicht"); + + builder.AddOrUpdate("Admin.CheckUpdate.AutoUpdatePossibleInfo", + "<p>This update can be installed automatically. For this Smartstore downloads an installation package to your webserver, executes it and restarts the application. Before the installation your shop directory is backed up, except the folders <i>App_Data</i> and <i>Media</i>, as well as the SQL Server database file. </p><p>Click the <b>Update now</b> button to download and install the package. As an alternative to this, you can download the package to your local PC further below and perform the installation at a later time manually.</p>", + "<p>Dieses Update kann automatisch installiert werden. Hierfr ldt Smartstore ein Installationspaket auf Ihren Webserver herunter, fhrt die Installation durch und startet die Anwendung neu. Vor der Installation wird der Verzeichnisinhalt Ihres Shops gesichert, mit Ausnahme der Ordner <i>App_Data</i> und <i>Media</i> sowie der SQL Server Datenbank. </p><p>Klicken Sie die Schaltflche <b>Jetzt aktualisieren</b>, um das Paket downzuloaden und zu installieren. Alternativ hierzu knnen Sie weiter unten das Paket auf Ihren lokalen PC downloaden und die Installation zu einem spteren Zeitpunkt manuell durchfhren.</p>"); + + builder.AddOrUpdate("Admin.AppNews", + "Smartstore News", + "Smartstore News"); + + builder.AddOrUpdate("Admin.CheckUpdate.IsUpToDate", + "Smartstore is up to date", + "Smartstore ist auf dem neuesten Stand"); + + builder.AddOrUpdate("Admin.Common.About", + "About Smartstore", + "ber Smartstore"); + + builder.AddOrUpdate("Admin.Help.OtherWorkNote", + "Smartstore includes works distributed under the licenses listed below. Please refer to the specific resources for more detailed information about the authors, copyright notices and licenses.", + "Smartstore beinhaltet Werke, die unter den unten aufgefhrten Lizenzen vertrieben werden. Bitte beachten Sie die betreffenden Ressourcen fr ausfhrlichere Informationen ber Autoren, Copyright-Vermerke und Lizenzen."); + + builder.AddOrUpdate("Admin.Marketplace.ComingSoon", + "In the Smartstore Marketplace we offer modules, themes & language packages, which will make your shop better and more successful. Once we are ready to go, you'll be informed about the latest extensions here. Stay tuned...", + "Im Smartstore Marketplace werden Module, Themes & Sprachpakete angeboten, die Ihren Onlineshop besser, flexibler und erfolgreicher machen sollen. Sobald wir die Arbeiten am Marketplace abgeschlossen haben, werden Sie hier ber die neuesten Erweiterungen informiert."); + + builder.AddOrUpdate("Admin.Packaging.IsIncompatible", + "The package is not compatible the current app version {0}. Please update Smartstore or install another version of this package.", + "Das Paket ist nicht kompatibel mit der aktuellen Programmversion {0}. Bitte aktualisieren Sie Smartstore oder nutzen Sie eine andere, kompatible Paket-Version."); + + builder.AddOrUpdate("Admin.PageTitle", + "Smartstore administration", + "Smartstore Administration"); + + builder.AddOrUpdate("Admin.System.SystemInfo.AppDate.Hint", + "The creation date of this Smartstore version.", + "Das Erstellungsdatum dieser Smartstore Version."); + + builder.AddOrUpdate("Admin.System.SystemInfo.Appversion", + "Smartstore version", + "Smartstore Version"); + + builder.AddOrUpdate("Admin.System.SystemInfo.AppVersion.Hint", + "Smartstore version", + "Smartstore Version"); + + builder.AddOrUpdate("Admin.System.Warnings.IncompatiblePlugin", + "'{0}' plugin is incompatible with your Smartstore version. Delete it or update to the latest version.", + "'{0}' Plugin ist nicht kompatibel mit Ihrer Smartstore-Version. Lschen Sie es oder installieren Sie die richtige Version."); + + + builder.AddOrUpdate("Admin.Media.Exception.FileNotFound", + "Media file with Id '{0}' does not exist.", + "Die Mediendatei mit der Id '{0}' existiert nicht."); + + builder.AddOrUpdate("Admin.Media.Exception.FolderNotFound", + "Media folder '{0}' does not exist.", + "Der Medienordner '{0}' existiert nicht."); + + builder.AddOrUpdate("Admin.Media.Exception.DuplicateFile", + "File '{0}' already exists.", + "Die Datei '{0}' existiert bereits."); + + builder.AddOrUpdate("Admin.Media.Exception.DuplicateFolder", + "Folder '{0}' already exists.", + "Der Ordner '{0}' existiert bereits."); + + builder.AddOrUpdate("Admin.Media.Exception.NotSameAlbum", + "The file operation requires that the destination path belongs to the source album. Source: {0}, Destination: {1}.", + "Die Dateioperation erfordert, dass der Zielpfad zum Ursprungsalbum gehrt. Quelle: {0}, Ziel: {1}."); + + builder.AddOrUpdate("Admin.Media.Exception.DeniedMediaType", + "The media type of '{0}' is not allowed. If you want the media type '{1}' supported, enter the file name extension to the media configuration under 'Configuration > Settings > Media > Media types'.", + "Der Medientyp von '{0}' ist unzulssig. Wenn Sie wollen, dass der Medientyp '{1}' untersttzt wird, tragen Sie die Dateiendung in die Medienkonfiguration unter 'Konfiguration > Einstellungen > Medien > Medientypen' ein."); + + builder.AddOrUpdate("Admin.Media.Exception.DeniedMediaType.Hint", + " Accepted: {0}, current: {1}.", + " Akzeptiert: {0}, ausgewhlt: {1}."); + + builder.AddOrUpdate("Admin.Media.Exception.ExtractThumbnail", + "Thumbnail extraction for file '{0}' failed. Reason: {1}.", + "Thumbnail-Erstellung fr die Datei '{0}' ist fehlgeschlagen. Grund: {1}."); + + builder.AddOrUpdate("Admin.Media.Exception.MaxFileSizeExceeded", + "The file '{0}' with a size of {1} exceeds the maximum allowed file size of {2}.", + "Die Datei '{0}' mit einer Gre von {1} berschreitet die maximal zulssige Dateigre von {2}."); + + builder.AddOrUpdate("Admin.Media.Exception.TopLevelAlbum", + "Creating top-level (album) folders is not supported. Folder: {0}.", + "Das Erstellen von Ordnern auf oberster Ebene (Album) wird nicht untersttzt. Ordner: {0}."); + + builder.AddOrUpdate("Admin.Media.Exception.AlterRootAlbum", + "Moving or renaming root album folders is not supported. Folder: {0}.", + "Das Verschieben oder Umbenennen von Album-Stammordnern wird nicht untersttzt. Ordner: {0}."); + + builder.AddOrUpdate("Admin.Media.Exception.DescendantFolder", + "Destination folder '{0}' is not allowed to be a descendant of source folder '{1}'.", + "Der Zielordner '{0}' darf kein Unterordner von '{1}' sein."); + + builder.AddOrUpdate("Admin.Media.Exception.CopyRootAlbum", + "Copying root album folders is not supported. Folder: {0}.", + "Das Kopieren von Album-Stammordnern wird nicht untersttzt. Ordner: {0}."); + + builder.AddOrUpdate("Admin.Media.Exception.InUse", + "Cannot delete file '{0}' because it is being used by another process.", + "Datei '{0}' kann nicht gelscht werden, da sie von einem anderen Prozess verwendet wird."); + + builder.AddOrUpdate("Admin.Media.Exception.PathSpecification", + "Invalid path specification '{0}' for '{1}' operation.", + "Ungltige Pfadangabe '{0}' fr den Befehl '{1}'."); + + builder.AddOrUpdate("Admin.Media.Exception.InvalidPath", + "Invalid path '{0}'.", + "Ungltiger Pfad '{0}'."); + + builder.AddOrUpdate("Admin.Media.Exception.InvalidPathExample", + "Invalid path '{0}'. Valid path expression is: {{albumName}}[/subfolders]/{{fileName}}.{{extension}}", + "Ungltiger Pfad '{0}'. Ein gltiger Pfadausdruck lautet: {{albumName}}[/subfolders]/{{fileName}}.{{extension}}"); + + builder.AddOrUpdate("Admin.Media.Exception.FileExtension", + "Cannot process files without file extension. Path: {0}", + "Dateien ohne Dateiendung knnen nicht verarbeitet werden. Pfad: {0}"); + + builder.AddOrUpdate("Admin.Media.Exception.Overwrite", + "Overwrite operation is not possible if source and destination folders are identical.", + "Ein berschreibvorgang ist nicht mglich, wenn Quell- und Zielordner identisch sind."); + + builder.AddOrUpdate("Admin.Media.Exception.FolderAssignment", + "Cannot operate on files without folder assignment.", + "Kann nicht mit Dateien ohne Ordnerzuweisung arbeiten."); + + builder.AddOrUpdate("Admin.Media.Exception.MimeType", + "The file operation '{0}' does not allow MIME type switching. Source MIME: {1}, target MIME: {2}.", + "Die Dateioperation '{0}' erlaubt keine nderung des MIME-Typs. Quell-MIME: {1}, Ziel-MIME: {2}."); + + builder.AddOrUpdate("Admin.Media.Exception.TrackUnassignedFile", + "Cannot track a media file that is not assigned to any album.", + "Es kann keine Mediendatei verfolgt werden, die keinem Album zugeordnet ist."); + + builder.AddOrUpdate("Admin.Media.Exception.AlbumNonexistent", + "The album '{0}' does not exist.", + "Das Album '{0}' existiert nicht."); + + builder.AddOrUpdate("Admin.Media.Exception.AlbumNoTrack", + "The album '{0}' does not support track detection.", + "Das Album '{0}' untersttzt die Erkennung von Verweisen nicht."); + + builder.AddOrUpdate("Admin.Media.Exception.NullInputStream", + "Input stream was null", + "Eingabe-Stream war null"); + + builder.AddOrUpdate("Admin.Packaging.Dialog.Upload", "Upload & Install package file", "Paket-Datei hochladen & installieren"); + + builder.AddOrUpdate("Admin.Packaging.InstallSuccess.Theme", "Theme was uploaded and installed successfully. Please reload the list.", "Theme wurde hochgeladen und erfolgreich installiert. Bitte laden Sie die Liste neu."); + + builder.AddOrUpdate("Admin.Media.Exception.DeleteReferenzedFile", + "The file '{0}' is referenced by at least one entity. Permanently deleting referenced media files is not supported", + "Die Datei '{0}' wird von mind. einer Entitt referenziert. Endgltiges Lschen referenzierter Mediendateien wird nicht untersttzt."); + + builder.AddOrUpdate("Common.Resume", "Resume", "Fortsetzen"); + + builder.AddOrUpdate("Admin.Media.Exception.FileNamesIdentical", + "The source and destination file names are identical. Path: {0}", + "Der Quell- und Zieldateiname sind identisch. Pfad: {0}"); + + builder.Delete("ShoppingCart.MaximumUploadedFileSize"); + builder.Delete("Admin.Help.NopCommerceNote"); + + builder.AddOrUpdate("Common.FileUploader.UploadAvatar", "To upload an avatar image drop files here or click.", "Zum Hochladen eines Avatarbildes Dateien hier platzieren oder klicken."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.resx b/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.resx new file mode 100644 index 0000000000..521bd8d863 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202006250801086_V400Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + H4sIAAAAAAAEAOy9224dObIo+D7A/IPhp5nB3nZVdTfQp9E9B7Is2cK2LbUk22f3i5DKRUnZypW5Oi+W1L82D/NJ8wtD5pWX4J15Wa6FAspLyWCQDEYEg8Fg8P/7f/7fv/7P52366gcqyiTP/vb61ze/vH6FsjjfJNn9317X1d1//vn1//y///f/7a8nm+3zq2893B8IHK6ZlX97/VBVu7+8fVvGD2gblW+2SVzkZX5XvYnz7dtok7/97Zdf/sfbX399izCK1xjXq1d/vayzKtmi5g/853GexWhX1VH6Od+gtOy+45KrBuurL9EWlbsoRn97fbWNiuqqygv05n1URa9fHaVJhLtxhdK716+iLMurqMKd/MvXEl1VRZ7dX+3whyi9ftkhDHcXpSXqOv+XEdx0HL/8RsbxdqzYo4rrssq3lgh//UNHmLd8dSfyvh4Ih0l3gklcvZBRN+T72+vrfJfEr1/xLf3lOC0IFEPa44a+GCzJ3jT1yvaf/3jFAf3HwBR/fPML+e8/Xh3XaVUX6G8ZqqsiSv/j1UV9mybxf6GX6/wRZX/L6jSle4r7isuYD/jTRZHvUFG9XKK7rv9nm9ev3rL13vIVh2pUnXZwZ1n1h99ev/qCG49uUzQwAkWIZlQfUIaKqEKbi6iqUJERHKghpdA619bVS1mhLfndt4n5D8vR61efo+dPKLuvHv72Gv98/eo0eUab/kvXj69ZgsUOV6qKGumaOivbxropbVt7l+cpijJgjGpkH6ttOtJpsj6/yzcvx2V5nEZlOT2BsjitN+gsu0owmaKdL43Oygvc7ae82OCCCsWYP3xR9ggnp8V1UqXTs+TVQ15Uuqb+9EuQucVqXdHIb3/6UyB2nZxon1EVYRVG2GB6mSCNvUdlXCS7doWZob15eO9TssVivrnOGw1e+krmJco2qDgqvyebe1T5Ymux/CPPpqdD29T3Itphi6rCq5bQd5P6WJKfmHnzG/k7zNyoCKAviyQvmmVYvaAbKMPr6H56fVjf/hOvE9f5UZwGWH2ICVU+2FLxr29HK1BtG0bPx9jouc+LFxcLMXp+Q2E4GInytjTm4R9/MVshLRnofVLu0ujlnEiijfwY888VqqpmLNa8gzXVXXJfFw30mw7PgYPWxkHforQOsYBZNtvQSk9dJ579lMdRmvwbbfo2Hbi3w9Eyr4DwwMbyttrpsJtawPKLsvs6urdkEQAPmTqE6fOhyOvd/OI1tL9U03PJ95foR3LfMJBkJl+/ukRpA1A+JLvWrydK1s0Iflrk28s8hQR6gLq5yusiJuPLtaDXUdFY/W46ZeiWpyrp8Bw0iLwtzUL460Ti0s1MR3WVH2KS9nHtf9XoCuXHGIeq9QB7mNM0uj/b4sGeJinSkDuIa+ey8t4rBfYFOG68wpngSp3ZqO52Mi5R2ei4Uq5AOUi5DpUBDrqRUaNS6F7pultnHOogBhqH86Bh9brO17rqaa1RIb9No7H71hfawpyVRLou0vo+yQQloqt6ndcxpHzCWVX+SgG0rbQqxEkpXKBim5REOC9R3JydWCuEKxTXxI34hsd1UATytoyONk3PXZyXO2rGMKN9jnY73AV4vQNBb0T2GXncrIawAhpW81oKwTYCsf6I8MD/8raO0jR/8jYlOabwXVWPm/lHBZlDa1xSIaOxgqJFA9zIRHKUKwNwQahM6kASZag8OtmfU23wa6SltnFSG+xM2jvmu+olM+MHLSFva6b1kd+kFwhdYRZuFwU/FXUdPZ88o+3O+9AbI+o2vAQPNwUaXRtXyQ/vs+c+Qqplfj9cQa2fCYydCTQyr69stLjpCC9rvEtAlcGYRkjJKHoAdb8HKC+12mE56VSCkV4ldcpesTIIDvrUXZ8augs0fjmjiCjTsxedUgqj3K7ifFSqjbg0PNV+tVeU9S1z6uXsec3vE7zzP9+R6c4Lpn9dGenmWG5r7xaIsNF59rUaYl7f4y/XydbBjb7bhEP2KSor/ClGZSlFaWx/t0E0icSR28fYwDqRLxQtawHC1o6mNWpQ1a3cBDh3F1shcV5nklWmLwW7KRQKXRQhrHcl0csWK8PPqHrINxJXBg0CdhSGEF0VMJhtl5u1DOwpKWlXtR473UuxVOghAGLbu94SVlGUhQFJKgEReiyD8/L3eETi4apRmt+/OYTirTWQ6hSDB2o5hGET6m5Hhfl0zhZPnslkR+mnJHuc/i5AtLlH1+i5mp6QpKWr6mXctLr5BptLcQGCIzQWWadlrtEWb/Ur73NCw+sQYeRjjdchwozsIiqwBuwnx39SNklEAkAERLpeYE5O/m3p/CGO9sHzeJ3j9RrFFY/K8Pi0r3beTLCKo8JsJS+KJEaXUXaPpudecknjPPuIyXTRHNZ6XgGMysFQPtrt0sQ/3iXk/YfVxvRg7gxwE9AjNGhvtsjv0V2E7cxvCXr6rA5cc9S20h1Bx8+GG8EO+jqn9+HAnhCAk28PIWDbjc2ghtVuAQoM8AsMpaKjVAQJ68+181tIuxfGh9vPjMMOa+Cj4ddhjyVva5mIr35mSLu+5s/XEl0gzItZFWCd7TsmYnyP4mQb4ZXyosC/umwcf3796orEUmGjwbmpo23L6BM1c1Vh1iergonTU7gCsnGseYn+VSfYEjjO612e0ZHQrjMjYpps19TPTGPVNFLmfXtlwESIV1oi062c7Fo41eLJa3yjldZ6JJ+jrL6LYqKLC9PBcHVU42FADYbEwjuPCn/f1LGxaTOCq8bSQxkMYwANajJYOuel3QxjMtBT5eGYpdEcDAd5W8s4Z39unynNeweX3apcdgcn2xqdbIH9TiFdYgcf1vw+rBBe0tBuKmfjVO+sgo3ZMP4qGjfss4IhBCtPAuZn6o39sbbzmrpvBgwHE0/e1mmeYmVhueBp7MI/BNH7R6nqpDfM2jKPXUGOybNSbSsFubn9GStUOtR7vgv5jaxpmp7mKj5geNmu4rhPqbX9RszSTZP7dmL793uyqR6suvYRJfcPlVWV9a73Z+V1EWHpgRLpLWNlfUw2WLn6YulWoABhAWQFgO6HmxoJ7RIAWwjDWt4DUbYBVyZaBTyArWOKHp2meywo1EkaQtFVBsy2w50LbsAmiSXloGDTSw4lxpTKQW0NxutIcr2Fbp29ycKWKEjbFNuSFAt//KjoUlMuMV6BcoF2IJBX4GiLphMXL9O1wXEwXuVttTFWYY3XiS4IXqW1Kt1pmBDJ4yh7j0hi7F5q/Jao2UycRlce00ek3heoH5J0U5BVWqrL2jWpZSFRn9GlsNJgQWwVrXx5sFpz4Z4Bi7L5XYiWHiHIBi8FIGXdNe1Reltv/RRtgyKYnn31LioRdeuKXg805+mlOvdgGCXRZeXHyvbBV0FAjkb3mRzMN5+57JAcVk15W+8plf4uySJyx8VEpYdJ+Mobqx7HuDyqw6TL2+poNV0c+goy8am93d7bLiF9iH6HZrlxNOv3AKzodfdB3+ce0C8jWN8jX1E+SLBWgkOEU7bGV3PfvEm+G0IzfEvKBEOfZZvkR7KpozR98XaREZRJSj03QQsEXWodZ4h7yQQvUHipQku0y4TLNG/+zBmQQS5Pztne0WabZMf5dku5gCdrrJeyQJE5gW8IdXjCnfMfIocCnVVSwTyX6EeCnry9L0e7XZH/QJvLiDzGcUU2uT4z/SWvAmPs0V3jBS7lBu3dx3BIV3wH7uqxnl7oqNgMvO5XX+rt7ag1Jmv2Q5WoBDxUKp8PyV11HBXeZ3o9nhDWVXcv4rx6wBQforz9+tfhHI01zb3yIEqtJrZ8RVIGpS9Hmw3XB+8xnZXv86cszSP/uLcOj/c1oyxtBbxH6O9Cj55bgTu/E3A6bpw7NCfPu6R9r+l99MLjNEPR5MFqb6wEYPuPUXkVYasJhZpVFpvlKQ/uzdcSFUf3BULbALEDDLJZslCclZckVXARIKPkgOj4JU5R2ylfHUdjvEBFkntL34CzWfsbxJ6yclaS1EAnGanizY4khXy4JJ9YnzabzCjtMR4/EBfMZHcEz8pgeUXPypPSm5zUU4XeTskoI/Ev2Q8smhhdmwbKe+dW5fHj3+uo880E8HU2GI9+RAmuy3g1PMO9wZ46L1hJFnDkn/KndtRkpQnw0hjeICR3L41D4DQv+j6+Q1RicDfE76L4sXkileRF8O1lsxskGM9aWuIdyLDp9bYomk0/nqVkW2/DTFKLMXoOh7HHclWhXQhML+Ql3SJPmQBT4rLrG2LK7cP5EIfHW7kTBkCbDmuivCQTyFjHuoB08F398q6uqtG54qFbCPT3pHxIk7IKg7RTfinCwovXNcZ/5XxnHO9OGnRJ7O1fY5AEX4HP0820DQwO7HLCVAM7jCdKLQdijnNIZUCChR2SEtC4utQGjph6d95JVqGiDMJfndpmMKOJmaJT7LO2iTdf1wlqZdJ7wcM2BCqrowqrztu6Qsf59jbJukPUgEw4Qfat70wcfnAqs/u44OiZewfhWWRa2gwrja8+GRD5KZNwJzZru3Pav/EwxAB4On1MnnkI4z1e750XPDXJD1S8kMqWfq/eksWUsA2MbpeI8joqkrs77THBH4JMAVG3xcv53XmR3CdiCh7dpq1E3UIfxMMz4PuMorIuEKGhggJBruwNbdqmiDJES36wqM1IW2ebFDWHpxpnZxhZbNu7QMUZlv5QPjYGKaFGaJxXD3nrusNrjP9JQZJdJM05nb2rG3MJOSnt6lt3JfR9+Au73Eb6W/ADqPVDCayCUQXY3QiwQnwdDyILrxPgbOMB6QVA2WcWUOwwXS7tLQPkGLrYygURCuWltxHsppMkaTCjDFR2/U0Kb30FottGq/OtsVAviqhMHkQ2AAHOseP69GoAoCqqFACT3kGEYB3H0Wkzw0uUiv5Lg121UbGOPR/iVuQdb0EUvWYBZF3moBz72zhN7pK4ARl22sr+w1UU41FXkI1PU8txvNJbrj1eBk7UqlSxVKnSMI469Ru2xLE1D7k+lN1X1FNMkEEt2SyZVHWcKh611cAtRms7ROdx0Rs0JQ+ygCIT0uVSLmSAbNmQD+dQdJYHFbvLQkg7zIFZXxen3I5Ad4diiDOEQoEXRAi/J4ZaK9Ep/zXeFpKz7jcdjsPFCXlbGmfSRBlxmjPA8l2SpiGiRRpkwR6Yfco/IULEszLXJFv+LUSSpYcCIdMG/xCgwS/1FmEp5RrzjB7+Fnlv8cO5boN5kztMx3n+mKA2jsb7RDZwkPTRZoOxlKd5sY1UjqjQT1ZcYe1D9pA/kky2nDAgN4M6p16wgwDE9+tAKK+lhUHptcAwmA7LjLytbt68Q5yWWa1ub4kLMdLcywmTe38NelDnMQwp77ytq9YKTvLO+gE9xJ1GdJB2ZzE1PBuyvrea5sVH9PwtSuv5W++k7VNOzqCmPhcLZ+Kcld2zV4K2cZI0wdvsf+t9xHWQOHlbF2HuqTNnA77IgsWHxyEPf+UPHLkutLPEY8x/kT1JMQvSrkU/KpFI4uuHenubRYl3DHGXT+En3HOuKO5GfvDdM0XLI4ZnmlytG3qRUJxvyqvpzzoVdW0909yB7LSHuBL/r/bQ1/L4wHAYRgMw77qq0+6GBjfVIe0ODvXBDJG3NRLN+5pQT/RgiJp9gS+2MxJN1L5GE/JpDVoXLqNLtXJroId9JLmP7/AX3B7TQU7lbQWy8EPdUT0rT7HVU4/pE9Zjj/UR3J8jt9MluSttYHiD+KkRWB5ANfyQiLIIOIHZ4BLxpetvSIMh0BuJALaDuplc3dDk/plUjjxnJ8OthrGKbCV1wCLzhywdprTCFIk8PaIuTfofUpe04YX+WqTFM1mK7f5yI7m+TXK9zCGpusRyYVxULeFmyTfTZoXzI8pHlO7u6vS/UdnkcAmC7EvugstHQm1jcyVSKYngdYol7miRobLEsqoLShfq3HDyLBmRtIY6/FhezS88rpNqF/XTv6s6/DrYLvK2ehp9qJOh1fa3Q5KsTHcwGuRFvS3lYZ+slYuoLElm1NkaamOc/LM7t9iuIuULimG6Pmty3oA5rI7u7pI0CZDht0lt5H1ME/gy9lHMHChOehk7DB99isrqbNfF+k0egRT0CIp0/VN+n2QEg0NuAFK/T5AlRWGdOzGtK100V5BADZ0dHGbCTpOirDSM+NtvIZ7HIbMxS0MkjbruXD3IDGHtvIuy6R8W6s0IbeqAMCzxLimqByIgtvKGDaWNsn9hFqtvUZdrmYT71aX/fnOL/pFn1Ho1Wc/xItu5ekhDvh0nAoUNm3pLIhoqF+XY5l6dw8DoW3uPty+x7inl0AHn3eIn2dr10nVDgY3bOLFU2JUCILZb0u42y7BKq7rJwwJ9ZUHkHebgbHvNe4dU3e5gbsa9p9hvHkbYHEsBrbMX9C8KYLTd6Yya6hSgcggQnHQYILDtUMgb6HhvmB7V1QPZlbY3ei9R3L48AYxJVQMcnFEFYZRmtayfdyQK7yIvJSkzhmJwIGKp0GsAxKmL1/kuiRV9bMrlnWSL4V5yMLbdbE4b4B42RWDn2BKhX1yxbZcuUVUXGUmHj2QTzICAXYQhhK5KwOy7/BQVm4s8yaryY1JWslNZAE7SezkcMAYFsO1I+kuQRouQAAysQhyMfBniAa2vU1PZguSJUngokPxSIPFumRTSlvDfozRFlYp5GAiw3zCE0GkJmJc3eWABe2dyvt3m2ZsOwcGRLG/LyOsQaI8yy/Gbidsh3M5jloZ0rvMwreh9DqHagS9daviUvoBnmyWRfo5wuifvGl3z61wN/TZ5Q/9IdsRcjVLN/f9Ar/g95BnS+qEC6Yj+iZ/pmd3La+1047W3dYC7rlyRYDfx5dbmEnuFXdE5DlLsInvhVtZRFsorfEZwOQQ4we5wHewPhYB0pPKOnDM4iPot0At2/YOHvmd1+3CratwShHbByTZtUl+dk1i/S/P74azbWqRJ7fINhWMd8XBdZ2aJMSODJ2aAtYDKXdIdRpClKFrfjIAjO0HlAiuBQN5s1HbFg4cIgsNaoNKswa6+foqy+5q8MLaSkFHrZ2GaYDzqSG/oQxszKBbbRq33iVMt91Jd067Vr1BMOOXd/eR6qydOdF+eZV2vvWN1yFNT02vcfBNiu+qSr65TmsEewu7wHQPJ7O2fkQuPtE0WO/F0Ds+ZOMQZdS+YONQ8vBIfJsYtbGK8idwBlL0Dn1A4WVW8i19pepl2tV+Ypd1ssI5QbB+ZQtDsYyFsHRjDsqbuHgUG9G/MSS7t4Ahif8WAXX/VHRWhgf7yQPJuC5DhLixRFxJC3V2iUK5j28b0ztcy/R6V3QDD3f7lrpzMeVtFeSFHf8nFiQGHiIxvOQlJtOa6pn75hkGzDk4buuTLZS1lQvHX0C11hA1p9IaCBcJsWBCBe2RwYdglAKsc3ADytproI+8kEIH8yoF8bLYbzYu9vFYR9EU9d1+QmzPbOsBPqnTUrmsjBUn0liYO0V5LSiMSJdrUPixRQ1gaECItFXwoJy4F5K/Ou8546fMGx0Ghy9tqqLQahU5mK8Qlie6ZgQB3gWxdYUOYspdl1z535XtNJISR2eMJNcU9vnbZsfObHdZDj2ByUGlLAs6tFhddBzsgsHfNT1XXWgD/tcRzGTmsIPK2GgJ9KPJ6N/FbDaYhItZ3xedzjn8Zr4h4ESrIKkNUcZD93M+41vy+8jOPMizX5jc0EKfNqTJYm9MA/tq864SXSm9wHPS6Qsn89Nr4p5TxaQ9Jh/fbjuKAZxutXPqrHtCJAukmJ9XzBW/K3MMVSe03FIp1HEb00YqzXP+ZMzSSUNopq7iUSXuMIJtSE3szAo6sCpULKyUI5LVajl1x59fDQzo/dThj80zM/M2eNqpr9tjNUDGsS4VoLhKv9nsMP1xVcNkhfjDIyNwC34aVGAp8EwrBRZ2FCBv4NuAHA8nEUnkH3QPfKMMFtuKdzCPemlfaUEFj9Absqhg9KZCcwoFj9PI0/ZaTLNNteh0HI4+gKN/wiNaxNSG9OsrKJwc/oHz2B5xwBB1HiBsangqbk4OJsXIKWO/Z77vmMe8tioN9r+bDiU82JrJ223v053ddwIrPAKZ4q4IQViqGnYi0MKzoUUWguNHl1iHcnLhK8sL76wkhA7yBTnHWE14a4qAb5G2F2vsvox/afNTdTmJywz7Y3vvqIX86zz5iQ+IiwFu8zeb2Q5N67jp3Cqme7FRhmd2+787XbZfVaGtoh8UUgPrefWc16li5njdciCB9Di1UTjocSB0aJAcKhe+g5eVthYpHoMnu/xrZVK8MmiQV8UqMK0suosyia9t9gsx6CG0ls2GQb1ZDaSp4bflUuXW99IEK8UExTK8Y5kki2M9yO7q7ZIYUa32TnU00Y8vnhJcb7punqaM4RmU5T4MXJKkati6N3pMJnVZfuT5MlGScV7N2qcmddG1nvlsr1Ta/dvvPQXUqhIYQaKZEj01b3q+xNQNey00Z9g0H7+sybCpuy0PphrxhXmXpOxIG20X0Qg6LgiL7jKqHfDOl5pVwzHFdFCiLX2ZJ+No3dkm/B4TiZEseDr0o8K9Gi73+9c+vX13FESHin51lIez7OMM7QfOolav6tiLvqZ5lcXpN0BoR64/Ww2IaO3mep7HmpVg8N83rd3ONkGl0npF2WmeeEXaNzToy3JCFKNs3xihHvIokxESI0lOEpqapvOWpCSxveWpqd/inT4/W8NDkTNrL+mStHBcIz867KI2ahNZTjqVRXpd4NBvKKTdhU5M1gXcSeBBoc7SddGrot2y+owJhOQqQu/kBxY95XR1VWDBu6wrNGb8mNF7+r+0MD0R0tlSoA8lg7/kOG57d5DRoTvLILo28z9OI/DHmLdYYdGYpjIlgmH4iSZdn2VcItJkupDMqH9FGNiWTjvD4x4/pH+MgDZ0875Ki8Yx8zrPqYeY2/xtF09OTlq/3CV4AqvfodnyC1P28fEDVvbP9MU/neFFWbHgmxqQafhdlj7Ps6Lk2Z1ExdJtnx3M2N7yvMl+TZ7fRDMZFt5o2BuBwF3Fqua/xDqdI/t1omusiysqoSRQ+w9PJsqZnERlZ45eorNPpyX4c7cjJwLwEFxudabRX9e1go8875Iu6iB+iEs15InERJa7hWb1Lp/U7TD8vXXPE4UBeoeseJJzdDf4epaj6qZ71UeyELxE5S6Q8CEbnMB+j8gt66txSX3Jy0B4zr0c6h0jGMdpV1w8J7l+EPzfhCh+jbHP+w2Fn5fMcevvSsPwtdKhcOD0GgVzjjPxeUoa75hz81HvSvpbRPVI9LQsB3nRH3mNn5VBCuKMC1PaG1ofkrtklagcBAd58LdHme1I9CIPRQwuDMqji9Py2/LXioVicDK4IfoSbKnfq2RdpatOhWNKzsQjuGVVu/9j2BqEt2tAa8kT6xCL0OraUKbTARo9v+7EEWWHlNyf7UpHsbAnwUjVTPMW74B1m+aPgIIBE6fk+B277nrZAT6BY85K2nLLGYUawxraOOurRlKBmP8QhydsaTrc9/dyt+vV1lk/zBM14wGZqCow1NNZA/1EQajW0rXB3wjqFJWPUcVYbOEk6bNa4xhdC2A5iLm+rp5evhDJLfQhk36K0nu5MeiKF0lPT3Cwfa2gs8v6jIJdqaFuFwlmEc+wujAYEG59eCsdfyRwUi7yt3ntIaQUo65yZdgoR/zhxwMZZ2Xf2KK6SH1EAn1yP8DivdzO59i8xMXYJcZPN4bscWpvnns8VysiGe46RtU3NM6zPeFdI3feerJ2zcuSOxom6tNd5Ik+Z/ZJs5B8TFnDT0cCaVDmeG1kdcUQSUOmqLIP3v1TkmI+Tvlh0SMlpshnHRPK+9BPEzr9on0X0RfP3OupG77XpyJLqokjiySOzh4YmD8SeYzSzjGTqwPHet9HaiFOTjG1tatotEuw7b5Dv+/wpS/NoEyC96VnZIwtmxn/CApKVqMdruQUi2vo7Su4fLLYumiuSdbZJETazounDOVoFf9y89TeVNKE0+YGKF2IzWtK2u11HYwh3an9UlnlMIsU3vekEHxYFtNlkdqjOxvP3Nlucz4KnXMD5rbF53PKYplsDFNSxrlDRtR7C38r94vZsM23lfgn5ZPNPa+X6Zp6jMp7Nl3muv++bjzEyq9xhazSBWTwEKG5AvISTsAHxCF6pgAB8BxlU8F2gfA4twUPg4K45up9MTexInsudN5FmUMb/QUFCUDSgCk5QG0rg8OdfgeOoTEYT8PCrj35y0IV9INIQenXQf/K2gtgg10UUP2KKzxR83zf3tVBt2X/7JcjzGc0t7bAb24ZBkev9gX4X6Fh9dkPLOACS1zCS+EjbsEx5sC4NcTMqHLGDDIA0UJOF8ooqpFGG0ICH8wa9RDZ0ChJE6PJqXZijAq0whJQDmbzC0uIkB1xMsMe2iMF0EAWFKKwpSx5snSwaP9OinzoXDdPKBcnOh2dlpVGGQrBMVFInN2PgPnnJu3keoy333V3+Gsa+O9psk0x/E/uPjq257T/ZawrQzhOGEHSyBCxc+LbDnQx1H4O41LDmzi7Rv8ijEO4+bAbNYcmQtxVkyQhmOoVae8JEa7Ta7jQvWm6a31Pf8S9qzomDnKj7dUB959psavmnTKcLj6yiu7sv+Ryp+iZeh4RJoXRbmPy6bcK567xV58J50IwWiN+1flePMUVQia8YggD8qiCYpzM1b/wBx3gF84te5DEdlkWV8giwLF5ERbelttwGtdEzDhXpKQ5xnyHYCVuYIMxl0jpikUQF5iEShDjZNi6MxbIPqnhkbolDi9VSNzQ87daSggHOLTls0NQjQkPQWiIF0vfbeQ+oil8SWgHCmGQw+i4HCWrqkFztsKz1eXUGXeDiWoyqKM3v3yjxHpbHyfU5TPjzHZt8zHGtIAkzT5MUDwx6ok27jj6dZ90owYcujUKsXJ6hdJJhJSNDAm1UQXzw0aiW9WGfggscRqtGZ0oCFRZLuihR+e0NlEN1VosqtAetaKvO9vN54qM0iVSOkz+E8GSEe623jV6ZNp/DZ7RJIryi+O+q8jRXRdr8Gvi4QqmeJI/vLq1ihSd9/bW133oE287KdiV4KLPavrpocTvgmGDRCb7cHBaaw5Lgt7nQ2vBhdxSW2E6jGFVXeVFRuBqdwha4YL1GW0zDCn1MqOOIATVbant1Mdug51a5kA8BH6hX6TFJCOD8ylcIH/RQ4D6ekOvI6Q161u2BkRyUrLOS/aOhkWbrKSfELR8cbgTrzD+loXcz8kQJWHB0scw0Y2BCMPm3qEiibHzL6Tjf3iZZ5Lm51SM/CIW8ravHOoBMqNv4gFe+yRv5HGX1XRSTSSxIXnXt9YcwzVoeIqmRtR0Jhe17sgmH7GPYbAJRiRrK2caHGqIlP1jUZtGqZZncZ2hDOQZU9nGgoEePXAT9ISPJDmObIqJsclV4G9F7eRKMlWb8GOaEttlEnNfV+V2DtA3NCLfWs2km5Ou9Yvm7YXEItoBpVZmX3Lh+yENOg7blpyQGtVwG63peQkux62BZHFYjpqu6DJup7+WJYgfhkLedvB+WFy9vaEQH60/elmZL9KdJdkTk/xdpXUQqPT9N02bZrAzbNvJ7fcqJ7TJTYwGcbGfle3QXUW+UmS1hvlvB8Pu/g9jL2wpks3Vogh2MXqPnCn/a7qZ/II+kpf1XjdcL70Rt1JKYVUWehojSXCjahBchCwvK1mxytpV4nnMZD+g9Nq1jO0L1IZ3BUHnEzZm40vcH1riRal4tDZT1Zd5DOyQh3IsBF5PDKmKwiuiODx2jHlZxNDpr2tOgx5DhTMHVHz1y+E6ed3lRfY6aeOUJ7ogZr0ndGecVUh/RAPAmK5RJNZlSNqobVBePDQXUyiPSg3521pyhzj889wBBLL0pRUli71mJoacmsRi60AdaAHUEUFQ21CgqDBPoleA65aBPzEV9nFr/qx0rsCQDXeZee1CxMKnkFHbzTyxG9GX34LHWbdK+GRo6KzEmrCBRHCALfzhjutn+hvANfUqyR3J3dclHUBws4TUsYWYruckiGNL13d1xCO3/btAeljOV5gPp9lOsZaHeiDssZUA7h6Xsd7SUie7ytfjdDc8hzJz3Tgta/ziRw+LVqJY3PYLDMqWQ245G3o/vfR1fk6JydTurExHVhIe110WUlQmla1277ZcCxXh91Diqm4n3VXgtFo0hEGZlJWPsBHLq62LHD1F2j7BdO6P/fphHULn3nH5DgY2qWywVFDMA4qV2rxNUdGHnzpuGAcdB9crbCmSZBE3iTFjKMUjcN9hnymxZx1Ea163ofUbVQz6MsBHBgVsBMO8MgoSioOAPrd6woKPwwxCCApCAhQyHHpsYoKBuXkiidEQILw3VKDq3K42tYdgjOGgneVuBVn5n+RlWM/h4il7s2CuIbIlwsMQVe50atXxEXqjxZEWC4sCM8raCuYqO0tt6O3tc+IwW+W9/+tMk14y7PwO0HMZsHuUGNpyhcmFdAoH8nPDRC/HZ9daDtVLo6pdvGEQH1aDgTJpQVy9lhbahTmJtA9owuFlgX6jE3nmdbRqPa/Nw2UlGOhvgafBtgifiOm8mJuBdw8s6RfKgNXoab0ZQyosJQoh+ShjM75EJjOWk40YjkSYVyl6mx9oHOVYwcztT3pnpMRpSfwbTlWv5fEdGrjyI+m2ShpksdpN5Sae4NNLNOZxkf5CamwGMSrAvlIrJ9UUQ71dKSeiv89rOvNQ3YjooBXlbgWJMVxydH/Bq5X2GaX38QHjcO9h9GSOAFQzQCpCAgA8UQnBeKuD4AcWPee11Fah7XENAdVADa1MDe3lftOF7At95WkPhPSuvo+eTZ0RRwxUVRnSM2eA+L7xdIiu7HSvQLEwOnMmUsaCEFJc/YdgbQCWOytq0juCkNa7o5b6FWwmo0w9BktrjeI5iEyc/nyoJ4yE0cOadI6B25tZZvMVprezcXFEorgvC152fy/297OHlUxDhQWvJ2+JJNs3L17YhDl5Pecn3a9xYJQ8igjx0I1amn0g0qwM8mmhYMaichROwg2QpmPglThGbptRRGgiiC1Qk+cY7bRA5W2nwlX6Irio87UQkQwSHhtpbvMeLfIA1/ixLqiRK16zJ6C4aabEbtoZcdTGAWn3FQttGaUnX/7nVMr9ls9bnTmq5YXr86VPuEv2Fa90TBzyF5aCN5W1RZArhWAn1OKz+4eyJsvZMpJg4MoOiTMHcCPCj9CrABL2kgg36yindEPTAKVSu7G2Yh7IFMoZQJwT+oFLkbbUBOrhHT3mxUUjwRGf0uuC5aVp1Cswx5mO/pfCwBKpd7/n9J/QDpd6XNB5I7jBUltS7UjOHpulbD3SWtTvabArcWABBczQodBGD90h9r/DXP4WJz0N3qChQMUtjQZ9yJ9ohyOsmlu1+rKodd0EHWhsCkOtriYpAVxyCGEky40hpFAU0hu7ukjSJnCIahrrl+POwpsjb6tSj9zl1GM+NoxtJvpvplT+0E+jZ42aAorYBfKG4BxAgPDcA6SWKGzvUPpaPeDUwujcDkgPHy9ua8f7LRAuT8n6stev2bJMGlDjtTdOBR6U3TWEIYBMOgnlJITaOPKQwfzPUPwjgzyyAV2mtytkwUavBIrii7L7G2w67GTCWoYYxktgrQ32+3ebZGx7TQaimFio8og9FXu/mZ27c8vyNhrqsMn2SDPOEKQ9oi5rUV7epi+g19cs3DJqD3MnbaggVgHNniST0k4bfwlwUnZL7G9wutlsTy9z+c+D2yfnQNpGZ0mU5lY1XhrrD3GzPNU7eMBJ8muMdEu44/vcoTYlr2fvu1ce8rGZ4F/hTfp8Hy6sR7tLEx2qbvss3lF012UkFuTSChbB/RPYLqp7y4nFylrkoEqztXhq9cFwXBcpibyuyw3nyHDcJ7i6xqnFGrcgQJW0Evs5HRnijrEVd7NMBi1f8tDXsU2CJM6MfGQcuGRIDpR4LC+p3Y3Holsdztz2SwzItb0uzTE+TV6ifmGOMQtX4FG1fNodFRvdP/uy6nMzzum3ry8QL+BZPOvVE2lTt+V7n45UWEbDyYU0pvIOeRQfNqdvqtpNso3nDLZCNNWboOavQtgxk3TZYv9TbTuw8w9KnySLEYX2PsnxLXnQf86MEv7TGNUmu/A8z3NwG67RlA4cnvAUIuAvu3gb03Ax3WA6Lrbytn+A4Y0qPTIwXA5IGKSof3a8HktqYI0VcB8ZUTCtFLv9j4ss6ywI82fo5ih+SDC3F6JhXQq3ep0nWWDoSbJqe1HGM0Max9klRKFN7hfFI4D/vSWDLBcK7w/GKg1lYY1dXH+fpatxI980028P7ZVGN3LCVqF2zBlbcO+sq+KX9YroZQI0e9Ke8Le0uehIdpXtTK1SAbp6dPO+IiKoTiwXae7SXWqSt/HlFdx1Af2ZeUI8KtFnosfSMBfYW1+48Y9S4a+e+oOcKr80Oq8hZ+THZYFnw3l3VGV4iuoU9XCQZaDlOrc75i6TG+t9Jnf+9RjXanGDdnB5VFSag4+X+bpUt34AIDzpe3hZFsBBPn+BJIG5CJlkj4VFuRgRQW4UUjSkJptPZrk8+adbNie61fcYaT7PKTNUyoVM3pTpSOSaYB+UazjWvARUMVh287bEVTQ3bsQzVjIbTQduMqK9iOygKpcWYmFq6IVFfDUdE1/DaVTD9DLL4HJYchfLmrEm31Yaw0/ybk+t8/jYv0S59CdKwZkt0PHkT7+J48jau6tt/oliVkCKQeUDiYqaPigl5eHeFBfS6SLxTJ2E0bm67RoHHcV77p2nDndh8jrI6StOXcHuucXWB78wFXuf43Zb5ymg6IJrkuhHdsMDgQBgY1TrNAnotz2y33NdnGs9hgdaIqWobMc2rVl3Eg2735PawFRDVOf8IL/KCP0WwDb8o8UyGIZD1lrssNWliJmq5dWJeld7PWJNHsdFdhKUar6qNLFDhGoHzwBxH212U3Gc++qrHcdBV8rY02mKqC0xaG3OihgPZnEtGkAUOuOum4jo/ii0VhMXDoo1AXqMtXp+c7nMOIs2hOki2s2RP5Mv8mTf4nzFk8yjH9Mern6KyapsrUAj9rHUrtLZ9m4hEOb4wya5+LidDqIv1Iff3oZcJH3eB1Enwq8Pp0Fj7N6/af9DWNl7gvqCn8hMi+hnzNfXamvs6B2M8LHfytmCKfaiToQ/tbxeRnH/PFkafhHV+2gf5iki+58UjJuHEGURO86Leeopig6N8I6A6yKCCRUJKXqgE280M+j+bsEvilT4FoMzEKPAvmJdRDiW4qBWgfm/NF8kPTIsh3tdVYFk8B2lVSGsIhU54KJSkXuehMOm3F3+c5g4reWVzfv8SeXAzCvAoZpeE893LUV09+EeWUggvUZzsEuoSwDImjVSH0mwM6lFWrdyw8KMiVYAJmlQFaxtfNIqOSe9paGnfRyBdzylIryXgM4pKrIjbdwG97vgzmA5rgLwtjZNwoocLFn434ZKQzvDirvNtf+uL57Zi8h5rvKx029wIkjIgOwjLQVh+JmE52+7ygjx8fpc4pTNk6h+EY23CcZqnm2BPH1jf302bCwNhbu6HwRTkulrL840ZiTbYuI68Dw8ek53n1jB6RH4Y2sQr55n/8QWWutMEpRvy1wxZV3o2O86zu+S+LtjLQVMdaZ08Y+1Fz/2EeevSepsN2UYmbu0SlVhBn2V3quPiME111/wwcqcECu7305k1S3qVUQ4l7PcUoH630V+y2D3NDOHOPlHeGwrVYZVWiHWQXDMtf6gzbv86jYPNKM3NRAt+k2HzuVpoG9DQ/GNUqi5u/jFcBrkzy4Qdba2+T1OvGU1jZAEXFnPdZiqL3byH5tH2z0RZvke7NHd9a51HcdBo8ra6VclXpS0j1KHS081n1IxMGWLDYhCOHyhLsT76PkxD2izdIRohDwteF1FWbpMm/UqIqYBwDiNpUhA0SgkGc9g5t24tTSKCMHNyVd+2foLJWzKOogzECE17BoedAQcXJjqRyGPyA32mUv163EpxuduiSJvd+QqBnRW/MN8MwOO+SgYj7KqkgH6X/Z79HJ5M/YPhIW9rxQ7PibIfErVNfnWkml5vN0tEoLFqrcgfCSasaZMTvc/WLYq98HpGk4cxLluFENIb7OcXE5kSM+Mcm078J1ln5/C+9uzY+nxnccPOuIc4v7srkedF3uZ6gx+Kd1EVP1wl//Y8WLjAWqN9NcpTzgKaV8f5dtdEXtkYpMGybfwj2R0V8UOIAHZSa3xN0N+4G60tOD+Fl4HHZ6PQWoLBPP6M0Sb1+MuhJLZpeI//uyh+PMuwvMSPnvH5x1gppvn9GwnGg+UqbytI4C/+c1PH/poqUMTvErH1EtYDI+x1sIIEaivYPwfVTJjVSIY6+oF0oMbj6OH9Ikbx6nAXxUSKC4/r5b0ugdAdFIm8rWW2od8S9BTIb7i2aDXMiOg+L14C8DKP6sDHBz6ejY875R6AjTlMBy4+cPFsXNz5vAYjyJmJWUQHHpa3Newqfg20O/nND8/0K36RlyW2wVN/LuNRHfhsrXxmcdUnqy9R3AQu2PPFluTL6hEcuEHeVtCDJsdDHzJTvu7S0d6a2M/7PdngTfs/8kzTVJCXf5JK+WpuoAPA9b07u5pEfyqvUvMSLOhTIgxNSlv1c9Py9+hHAooFBzoE4/VeDovQQ6eOSA56Vd5WQynvSKECN0boHcBj3RyqBgrc0AydJO+b/CR1Ht149dAcY1GnNVMrY1R04YXkb9WxZaDj9vUp/3foPsk+FLnD4ac4e08nz7so2/iP70t+mqdp/uSNBz19T7KNP6KzWPf4YAj+r16UUvZriEbISI7zVPksa5jhfKy21FtlkzVzXJbHaVRqbkiGyJW6D3ZSa/6EMZP4IzaVKWVmJtVb2u1Vp+gqzklc91l5mkb35TBaI3uJ1C/fDFiCGUiYK7BaTV9I4BilWdkJ+Iy2t6gYjlPIewHforTGf/wiTBYLOxzadvDijScGfvDXdOB/EGndUlVB6U/5fRJHKSHV+Y7YWET6PWgO4FuQ+kfZxpT45yPZf3WgYzcX35IyuU3SxjB1oqLiAGLEvSBFT5tqZiS9QlERP7RRfeVIXU2tbrQXTQq+XhA0dfpHYHtB+MV9Bo/zbJO0JmboCRxQLzh/2OwxnT68yauL284oNZy8ryUFrJu199EWT/LGb9r+XkfNikJuxhZ5kxs94MwB2BecvKtdkmXU+qCjL17/sYGbeSo29CNBT922IhB532HakjyZPOoFafsuKpAxYdsnaCxUWjNQc8Fo8V+1od7v7qUSImlIrPhHrarma/zJRRi/R2mKqou8rDDhL1FUhlGjnV1UvgHwL2ldbLZJZsozF9hgfohKJBNGQPsm1FKrMQWJrqZsnT9o+oIJlmAjja30Rx1T3iYjy//JgTtOoxhVV3lRNZswd7Zo7Yo3DbryDY11QWb4mFQlcVCZ8kMDdUTBa/jhU3SLUhpewxGsH8hjh9DQtz/W+ZiQbAJBp45GveRG7QHFj7f5Myrttmum8/elxhosiS9JqhjZHBrNx3WCiguMCR1HaVy3m/nPqHrINwHtHnkjS+49iJvGWN+iIkZZFaUylSVq839iarXpMiTrutH8HNdFgbL45TLHypUsU3UawmQa82VCDSw4K003PiebXY4F+D1tbur2F3TFrztTQTpKn6KXsqnMtKbRh1Q1qi0XhUiuoIwPYHtPLEFXvqGRLrk9zIttZLzB/5jcPxivX/lo//6ny0aEe1KXUAfvHj/lcRRot849Mitrack9Sprfms4NuSmNDTxEVIWx0dlGUXosTqoMHf4qkM62Jm9pyQ16Qi49XjQv05pN02fckWSHu3uKJY8MkKlsJCdHZZnHSUPCQdZIMtz2tOMSlY2v/qZ/8Ibr/0m2edX68JW1Ro//y5COBarw+lU7IkxMrMn+9vr/EoZv2uAQiUE12A+Ba+RXdky4kfOszfv/6qjZ1xI/fBlHG/G0BFN0w37phIZskI7zrMTsgZcnMRAiyWI8b6nNUDgkhvEUpJNDc3zJe7RDGQmFsJlDk37QzyOJ/Rma5Yipo91f31LMasDDyb+bkOWmb4YMDFaRci8Nbc26cFP7x7fKcczFtMp52wuO7d1UZFA3Y/wI+bNLC1pKWdegLsTDdLVJmcuup4AIgJB6iXNiagtizsDdFgQz6Q2NbjFWf5+UTZKIm6MdnjwS+NDd9UuQnMdVlSDm7uFt9LKyDYAt+yuKmjYsSPMZbZKoTZF00wYOSgkigkJkoKBsKAEgB8avQP7LmzfibsFJHuVdmUH85EQ2abwL/VxKztrOk/wX3RypZ3sElDNSIq4UejaiEC/LRGJHZmMhkbgmTbc1VsBAza/OlWHARjT4BMzEoJexVN/bGXgK6s+snAXR26QDdL3FuKyLb+FG03CHjBXkVSBu46FtmE7RklSXAU0sv1fTD2QGjtXPmzHX9o+fL8KyVxW2/JrrGFmMbo6JzYitQBkPgdAQozKANlwKtwBZrF1XV8eeyhHMwJnKOTLaWbVVltaifdfP786L5D7JdCqUh1foTwe1KWA3Z8lw67SmL/OpPRmtLdirr7o4m2HtkPxAxUuTnkzHBTRwYAZjUAPcxfRzchaDejMjf0F0Nmmerrc0Z72rsw1e2Su0PaqqIrmtK9RmyL0ZS3QMZ4JDwYfS6g4MatQVyOPJV1/fku0wwvlkwYYFTHo11lqPgHRD6bWnMSey9YwEwYvzufbk3L4HPA6PZQm+hmfRnJeHzC+rYWd7Rp6PhV2YN6RpIe/KIrznwHUr4bf+zOZmOLzRMIFQQcFthgdCZs1YHDitRlFKRzEfl0rny2ib1dVZDZca6kQefmIe3eOlXDaGBRh0f5UonWf6hv5D67GXVVT57Wn8Dq57aZOQA1/R1mpYWDuiGT35uvk08udT9VbF2YbKF6ozEz/vsSJWjWMhBt5Dhdw9jaM9g+LgIPbsQGxYkse6hHtf0ocZWEhCUwt3/uJsw5446qaZPSQLy0IsboCRlIej4dkJ7M+MTAXS2qR9puLi4cw349tdMg4QQVXByja8BWAGGMuMaV3G/i5JySuBfQPabrLwwanAoTcnhShdDtQgSVBI/OQwBF1/+QoqenSwLmQRmoEWMQnZlzemdKOYQWPp5stoPaRe0VpEX5GcMuTynP6ekAAJ8WUPZMOQIuK9ugwk7f4MLCidE5O2F7/6M/ReH/goggbnPrcAx3AGmLwjczLSngYmDv3vcjlZcBRfIzhjCQ0szF+y/szJZjKim7ke2LqLMt1xvm1eiB04RMUOPLCM1To4W24T0AOMJuPkdayjshHMxJqy+TFpvq+ztCP3shGOjyjd3dVpRvY7TIHOp6uprnDvCjUdfLy61uXu3m50q+Nqu4HN5/81nGcLV3BbcTH2P82LetsIrXajLYJCbD1A2fAxgNpqWx1kpZd3Ygb+khN3L/bDTfev810SG/IRCytlpAbMmpM45AuxEtyLuXgJJvD+MNNN8//2RRDlZFOAUjay5iAaKcA+VN9Wt3TK+j8X4wHzYdL0WGsNSqzlGgMl0w55CvXVYpYx30r5Duj6vAqPmQ9jvluB+UXxi95KogYc3gSjkMu4D+TrlbCgZAyz2nDi/Jg031RYnhW/5RW6GXlIyzAsvJIhCagTU3JtyPhynS4azSjm5Ex4roz15KKemi/oqWxuE2jP3ARIiCl7IBt+FBHv1ZmbtPszsKB0TkzaXvzMbei9/oREBA3OfUuficg7Micj7emZ29B/4zM3aY3gjLWSMzdtf+Zks5/izI2Mpj+mGThExQ48sIzVHM7cQPQAo8k4eR3rqGwEM7GmbH5Mmu/rLHfmlqep3opjoMATtJzkTbc4LWMQ7pXlBnZ9BlYD52AvLDbS86OsfCL3NHLhuXmWKSg4GZ+1ILbcRiOGDmEBBl4HrwEdn4nbgLkwWmkx/KK8hnfWTRr85kliimNU7CGpI+NBGtyWE2VNSbgSZvd18KZmKDPxqWbuTHm2rbX4vQsyri5BuHnEPVRJlzjeOBm7YXN7GX2vGskM7GsyfybdWPygWDUQ8s2Jg8m3mbm4aVLByc1Y9oqb6REtzNH0fNpwNam3GGefPFeoyKL0qK4eCMb2NbButdEqaKPaEI+rKtowu1kH9kp3Ww1pBpa3muO90ObN67GmlyAhYIijuydpzVkXRGx+9zGQB1TVixmYS0Vdk+bZmgtzlFZdsmABuWg/NR3c99m4bi91V/8U0FfyqOfHpCRv594MbwrJuERZS/VKEV3B5cUiuEEoH7LkWaTludRoKDMwrdEcmvSjr7MuHm41nxU/tWI8F/e2rQGsC+rslfItM4ilmJaZN5NONBWWXdyboy81j3Jw0uXd9tCcx7s/LCjp+VwrvDgX+8RsuiRvAuQEDLePadykvZ+V7fYwZduH5K46jsghA+7yQ1Sizfekehg5SMYumnoQW/ZVbLhS14xMLULcHy6CyLBXM/Ce4TQYcSKIYXHGZIyIgYV0/ALWUjGlq9WobhBgT5kULK9DjYYyI08r59CkH32ddfHwV1rE7BiZqTobN7Ot7o8haj6YpZganE+TzjAVlzVbv5A7HwZ7pBFOarJ+sby/w+PdH9aU9HwuY1Wci/XvkS7RExafixwjKHv50freVZUgNgTgbRhS2dxeeelNRjIDt5rM31548KGBmBkC2prGag/c/9g05CYwXvlarx6SXROJriYSCwa+ZttBWD1ky2Ldn+UF7vgM8grPw/oXl77f7Z65ZxUdXzDQKqaz9cXBDUBpyCVcvR4WBIcwIyeCc2TS/oBg2fAC0o2dccQKBx0w2IDH7JOv28tqlvVkLttZRmNTntqtIHLle5RiXWBsRcPgEHMxkDZMJmlir2xm9Rhm4E/1PO2FncwOQW30AbAT8mQAWzsAV81t0ClovH6r7hJVdZFdon/VyCQDJQwO73ooSDsHAdjEXqk59RhmcQqo5mkv1NzYaUPzTlYh+LMsIe08qw1Y3jR7HBUV9Z608ghaUQfejLHgdhsyeVPyMAn5c8/hlgeDns2y29JOhUkvVvCuvTAS7bIhrTE5E+7n+qEdxhL8uperiDAKXfSYrMLknLqPIWW6QSzBpnsYYMZmsNdxKAitfd/A+VGDvWRM5Qhm4ErlHO0RS17tUJzcdVcbj6qqSG7rCpmyqLq2gmXhig4srOnBHrK02YjmY3GzOd4HlodHcr4jf95IOFK6utvjAg0MBRorY8OhO9BRmpFYLi8qHsOdw2Jx5w2jAxQQw0oXEiWDu+l2NXkXW3KU3TKWNVj0l5c4/7EvvmKZ8I27/LV4lpbCflm+ju7lflQAViE3DhLCYJabYLg43BPfHc5vUZFEWTVMy3G+vU2yBvAGS0zyAxUv18lWkRLTDo+CcAoUDjQ17hB0xZwe+GReWMeOzqcXbOfUpGd0vaWFXzU+ww2dAYpVcPwe7+8shrUO0djDnZ7BqP5e4xLMGV+zxEsqaDyrEA2mQ4B8MANfcjGAOroOjofm1KRndL218b7rCuCg9gMw9M+k4Nej1fdYlXP7rPIKDbsMvbPOAoeCzYHqDpxu1Ak516/YKecwwPlEwWbuLcRiNe433p+h4Fk7BlVgMpcVfzlRdcNAWuQiu1a5MRjwYtJjwBMuMjRiWVqaZAun8VKjRbCMOfVzrDDGo1ve0vop1hZ+cN+itB5GKHCrJWsqkVlISYMngKio+yMXG53UrlZ6jAa8nCQZ8YeFVPEolhYuK0eUqbfJ5cxmVX6jhZ1D++0Bep8/ZWkebQxeUhVBwayyHZRVJlkR8zIvXco7MgM/yelr0vjyj1oODqNou0vRwAc6XcKCB1ZUHHLovFPCr+HVFNyXGRUVTGmTDrA1F+Ow6wQVeNQxMntWCAaHOGyAtOExCXrLh4MaPlva3FMPZQYWVU+VSQdW8U7QOAzdCYoAOQFb7uO5iLT3szLh/p59jLaD4RmeUEGxAkstMIOlWGxnn9hSN4j5VnHpfO0Dk9JmtSLokAWDGNKJEzm0si1G0FDDFmMRxY8GOywIWDr6Bs56+Dx2m33W8mKoGsIMIqian/3Yo32Osvouiqu6QIUJQ4LgIEtSkFZMCbewzPZf3Zk5GExJ7/1gseGxpaMdniK0uc7pUcmVvqae6gUjl1eLZM1AnGfO21506lZnGxL1VaajztCCubkWgCaXdYqukAkpesjgFBgQAwPvyk4a/2244R9HFboneaP0mlkEBTPbdFBWmW1EzMtoY3lHZtDEcvruhxYe+q+VJAEyOCMtIEkX0QvJRfoZVQ/5Rk8DGBzcCdOQVttguIn5SELQtjj71uWJ3QRQMKnbAGWV0U3EbUuD5fch8kHMoJrks2PSeFdl0czRJEWLqWhK4GXZo0dQ2/zRUCMzLv2021uvsyFoVWo76OTBJL0d28CM+hsV26Qs8QfSg89RMzs31NfmYXW5PjeqDup3qKaVnjdrGrKm+dGtTu3ZjW0On6jVPBt5SLm6yxlwDyh+zGs+uEn4LNcRhghAtQHWtVIgpq1Dx6XCGFcnCbbDm2PHYjnfJl0SKi+YmjiuC0yK+86OvjnLEoJX9xaMohacqJitYJerWNWY+WMoQTbPRp2Zw1g1mQGjflD1VsOFfZ5vgW1MeUSGwIQ3nd7bMmwesvQ0orG8UrYd3QLsr5tvox0cV3cxaSDT+gPP/af8/ob6TXhGKgCKOhDPUyA2fK5qBUrQzXV+dZxtMJ4ZmNlg7kx6wVVdBftqk1ZDwBMx7H5mqVaNYGbe3Mvc1EZcqOM+S65z5bZVxKwuxGh7y2BHd3dJir+gG93bGQIkqOh6ICs1J2A2fy4j0MZI2oU5lJSMsEbL5sJvoB3FaedOMwq+h8HhJTOFHZ7qBRNEbxl8v7wWU49jloVTNU82mm3RyPsrvK0iIbHJNipeTp7jhyi7R5dY1I7xXgll8Yv8gElXEzxqIpWsTpi0rYCs2/V9GlVo3KcZ2NB4Foyc+HI062DQ5g87zmSqhGdJFv3CvAh2Zm4mBAluwX1M/eXYLn5AmzpF11H52Duf6G9y5tNUBFlQrGPFkLomoczY9FhWt7qbjmgO3jacT5Ou0PUW4+y/16hGm5NtlKRHVRXFD40fVR85qakH8TVYxYazdW0uE2Bp2KsZmNNwTkx6snzopWIwZE2I7p14s6s6N3v2zco4tB/RGj1E5sNalsW5qTXm8q7eGhj9ph1Y3IbbG/AZU0HD1I6szDYBMDDT59XZDrqRzMuz4HyZdIGutwZOpYSPZjE71UgTZj6FTLcKJl2SSsyquFk9osVUMTCnJn2hqi3G3qd5UW+v6tsyLpI26ab2JEleBWJoAdqGmRVNWZ1wBrF59Z2ZgQH1xLfxuy6YciL5EVXoMypLYr6cFvlWy3eKOnDiCRrcLu2EvKH52c6gNzPwnQHxTXpB11sL813ntqw31piU8ahmFmc7sS/zM51IdpM+jLUWY7iz7S4vKty1u8YjYeJDlVeBWI6BtuE4RTPWHtMgbKfv0Ax8pye+haszye4XdXaePFszn7wKxHwMtA3zKZpZhvn0HZqB+fTE3zvmww2leRus3LOJmifECnLGG2HteQ9oB3L2qBh8+f2xbiiz8ax81ky60FVZjFXfRfHjWXZV5fGj3X5YVxFiXUkdGw7WNrtXMcCmo5mBmU3ncy822rLB6FI8aurNzNP7mPfRcCwLMvReZoHM6rMKbbu4Q/KnIiWiAAtnRKTB7LIiig2Ah51ZvdZr+YohzMCYihkyab2pPg8nnjRJKnCdCtdARW9kb6OiOr/9J4orUoSe8cTHjcaPsiyvGix/+Vqi47Qg/FD+7XVV1KLtS1BfoWrwV+ySuHz9qv1Op3QmBQA7ctWj5y57UoJALEP5ixYX/lXhHQSEpivSoviUx1Ga/BttuumDO8VD6bv2Kcru6+gextaVmXUOXVXkRuQlKhvOk3aPg9Mi59NBQIjFvB3mSMesFRrMdDISDXo6HhvCygbEa5DR+V3gaecywOg6p2JrY57uk/RBSMakfxokbFpKERGb9lGHrA87AjGNsVxGaPJ0I+nSWGyGqgsRkeIawnd0LMslnwa5VUgSboZUgUvPTmSmZdzUlBkoSKzjEW7wB17wQEQMgJ43UZr8QMXLdbIFsdHlpiR6V2cbvKvGq6yCWCOQPdohwQeeuArmPV0d00aVGoCF0c9dz3MaUQbAjNWoSoVqkXSXtsCxDvfcDDvSWCewzuNAtCjfpfn9RV6CuPoyLZLmFFuGZSg0QyO1l8ZSE0SRDINZLz4Ueb2T9qIp1SL6gp5KmaD2ZXq2ztP0KCufJNw8lBohkqGwMiQUVgoApkV98ow1RhalR3X1QEz5uDGz5TaWCl7bWJMzBsLaJeAxNDa+kvPLNnQeVl4AoF6DfUjusLorNlrsAKA5dhVCM/rJeHooNEPzJa/A0Q2FeosUPeFOX+R401eq6CXCGZgCD8luK9GxfZkxEhnF6HItsu9RmqJKNU4aQj/CS4RXPyw4/6oRrLgZAIOx5o3AYz5SjJeFMV3Ur3YoTu46UR+MDcXyDlfQjwGs1j0wDY1HAe/YmHkzpsRr3leRUqp54sQMEf8G6nG+vU2ySEYcfS1tw/S7mFAT7OOibqOw6LppEya43ZGO752boKefsHdryLgR1xloshNaTEOXgVK3VndPFILL8/DSo8771r8DBrrexjfZTDbgEjkcHxoyQkKenJGjaR8C0k0DnX8cpDub2dzAL6RxChl6hNjcy7IFk87rrDNb+aSVoNEq5hi1RSvlYFn2VO3SDOWKkxk3klyAlm2Y4NZvcsfcS+A+l06JZY6qcbar0bXJ2rT+YbBXRr3p87WA/RgT4GgHlcp3NlT2Ew2ar4UCzVCo3xKgDGH9pZQOHkavMh/QFjUq+xZ2DjIABi65vJC44pokB9qNa3PrXuIWHBIZmHRCse+ly/XIhOvXcO/AK/QWuHU4Dawv4HIMbIaBl5zM0WuQ6n0I1M0y0GfAXO/Tnktsd1FyDxo+fZnBYt3EOF+j7S6VaA0OxMid9AlVFSroU3+Zc0mENHN86XAD93+0Vh8d9Q3beWyEvZa2UVkX6DtK7h/A6WYATNG9TzDXlpJR8zBapEzIMYSRC/HWyfVLFqv0z1hs4G9jY/tgHxsfiWmEVDFcLuZS55SGI1xAH7UsSsniyE8ppQCc6dHmiwoxD2O8nVfg5EAMbEACtlGcvrEQ+oEXeYn3mmmqQMnDGAhoH3ADi+YYjmOAaIxPkSGjg4c4hFQ0iTrq4GaMWaDqKMIPxgp88Audo1NRbwiFGgYtjYcQ4mtMm+iDoegmxrgNPqKJJZYpIemoET0VYWjN+MBKUvrxwS466sHYJyYdk7BPFlQi0tCkmny4BrUhqnJBKQp6mjQAEFY2fm8qi09i0ifYInmV8PJhq6pBBKVCYBTEVGIFqEgPzZtyVLTKzUVUNCaFSC8ASj4eERiiDRtFoyAPgA4gigqdM1nIxYo+kkdGFApGN4YRVE6QRCd6Aqo5icHGI6lIIk+lJBkNmEDJhzxgaiQe4Rh75U0lPrSKSnMGUEoBLR+cvBJEMSAeTEE4BW4ph0FIHSjHxG7dDCFhItFgQPmYQHiIVHx4mYJOME5ITY9hb6F4q2/u/O68SO6TTMFYAqh25vkaCpYy4iQB30w0YmP35ARi4PSjocG9ScMgA+jCxSeGoo00LvCGjkuUksyounbwJlgUBFbFQ+opb9Q4ZLkCMZ7hZ6XrxLD9NpgJrooFAdiaRhS3JDHXgpys0xDTiowuBJySdLMSbXgFfIzpldJMhNUOS6iioJh8c2OGeer9Et+wnskEUPNR6RnMkVyzchftqL1hw7vldrG0jt6ClVVV2chsbLqBmSxtBDKWVdgD0VXPhyC43UD1/OhHyHnYsn/XRbHR4EHkw+AgIdJQ9wsU9OARTWwtD4/bsPdm5KRgAfXjYPdJ3mRh0QHEUW/hPDy3N1RH5Y5aCkrvNh2BVW5YMxcshQugipTIPgR5l6Qk381wN0ZBFQ7UYDhsjQD04RDORKTuls+Y1ERBJQFWPyq+iopO450kA3IJiCE9JJsAB3r1N5mUJ0oikHwgAixEGupulYImIqqJD4uGBpWuQwDKYAxKV6ElPeZzDQ5NXhToR4KezAgjABuMia8TgEwCyjmodZxvm8RQ4xVBmFACnHpAPLi3wgGRAgSS0tzdcL5spuUjSnd3dZqRRYEpUNjQuppac1eDwJuqhs0olLqsGQdyD/dLlasgACUfnwgMkYy+9aogFoBs4tVuvCmrJwkHphkGCy0lSn+HV0cVDt0cZLmhL/BKaELDaEZAgUqoEenJQCMBaMBcSA7JHN19aCVntDBG89iOJQxPtLhgYggU9VIa9LVyldqg4ExknRpLCNVBoZNxCExgH9p8yyt0Q13fV5CHAzUYElvDe0mS4ZVRK9Ri31/kV+4sRCD5QARYiDRUagEFTURUE+8shgaVBjQAZTAGpclsSY/5bOWhSZOdhRzYYEwmOwtLMs2+syAt99b6mCEDJpQApx4QD+6tcECkAIGkNHfZWeRpqlQ0LIDCgKfhQEd7my1EtQdgUEysWMY8JzdtvhJ46DSIuucUpGz4Q9YVDRFoVNCRAkRJRxLgta0JzG7SK9K5X2ByyMDV45HU8t80KnBL6CaZAs8A6S4i2MhpCsLr/ZtQNV0stCxQ2byBifdPqrbbDIJ2dGzquA21iQifmp5NIwqaggHsDnRVZTRSMqlZRfngjepDZNbkbFLQ26zJiRm5yTBkcL4EwsnHBoFD1OuTUCnIBKKa+EypbVPFbxyErvsqDjKmwVw8AWXsuhkTiIrEUFeQD0tZT3W9g08LZnDXA24DigSVXSEJRckuv5ohGVtoy/G1/DIhAdsGAOrBzOwqgs1GQkovHkQjPyOkVAr1G0Ie0zwkUMQaiUAmnVfEFVkTYp4Yoj4v380F7vFDVKLN96R6oPLsiaTRVZEPTlMTIhuVU1BBNR1iGTuF2jVD6RJvxpSHchrCFfQDBeup6GehmdRtAKSUzlEoSn6lJ9SYnGwty/EylackLNvQ5HqPZL3UqH4KRKOnRkipxutScOo0HoVpShIAaTqV5qgSXj4kVTWIUpIsowqiKVuY2I6F2tZKqL6S3WAdxcYSr+tcudwd7BK1yoWTg5CPiwUErwmOqWdVNwRZPFMKJp2m9mZMfisnAguoHwMDryKJ3kSDUUKRvDIqO2+d++SEWo8CD6jbA3PwXvtpHtfETgUmJbFSm0sg5eOBK0DEETInK6gkwTqx1mZblSoZCMx0KFJ1406dOUwCKgW1xhgAIVXrC1QBXlTYRNnKRQtEOpNj3kQDSWH1PnITPWQZNT+zNuLzj2suJKvAVSuQtBa8tAl505XLmxz5XBeKhT6o5FIObDFIlXR60m8uGRUaVri3pLAW41I4uzwpNuutzi6SWH/vkANUHEND8GEjouclEfw6gAHJNBW1g1XXV1zflL58oCewps1pCa567uFG9kgDIN4OaBTyaY8NVArqpy9UCsKhA9B+zIwpgsuJ+g0PW+FRYnPlbjWFZxQzZUeM51TCVB7JeLp/2xcWpFPGgGlJQEN7Z+Khccl1VNv/UFRRPLximr3IGIWWAqaYFJRWPz+jnwTjLsyXKUnVJf36bVLbiyr6lXzKOZnViFJ1hH1lyGk6GBReVKExLTQxTBeA2eFebZpqihwkxF0sHGTBic7zcj3wStQNX6Qgr1F1LQlMsCjIDr+Lpae8UbPyWZjCQuUtKnlPzSdFhcSWRgpcFhPkMDmqhg2mSMEZ4dWRieDo6zorDhORCaqplhIW8FW4G0mhxVyo8ViTSYnOYo76J8bsJ0rdA/mkabnEY8NmaFJZ2k2mxpHZnm1uM6d/lFB9eRKAkg9FBAbjY8eXElUhsSKuqa8DDjZXtN2laHy0Uc4vHKR+ktkK3jzDoYN2kjJiO9BneIBSew1HAikfEFwBog/9RKaCQhKEM1yvGVtWbBhEIJOhKDYF1mSZ1fAfZVi/iRJhtXIgVJkgcfw89KKVHexc5CDkI2ABgzw+IPMnUu/NhqFA87CsemEC4TSD4MClNOlfvdURhcc39QLF5mdVUgeGVIwHrABSyDgZqgTn1FQSX57huiy/FSSrojBT1DVDvp/DIp4hI6/YhVHTmdBwgLYZZV8pJOUGnNMq76Fh8gZ1u/tXkGkAMhhJDxuAKAMqgBZdWfMlTCDzkJtbqa0AKEUwlwAMRlOYpRAHkE2tnoYmVVwiAhmMQcUltvSYl02YF+CVhJFAKgw5sAJoIHLv1KusQxjpLLQaH7jvGwbjU0Uo+XBEYDAutYfS3kcR0c1CmT6604CNZKCquDSwhuz2wAiqvz8AoZ1HO9NbY6U6AgEVagSCD/g448zaCXrgkX4gsnu9FdBWZjUVisYIAajNjF6ldGlM/QQmnJTDhTkfUPyY17xjWfgM8qtpXQW/GaIAuRqsq+Fv0/YgFxYPFOYuRFwX5GHebrG7OcuwQEWp4lafqoJiuVDVgy9IsBV0dyRU6Ke9TcK23N9j4b+bEFNa13zgMhQmJDa9iGrYIqS2dZPq8hxIXCU/cAOf8vsb6jd5gBt8FEQBLh+0ohb4QMgIoiamCi90b4UFCU0+1Z0LEM5sYKqbFk6Umut+hY4mhrTQ0UA79jnHfHR3l6T4I7pRXPgSgRQTx8OCbNADaZhAwDXx5a6jOOXyiMlOwySQKoaGKsAykpqkQZMgnOE07AovAOTwJNlGxcvJc/wQZffoEk/TMdb2KIslL+LqKik2VLq68Eu5uJJmo6bFC1KzKQu0vaV70PxhTEMW2nCQTKUQVGMRTk6u+AFt8F7wOiofe0OE/gYSTVdHMVJNVZCAQh2dhaVtBboVQZWHIOzfa1Sjzck2SlK84Yjih8akUzp4dVXkA9bUhKgKVlGTVdfK1D5hRftX8nfpDWo5DflK/l59WNr2Dc3wjj3Vi5u2L7E01aIU1miQTBUNDY0pxyIF6EWXT8eR1FdzhqQr2bIJTYIp2ZFuB4y9k0+Z64sXV/VtGRdJG3CsfTcHhpYPU15J+j4IDa2moQL5xFsQbEn8wDbXZ2zEE8VxWuRbFelU4KrYH2ktOLCIBteFFclRz0u669yCcBSw8djGOoGJRiGemGRn211eVBdFfteYGhrjUQEtH5u8EkQ1BlpNNAXiGWzEk2cbyimg5QOUV4JTiBtTToF4Nsq9R7s0b32lXUfkdANgdYMTq8hpNsKakA3ADFkrytlweSMyih/PMmwvxo/G66q2jnysuqrgM5twHTVRtQ1N/ZCtpH1FGK+uiv1gFUG9QYk6V4hvVpPENp1DjvwJx7eKYPIhAdBwdCsNpotwFVGCG7Wstj/0/evbFgVJdhMlGSqGsr++JRp1G3Uf/voWg8RoV9VR+jnfoLTsC/oj7bFm9+XV1S6KyYnqf169fvW8TbPyb68fqmr3l7dvywZ1+WabxEVe5nfVmzjfvo02+dvffvnlf7z99de32xbH25jxW/+V6+3QUrtB5UrJm/YbdJoUZfU+qqLbqMSzcLzZCmBX26iozm//ieKqyfrzzEnHXwc69w12Vwzap/fEuSPQ5PSnBye/O28YaapxwL0hfXoDviw40vAUD4so8WaEiJpuST1c8yqO0qjAgrJDRfXSW1AbPPI8rbfZ+DfPc/LaVy8lZkDym8VCfzfHdla29brHF5lusUXmOD9W25QfY//NHMu7fPNyXJbHaUTOVGhcbInFWLM4rTcIa7kEDyvaccMVSm2oeIH78oTlHRdUmHMRP8UQgDn+vjKLdPxqjuk6qVKOcbpPFhz4gO0UABH93WZWqiLnp6L5ZMcrIo/YYPiMqui/0MtTq7JpTGyJHcb3aFhIRaRMoR1egPjUZ3Ncn5ItZvbNdd6fWtAYhUJzvJco26DiqPyebJqlkUbLl5ljbWv8I8+4odPfbbF9L6Jdl1wOQsoU2+LG8vAEzJRQaIv3XU6CcXgVw5dZaJciyQu8lnHaZfhqqV2uo3tAwTRfLXRM3VgA1/lRnHJahimx0tH1bZqUD4BuHgtEfH99yy3/vIXxVjAxOIOPN1jMzJnomQpP97JqBkzCKaeJbaOqPY2FI9o2tlbN+6TcpdFLF5VHY2JLVjPb+EPVhKL6THSHxGGSpTXXOsFNHCiLovtkoWIIEfiBDB9Xwxqfctz55N9o03XfVx3w+FyUggGOaTin7QOPY/xqYfp0L+PyuOjvFtgIQRA2Ez8Uec1tLPgyB6wShPa4ALlhCtbD9cPDxV68LnmF2YTFpVXXqhP7Hh/XKbntCrP1UGiO92uW/KtGVygnDhwWK1dkjvM0je7Ptrg/JFhEHDpQbLH5qDiLsfmw/KZIYn4qjc+fxsBptcxVVTRPT5eNFzbAOsZhdF3KtGimkfmwa1Dfe1Gc2BJ7jMCqwRXZbMPI+f5FWt8nGb8Po0tsMF7ndQxs64bPq5EC8UqYjwTorswZcL8exZqc2MvPGn1rMNDUjSj95k+FZ5pJPErT/IlF0H2yWBQ5BuR7BJWbY6cD93nMfNlq2I29beDDZaqbFQbMpa6+VjP4tECov0DOWZVMiYVXM3o+eUbbHechpj5b4eostPbCHYeQKbOQRHLZi8PWf7M/C2yv7EBHgW3JT6rm+TvyPpJH43JxsGjqr1X2pOddjmddZyXE2eNXC06Mc17cuk9W8lHfAg4e+ruNQ+Y+wd/P8bcIzz7vlOEKLda8AkV4h3qefa24A322xMIBsNtIMLIlNhufkgS6xeTaIYQXKl+Nngh0LONxJrN/BzKndZoCbp7h63Iq5l1eYSNHihUotvBWP1eoyKL0U5I9ch5rpsSit9HmHl2TmCi2l+NnS1xX1Qu/2NPfbTYDSVTym4Hmk4Xq6vj6GltVKf4tmOxA+SHEQ43rIiqwCukpJ2yvhFKbXnaX/XikTIFNTzHbJf9GfA/7r5Yb0yHi9zq/QimKKxi/Dta+/8MTC9AwpO8vKPCS1MSX5GY1j5MusArfejrPPuLxXjR3KLkQLqbMIugvKoeki12OSS4CEAJYMiBkL/z8mBuFYMLh4xznBftsZr5Hd1GdVt8S9PRZOLsSCldjYI7JS33syx6Lg30pr7pW+7LvMSEH3xe+zIIjS3SBihiTSFCUXJF9T2WIoXJ77Efb9h43hLkvs4mUwayFmQoJksmW2MRxbEB89Hebc6p/1QnW0sd4C55n4jE1VG6h6SRY3bD1s9AsMM0lEBm/shCW61pTr3spUVjX6MLVaD1dcnFzzUdjctB+6uqr1YB7tC+mKSzb68lgDvs9Ha7Drmwfd2VT7UbC75sO+5u17G9C7bmXWvHHbFFey70sY5bJWi+vO81Cf5qnmBF5HOPXOU2Go7Tita7dWXaAu4TEEZ6VwmJKfbZY+rBFK56qj18tF1EA1fjZQgELi5v1EpY8oxRYg8fPdqYGVjaRaGm0X80xfU821QOLpvtkocBQcv/AMWH/7edS1WfldRFhjhbuGTIF8y+cH5PNBnHC13+zuI6DjTRBhoePlnLX5Q8A7VeqbGXLWKPB7RYyPPX49/nd/wGvaA3G/9N5UWuqz7WstedI8OnS3MvaVVpzgV/tF5tzyOw9Irf0+zfv2ENIrnAJ5dtYTceij4/+Hl5AXAyzo/S23u4HE1+iUrhr1X+z0PRtHomLiF8dmQJzfMCeRrWZOc6zTUJ2s6+a0PyOkO0sCG7GAu8rM1mA0RLcIpvwhdkFJmrX3LxU9VmkhtSx3vMkSaVrOlHS6tMI9ntB576X6NulbhgIz/D6zBCPzeVegRbFRHZE265gSIyfl/OE7t2duwv5U6PWzOTOQzOzDnQCzBXZmrVNaG9z1x3kTQmMxe4pKZNbzI14jfmRbOooTTkjBASwxJ+kQoYX+rvNcWi3FvKnocPnWY39hya/puRMRiy1C1mVIhYKLbxuG2wdHOfbreAKYEvsOV9ykgYULx4s1tWRq1QY4nD4p+Et+tjsEv1I0JMQkwtBWLSw2xX5D7S5jEjCmCt+UwEUW2iDvNKghyHse3+dV1EK0weEcBqDvBEp0M8YDnn1WHMdJB/cggXwWlt9qbe3vLaQwZi38qHir6O3X2w8vB+Su+o44rM00t8t+tPVgcwZvsw6UOq8ekDFYB1CoVIchHULoyEE42fKLeS3xnoLC35MTKCjzYbDxsuyFtpmdt/nT1maR8Lsjt8tthJdHSH0i/pucVqRpa149tU5OkDlNvL33IrT+Z0EPwxhT42T511SNLFp76OXEqYMD2PfSnN3sI2Ag4JFpVBWx/JXETa2EMwyQLGFNmVqCtnLhFKrXn8tUXF0XyAk2qZiqVUY7VhRvLwEFNvI5fBGKi+YVIGN/uoqHb/EKfqEsnvejQtDuLZwgYokF+ZRBuPQSmNgNGgETQxB2FCe3KU/ySK8MxWUIlNkg1N+e58vs9lxtTvTKO1rHz+QnO387ksGZXXKC+cKYAps8J2UAm2bTxZRGmMCT57NuCIrm4ycwmY/sMTiyniv8iAysRTIJvA8jx//XkftNXgu8pwpsnbjNfWPfkRJGkG+ETmUW0vwIGAIm4iXTIFdLLXYDeRP7di7Z4SFFFVAudUuKbl7afwdp3nR9+8dEvLJKAEtgpmj+LFJjExu+vBDEQot99vy5zaEjbf5yxzyNhtXCJ7aZFtv4XmHIWxbiJ51LfAQ5i30da4qxOVSYEtsMTbvSBR5KoZuQeVWUTKo71mHQgiZEQEs+QhtOgwJv1QDxVZaiKzD7+qXd3VVCbcDhFJrzN+T8iFNykqBngexoEyre1OExf8C70tFTyEMYRPskaZN1STmL/gwJZah4UBQuJUMphsAzfjV2jt8nJcV3yeqwGJN3qE4iVKgd2yJG8bhQhm5niReN1NCurXYXTnTtsfDWXBY5249ySpUlBCjQQBWVgBRxAwWBLGPEtDKI2DYngrQamd6naBWDjnNyBVZ2TeorI6qqkhu6wod59tbErOBF2NgHFrg+UP2Zfi/AxG1360jaqF9r/1Odz0RwsMaoVYvCjCHtlSKRQo0RziCdKVY4cWePvNcH0zA+xnYQosVIOgrWvsQgY6Jn/xAxQthNcHryZXZW/Jfs0SIjODLbFfM8joqkrs76MAHBLCJYcDqtXg5vzsvkvtEuIENFNvsNUvUGQyAa0wsdcD8GUVlXSBCVwl2BsKhBSh/gFDogJf8UOKmASzw19kmRe07Q2JyLL7QFu8FKsibj7BbUgLi2AahgbqJAcJ5FHnr3MQrnHIkNJiNbZhkF0lzEiv6A5kiK5sJb+zIEXlXXbCY+OLVhOC1msQ7VWCrj1wyBcpqThODJ66ntitp4+oo3yVpKogBV2SLExZfvszCr/6Uf0IV3tGclcDLGGKpBeaHAiEVbqDcYo5qvBVLYhAzX+YQj/It4tQKW7KQNRrcVu7qHef5Y4Lakw4YMwexfDzO0WaDa5SnebHlp4orWo0WxduxCpsH+Y8k8301hEHl8lKauv40arXT4hIzdXYVfXtLlttIjIJkS/ZN1JfK9kbtvTwzvlGYXLK+Kauv1WDAdfPiI3oG3qjhiqx5qH0aCGSivmjJJeis7FIYChFp/efV8HjnoGl3GGR7EeROyojO/XaKCsc0LH8R8IpTt2WDMQqF9l4d2J9jyfaxPBWhC6aLSRIn+iuiCW+iJCk29YfjB84UFEvtzrWvH+rtbRYlwIk2VWSOs7usxGIbPu6bUbAPHt+1KPWBB1uWDKzjOewhVL4W5TQrwNgBWGX3JRYmeT8QHiFT4ICvMZ2kSIdSG6PlokBtNj/gHXa6aG18HujZDxabi09Pi2H9dossINUtGvWsPMUqtx5vdgiBw3zxsmZ7e8bYP9YHnmgOhWuTgnBZegGEHokKFsjZG1Ig6P6rMuH+DIKx8LOKJDwT+SbX7wjWXtx0Z1tZ/fUzbIjcj+3oxQNb+rsFtuZCMIep+2YTb5Tu7ur0v1HZXIvhA4+4Qmu8X3I52qEsvMhouFuaiI6TGZdkdByK2VzmXbOyV18tFemuOxrhujR+tlHK/d1vXhuP33+f+1BLHfwuze/7bBn2fKmsPdUxzha+AckUWOwj8RAu8lJ0/FHfF58lZqXrNF1GRMZz0VSimnT9bNuXrKJjoUWcbFR2o+GCZanvi8/jaV7UW8JV3/LK5XRJU3+aGRsaFRNuUwUWvk3Sd9ax2XxZfHa+oKfSXRcqa0+rC0Uzki1ZTrsSokA+Ovr74vN+kacpZkEMdInivNi4aFUtiom0KW73KCufRBuNLVndBtIzlq/D4mFKz21Df6gTiRXdlljYliV59ZY/aRu/mmM62QonVt0nCw6MypKkAeO4b/hqj6mNnBIzBPKl9pivIv6cny2xif8KnX8udNaDo7u7JE2ATHZMgQU+4A15+xfkQ99oOYqBg3GhcKkbLeQFdsmulytacq9KuvIpv08y8DFBsdQOc5/bQIpcALCYrSitKyCEjv4+rzfuNCnKSmQf6rMd9URU41eLXgV7OB4rtl2UvQi2YfvRflWUX0sayyz28klRPRA+4rby42dzXB9QJjjl+28We52oS6NHQl7rktfFQLENv27RP/JMUPD0dwts0XN35gBl9BJL7fi42SqS+JVK5Ga6zA5rm/RKWIK5Inuc79GPJAZeQYLKV2NZd2uJ58l6vyLZG9bSmtPY1Qfdb4YphI0fTOsHDMFn7hAIKfz4Qos+ChGbx5bRmp0g/ApeC/nVAdNvIKbfbDD9I9kRR12UiteVuCKLXdUDXmSgBZwpsJCfPgMmJz/j5zms5IW0d3/c4Ke9eyyORzlw1Wn091kpiU1lCmw0ZnZfAy+e0d/nXQ+6+7SU3cQfdojlFsEuoZ8obnsjRQuVW2hrFJMl+R3/ptj42Tpo5jq6L8+yrltg8AwHYcGbJPcYx5XtJ4u9SL7h1pH2i4X2b7LMt54kMAH9UGSBszsd7+oCj5/BEBYe/zEtu7wRKZDNfuWeI0r7xcqCaPPTiBmrmBILW6vNQyPgo7/bSPjv8fWFqW7s7p1FMJyu+pkEAxqf4+a5jILrfJfEgu+h/7hU4JJ4GmqdYPsCDn26WIUDOHyaJJdrR0uKWcNiAeSsweMqaJLKEwZ1gAEdi0oaIQHof6QLLJb4Nm0Gt7r3Hy2Mml4Ts1bM8NXCISw+ZPPN9tka4gaC4nHo7/bYZDMJldtjb+7Dg3jbkoP+W07/Rf6Kz1XnzarumucFQZ03lFioBME7a+uZlVrijlb4l2Ed47VU/3kJhUfEHDTrmIKD8jPkmcMd7IlMwEYFBDABGzyu6lBSeRqduD4N9nNz99weD/fXDReSwz4o2E8KeyyOIdxzZrFZ35lFk2GFY5b2k120kRhpNLX5/JOck6zcJ304k1C1sz7f9e/zbGGp7A7DlQvPe/EDHsebKLLK06xipEXo9snc1mj/fCq5TSZuXOmiOSzSBXnQn/sc+W4/7SZ/zmtvB3RaE7o4MBQtYwNhE+7pPPuYb9GFkPGPL7O0BT7UqKzK61y81AkUL7kTXLtl5Ws7LHyf7zJPe7RhrvZRCD1u+SmxTKOcwp6I0QORYezLbILv9jJ91slzRS45pkd19YAbTeImzru9cevHdSrMDuxnh24f+DBEGHlPlbMNocldwqt2qNwee7dA6BoBwMzbOicTe50/Im6fQ3+3xHYUx6gsZTiZUhtnRf4jwQul7FojVL4aaW+WeD+xbq0Ee/mV1JtGUJvGoMh3psASn3jdnPpsYzXl4htEw8ellFH3fE0XPCVk7hFKbSzl9g0bCWqg2HJe4HuAXJF9f2G0YqmF6oheiGjAiIVCa7ztQ/ZSvSQDsue447ooUBa/iBdfYAibFtp6l8KFU7bEvs/qu5hyKIswpP7iKReKNH625ev6tiKJ986yOMUdg9ibh3Bs4eRZ18IAYd9CkzywT4KvHAsM6dmicmwwpG2LnUpQjI2HcGxBMRYewrEFXFeUPRjCUT9hPZ8Qyz1KTxECSWYAHqJtkJgG4CHaBslsAG5zl6OpItzn6L9a8gfMdS6cBj+8wRVZneNgQr3DJZnwejlbZDtiohoucXc2wm4eKnfBLsNqg+0S3eEuoA30eClfZoP1KSo2F3mSVeV3VCDMjbzXVAJiMXMPKH7M6/H5culRlxrSo8Xyf21TTVMtiL29IfOnQ+UW/uHAGYiGvcJOsofYWXuvyU4GS0QrfMeYRSCjSA1p0X9cS7wFOn61wyQazeNXS0xQDhanEX6Oyke0UVNTBmPX5+MfP34Te9x+tcN08rxLisY99znPqgcRqQDgiv+/UQRQmS934+D3SYHi6j265ROsSIFszmKGal1GrY95CpzLyKB8WoI4SA7l1NK7KHsUN4cggDN+UVhBADf8Z8dy1KTMCWv/5rEM81DuhP3sNuIjRvlC+3WhsUnAnHwwhIWk1djKLZJ/N2J6XURZGTX31oWFTQHn35rIpGpI/xYvUSm8jKiDtdGOO/JuhoKeMIRPC9CI5FBWsbaDlacYkALMJjKgiB+iEkn9xiCAzU4wgQ+2mQJ7H2W7V4Q9lH2ZPVayRSSpiLqkT2q/onElmxj4MBkt9yFSnd5GXaJtlGTCdlMCYt7Gx6j8gp4698KXnJzNxUCaLwWYhd6LY7Srrh8S3OMIf25OND9G2eb8h7AJUIOu5tCsd0t8LfF+7WNSVv6PrQEoXV5cM0MzzRHb4Kfl5ZT6bukUAY9wLJenfUtJ8SG5a/ZsAZkLQOnCXGZopmGuvm0eC/3dQmuXaPM9qR5AJhMK7fACz3tTn38HjBuGVz34c7aY7M4EpLgFeIVBDmXP/dBJJV9msTIDHmJ7z/BZ2fegSTIdAa+lAgD2Y8eb4R20P4PKbaytONklxLwR7FiuyAEnEM/Fl1nY4k1eZMDcpr7bYgM6yBTY3CApSyHsefhow00j2RuDU+QmAOAn1qqDxggQpOV4Y1NRd8JgLdKgJMBqLJrfnrwI+DTl3+uooTKLavxqYXhkSXVRJDF8fi6WOmAGT8fFUhtKyvrr2ld5P137CB7LOxy+93ugdnkFBy0BcW0DJIMExMJk0B7N+h7JKo5inY9g3+dPWZpHG+BCKldks1D1VaVmDwBgc1UlRlmJehzCLSyx2KLvWH9+R8n9A/+mC/XdIlC0zjYpIqsMFyJKfbfWr8ckISKkYdsCK9dh8gMVLyQJjOCZ4MqsZI3EBNIIBEkTAdZlVHxp7ml6GxVf3F5ZVNSd0qjwNwC+CJfwvrjdu7vOh2fSINahi39i+5b2ZYfxdokYXZxdRlim4dSwcfztEDhd2n2zxQJG1XFFdt4yyAdBf59/B7p3EkTO29p8ID5S02NxEBV51XVr8usiih/xwKDjXb7MHuvXIoVRNgUW+EgAKGStMQWWZ7oIPnzmy+zNLBCtUPg7kEZ/lw2NyUMq53TcDG0CEQj9dwc3ECzr1l50e3fLQjz0PUpTVIWxhmhcLnaQpv5EfLTKG5Gh1p0w5y5tDdAi44occV6Q25KY5ArcI8iSMTiXKCp5N1T/bX770f3V4sV2YFVdZJfoX00aGc+tF4XKadOlrL9uZRN6uQqrvMKdNbSidZoX7WxBckcV2uBtph01/lJRnrlCZ7zyiD0loP28wRkcxVIbTo3u7r6Imdfo73MoKjmlKfGFL7VLQGzaIFfYrvPWLuGRs2X7GRi62MYlb/48xio9xOaFxea0gdGhWPd6cBEV3fZLzOxAl9ietEAY2RKbDdtIYyi4CCpfzJEZ8OR9ilPGflSYhVGBNs2RMzxuFmKJNfyg73q+udqheIhrH+feL7unCrVL2k87fNOoxZDiB4/kfAde59HAWgg9uaZ5mqRY+IQrFXyZlQp9Os86MshSfzLF1ieF+5MZVzVZngaFArOLcWGFbiJDA+yDGf9bnllHvtl3j9IkEnLRN5/m4GbpuJrTGSDinimw8QGFfccAo8j56+jtp5VL7BSyGkxK9+ftnnVKzZQr1jRr7GkUo+oqLyoBJ1tiifEabTEBK/Qx4T0gQLFFeF22Qc/tUkI+8O/RCqWr0QXdnDdvfAewfzEed2MXrLxWOd+jpyg78n6LigRvGwedepxvb5Ms8rfT9PjdecIK6UQ222PNaUvyweI2E1ZU3P2l5ouFjRJl9V0Uk2wKBbklDef/gWEsQ+mBGHqrEGaU3fPJfvpv5li+JxseSffJHMdHIHzmo32gc1SihgjQAa5Q6ICX/FDipgEs1uSyTO4ztKGMWN4oASGWDrLuHVvkkgmPlS+zCdxv4vE55h6//jwOSaxF40fYZ8gVWdp353V1fteg6NKHi2aeCLKaFZBmHb+1jsbksKqpq6/V1iH/v0jrgs9WSX+30RyTvG76KceF8JWMvmjJHdhZ+R7dRUKSJOrzaqRFYnpNYiSGswz30SneVZI6AqFyizBi9FzhT9sdH0VMfbdhYHKEnxTQ7ez+u8Pid5xnVZGn0EmkDOYndqbzEx5E5gII2+xSFsKN5r/whV6qpnGVhV+s1u0qk2E+ed7lRS9QHF6+bLUS3x2cYbiwsj/iDaAFVMjWasSGWWvXxSUTcEgw7pjZHgN4UzfDLOS+rU1LHlwqXYhHm3/WZSWGmAqFFg7BxnEnQyyW2li1FwW6QimKgWwOTNGS63Fzqg2ZyEyBhZs2yR5JMBq4kxEKf8LrVpI9ZEPOSfa5bVxCsM2uBN2kGpbvg0S9QmAH3XrQrQfd+jvRrX2CIs/syB0Wl4zI0qrT6Me+PTF1HVtiEVk9pnkScgrwZfb9FFA64jsrm1z7iaA0mII54snn0uMtk/HIxq+2mMSViv5u4a/BI/mGilLwUzEFFuvVQ5TdozTnXFTU59Vom+sEFc3q56duBjQO+kZRd/1HEKHv3V/morTxZUtcfwkR3XIcpXGdUk9bcMMUi1cjJo3C847y67E4CIm86tIOw0VnhCQICjEnBI/zrMCVp5mX0EvyUXpbb/kNY/Np/5f1/k9BzXdfV8PLzKu1nv4TGpWLw0Rdf6IFeabH10/rNJWeSQqF5nibJ2zHt2dPsug2Fd56lcDY7Iy3Cbbir/Nmzkp+Z8wVroa5L+u0ZTDvLJU9IqfklIrK0zA1aRE4RaE+2+ES32gdv5pjOsd/RBXvTRu/Wjp+AJfPTx1gwj4M55+tbsTlmK9OhWBpy1A669NEMIaMNrzPMKnxfr24FyIs2KKfXn2Lr3l7Mb2AzoHvDXCslfXXHFPYqBNilXSOEBi3HMrK8xk9nzwjgQxMgQVVo+fjqEL3eSHsQriinyfCUk7bUJc1fhaFFeBUGsYZQnXNehIttC54GiGAeaPMDqe0e2gWX6K4LkjcbbdJD/UsAYTV7WkCQ0wTbQW55sUUf2K5xZbu9/ruK0+2sNwWgM1m0+svcYqgu8NMgSW+C1QkufCKEldkYaMRr1dTm7MkmAKbgzc8fXBKfqZkCcsp1OPrZ1lSJVEKCjhf9hPLeTMBuOBT7nn+RiFykG5l7WkEm2oS3H+IxfYnzmFSTYJZcR0S4u4zcxLMwRi0cex6MSmMYRpGbc9j/gu9POUF7zBgi+bcTYAHMYqTl4VYyFuvuemzGfUYbuoT+oFSIZ6T+m7jqCJ30aDHB9gSu4NBECFTYLFw7442mwLX44g2fl5KT5PsZEIA4/DRZktzh4oCFQIupmAOva/guXt++9x/M8fysap2ULAS/d0qMBV4j3z8uhqVdHR3l6RJ5OvtH9C4rGTyuhMZW61sCkbW+Hn+HQC0I5HvRxYzgVK888Wru6/x06FxMnukdadhlrXGPE0TR3qG2Z9PDtN/Ww0b4hUnBBsOaBzYUFH398WGV2nNBb+3XxY5x4qy+xpbOILxSX1fDRN/QBkqkjjQaTuPzYGl9SjWztl46/mhyOsdi2n8aoVJQDJ3yJTthYOl7pU8oC1q7rLepr53S2hULvdL1PWnYd+mUc7R3X6a0wHyO2K4Lg7Bh9HahxntGUxSbxrG8mcK8Uqk3Vb9qoQjnOnvFtjImZnokqA+W7hx8iJGuBv436M0JV4NzgEDAlg4A/KSf5yu+2TllsillziEwiUjiT5W2/RdvhHWYPq7zQFBVmHh6XPbfkHVU1488ucFMIxV/AgW6pdGIo/rokBZLPRfBmPdyslz3NzovIwqXWMy0NWoz65TviEb/dhcQqOkVdeqSPseH+cb4dkyusTCq0rcXaw7tflirQaCZnNt/QZYZ26jSnxPQiydL8RQKqAOrwvMp1jX9JScdNT5Nkqyk2yTCLkAuSKXa1LNa5maa1IsjGUrX+rtexRj1ZuWAH6mdG3XvDgM71GWb8mTDfydHRWcc2vkSpGymRZgNctWoxq6TwGM/w7MdQ8grb52F0lg59+e7BhjvBKQK3RR+RgmYFXE6GIIGWGZhqPopoUJ5MqsrqnUWSYYDtRnc1yfo/ghyZDIqkyBZRghuHKyJRab0CRrbAwAJVdk0cs6jhHawP3kyiykvij4RaX7ZLMVyu/J8egFwjtrMWCfK7THC0ZlCIWr1CnhdImnEtmfHVWI9IXHRZ6dPO8IfwgXTPkyC3+ucPva9ua1bTCawvmAVyoxq0X/1Wqd3p1ngBZgCizmHz1XWKULGor+brNqfEw2G5Txi0b/1cLGrTOsgboFgrNu2aLV6JG/16hGmxO80UqPqgp3z/9+A4jS6Q0eIzzTqBiqcR4RV2RnruJVhPhFxCvpQqFNqB1/HaP9YmHsBE7246+gPydbIBHF+NVybB11weFRZWsUy2DC6CeC8+XMC7XqnBY5l3Cq/WJzlYi/QWQXubpLX3gUw0cLO+OYsy2ObWq/i7lFsvlgsyO4/SeKK34r0H206Ee+4ea0/bKkN/MK8/x1sxnnjgSHz3a4oE3T+NnCgmvWvDgmr0kKXheuzKqHG/L6aJSm/JODTMlqlCA9VD8tSGNyUIPq6hP53BqlK069w4mCuBozBXbH0uKptJV2zwt+6958sYtsz4QBjV9tzKayFG8RjV9td1xXJT9f42er8XUvDWKthvcf5O5lKQwWAlmN3B5H212U3Hs+e9NjcTnhlVZdqz/iZ15lpzp77chznR/FfEwPU7Iauei8iP2DbL7pZBlkTjllNRjWKizrMMs/5xuUtjdx2T0l9d0mdrys2poF4sjDFVmZ/q3l0l4s4jsKFO+jwgoXvz+N2T2Z+nM256VO8cHL9ivkCwKKXXD/psb9mw/uP6hx/0GOe6El4Qt6Kj+hqkIFlp0hJ6XfygDjdFggTBFNtE6ArYtvt6jg5t1u2d2hnNHNEejpiO958Yj5RXbNCShejZyd5kW9DSdiAjoH6TLAMY1gqcXJR4jCZhBoCMSjGj7amIu7JBbSdfYf5xDMxR5PTH7gfnV2vu9ziTQupwcS1Qgm4vVAqo+cHMi4my+z4UwZTrZkfrOcpAnmetR8sYrFQhE/d903GyxdLoN3L0d19cDHDwDFTrgvUZzsoPfRIIifWGN8RlFZF6hNU+rrn6BQOXknlPXX6puYIj/VJYkIEG5rJFbeir3L9NrN//sEN1l6G2w8NneGVKA48ORPzpNn211ekPT5d4nvXXMGlQM3auqvlRVP83QDZaaiv9tEQKfgi8D0d9u7CRA+tmT+SMx2si8RcdNvCPtwkygWWyiHx4RLWNF+sbAVo0c+YrX5Ynur7DzjHZn0d3NsWJudJijdkL84dytXZM9px3l2l9zXBRDAJwGx4JbnqojE2aU+W1ieDYL+mhBrerJFNgcnZZ1WZ9mdcHYyfre+AYL7oLgDQpWuZhG4esniMBfBRkQucfeq2tOo/3DXwK7yuoiRkOWG+mzbK3FJcbtS1mQGeK5EdEyB7Ug/RuUDNNT2u62D7UxIfzx+tsV1VRWSi9Z9iS3Gd3meQvj+//a+bDluHVnwVxx+mpm40T726Y7o6ThnIiRZPlaMbalVsn1vv1RQLKjEKxZZzUVLT/S/DwBuALEQK5cqvdgqAkhkAolEIpHIrL7raK1JyH+HRBbMRiycP6NN+CPYx6mDHAF9aCZOa4Mg/EiJWidl41O0n8dUOF2pYG43v25WeOomW7ps9zjbOEAoBO5NFiT5LsLPqXhjJqpj18twH7pKZHXgZl9s9Mu0TL7V6Ykx+jafdT0g+O4i5r4iuCXXMk2XTO2pgXg7egRfmSgrVIHWWmR8QptvM9u3nNg0KFDGO9arTQMPLxQDBRTarJSgCnWNgCyWZgmu4c/HCFInzKHNKde5malFYc0KvUuZXuH4ykDFqEJ7DKd4zHOxhLMg0zDaPvFZa/6RzGaMIOR3fW6qTCfsgPLKp1LdLu/uctDbxppvmk6BHFdALdfJoAjvV9G/euuD+KwxA3Cp4uCS9Li3X6fems/S3R7fxMo0FGEl3Rvbf0T7kyy8Z5y52FINyDEIkn5s4PbjbNSB0yB8uEjgrIcP7tylBEANVARlSPN2J7mqUhpzzsjN56mcrpbnsgAX0F0QFmUGMkcvKzgQTe6LlcDMVan9EYEn9pTafT3gW94mjbcbbupDM3rONgTilYtmx0W1LHfDRD1gRu6XAxBeWWh2LFSfIOups006TMIySjksB+DtMqHSiN4LNKX3RtA+CKB9OGh+OsvSPF+BOHbCUX1oJhvbIIjD5arJ3CqT0kXyqg6OkSuluLGng5tT62djgkR08I2TVYmOv329L/d87puvGo+0os0WFP9I+wHoyO8aeEVF3/5af9JYH7OOkf4anYCQDCgUuivp0MEylBAyAH6kBO6VCRFXfdO5RswgSQh7xr5Dlejbx1n5RZdoUIriCfQIrT6NKxtwHuWPoDPk0SuFKdWyaNfX9Oh3b10zhYcizU7BNko4SejI71qz83T+vA+SDZOCiSrROA+mn9I4Tp96Z8L2qwYk8PQzSjYMqO6zxn4e9jmv+qJjGH7pr4X6kx4WEADzxq37rOPMsmPiljbfNIzBeX4WB/3k4t3X111WvMue5HkaRtgJnNlq4erGd2Drk/0+jhDltYkPxxwUb6vydv09tKndmA85O/qGFUjCDtaVyypnM1faiVvQPBZEg9niYonmTZBBRdcUzWawNNH87R13vtVZormtWRPXlrLsV2xtNs1VVaeuojD7LFTLOW8BOphzDnJ2M13DmWyiT6MY+XE006My2/0moinXmWsapuWY9oC5nPYeaDvG1EbsLE02EZrPNxf5tzKOf397F8R53/NiiHpr5rkGT0G2uUqjpMjrjC3r7znY/IyK+1pXlJmABxuzRl+miQJvDXZkyWU0LAdMNozwHNltaBjciarVfYS/68gqpo0LYdUDaslHfWguxVUf9hwZaJh+axaqbeDr5vox2Eq5h1tdcJXZ1VHgIA5gyylpIDpgGh5ydpzdQRpbvWkTRqyH5rpfkzUB1uX4D7VppmFaznAHzMEc9zCzm95mRMaeXPbcRTo5aR5de01Fp1eymtEJlupn1odYGlNLFiFgTc8m7YWzFod0rUTMIbg3VhrtBvqsWaJF0sl+MBkjoGSusFRt/rvKommva1QlOpPfgJ7jnLe42U01NTajGzca65nKhHMqizwfdSecAW1ryDKzCirituAJvwrwO+WvoLhPN0qzLmrBaPlkPd3553diq/FTQF3o/XwsF8wOzYlSgx+ETZiAJ1RFXY4QdGPJEj2oDnhChOeCmaKxiVyn8PSjtDHwG4gMSKia9gbB68LRbQeG6dCWROM4Z0Zo7kRRrJwgSlC+U7pKe+laf2l/582HVZWMETuj5F079O51F2Ci8n0QotGBNT5FWV4glrkNclBVefumcYlpnPtqMfPP+Cyu4rc2FeARKboDeXGTPoDk97cffnn/4e0bnAUYpVCI796+ed7FSf63EM9DkCRpgUn//e19Uez/9u5djnvM/7SLwizN07viT2G6exds0ncQ1q/v3r9/Bza7d/3mNVglKL/87wZKnm+oFIyEO1bjjoMiWr990+/ubxfJBjz//vb/vfk3zTS//V/AMGEz79fgjmCgvk2w3/A3DtMhxH5/G6Hxxuv2DwDZATuQV0/XUS1QrdY3yECJ3mC3Rsp3UvCkz2bVTfIYZOF9kP2PXfD8P0l4RVYOgmtcNuvxqyDeoqe4mng1/hUOcEI5UDo3CxdEJmFcbsBFsoogqcHehs6LvAkCAwsKEBZgYwOuiyjjgM7aAc0BJOx7xgX39s3X4PkLSLbF/e9v//KL/lQUWSqF+eEvfzFhGCdkQ70jqMPBuuE8BJBy4XME091MM+5H5rx8DaCwzU7yytnaBhLpru2AxgrczyzY4zh2iQg3dVjIA5CaA3MqT1N0g2kpRdoEwcTOowkDk4MuGpzID8r/zEradq6lqmBI5zW50hA8t+5hR6A68JQGSvT++ZdftIHSvrqq7Kc8RVDTLnB0lmOdHjHdqP7f3lz857oeozX6AEfDdkZ/BHE5LHfVlNUmOAQxbBoE1c0RTW646UsKj4nRv0B7xDsGvupC1OrNAjNYaxRiJi2TzX+8wcv9b2/eD04NR+EgMmQ5R+jPJgghsCjubv1CwGAFKmD265t/SxvQWDhYxi1AXxR9cIakscRRX/g10x3Deh/c5t+bTFQ9gGdljG7V5Sc4ffDfk+ifJViB9AxHmZQB11VFP8XB9mIHUW/CBzo+0V4XVhquw/OXgao8uj5XLfgquvY1yCuT8xEsSotdr23ZSt5fDHa5ZrAHV8AHA+nQAHeoOl7kKLPcVVxuo0TAz2r2zZu0DMVrgoWhzMpX7TPK+p3uEbCx2Posp15i4LORJsQUpHHzUO1AhvqkehNqsR30+FNf9LAQrEQQeaWnjwzdWgMRZW4ieziGxTy8FQyuVI66lQHQXOPbcO9N8Hz+DHZ7KzsyBFLrM1W0eq46o7KNNImsbSyZleysmMscjkwC69yiKK8KiXPBAXP+L9rnAMk1j+yYrKbEcPhPQWTS/g6rEAK1EuAYgq7cliFhYj+CC6m8pUw1A8NhYhHaRvCYcrlH/JZqHIM4ex4VN7aCg9K6FRHiQN1TMs4I5wjYlyBHHuQhek08DNJCgLxesVgayT7B6rqALSWWod9GAafXNdTzZzQnQfwlSh6cOw4Emy2ocnu7oB9BqwOfmEsM7IOlY79TQa0fRLe3DDRRlLlJ2DLlqB4TtshWIa2awbUd1PqNnP7RiGgq2t/VqIHsi1MnmOvJ6NDanKPym3QFYhAWfcAGdpwGxGUdkN+x8naVRSG4DpItcM7QyFXkMvmcorw1WyvV/3OQN+9l8vqNlNVJwpGPxuws2ACnwfCmHZpZyJekGn4Ed0EZFyjM9FfOfYyCDFZWDhuGflUODU3vzQAisHYb0PccXIEshMRZyqn2fSoDbQPCaBfEyAsd/pVjd/L3f4UjjS6FoHQ17upkVzGRp25WRZAhV3ugfUriuWNsHEG6Bv8sIyhxz+BZGEXJ21jNGgvFlfLZTBLeKPDatmPUDs5NtOs2HIe3lNSz+1fJZOgZuIjjJS9RzevpyBGyryeaYdyWd6JxqPALzyAuFfbXU8UsThVWx1f1rbuRHMewb1dJdvXFa9PORrbqOoG0naM/ei4hv+oLoZOYMdoq9Q7byf1WjR/a2QJFhu4k514fKtDVtu5Rp+/Q+BUubvK6XAuNpnEPCxOX0yrOlzEiTeu+95E+IowGofKEAbYx2aqiZxCb9Nc21F/ISP/b4KzCDpTJn9EG5SUzVrc+g2h7X1gAkO9/Kn5GDYD1RX6TBXBNQeFsdW3udBclkNJ1B5BTZnIXr6cUtYjUzQw6/Bxt4F5po8f9gFo8IWONb2rqUA2GR5u2tfoOrKkB4R3+GHSgOq2M9jygP5q23Sz8x5uLvHqM8Lc3N3AORtaLGITe8xCydNFbxSXzytf2Jv0sSD4CFA4BCpbwwepM6HQ/QicBZCBNRHuKqn879yGV3ZjVYSmqTJ7GdgTZiVHVOpvBE31CujrxTb8f/ur0lFYLwWMQUh8JfoZDewtHO3tx+sLtqgpyeVTH36suY7ymttw0tNLr/No35/Baqx6nI+Il+2vUagvHDqptwmA7iD+iPIK14ZBHj9GmDOL4xUoDRuCiuB8uxKUxsw3Ab0O2l0spNqmeCyUD1nYN82QDt+SzdLcjDnpWABsGd3LH5dCpqYbhyv79evvGu7i6Bo8ReLJSy0/2+yx9BJvrAMXmWJU7q7WdFk7hNcBuoPCKe8Ra4ucK5Azd7VYPpfMlQlynw52w+FbubgHnWKH6WNQAhT+KSMuEbt3hRf5HdFecBZnVBWMDw14Dqf2CLot7kF1RuQdM47pheJ0yM+ieri+sSjQ5BXp1E7+cbDa9Lq3Qv8g/pk9JnAZ29781DEsfuySulm8Dzs5SEjxX6+vyjoFngl0D5Px5H2V4lXwMXkQQlWwLNUD8fK3y2LLm7s9BvgqgDgNczCoNySBSVa+9zYUqJOx7DrKTbQbArm/e1/fl7AA5e+BykV+DsMwyy6e9LZCzlzAGldiwk3ckvCuQRanlMm0h4s0fg7VaWBc5ehJ9nqAmVvyKomK4eV8NhSw+oQVxA+3sHgXX9uZDe5E7edJ9kZ/nVkNIBH209PMLEnSFkzzCJQaBVTHrLY9VRRo+/L0MahOGhSSvTlMY3sljEMG21InfIjIQF0ej3StKnNH7JX2qaEUbDRvtTl/7j+5e8An8U5o1+J0CIiCHCdjTIHzAwV7Rowo7DPHhDsG7qMYQHi7aY6mVSoG3Lzgz0a7cuZiYCl7w7ApeA2NVgL09HByrP0tj0svESCeJNqDBrAZpJZvR7IJNDTEa8OA00bfhWkaVT8uX07IoUqsb9RrWzyi/j6O8sAdYC6wYwMUHdx/KJGT0sgEeKjCoKLQyVVEAnO+Pl/HGbwf1WeoszT0+lNlDOEFsTIjSg0qij/ZhDnoJ4uBJDQm7fqjjCHJjiTtPCpDl1rxYi2gKKvDMQLUYH7VPeCi6iUC1dq02NqghgLw4KYosui0LcJbu0CUs2i+9MqvjN7s/Kfc456NNn8Wcg6ecA92zit+xaXcl1zKnBexW4Li6UOG/nlC/ZF+3ABq/v3UTC6uuMaIL4r+t8PtgFq6IhDUKnr9y23eRw5wSaBYS0gS79/am+vm+joFzGj2C7AWtfn07JN3axgrZHGO+J5GBYw3d2gaPSsvIb4IsursT3SJRT0i0e0CbcvZyeXeZRduo/yZZxZ+6D8CG3tMgB7XaaW0lbGF9BUFeZgDNhnTw9B+JtF2Yv7zX6gb9QXdlcB1xWiabGFSJbjg2cVvhUoG/AtkFFF4ubLQUQDQMLuGt7tPK7Au1GbtLqCi5ivCNr9CmpagpQ6ZB1+w1NHWk1EPRVSv2GPzZBr2nTJ5kYcNUfhrFse3tAwbkJE7sU/oFoMG6yD0kD7i5zwBQh/+rLny4sQG4rnuwrdxLfgRWy3nCJAJCOGdp+hCB6r7FyhLo0HfmZLOBEPJPabYLmJ3EZYQkeNws4B6YPkbJcaRKqIW0sT5m6VLtR3De3qK9LRiMDmwQXmcJaT/I88kx8PAgE/1Fn4fO0jjNPoNnbpINW+D1/FfpWRyfFVztBhd5HZhOwOg2uTwqU0OlICPt+BiY9Gra5zP1acQUiV5zK1QcuVyEPo/C4qh6JvJ+UECZRTr18LYiiiGbt/c2Vjobuqa/uS93t0kQWV3Q1y9yDkbPnotZ1FxetwxS8csxiO+OeFPhWbW1kpztuFv6MzVgsHJjB+sC2ZGqMG1eIkw1fhRHlNZgYk1B4K+pciRsW1ohcJF/ggKy7B4kqF7jTSbRmzu0JieaG3y1X6MHxxVHdeJ1Qg63wZt0qvVBrxftlHDIqRMcR5TyhmTzDH22zLOvbatyU75RGq36TaeVN+nSdOVmWo6Jef8oo7ajEkdPijDIuwiNg+4k5SgB0aBBTz/g4444hroCehXk+VPaPUV1Dbe68LANkVHBWgW8IJ52eDqPm+Do3dLJ3V0UR9aBF+wTUfoOZNxonichZQ5znetS9T23AQuhzHiSTUhjJ1xToOQxUccJbtkg1kIySY8IiUIpEhNHeSwQvObhlhCk7qg3AE08GoO4LIav6vQvQobjfBjwxKcoy4tBc+6HD9rh6dAo+oArTmeoM8cNlL5PqP6swM1iHySc6H5a66kCIo/OqPNORD9wBgcpCpa9+DmNsuIerVC75dmC0feShKrihh0Vo339R1AHJ0EOFmVuty2je+1/pAmxuVvhRuettsyWA5cxVNrKHbq0KFzI6iq4gCv9qoH4ETxGITfwuFm2mMpJrd561wYm8j4AG+/extPNHB0WgofQwa3Oc/gnRfEmb7ZieVu74YWnYD83Xl3OgHEPrEaQBHuuISxf/mJKK5t0zzNY1nRzqxcThAODZehBLAXeuwT2wQmwf0T7qzRHIVnM0lJyTBH3cNfm61pmq7cJyeSGtc1tjcoy/zROt1f4TfjhC/2L3InbxZcg2ZZGwf+7llYmcrXdRvHKCr+5INRNG2VzBinfKnosEGEhWO0PIET9nDIR940EQjNNwTa/SGpEbTj5AsUqcaNvp5vh3Uf9bUhtwXUSsbSGZZ0FgAgK6gokmkgnwz/PDLWvwXibHcPdw5MxdAJsLzgWpeAm3Ueh/jZRN7NzbJr6rl0pNKTaTnKlcms/4p3HxHm6dMNmOHO9wmsXM+cxLF5MrUkCTNysi6mx8GWMpptjKlYVY1VTfG+GpD6yZlvhUz9GNb+1/bPBE6Zv1ZaVK46APlU/bEPAN+Nrb9BHUARcZwiteplncK0zyEDvDUxZxyHzFcTRcEgkvY3haPYEnIDHcGOo265J51K/b4o/GLwpdn34+VarDXaJM3hC2EAk8ZTyg5e2RrYg1vXZCZubxGJbnGtsNwzHIBcXIYUMuHq5jKpnKHI5AsJsRCOcUL9BNfpY3tgf0lURft/tBhTy3nN0f+XiQu/12moZ9wnHdDU0jyuD1zudBqzy09c0jk+S/Ok4Hlwhag1kFW7l/EhttoM0KdR+pIVdliffyZhTtGcePktNrjK5Y63qAVMtLd34q7lQd9xl78Ub8h8lyIv8JkULyAaYq9gH89RdjPdz7Ye+12kMmrgPhyESJr9gI4fWHImqtWVkhUnjepw/o5kM4pOyuEdzGeKOrkGI5dvh702TM6I7F/xmKi+Ih/EOwdbC3DH0S8R4N+kDcHM2wOBOwhDkuTugV8ibH06w+PWyy/DA9W55+EsPE+rQtx7DcxonAlNsdz80uXyhX73ZvpqzRKb35s360ZydSQy3dPFYtMHKBayr4AVZmxyCqvLYWgkvMV+flVkGkvDF2fudBuA1+SpZHnn1r8ar0uWz3Pb5sTthtipvC5Sv+iIJY4iq4nD82UwSN52dP4/TGc7E3cTAHYtCqtNxKK1lwzgU1p2NShnsSGOx6ndGibEuy/knAHyPqbhn3wMs7tn3aNfw3TxiwXzinRFtAnWrXrfAGTgN4iDxmS8YDxYSUNeQmg1hDPDYlbcu4NEDEgE25unEFLt5CrLNVQp37PwnyABcK5bRAO9B+JCWXWJZ19dXTAf5f+4cvcGvtRqBvV3XEuwmvll7jNk7oRHbp9G5DCU6w8vyDM4/rXiZpRTPNqi1m4lAKDnTtBn67G5Pg/wBbERDZ43p2eOjm8gACNj58z7K8FHwa5p0iYYdwv0vELihneTLjxEUbsVHcNtF5zG7PWnB1KH3Pqexq2BJLHCHjEAAPw2SB2dnwx5cZ0uMhHtx5hpkm6PQLdiL28DRhlRLaKwUuAy0iWyxcEH8C6+0myxI8gA/nXcUU0sE3hm7iTq4BnnJxjk1lEZ7FFXb/eCwgB1iDQ/SrU7kHvWrErXNgWv78FUQubr6bY6+1fnMzZjWINHhCwVTqmNZeTHe+Y4YO19vbPK4cA12QZQQxyyzHLffwFN9Wv+WonuGkIpBZuSDEYZgX9zcRxDTAH7GV4Sfg2Rz+aij5Krn8qvPz99zeGj4HEE+OI4kKK3xUT87e9vS6joEN9XvvW5m5/8wQtCJP6I7fMQ4Nr5q6Naf2a6l1eR+z8HmZ1TcG/JXr7k1KlR+Sx/GudE4+Ri4t1G/CBYwykgmgmNzY9vMg/1tnWez5EXeoIrDlgdaicvEdMOj5N7hAecaEruPkObiSrNsIbpza1rhCNHOMKzAuUPvK8hzwufWMjNFNyNYl5wssY2et9KxvP1riXXqZTShFng1bXozF1l6vydRcZVFoffL4LYj73e/Y1AzCiW+76qbY1i1mfseMro332Pn7f7R/b3jx/QpidNgY/1C8iJvQDnRmr5AFk9y0MAUKoxKWzOU+j9BtL030hmV4q3iDL4fgyJw5bSMk9rimI++VgSIo0eQvaDgJ1ajWzvhkfA82LkqgzJ+03QsusokWsU34tmY3XO7xjkz7VIezlsdJu3LR2TxmtzLvRpxK2dpDKHn42WkkObenZ5cnjnd3tWQ5h6DnDj99h4y4qB7tt2RZP6dcA+4yYLwIUq2Du9vG5DfM3mO1Q+/6MepwL6QfvU7fMELXN1AN6qSI3Bj7I3NyjsWa1FLr9Erp6qlg+tF1njv3UqjzBI/gzgGxREpSrwXhUpJpXCzLsnrsqNb+9mXlIwffhWzCrzvhwJUL1foCSPkjonuV62Y1yQk6TUI8s4SZmBrECjP/awGTvOU9yNTuQpNdQ2KMkuuwT9RwJZjlZ/T7IqTC1EXlyfVYvqUZhUjuTHa1OwIsBFXwWhtAlTVKVDrKXMvDKFt+qa7u2+pq4d0DoULb2wJKeLi4Xn19usmrdQ7gc1ufOPECPGnV/cp9uI9C7LjOefYy+SrIKsPhlbXCNUVipm7EtnWLjF0xwH2bkqT7zITewj4eyQJly7IIPuje3BvurOLPXpxMrCe+dUejmHjDt/O4zEIxInXDH/gL/f0Ex1l04MUmt3iRm8mP0UxHO5+5Dttmft0mdSjJ4mAaZWuwJGCIBnOY1gbfPpdseVs4sSexFEgD6b9q7aa7iqaanUl49cpXxT2XnfvS+OUuTzSyz9puTKPYU0eB9urbhK2W5fLh5SfghAUqzQrxK8U1eHcgB0cyAJ8jiy99BA91Y6FPvgIfFzP0k2wPdb1J8tloPiMxzB1YTf2XfZCd/GGa+g/giyCR6NWzJ6lu9soCY5GC3oodSZc6WkUlBGuYX4NkvIuCFEggQy9ROY7c9j2YnwEVwrSjvHyBf1ntPEH/LNfd+MgB3jkza9EtbpBf9BdGRj4TvI82iZgQ6iXg+nJ9RUOsVezytNwqrWNJbExHqGHJ/p40K1t8LjIsTe+lb50cGZEuCOEDy7Me1iTvCyLyzsMEhPpQ6Ui+eEYtthBpcokITn89yous0DugGgAWfLIRwJa4/j0JUUC3A9s66PZRf4R3AVEdCL3R4m+wnkMS8BW6Db/t4O2rpOUdFLZXarhfl/GWLsyBt6A5wJ+2u2ZC3fjt93/LKPM7kkbcWBKiiyNOTeMLu0PNhmpbZnog/ND51Et/mFT3KDp1Kel0Pmr1nEMcqMks56JtY6Fdf68T7OiXr8mjk3GK7YiIF+BV93V2Moy6f5qOfPHMOtinrefKxKW5Z39SNuKtVfjfK4gBSbPk81/w4kkPTqdX8NWL9pG6Ogih5BWIAahZcwGV0dKfL1t73/3JUoekJMY7yA1n2dUgjNufcV/NJKzT7+x2OQOpKjQ5SF4bKVdUWt9FbdHJ25HXizDh311Ee/yKLYM+d9EODoGWd/QKg/B55IHvnchpIjQA0arjQVjaVPE0fcjQtSoenm0juBrAojV3iV3LbdGyeSwItqTFJAhmtrcnVZLVb/7pl39B/pul2W6BcPurZboYF6x25/ROP8AWT50+6XvPHh2HyRbEKdbg/WmLH5voMipnVgOX/5OfJPv4LFZ8wLoOjURDHRrF/4dVg/nvD5fOgvisIyJPCQedBcsaI/a0VN1N0IemeiLXN7azwUKs3QMs2GrHeCBWqNcPWkeFcT+/B9vLvLvWCX925sbKAiMDAG35c6IVXDLHosYOKSYKi7SkXnvYGQsNRkpfh94+JGRxf7yF5P9Ev90j+2vetja3MaR2YiPQTRQBIuTb0nvKNSuemF1Vd8vo9AOKJNwl+33PEFNLYMq7yATbm5SPOY+PBWvyxhUHw+EmxBBRrd4bUMrlRlBQf0aSSD0UOq0jFDe0XUDSFsBYXG63KPBHbD0ftCHS71qtMy0aWEZJQaNhOPulryfgPDwRbKuT8QkXkjOHGK3CRy9M4jG1s5jx7+0ZtKZvzKjITPO0uEUCxpUvzbfuIB5kd8Ez+fPgKDUBAwEcganc5tm/XOKe6faibwYXDy9mUAEHI3jAUO5gT2RBeE8hoX9Y+LXC3SnK10jwGdYZsijuT6RHlGo5D7pBscnBoLzqMW68mKUFCQ01cfAK2cvYQzop95G04PAXIEsSsXJotRUE2RswdCscpKsiiArFCL8j6ZL8BLHa79HiIooiA1DgNOtZ58BG484/PQlPYr7LYJcLb+s7vKiD2DZce4FAXvNXkyNzK+VxfDwebay7kMkntJsI1WcTeycg9r4BwOg2lZ85fk/EjkFyfwCHkGsLxhgyzVuSt69fUJ9mviy3KPXdArpCIyvlpwmp96fbDYZBKjDzzoReX2IaSXvcDhCQzml3v/FJKz6HcgykPmA7TQoMGTqLS+sk+p6wM25C8LS7PC5KPa045FAMOsO3/ecn1HeU2jJk7u7KI6C47CN11LCQPlrGlrpXA6OKLrnJA31Koanc6xnHD4fcD2IHDr7WMqWMbxAFc/lF5vYB7fBfeeV22bCbau4ZFzQrYE6uR0Kkm0JFSCdkVPmQDy/UXhUV8MuGBGS8EeWlnvnHAMBG6l5/YlcQ0AO9Ds1bx0/7wLUX3fcgx3A7xhv46PgYEywDuu5CvGgC5PLPtapgP1x0qrK03j4HORlCx06Rhtty7kLl9gVunEbNCEYMPinNAsBxBH+fxLHyE5h5Qz2Oc3Z7HbW4XLh8T919PjfldvM52IXn6YbYhe2SxmVJgVcCU1k32+geEqzB9eTfZVFuyB7wWvxrMwykIQG7y/4UCxTgGGQ588hfkx5DWWENX58YD7iSTXQX+Wu2UOgZvzO0s0AbH0PD2wRU/Lw+aupLPES97Y6/UOpvINTRoQbdATe2GOPs3a5WSAmEstzyWTHoRDKhSg5Tza8uIbWO2T3CAell8wdaBwY4rdy97FaNVZOJu6fCPUgfgRJukPJPbrnHs79+XpdopcqvhT4OvzlMewnczeq+TuphVBkotdOQf5wRC6XJNkGKfmo1pY28esySdQD+bY4fA3C+ygB6Pe6BWIV1oaAaGQ6k2P0iwMrGvYStAm8UwNYf4oSrCtYTV0DxAM67/Xv9ldlGAKwcZQp/jzL2CeLRkdK+HOLLjuvADzhW6XEaSCp+JEMKy9GEvIYRKPC0Up/5Q6HezRyDUmT8+c94omhF+UmymzlF2ggCDHz0NLur46c3xR6/waeCyh813Vzy1QVUZpZBu6B/Lm/TCiBYqJq12SZilrRqBhlifocbeDqsjo4lAkUiPWO6eFG/O8lKMHmHJ714pOigN0cy/sIgnCTTGJEY+vgXXCPQjYZ8sm1UeCroHvq4Uy2TR8vcHCbMfEX/grFAV9s2wJGRNeTajhkbWv1UTNZ7sewyF1sS5+ylBN2ylLNuUmdg7wG+/hFD66S7nTmGuJpGLoGuSpv/xuEzKsSa+mHbszc3Je5tPOu4Gq4ySLLh3QQiJsTINYdwhDlMTWI6kq1tttKQbJB6XmDOH7xoCmRmB6D7Kw3Cdn+aBD7rb44Gd7UhyK18e/yneN7lWYiU4TanUwOZ0GTWEVtL88H32eZAK4OPqvcKqw3ChBe5a+Ekg9zIHEl4zLORrDbB9H2KJIh8daMoWcgf7s0A6a0R45/Gerwxrgerpv0JNRYFBohZrGZsslud6ysbH30Wo5O/TXdABxSybml80uQFxX0DAwLC0VtvdJ8qqdIAygbvHWcve7u4k2BREmeUK4ZKcwcm31rOHwvdXhUSlrawvrgENavmrCUZfc38JR/AUgcQjYmAicegQjnUi5PQePmBKJkxH/KYwa56rC5rn0khNk6TJRmjoRwmW/HrfmA4yPiekxNklf9TLOHKNl6fX71Kc3K3bEtVW8LdPJoMng6beNS7aNw/iHMrrLoEYJsXRqOgGvt09og0705j9Ktrfj0JjVHg2xrZx9VULT/bOAOj4L0ujEAoPi8gWUc3TrYwenLSVnc23kSEMCuQRjtmaRyM5QTX0GQlxmoooYeg5gYNCWYxKjyGwHrGk2F71cevsKv1gz2EfJskh+JAvXKY+Py2MVun2Yo8PtddBzvy70w2KcUZdvwAzqKHWSS7+bdFo71+5yK464BMsBvPgZFYGVXe4j2VkeS4AHYtK/egV0mdlY9uCQ+RSDeoF/uH4A1HHSWJnfRtswo7zcrc+35c5EF5BRaPnSOy13SPnByAPEa5GVcXCR3zLWCEbja6xxiZ/VEpW3v48Hx6iUJj/2NmMpUdMO0Pn2poDhNobhKyywEphFvaPQqWHL06IiOhuPI38Bsh5OTd1GVVG7KRssNFcdTeC780MrJ4ahK61D6RyPFF3fxOcjl3sp/NnyDfmH1dqiCsSoyV+K+AniapiLXASUhD+fIs+Hg/BlpQB/BPk6PJkdCfcIwStx9J3Vsn07rd/FY3a2G0vGUvaqv5E5nEmlGxZvOAK5CBCRtmCgE8E0WJPkuwu/Z7EeVB9HK7g7XRmXeGHyTYjCmq/K2Otq6Bqzh52Iybxi8khHeFHUX/iVoJUSP4CsR28bQAVTLhVRzn3q1DY1iGzJ5e48WP/qrptz56ldOPa29B2TpYwRHxWdy64u8losN/1q4lNlv9NVacmX1ms4QwWVCyHyudHj4EwltVxaqhtEq25czU5Vjte3y7i4HVm8fsKuiDYDToAjvV9G/rJSRK7iiqyijc3DfPEt3e3ynrKdsaJ868VXzP6L9CQRn6bt6FoMg6SIvO9zLT4Pw4SKBsxM+HJ1Plr13y1WVftjo9Fo1XHgqsTHcKSDn3wVhUWYge32gYRvbPAJPugfCOVxHNym1XxngSBmgFpiv83+k81+fkGo2OIbpb1WE9040lA82UHxL9yzN8xWI49fpHWF6NfwMk7LO1XQYYy4zKymFBkrKdQdj3ViS0HensTNJwIoPmvSQNIkv1229bs/JP6PNFhT/SJNByPpRAaNiIMq6iY2SG7xcdWba1iZJBB1FPJe9X1WlowfD/C0E+6RfmckJAO4cpRBoFBD9oAQfJsogFBpqZZelI8ggymg8DbuHLdckEKPAmJWtV/cWRzmMgBNbrxdZhXM5fwSEWc+xLARZfV+NfjMmVDPTvI18rfjFh4zVQYAEYoDDKdhGCZVqz+gy6D59On/eBxBNqxuqb+mnNI7TJysY4OlnlGzsgFyEw4GDtRdI8TKw6vTTOCM8z9KYjcltiyxKIMUmj7KFepbnZ3EwkEDdAK4DLQMvpuk1DYyGH20DSgp8o9XYN030jQbGmlU8uPWbvjj1hxUVqjM98Ug0tbt0IPHXvPbomvrJ6lXdqtR3iiaT2YBQncy6K6O5pPoyuz+ynUsSfT0MupY+ZrK2qzRW52BrMpd1a9Wp7Dozmk2yN6PLSNu57OFvhEPV2MeMtqGw8R+GM9oCUZ3TpjOjGaV709VFu7ZWs0pRYICDvxlt9hPygniM/ZPsb6l7KEOD5szSzX3ObnMlMMLEiuX1IuZ05A1AeyZR+rsVKKryMeaT6nCps8oSoYdHv70XvbdWq61nWHI08TTDox8mljnDVwF+PfQVFPfpxnqaKWjjzTXbraaY7LU/+Flf3Uf4UaqjaafBjTfvnH41/eL7AA5+5pvz/XUaA3upTgAbUbL3ezWzcNTND37Gr9I4/pGivOGT3nwqjIatmgwJPUnyJxO3abKtj0lAAV7P0h1+/H2o41/Tx73vNIwPUwFUiYGolO0MToLZjXXX0gdznMbp9liYw9VcojG7SnODFxpdSx9ziaPmIvBQ5IKDnc2WSoPIr11TKxTw+Lp/NlVbTK7BYwSePoN4f1fGieGVzyKmkiLY2MLUNLdC5WeQ1yPue2IPfTanerfmbu+v5snZdoFCdnYPlo3ieFSc+V8gv4GjGDsA9S3VhCTi9ZM8T8MIz2zjopGi6KNVpKVrkOOgUOsm+H+P+c+TzRt0FkIJbuoKNUYrEN/9qfv4tYyLaB9HIUTh97fv3/aXzGVShXR+c4KfcyMTXB4GG3Y4IBkbIQ4czGl8uBVo3P4X0yVcxgD5GkZBfJYmeZEFcLjZNR8lYbQP4v549CoqigdEaQuyX/IR7EGCvD5ldKv0S6Z0YPtvu+nNwNB4/PaOYCoFXov+hZ/wYNwWxGgk2iyX0aWHwWIUTYvgL8pe0/mlop91aL2+ctZNMtmWml+6YBSG46JOISWo4YXxqAEYgfn4tKl0TGI6GRO2l34nezgTyGmxviSKQL5ecaQ1MfFNW2quu4+jMB/jjsjDpSv0wnItySOwm9z9UtCn/Bp6cja7CbIt6FuICFnHm1rRlB4wm2lN+RRsJr8LH4PNKh8yHKasfoMj5CqiKjWZ1Hd6Hn/505/eM1PJhYggqUJ1xB1kDyMwSJ9YlS7rV1HTMgdyMOQN0wisEfV0NeLr4bBEpKh2VXjNgBnwX+ilhuxgR1Vi5rAtWApTcMmdA1eQ2E3GG7XJsMciLA3mc+nrJNZDnD6EMYUehY7qfNsevvo0KfNYk9t3EgZbFVBTukKPfJMQrM+QosU8pCL037qcUn+bb6PwFYUvhUevxI/eyxsfP/xEk6Ok81bITS2rGia6vLvMom2UuGCmge2rcXPnyJjlMwLXh1/OAs3QT84KcMFHjyB7uUEhDYUHWrISdZilCmbOEWJSp2cLErepeeK0TDZwvyvA7qQosui2LEAV7HjdlQhZhYHBm2CydExdR0iZHEmmshf2ZMdmPB4V06qCQ9d6Pqxb8yr/eZWRFBqLNfUWjFvmm4TlNHiMcOyYCZs5ZbDB3fKVS+SdzoQ/Wtu2IO7FvK4rekjzOOuALiv6FCkp7XXl2fDVYra1CZlqfGGlxVMzEVXUU3dJqAHCeklWogyYVMGoNkwRStxyP5ZM4dB5ZTntbskGs+K9xYi0GTDb+KJNm8+mFm9N/CSH9vIBg1YTFYuE1H5buomTG/JL0NXUVu5m6iX2efvritkxg/5txIgsQSE3uQ/tuk2KNeS02LTges2OtO30o+7xUPEsZxqCR+AqaYxBQZfyoIITcdaAn6KGgDhYvtKZ4wnYShx7cmyuOo1ilLWSH3fTiKcGti8tubdcblCzPFODPz03QDgFhLjmEzHTzatGmotKW3Ywm1dDkc7mNRlfNZEAlvFgrcGWwqH7uPjnaS0pKn1N/iqt5R3nTo8DG9QUbDCqs6IWH0zvpNgywlWGn6y/8sOU/NCfhMnYog1z4lxbEbNC2ycFifi6eDWjo2URCgZG9ybdR+HYXIA7Zdmg/nwYfFARsxxGqEIfVel/pJNHZQjqJq/+PIqyiTtkUfDEOqKB8cg8io+vGrzmIEI4eOvPmE9+mUDoqM/luBIHV5+B8iFC22bqfLLQ6NqL5ryOqr9gtCZjoibm5TKsIw22FA7dx8VbR1pSFmEdaXln7NPwFGww6mlYiw+mPwa3jDCRdeSVH2ZpHUHhtpexryBMaV83/GHx+wkmYxF7SReafc0irTtTHrmkwpHBoPnsxx1SdRYdcEtNiJKcgdUnvy1GmNdx5pZ1Y0wgLoxZOE5wwDFvjkmqFmHTk/EZ+jbIa7jSxEEp58dz7NDNk+9Qu8l47/y5AFkSxCdlcY8ghriwyu+yDGEno4DCS15x8eJPSt4i5CAOF+/BUU92rMN9UkDqL0v3z6vIUOmIHvCJZ38RImdcphlTiKhzzeTSoomY+j2HZ8rPEcQme1nzIwPPNIQyiTkXH7rCwYRSpshS6bdpOC9e46wVfRlxBCymLlOm4i+M4bQbHzZtz5mjWixZDDxawEfjnY6QJTHM/B98T8M2Iz7u1mOcqZ90/xHdFWcBPNpflSgFVg42P6PiXkCD+TQO3KU1WFDAuo/+JMlYIeFaWpR4gjsVk7MIpevwCTKdUk+yhoc5F58RlB4tDnDEbdp6T9NwXrz2nVwKM9WG5sRqo+lIxnxGzei0atO3tADz17O/ESlpOwyqr8vmoY6Q+evZ1+AJcvtVCgHkjXBahH2SgziFDrd88bZLHlWLsGTy+MzpJjigjs+HXUYTQ6a8Qk3LdHEMVvfRfocCGMx5J2uQpANCtR+XzUAtHfPfxhpUscWIj7fprHnmHMbgQBf4iQGmM7GOmEjZLNE2mPaSFqGxHzWYzus1/dveoE/GAj+DGC7wRenDFMoUIr2SxevAND2L0H5pfhpD352WHUZTUfR5YerjdlFmyTX4Zwk8BLvwdNAmUO6dmaiSxQsWmp5FCJY29N/46spr7D+h3jLloTnFiJwFWUGkN/KZimuATfoY9c46/cLDSZnF0KbS5wxSZDEstIhdag5sNuZeZcRdk29XDG/N37lnDow1oquPEV9N7fFT97/agzC6qx+FtAkKF8BjUvx5eIlqLp375OQtgRX5qF/u0c+1Cl2k6BnmBzVG8CUaJbQqYNlU9GPiNmchWwkqI1bpMMEFMFPhqk6tb8Y5StlszWyTC2wZBVPzfKM63ATbwRQws9EiOpx5uJClB6ItECRpaAjT2Wh4zDWQBUY+p5zJPAYWU53wKbkMVp+c0X4EWRQkRStbz9LdbZTgimvIE9EjyF5uop1YtFGVqAd2VIGWWXAYNx5rSav7eTsqHCCv3CYjVAULEu05899izuazZ9Tx911bHp36qK7Ann8vYQmcpe9JJOZRqhLJC3TBQYpH8QDNmvVItOfGf8uViSosebDSb0kir3fMzlegPY0MWyWZipxZH9sSKaZLih1ZzSdbjmp/lBCpwaKzsTv2beiq5PnkjlG5WAHHkbhXk5O88LERD3eIT83Nos1+0WJ3fnrAFALXRiGYjbTtE/EjiMuWSeUU+uGLcTkXk6uCZl3RJw8b8ZMfVq6o1eDnPoCp2Xryg7zKYW3ZR/LlnLs/pk9JnAab0dOtNB3TNvT24/LTa7S0qPQ1g7watXBYBbt9DPjIm07f7MSC1tyMKBLowZ+MF24ikEH6QuAxKj3mCS/qTIs9hQvx9SCC0Hf0qHQ2i9DzHVvN36o7DRONaLnV45+Z2GrbbXIBHNRHmYeKUHlaHj8xFC2BrTp+UvFq01SAPfFV2x/+o+92xCldvirNEqWlU0/ndNTjrwHHNvGEjufUNiPuGsmlzYK5JnVoqzDIgvDB/dndJ3NhlDksXn0+EGFVUbOMk//XICnvgrAoM5CNbgYiO6cBUgUHwBYkPctgjCa9wvpkD4cebG5SkohBdWlOmVeEbMavcDCZV7S5rp3z2XLdkBJlIlCOgPu0OWEqDqS2oxlxYX1WXZTY41lBmLKDEXY61o45yrmWw9QeQE1o25qev0a0bJmwV3ObNj13XZcxWIFlia0a53N8Pc5FqFfjYEQYTdfCBFnLagPySzy90mk9CqbTZ4DJOI9qMR37nQUF2KJIomObLpqO6Svv9uPyTRYtLcswV7SMoLrlaU2gJ+nTdCeWPoIafpwYdGbcEXfpC552omfEaova8ubFdKNveeacN5Mt7yp4QUkBvoLiPt0oizuqFX14pEvGcY8g+xQzoqyan4MlNRYjcKOEQqWTJsULc2PJRYnFubLk6ALSkiVnIiURGjUONUYL4cMOcQaVw+IzgiINvpo0rxIKB6u579LNmPRGZNFoyZa6TsWsL63nLRUTMRwjcKCMRpXueywxO9Zc1P47X8YcXTLa8uVMtmDy2YG6WcbkOYmvkzLRpeS0LK51EG9NxPQpmWtIJpgZKy5KPs6TGcc331hx40wk4xXIdlGeww+IjK8BlvZr4isI00z8/JKpSB1QmcJxjs88kkSIkTX82HGkQ+mHM/n0KVlxethOJybvQfiQlv2gF8xn8e7N1KSEE1s6jujkkiVHzWdYi4Hx9MOeAgqV9vB+0wmTBoZlBine1kaq9UUSIbiOEmOLr3v7Hfey//ULl51KkqFHqU9iJmbDH02eUTlFrubZW9pJLlFS1PwmNTVjELdMqZPptN92Mv5EbPAIZ+JLul0Tf6OJFLJkvx6Vr7JfNgpDEr2KsPGV9VI2Zn74jiRKpbseirNgtUXkLZyOq8bMVqjLTpMnKnTPP/6Cv/RZ5zBYZjGscnJ3F8XwC1g7zL0s08rbDmlA3delp17uSFHaeirEJtxy4sqA4TFylLfNJ+bY0oivB2HK7+jRkSeo7XR3m1DJRwF/ol2QvZw/h/dBsgXXcEWcQY0eJOGLhL3qCjRrNR/VpQxGgb6DrL544gkeXX74oaJDyUQpnoB5sAb+8coTE/AENfLTMUN4DzZlDG6C/KGx+pDfxA46ZCVqQqmCcXwgWCKEKPm18IiHzhPXcShT6ZZsNxnv/b0EJdic74IoPimKILzHpumxXyRxsegFb+bWWP5bJT5hKh1P/3BJwjxIsgbbAf5pKjEs1BaMdfqeIQNyR3CWPFhjOgc2XFdEhJz3z8RsU5XISaYLRtk3CeRF/OaJy8RD5Z3LlLoj8ZsDbxEiTkiK3cQehXDTZYPJZBvRcjL2+5Rm5W5V3uZhFlWZs1zfh4iVMqZvCiKndPHWapYmHVvThDGmo8egAF9BnsNteP0pS3fjcQndeS+0D120eP7oEaTSIzkZc2GQm/SVPWbCHt1UTMYcF7t9mqEYUXco3LNPu5OYSygcKIi9koMwGNE0aZiKomQ7qbHo/Hl6VqFwoA9vz4fHKjRNi2MV2FGcVt6uXBrs59XTkamPPgchstDP+Vx77p0wG0GXSp81fpMx2mkQPlwkqyINH7yek7ywmQB5CiVhncXrSyLKFnHoEvHd/PP7zI/pRoyJasNz0yf9ScqLAuxqRyX0U3K/kpQcryDy80iZM0icGVzIIk9XKS3BI3BXjySVHnGTyQwDaRz/SAuoLNY8haanA4pNSe1OCr8XSJO7Sdf9doObbN2Wu9e2ZRpWhX7/lDRlChXuoq02y4aCMWwM0pHX6nMK/kIfTpL8SaKXEVX6s9p8HudVsA2PudoYBcM1I97qUJyMy76Bp/ws3eFjpqIAI5qMLbvIrkmQ1PcDkljCodbqbmQ2Qn+jjVzIHG2F/hRWH0eRUAac5Egu8YdnHvzT4DbdyTBOt5riiGgytjgiu6ZOgOT3AxJHwqHW6m5kNkJ/X6W5mDnaCv0prD6OY1fQ5yRH4og/PPPgnwa3aV1IEAZQtQOqAolqNLZIojtnnE66kgMSS5IB1+pwdIZqfwm5pKvBncqRpJMhUzmSUIJRmgsvtS0mdFHBttVr8BiBp88g3t+VcQLyXNkiJWg/umVKhAfH3s+pdEASTW1GtPqekg+pgqHbnbqWcM4n4yi3Fza8wZghM1GN58FZRmJtUlmmzsyHI7UWJKoWcO9syE7Lu2PW56Fxb5bPcQhZtKhgC5A1HhXpBnyKsrz4GBTBbZCzxz7UagWK1nV1H4Vv31SficmsvyM/tF3w+9vNbQrnOriNuyaMwOkBDp67XGYseLKU2wlZYaAr+FeBI7ky3bQlvC7awgHwX9IwiKN/gU0z45yOOHV4XXKqDXUeJNsSO1azfbZF3K7aUhXywKrI8I1RXgVHF1DIVBMSydQcwIINGsxgwFbh9c7WUu85JaICy7on6w3gQFYdQIQOOsP0TxfzuqVrDPTWi83NdNcr5/XXpLUZJEwsB+RCQFkCdImCmR66Il4PXelAD1QeebYXupjXE11jqLfuBT7bVVfG7acrVuokjTd8ishScUd1BZWu2ufg/L7aYmFnbY2hFV3tw7JBZKtw13GdRruttlZdy61OJupZ2uHwikI8y19QTQl3PTWFQxtqARcexOURqkq80euVczdXusrQ+gVx9Aiyl5tox+uPLuauY6qG2uSclskmBtVNr2iayDqSCSOr6XbexrSGDFZwl+JwEyXUmFZqmEqkN1NDtoi6RMl6a2hA8nJrSRczUVMZF+b8Ldye2xqyHbqtpNitpD95R4M9tDEPmQ7aEh78tnAAfHd5ycDvingddKUDPRA3EEwXRBmvD1ycr9W7ER2WyEJJR2oHJ1xZ1IkUfl1DBf4fWVruRZ3UhZKe6hoDPXWONEw/XRGvF1Q6LB4IV0JWKhCFXGFAlCv0I+hBDFtL2RefObi1hlR/VZl2/gx3gSSIT8riHhkgQmxiEB6/5NV5SMlbDGBXJ0hg0Ki/8/qrixTPCt/R8942hpnw3EBXk50h6JoDWPwR3cF9M9sMYMGvxsOCX1MRC0nP8t7UplEgBIgy4XQq6VS45jd878vvpCoTdlIVDx2YwRMk+CqNkiIXTxi3FvfwzKs4pJffR/sdX/3oirjaeFuq2INgxuhiWU9K8/YziGNQiAezV87rr1dlcA6h2gcl0D9LwFUYeuX8eaOqDI5nioUxXCviMe1X4Y9rv5aa9rzagzC6q0UvkfZIpEeL6ss0an4bZd2a3/yyfqrGjpe0OnfspC2MsFPGSwcj1Tm9Cbj2SaJQMlu4XK2fH0EWBUmXdOks3d1GSSCYF5VGEryk7Qbw/XsZ4C/fk4i3quliHg50DbPRUR8S+Wqq/9dfR/2GYoTUMNHmy97SyrFteBAHsrIKNmR9I7yUcdLBx5Rr6ixvqqxTV9dYR3WLIaU4fUriNOApgl0RV/ltS4fuAyOQXWUR155IlHHvArtiFTszXzx2RUL7soporCpmQfgg7KMqFPdSlQ+xS5UnqsnMzvIHXc5lCLqKwn3QuewyqCkU3QQp6ZZktnmudklWEOmXZJ2hYzab8JE9ZLN1uEdstppu56LFLqqohIbaAhemZ+PowIKafG1YUFkTHQU81BAYNniSyZxYoydZyjV8khXUu0JdyLuragx0WVUavNDnUSakSImSLqENS0NXxsW+Kx4crlhofCLK+EMUK5qVvmfiTogyXidE8ZBZBSQA7lcyocNW4ZpZmFpDuyyEAbAKcMu99+yVc3dbusrgVSFOOsHK8uo7/2oQFw0aSJucGByraFPEN4U2pSqoi22vdLGQEOVTLi9zAtsprxa3b15FDRQG+h7uVOFQxo1GzDmdcevxj2ncquqIyLsf7HTYpE5FHmdN6FQx12RO1Rj0q9ntg2jLO9B0RXy/mqZ0UNvFQTBvwG4f86U/U4Ov9/YqKVwcfQFFATI6bBHTu6ii6FKJV1flumwADU4dyeWZVvf98KqcgyJdQXbR2as6OPlBXmbgJ4i29zxu7pXzJ56qotbhxwgu65w/0mwVSbdErYGee/FJmW575bw+e1WGRPFLEkq2HrKUK4jJCoPXfP04h6xcYqrwr/P6tZR6Fo9qr1zcp+qoCuOdMV0La3J9EUSVNXwGZSKUW23Ih1BZmDZ+NxIM2Coyb0zlnhuLs7hjpobMcK3a7TVA1TZiF7x+Bf65kq4zNMhZmucQeCzula3CHWSm1qDQ7GLBccRlV8gXlF25Qj9knDduX2QFUX9kHcWreokLGltFdkW/Ptnv4whsbtK6fgSGHU0a3wqxqxRTQ+aiUVdS6LhmAZVbH8XLn3VXT/laoXV+HTKu0nWkvslrHQSayRtwBORXU+MGss3wvDTtxWudqaGGRl1dHYMhJ35BPYkz/7rFStk1sl55Q7gI6slw0XcZJU3eQwjJKsuwotqp33JThush3KS1ZcjRDbX9W5H/2OBEiutKJ5Nopj6hTIQ+dhqZKiKnPLqWwhm0jXfDPXi2paLTZltBwUlW3BVVKnKVVe2qFyWDf5LtyoWHya6K2h7GedIu2so4VSU7Gqe2DkZDaCj0zXRIvNckjfLso7h190aPaNPa62UNqB7fkO9z+w8GIQrix4B0S8lDwAqKysO+dzT5qkNDPoocHhd+bW+Dwnv82Y2I9D2n/nBQcpL7kjDnjYtKMzGZvLeHmELZm0IKgvQZJQal9ipSf8Bkyv16VXMpO2AqzcTk9l8cYgpFrwm5LfuHGgqC8OWj4+G5CbItcqTRHJ66mYSfeMSJiJp0eIhnjFCzy/Amyo4Gp5aYBM7DSoy95MWkqD3qXBWGMfHoQNaAFJFO1PFNeNQTQeIXrTYE0y9KZWTL8vj2ke+9ce3wF7xdHZn8/rNXIkU4ZwgktZ0RQe8hgoe71fbBFFoPB/VIdd2+jmVHgl9RIv7o97mV9OM/vqXacR/e4tby97TGnNDQcnl3mUXbKJGwAVPVPfk9gwo57Q5Jpt8Ki+ml6kk2LM7b5WqzkrxJHp9s4TvgNfl4WTgaSs0HSWSfXJPEil9Ry2EJXlDzQQ+9gnYwwPWQtFOoMKi9Jk54xeUEOBkWrQE5wKFozZmd+iocCbauD2Vb8MSfHAiHqnaftGF+YKp644Zxh6AXg4C8sBCrYcI2EmWMc2VS6WOSaDRclUwEiFvuZZiGuYVb3RvHjDwk9bWhTEvtV3GvoPWuQHE7USgIcxJ7uq6YULqic7V6RNIbM9u6vR2WWK7ElYcNfFzjnpRwwfU3BcDrQIhtVOLKTuZxDsNwGsUoC3ALWTIIvar+hkCJhyyIbiLndqDFVDN1fS6BXvQfCoAopI/+MDShb6TXIGwlH7cf/fA9uKUwNI8FqVI7FKeWH/vTaOReZfjqUI1qpvIyiW/vjaXrmlPLx4pmokbhpsRXN+TiiE/D9PaqeSOYil/VUcyPS2VI8poMISWgl6wzgDIVqKpDmRuAim3NNnQ7szVI6bRWdawRnWY28UokwcpWLFHPIeojr9smMpl092Ur+dh9+/HTcMvuoztSpfsQp5afDWg0clV2X3HlZRKP/N+kLE1X8MHOZCC9ylDCC5RnRloVy29dQeQTR1axRJJpQcchbNsJIgza+QTVjjNKpyNufZ8nJJHvj0p8QrfDUjks6Q0NbqNGpJmT1DTDJIvDKGUjtYY++EklNiWGphVjUn/ocHhABSMMt557CwwVFBO34se8NCVUxgy9Gj5m3R95vBid685dT+z+xm8g8Qaw9gvkRQSloEgjfDoamnrcFcelqm09r7MYjjbqqHgM+lXcEs5ETO3aOdIFO/wld2psJfc3af5JbeLUrq/KLLwPcrD5GRX3RA8s4UNNnJFDte1H4sVNhVF2zQeCWrcdfPEw8Bs4IoTbUrjilUIcOxqa7+S8K48P3cqtXJhqeNoIyQMCkajiQSCS0Z27dtzAzfokcmIxS1UhaX0fipEk9jQGohJM2s2wDK6L4UZuuWOaoWnCXIvXRK+GW6L7gcBxM2GQb3Py8P7fwRVTSVd0hDi3JbO/SiOOmx6Emme0g0e+fsUlnfmo2OlSgSeo6UPUcWPC4+byUO+25AsXMq+a29U8HslU8PqBLY5b08/mxgnKX8tuWax9C68aheUtrOt+hfv2q+nnDhjw+JdVHzwDOvHwF2VNqOX9QCYEBwMkWxniyj4Wx+RDITERCOu6txSMPQxXsmwYCq7IAw3dD5C0Xx40xRQUBhwkybYhSBXCZS4DMBL+GR4XtQFRgEpnGZDAFmQOcM2t8s51WVgKbeShmukKYINtyby5JbW9iQk2pBgJgSz1Mhhij25J7UGiBNRwyJjHkEgy3qi+UVUGIblncfV8VSFbEDmiOvl+vAzx8Cau0trbEl3acNIplIzGlAIhHiBetic8FLIsTksdYgM2HZ83VUbL3RBxMkExSbMkw6XUfHAIpLSraY9ifKQwJfmvXA0pMx5kn6oDKwMy4qAoQFeA7H+Bq7DucNsR+XZaIcBNc7YWdqs8pHI4owyKClwqk5QMOD87lLkar7jNj76Xq2xlJs5RdXo7uYs8p5YYYRsv8X6avkpRF6XgM5/lVbDbx6ADLJ7nXk1HqI84w21ywUHPYUHNYauvub8wkzQRNxfnQ7QhX6JospXcq5b+SW3iMrThzBQiSjB1venUowU9o4KwywxT/Ip+BJswyDwNQ5im03oYxCYpfsUBUrgUDFmiJhoEnJBUvsFx63nkBCrBKjGA3NSpBmRToXSkhPNreiLdJFqPk4iwVP4DrZi53JYSLcDaPV44SPwK3gdLJ4Iut6VjfpjpoNUbmR5z9Rv55Cve9s2U+RwYHUbqN3KvlUw0LHWyCCU26df1yR3cnBgUmF4ND0OiwCD9umLCxARJCZnJ4HSJYWSbN6eWn41bKwCjDbkKy0NY1xH63JZiDhDU8DAkkjBhorp+l8d0g0NnKFJgGnkDye5CtqP3GLpEdv4dzNnEAhxpwCR+CtIGfvlqDgOG4FXAGuK5TsRsLb9D03XIAHBGei/DlsLqGmghJoduyLx6IIsGXk4MZB7jgHTNMaJBEK+xgRZ+OWkeg0Yl9VHZ/GX1fRqqB/PHMeDGGarh2KHc+p4Vg8kHi5uKicwaVcfP4Gx8ai0luxfTCblnMYWyfXCyjFNn9yB8SMv+rTHzmct3qm0l/MP2Q/INWyrjRS46coCu7pThHJcZSiZXqyrriyQqoiCWvC2VNRATafaulO6r9zCpX+h8MJoHZ2xXw+MibOuJXCkk/qNcQR3rYTwJi+gRdvMl3a6Jv1F+R24kcUl1MYkMYII0pkwyVERdEQzHQyJ72MStN7yL6b9pGofsIXK9k9knz9Fs3t1FMfwI1pLHi2wlyWwYvlps+6Bbdl8dMG7cizUmcs0Q1PSp8bZd9lg4dhZ7bAVFInI8iHZB9nL+HN4HyRZcw6E9g7ITJKEgU9lQI9mgNHCpAWk+yk5LqFf6XFR9cTsI+Icy9XTthZEd3oMNVPpvgvyh2c3Jb1zih9pICKFAk/RQBbKhYDsXAnK32/+9BCXYnO+CKIbaaRDeY61Qeikw1MTPDQG3157DI7eGzyFaibNfKrQaGKgWeH+s2oK5D9e6gh4KYwQK64oJo0ES9NAFakMjGhBvXEP1oco0ZCOnZM2EY3Dw8VV5m4dZVD18GIz3z6/tQwdluqNAcEodeFlGj1Dd+ArVR7jK15+ydCcbD1l1HwNC99dzi6CLXA/FTaoxEETlpQ/DxW6fZshZ5A45UQ5pMZLaPvUXqlsKRK/EejjOn3WGQ1Lb53BQ3dIb1bOP4fgI9nFa2fRa0KLB4NR1TggHRtctBwxZaD0kp0H4cJFAPSl8UN5XBtv4kCKCTilAwjrehkni1j/URCYwzfzoph2iryApUUSc2jiCfvLdu9lqMvU+KTmmD/Kz1L+b7IuBQBbZb8BpHP9IiyjZ1pQhi+xZmuRFFkRQfCBto10hTSa5m3TdbydKKeEE9vDCNEg7R/MuQw7JxEyh+2Enc2EojCRRXU6UVtqNSYcEZXI5S3d401JkFKKJKEGNBUT/XEfhT8Cgvjsd2C5djny42npy5JXT8UxANsrLpzn7RBNRukELiP75icKf3D3J704Htkt+KB+utp4ceeXkihOQ3WZQ+4GieCvOP9VImhrOHKp/vupR0TePdCWOB5lIWjc0cl1VBTL4JMxkCGqV+RrnVPsM4v1dGScolbeq9iRoL3nD676vETQ2IZns8YNTyd80UQVag063VCRdTO9yhs+I3QaH1gzqyJyrPn3Wgzz8lr9X0f1R3z3pv72rgKCBh7MMsrbst3fIlLcL6g/wZ3Wr9jXdgDjHX397d13C1jtQ/foI8mjbgfgNwkxAiPrsgDZ1LpI79MRwDzJMAIlRU6Upbg0NRbAJiuAkKyL0jhQWh3AtYXdL7C6IbrhuweYiuSyLfVlAksHuNqbugH97J+//t3cMzr/VcZ9ckADRjCAJ4DI5LaN40+L9KYjz3tYnAnEGR/8PAL9XcwmXZgG2Ly2kb2miCKgevo9gD5INXHI3YLePIbD8MlkFj8AEt+85+AK2QfgCvz9G2PtRBGR4Iuhh/+1jFGyzYJfXMLr28Cfk4c3u+f/8f+hv8bTS3AoA + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.Designer.cs new file mode 100644 index 0000000000..9387aac6ac --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class MediaFileIndexReorg : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(MediaFileIndexReorg)); + + string IMigrationMetadata.Id + { + get { return "202007160058551_MediaFileIndexReorg"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.cs b/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.cs new file mode 100644 index 0000000000..66ff8fec6f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.cs @@ -0,0 +1,49 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class MediaFileIndexReorg : DbMigration + { + public override void Up() + { + DropIndex("dbo.MediaFile", new[] { "FolderId" }); + DropIndex("dbo.MediaFile", new[] { "Name" }); + DropIndex("dbo.MediaFile", new[] { "Alt" }); + DropIndex("dbo.MediaFile", new[] { "Extension" }); + DropIndex("dbo.MediaFile", new[] { "MimeType" }); + DropIndex("dbo.MediaFile", new[] { "MediaType" }); + DropIndex("dbo.MediaFile", new[] { "Size" }); + DropIndex("dbo.MediaFile", new[] { "PixelSize" }); + DropIndex("dbo.MediaFile", "IX_CreatedOn_IsTransient"); + DropIndex("dbo.MediaFile", new[] { "Deleted" }); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "Extension", "PixelSize", "Deleted" }, name: "IX_Media_Extension"); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "Deleted" }, name: "IX_Media_FolderId"); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "MediaType", "Extension", "PixelSize", "Deleted" }, name: "IX_Media_MediaType"); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "Name", "Deleted" }, name: "IX_Media_Name"); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "PixelSize", "Deleted" }, name: "IX_Media_PixelSize"); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "Size", "Deleted" }, name: "IX_Media_Size"); + CreateIndex("dbo.MediaFile", new[] { "FolderId", "Deleted" }, name: "IX_Media_UpdatedOnUtc"); + } + + public override void Down() + { + DropIndex("dbo.MediaFile", "IX_Media_UpdatedOnUtc"); + DropIndex("dbo.MediaFile", "IX_Media_Size"); + DropIndex("dbo.MediaFile", "IX_Media_PixelSize"); + DropIndex("dbo.MediaFile", "IX_Media_Name"); + DropIndex("dbo.MediaFile", "IX_Media_MediaType"); + DropIndex("dbo.MediaFile", "IX_Media_FolderId"); + DropIndex("dbo.MediaFile", "IX_Media_Extension"); + CreateIndex("dbo.MediaFile", "Deleted"); + CreateIndex("dbo.MediaFile", new[] { "CreatedOnUtc", "IsTransient" }, name: "IX_CreatedOn_IsTransient"); + CreateIndex("dbo.MediaFile", "PixelSize"); + CreateIndex("dbo.MediaFile", "Size"); + CreateIndex("dbo.MediaFile", "MediaType"); + CreateIndex("dbo.MediaFile", "MimeType"); + CreateIndex("dbo.MediaFile", "Extension"); + CreateIndex("dbo.MediaFile", "Alt"); + CreateIndex("dbo.MediaFile", "Name"); + CreateIndex("dbo.MediaFile", "FolderId"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.resx b/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.resx new file mode 100644 index 0000000000..4145116cbc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007160058551_MediaFileIndexReorg.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.Designer.cs new file mode 100644 index 0000000000..94d37bbc96 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class V401Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V401Resources)); + + string IMigrationMetadata.Id + { + get { return "202007241131557_V401Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.cs b/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.cs new file mode 100644 index 0000000000..049b0f2f23 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.cs @@ -0,0 +1,59 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V401Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CartProductCount", "Number of products", "Anzahl der Produkte"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Weekday", "Weekday", "Wochentag"); + + builder.AddOrUpdate("ShoppingCart.QuantityExceedsStock") + .Value("de", "Die Bestellmenge bersteigt den Lagerbestand. Es knnen maximal {0} bestellt werden."); + + builder.AddOrUpdate("Account.CustomerOrders.RecurringOrders.ViewInitialOrder", + "Order Details (ID - {0})", + "Bestelldetails (ID - {0})"); + + builder.Delete( + "Account.CustomerOrders.RecurringOrders.InitialOrder", + "Admin.System.Warnings.NoCustomerRolesDefined", + "Admin.Configuration.ACL.Permission"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Search.IndexingStatus.Unavailable", + "Unavailable", + "Nicht vorhanden"); + builder.AddOrUpdate("Enums.SmartStore.Core.Search.IndexingStatus.Idle", + "Idle", + "Bereit"); + builder.AddOrUpdate("Enums.SmartStore.Core.Search.IndexingStatus.Rebuilding", + "Rebuilding", + "Reindexierend"); + builder.AddOrUpdate("Enums.SmartStore.Core.Search.IndexingStatus.Updating", + "Updating", + "Aktualisierend"); + + builder.AddOrUpdate("Admin.Packaging.InstallSuccess", + "Package was uploaded and unzipped successfully. Please reload the list.", + "Paket wurde hochgeladen und erfolgreich entpackt. Bitte laden Sie die Liste neu."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.resx b/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.resx new file mode 100644 index 0000000000..f321dbeec5 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007241131557_V401Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.Designer.cs new file mode 100644 index 0000000000..5d7b94d21e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class NewPropertiesAndIndexes : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(NewPropertiesAndIndexes)); + + string IMigrationMetadata.Id + { + get { return "202007291847004_NewPropertiesAndIndexes"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.cs b/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.cs new file mode 100644 index 0000000000..31eef90291 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.cs @@ -0,0 +1,47 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class NewPropertiesAndIndexes : DbMigration + { + public override void Up() + { + AddColumn("dbo.Country", "DefaultCurrencyId", c => c.Int()); + AddColumn("dbo.CheckoutAttributeValue", "MediaFileId", c => c.Int()); + AddColumn("dbo.CheckoutAttributeValue", "Color", c => c.String(maxLength: 100)); + CreateIndex("dbo.CustomerRole", "Active"); + CreateIndex("dbo.CustomerRole", "IsSystemRole"); + CreateIndex("dbo.CustomerRole", new[] { "SystemName", "IsSystemRole" }, name: "IX_CustomerRole_SystemName_IsSystemRole"); + CreateIndex("dbo.CustomerRole", "SystemName"); + CreateIndex("dbo.Country", "DefaultCurrencyId"); + CreateIndex("dbo.Customer", new[] { "Deleted", "IsSystemAccount" }, name: "IX_Customer_Deleted_IsSystemAccount"); + CreateIndex("dbo.Customer", "IsSystemAccount"); + CreateIndex("dbo.ProductVariantAttributeCombination", "Gtin"); + CreateIndex("dbo.ProductVariantAttributeCombination", "ManufacturerPartNumber"); + CreateIndex("dbo.ProductVariantAttributeCombination", "IsActive"); + CreateIndex("dbo.CheckoutAttributeValue", "MediaFileId"); + AddForeignKey("dbo.Country", "DefaultCurrencyId", "dbo.Currency", "Id"); + AddForeignKey("dbo.CheckoutAttributeValue", "MediaFileId", "dbo.MediaFile", "Id"); + } + + public override void Down() + { + DropForeignKey("dbo.CheckoutAttributeValue", "MediaFileId", "dbo.MediaFile"); + DropForeignKey("dbo.Country", "DefaultCurrencyId", "dbo.Currency"); + DropIndex("dbo.CheckoutAttributeValue", new[] { "MediaFileId" }); + DropIndex("dbo.ProductVariantAttributeCombination", new[] { "IsActive" }); + DropIndex("dbo.ProductVariantAttributeCombination", new[] { "ManufacturerPartNumber" }); + DropIndex("dbo.ProductVariantAttributeCombination", new[] { "Gtin" }); + DropIndex("dbo.Customer", new[] { "IsSystemAccount" }); + DropIndex("dbo.Customer", "IX_Customer_Deleted_IsSystemAccount"); + DropIndex("dbo.Country", new[] { "DefaultCurrencyId" }); + DropIndex("dbo.CustomerRole", new[] { "SystemName" }); + DropIndex("dbo.CustomerRole", "IX_CustomerRole_SystemName_IsSystemRole"); + DropIndex("dbo.CustomerRole", new[] { "IsSystemRole" }); + DropIndex("dbo.CustomerRole", new[] { "Active" }); + DropColumn("dbo.CheckoutAttributeValue", "Color"); + DropColumn("dbo.CheckoutAttributeValue", "MediaFileId"); + DropColumn("dbo.Country", "DefaultCurrencyId"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.resx b/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.resx new file mode 100644 index 0000000000..37017bdeb4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007291847004_NewPropertiesAndIndexes.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.Designer.cs new file mode 100644 index 0000000000..e9d53d3e43 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class AddCustomerRoleOrderAmount : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddCustomerRoleOrderAmount)); + + string IMigrationMetadata.Id + { + get { return "202007301117363_AddCustomerRoleOrderAmount"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.cs b/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.cs new file mode 100644 index 0000000000..2b3fdefbff --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.cs @@ -0,0 +1,19 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class AddCustomerRoleOrderAmount : DbMigration + { + public override void Up() + { + AddColumn("dbo.CustomerRole", "MinOrderAmount", c => c.Decimal(nullable: false, precision: 18, scale: 2)); + AddColumn("dbo.CustomerRole", "MaxOrderAmount", c => c.Decimal(nullable: false, precision: 18, scale: 2)); + } + + public override void Down() + { + DropColumn("dbo.CustomerRole", "MinOrderAmount"); + DropColumn("dbo.CustomerRole", "MaxOrderAmount"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.resx b/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.resx new file mode 100644 index 0000000000..9cda56f950 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202007301117363_AddCustomerRoleOrderAmount.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.Designer.cs new file mode 100644 index 0000000000..ec697d5c0b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class RenamedCustomerRoleOrderTotal : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(RenamedCustomerRoleOrderTotal)); + + string IMigrationMetadata.Id + { + get { return "202008181949580_RenamedCustomerRoleOrderTotal"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.cs b/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.cs new file mode 100644 index 0000000000..910fea9429 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.cs @@ -0,0 +1,23 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class RenamedCustomerRoleOrderTotal : DbMigration + { + public override void Up() + { + AddColumn("dbo.CustomerRole", "OrderTotalMinimum", c => c.Decimal(nullable: true, precision: 18, scale: 2)); + AddColumn("dbo.CustomerRole", "OrderTotalMaximum", c => c.Decimal(nullable: true, precision: 18, scale: 2)); + DropColumn("dbo.CustomerRole", "MinOrderAmount"); + DropColumn("dbo.CustomerRole", "MaxOrderAmount"); + } + + public override void Down() + { + AddColumn("dbo.CustomerRole", "MinOrderAmount", c => c.Decimal(nullable: true, precision: 18, scale: 2)); + AddColumn("dbo.CustomerRole", "MaxOrderAmount", c => c.Decimal(nullable: true, precision: 18, scale: 2)); + DropColumn("dbo.CustomerRole", "OrderTotalMaximum"); + DropColumn("dbo.CustomerRole", "OrderTotalMinimum"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.resx b/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.resx new file mode 100644 index 0000000000..10b085d3d3 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202008181949580_RenamedCustomerRoleOrderTotal.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.Designer.cs new file mode 100644 index 0000000000..664b26aadd --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class Merge9 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Merge9)); + + string IMigrationMetadata.Id + { + get { return "202009021705132_Merge9"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.cs b/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.cs similarity index 59% rename from src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.cs rename to src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.cs index 0777552123..4aaa198af3 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201808051119578_Merge4.cs +++ b/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.cs @@ -1,14 +1,13 @@ -namespace SmartStore.Data.Migrations +namespace SmartStore.Data.Migrations { - using System; using System.Data.Entity.Migrations; - - public partial class Merge4 : DbMigration + + public partial class Merge9 : DbMigration { public override void Up() { } - + public override void Down() { } diff --git a/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.resx b/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.resx new file mode 100644 index 0000000000..0848e0a3bd --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009021705132_Merge9.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.Designer.cs new file mode 100644 index 0000000000..9a78bf96fb --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class ShopIcons : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ShopIcons)); + + string IMigrationMetadata.Id + { + get { return "202009041122208_ShopIcons"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.cs b/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.cs new file mode 100644 index 0000000000..227a055aa8 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.cs @@ -0,0 +1,73 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Core.Data; + using SmartStore.Data.Setup; + + public partial class ShopIcons : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Store", "FavIconMediaFileId", c => c.Int()); + AddColumn("dbo.Store", "PngIconMediaFileId", c => c.Int()); + AddColumn("dbo.Store", "AppleTouchIconMediaFileId", c => c.Int()); + AddColumn("dbo.Store", "MsTileImageMediaFileId", c => c.Int()); + AddColumn("dbo.Store", "MsTileColor", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.Store", "MsTileColor"); + DropColumn("dbo.Store", "MsTileImageMediaFileId"); + DropColumn("dbo.Store", "AppleTouchIconMediaFileId"); + DropColumn("dbo.Store", "PngIconMediaFileId"); + DropColumn("dbo.Store", "FavIconMediaFileId"); + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + if (!DataSettings.DatabaseIsInstalled()) + return; + + // Add resources. + context.MigrateLocaleResources(MigrateLocaleResources); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.FavIconMediaFileId", + "Favicon", + "Favicon", + "Defines the favicon for the store. This icon should contain graphics in the dimensions 16×16, 32×32 and 48×48.", + "Legt das Favicon für den Shop fest. Dieses Icon sollte Grafiken in den Abmessungen 16×16, 32×32 und 48×48 beinhalten."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.PngIconMediaFileId", + "PNG icon", + "PNG-Icon", + "Defines the PNG icon for the store. This icon is recognized as favicon by up-to-date devices and used when shortcuts to your store are created. Optimal dimensions for this icon are 196x196.", + "Legt das PNG-Icon für den Shop fest. Dieses Icon wird von aktuellen Geräten als Favicon erkannt und verwendet, wenn Shortcuts zu Ihrem Shop erstellt werden. Optimale Abmessungen für das Icon sind 196x196."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.AppleTouchIconMediaFileId", + "Apple Touch icon", + "Apple Touch Icon", + "Defines the icon used by Apple devices as shortcut icon. Optimal dimensions for this icon are 180x180.", + "Legt das Icon fest, das von Apple-Geräten als Shortcut-Icon verwendet wird. Optimale Abmessungen für das Icon sind 180x180."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.MsTileImageMediaFileId", + "Microsoft tile picture", + "Microsoft Kachelgrafik", + "Defines the icon that is used by Microsoft devices as a tile picture when shortcuts to your store are created. Optimal dimensions for the picture are 310x310.", + "Legt das Icon fest, das von Microsoft-Geräten als Kachelgrafik verwendet wird, wenn Shortcuts zu Ihrem Shop erstellt werden. Optimale Abmessungen für das Bild sind 310x310."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.MsTileColor", + "Microsoft tile color", + "Microsoft Kachelfarbe", + "Defines the color used by Microsoft devices as tile color when shortcuts to your store are created.", + "Legt die Farbe fest, die von Microsoft-Geräten als Kachelfarbe verwendet wird, wenn Shortcuts zu Ihrem Shop erstellt werden."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.resx b/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.resx new file mode 100644 index 0000000000..eab00470d5 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009041122208_ShopIcons.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.Designer.cs new file mode 100644 index 0000000000..6a218b39fb --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class DeliveryTimeMinMaxDays : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(DeliveryTimeMinMaxDays)); + + string IMigrationMetadata.Id + { + get { return "202009090817220_DeliveryTimeMinMaxDays"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.cs b/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.cs new file mode 100644 index 0000000000..4cf6cab153 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.cs @@ -0,0 +1,78 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Catalog; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Core.Domain.Directory; + using SmartStore.Core.Domain.Orders; + using SmartStore.Data.Setup; + + public partial class DeliveryTimeMinMaxDays : DbMigration, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.DeliveryTime", "MinDays", c => c.Int()); + AddColumn("dbo.DeliveryTime", "MaxDays", c => c.Int()); + } + + public override void Down() + { + DropColumn("dbo.DeliveryTime", "MaxDays"); + DropColumn("dbo.DeliveryTime", "MinDays"); + } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + if (!DataSettings.DatabaseIsInstalled()) + { + return; + } + + // Migrate old to new delivery times catalog settings. + var settings = context.Set(); + var showInLists = settings.FirstOrDefault(x => x.Name == "CatalogSettings.ShowDeliveryTimesInProductLists"); + var showInDetail = settings.FirstOrDefault(x => x.Name == "CatalogSettings.ShowDeliveryTimesInProductDetail"); + var showInShoppingCart = settings.FirstOrDefault(x => x.Name == "ShoppingCartSettings.ShowDeliveryTimes"); + + context.MigrateSettings(x => + { + if (showInLists != null && showInLists.Value.IsCaseInsensitiveEqual("False")) + { + var key = string.Concat(nameof(CatalogSettings), ".", nameof(CatalogSettings.DeliveryTimesInLists)); + x.Add(key, DeliveryTimesPresentation.None.ToString()); + } + if (showInDetail != null && showInDetail.Value.IsCaseInsensitiveEqual("False")) + { + var key = string.Concat(nameof(CatalogSettings), ".", nameof(CatalogSettings.DeliveryTimesInProductDetail)); + x.Add(key, DeliveryTimesPresentation.None.ToString()); + } + if (showInShoppingCart != null && showInShoppingCart.Value.IsCaseInsensitiveEqual("False")) + { + var key = string.Concat(nameof(ShoppingCartSettings), ".", nameof(ShoppingCartSettings.DeliveryTimesInShoppingCart)); + x.Add(key, DeliveryTimesPresentation.None.ToString()); + } + }); + + context.SaveChanges(); + + if (showInLists != null) + { + settings.Remove(showInLists); + } + if (showInDetail != null) + { + settings.Remove(showInDetail); + } + if (showInShoppingCart != null) + { + settings.Remove(showInShoppingCart); + } + + context.SaveChanges(); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.resx b/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.resx new file mode 100644 index 0000000000..5021045a95 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202009090817220_DeliveryTimeMinMaxDays.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.Designer.cs new file mode 100644 index 0000000000..b4b28db84f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class TopicCookieType : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(TopicCookieType)); + + string IMigrationMetadata.Id + { + get { return "202010011005387_TopicCookieType"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.cs b/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.cs new file mode 100644 index 0000000000..818986dceb --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.cs @@ -0,0 +1,43 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Core.Data; + using SmartStore.Data.Setup; + + public partial class TopicCookieType : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Topic", "CookieType", c => c.Int()); + } + + public override void Down() + { + DropColumn("dbo.Topic", "CookieType"); + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + if (!DataSettings.DatabaseIsInstalled()) + return; + + // Add resources. + context.MigrateLocaleResources(MigrateLocaleResources); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.CookieType", + "Cookie type", + "Art des Cookies", + "Specifies whether this widget is displayed according to the customer's settings in the cookie manager. " + + " This option should be used if you add a third-party script that sets cookies.", + "Bestimmt, ob dieses Widget in Abhängigkeit zur Kundeneinstellung im Cookie-Manager ausgegeben wird. " + + "Diese Option sollte verwendet werden, wenn Sie ein Script für einen Drittanbieter zufügen, das Cookies setzt."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.resx b/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.resx new file mode 100644 index 0000000000..e45a3f8553 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010011005387_TopicCookieType.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.Designer.cs new file mode 100644 index 0000000000..1051bb9ea1 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class AttributeChoiceBehaviour : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AttributeChoiceBehaviour)); + + string IMigrationMetadata.Id + { + get { return "202010030939136_AttributeChoiceBehaviour"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.cs b/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.cs new file mode 100644 index 0000000000..01d2fb1e83 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.cs @@ -0,0 +1,22 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AttributeChoiceBehaviour : DbMigration + { + public override void Up() + { + AddColumn("dbo.Product", "AttributeChoiceBehaviour", c => c.Int(nullable: false)); + AddColumn("dbo.Product_ProductAttribute_Mapping", "CustomData", c => c.String()); + CreateIndex("dbo.ProductVariantAttributeCombination", new[] { "StockQuantity", "AllowOutOfStockOrders" }); + } + + public override void Down() + { + DropIndex("dbo.ProductVariantAttributeCombination", new[] { "StockQuantity", "AllowOutOfStockOrders" }); + DropColumn("dbo.Product_ProductAttribute_Mapping", "CustomData"); + DropColumn("dbo.Product", "AttributeChoiceBehaviour"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.resx b/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.resx new file mode 100644 index 0000000000..103fee40a2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010030939136_AttributeChoiceBehaviour.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.Designer.cs new file mode 100644 index 0000000000..e662cced0e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class RemoveBlogAndNewsLanguage : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(RemoveBlogAndNewsLanguage)); + + string IMigrationMetadata.Id + { + get { return "202010121359446_RemoveBlogAndNewsLanguage"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.cs b/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.cs new file mode 100644 index 0000000000..53fcbb0cdc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.cs @@ -0,0 +1,103 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Blogs; + using SmartStore.Core.Domain.Localization; + using SmartStore.Core.Domain.News; + using SmartStore.Core.Domain.Seo; + using SmartStore.Data.Setup; + + public partial class RemoveBlogAndNewsLanguage : DbMigration, IDataSeeder + { + public override void Up() + { + DropForeignKey("dbo.BlogPost", "LanguageId", "dbo.Language"); + DropForeignKey("dbo.News", "LanguageId", "dbo.Language"); + + if (DataSettings.Current.IsSqlServer) + { + Sql("IF (OBJECT_ID('BlogPost_Language', 'F') IS NOT NULL) ALTER TABLE [dbo].[BlogPost] DROP CONSTRAINT [BlogPost_Language];"); + Sql("IF (OBJECT_ID('NewsItem_Language', 'F') IS NOT NULL) ALTER TABLE [dbo].[News] DROP CONSTRAINT [NewsItem_Language];"); + + // See Indexes.sql. + Sql("IF EXISTS (SELECT * FROM sys.indexes WHERE name='IX_BlogPost_LanguageId' AND object_id = OBJECT_ID('[dbo].[BlogPost]')) DROP INDEX [IX_BlogPost_LanguageId] ON [dbo].[BlogPost];"); + Sql("IF EXISTS (SELECT * FROM sys.indexes WHERE name='IX_News_LanguageId' AND object_id = OBJECT_ID('[dbo].[News]')) DROP INDEX [IX_News_LanguageId] ON [dbo].[News];"); + } + else + { + DropIndex("dbo.BlogPost", "IX_BlogPost_LanguageId"); + DropIndex("dbo.News", "IX_News_LanguageId"); + } + + DropIndex("dbo.BlogPost", new[] { "LanguageId" }); + DropIndex("dbo.News", new[] { "LanguageId" }); + DropColumn("dbo.BlogPost", "LanguageId"); + DropColumn("dbo.News", "LanguageId"); + } + + public override void Down() + { + var defaultLanguageId = 1; + + if (DataSettings.DatabaseIsInstalled()) + { + defaultLanguageId = new SmartObjectContext().Set().Select(x => x.Id).FirstOrDefault(); + } + + AddColumn("dbo.News", "LanguageId", c => c.Int(nullable: false, defaultValue: defaultLanguageId)); + AddColumn("dbo.BlogPost", "LanguageId", c => c.Int(nullable: false, defaultValue: defaultLanguageId)); + CreateIndex("dbo.News", "LanguageId"); + CreateIndex("dbo.BlogPost", "LanguageId"); + AddForeignKey("dbo.News", "LanguageId", "dbo.Language", "Id", cascadeDelete: true); + AddForeignKey("dbo.BlogPost", "LanguageId", "dbo.Language", "Id", cascadeDelete: true); + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + if (!DataSettings.DatabaseIsInstalled()) + { + return; + } + + // URL records of BlogPost and NewsItem must now have 0 for "Standard" as language ID otherwise no link is created. + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var urlRecords = context.Set(); + var entityNames = new[] { nameof(BlogPost), nameof(NewsItem) }; + + foreach (var entityName in entityNames) + { + var allEntityIds = urlRecords + .Where(x => x.EntityName == entityName) + .Select(x => x.EntityId) + .Distinct() + .ToList(); + + foreach (var chunk in allEntityIds.Slice(200)) + { + var entities = urlRecords + .Where(x => x.EntityName == entityName && chunk.Contains(x.EntityId)) + .ToList(); + var entitiesMap = entities.ToMultimap(x => x.EntityId, x => x); + + foreach (var kvp in entitiesMap) + { + if (!kvp.Value.Any(x => x.LanguageId == 0)) + { + // Migrate active and inactive slugs. + kvp.Value.Where(x => x.LanguageId != 0).Each(x => x.LanguageId = 0); + } + } + + scope.Commit(); + context.DetachEntities(); + } + } + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.resx b/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.resx new file mode 100644 index 0000000000..66f0c45a54 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202010121359446_RemoveBlogAndNewsLanguage.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.Designer.cs new file mode 100644 index 0000000000..316f66a305 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class V410Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V410Resources)); + + string IMigrationMetadata.Id + { + get { return "202011091154314_V410Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.cs b/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.cs new file mode 100644 index 0000000000..34543f8e68 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.cs @@ -0,0 +1,857 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Core.Domain.Media; + using SmartStore.Core.Domain.Seo; + using SmartStore.Data.Setup; + using SmartStore.Utilities; + + public partial class V410Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + MigrateSettings(context); + context.SaveChanges(); + } + + public void MigrateSettings(SmartObjectContext context) + { + var settings = context.Set(); + + // Add .ico extension to MediaSettings.ImageTypes + var name = TypeHelper.NameOf(y => y.ImageTypes, true); + var setting = settings.FirstOrDefault(x => x.Name == name); + if (setting != null) + { + var arr = setting.Value.EmptyNull() + .Replace(Environment.NewLine, " ") + .ToLower() + .Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (!arr.Contains("ico")) + { + setting.Value += " ico"; + } + } + + // Migrate old 'defaultSeoSettings' into new SeoSettings + // Old settings. + var defaultTitle = settings.FirstOrDefault(x => x.Name == "SeoSettings.DefaultTitle"); + var defaultMetaKeywords = settings.FirstOrDefault(x => x.Name == "SeoSettings.DefaultMetaKeywords"); + var defaultMetaDescription = settings.FirstOrDefault(x => x.Name == "SeoSettings.DefaultMetaDescription"); + + // New settings. + context.MigrateSettings(x => + { + x.Add(TypeHelper.NameOf(y => y.MetaTitle, true), defaultTitle != null && defaultTitle.Value.HasValue() ? defaultTitle.Value : ""); + x.Add(TypeHelper.NameOf(y => y.MetaDescription, true), defaultMetaDescription != null && defaultMetaDescription.Value.HasValue() ? defaultMetaDescription.Value : ""); + x.Add(TypeHelper.NameOf(y => y.MetaKeywords, true), defaultMetaKeywords != null && defaultMetaKeywords.Value.HasValue() ? defaultMetaKeywords.Value : ""); + }); + + if (defaultTitle != null) settings.Remove(defaultTitle); + if (defaultMetaDescription != null) settings.Remove(defaultMetaDescription); + if (defaultMetaKeywords != null) settings.Remove(defaultMetaKeywords); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete( + "Admin.SalesReport.Bestsellers.RunReport", + "Admin.SalesReport.NeverSold.RunReport", + "Admin.Customers.Reports.RunReport", + "Admin.Customers.Reports.BestBy.BestByNumberOfOrders", + "Admin.Customers.Reports.BestBy.BestByNumberOfOrders"); + + builder.AddOrUpdate("Admin.Customers.Reports.BestCustomers", "Top customers", "Top Kunden"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.SearchProductByIdentificationNumber", + "Open product directly at SKU, MPN or GTIN", + "Produkt bei SKU, MPN oder GTIN direkt öffnen", + "Specifies whether the product page should be opened directly if the search term matches a SKU, MPN or GTIN.", + "Legt fest, ob bei einer Übereinstimmung des Suchbegriffes mit einer SKU, MPN oder GTIN die Produktseite direkt geöffnet werden soll."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.PasswordMinLength", + "Minimum password length", + "Mindestlänge eines Passworts", + "Specifies the minimum length of a password.", + "Legt die minimale Länge eines Passworts fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.MinDigitsInPassword", + "Minimum digits in password", + "Mindestanzahl von Ziffern im Passwort", + "Specifies the minimum number of digits for a password.", + "Legt die Mindestanzahl von Ziffern eines Passworts fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.MinSpecialCharsInPassword", + "Minimum special characters in password", + "Mindestanzahl von Sonderzeichen im Passwort", + "Specifies the minimum number of special characters for a password.", + "Legt die Mindestanzahl von Sonderzeichen eines Passworts fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.MinUppercaseCharsInPassword", + "Minimum uppercase letters in password", + "Mindestanzahl von Großbuchstaben im Passwort", + "Specifies the minimum number of uppercase letters for a password.", + "Legt die Mindestanzahl von Großbuchstaben eines Passworts fest."); + + builder.AddOrUpdate("Account.Fields.Password.MustContainChars", + "The password must contain at least {0}.", + "Das Passwort muss mindestens {0} enthalten."); + builder.AddOrUpdate("Account.Fields.Password.Digits", "{0} digits", "{0} Ziffern"); + builder.AddOrUpdate("Account.Fields.Password.SpecialChars", "{0} special characters", "{0} Sonderzeichen"); + builder.AddOrUpdate("Account.Fields.Password.UppercaseChars", "{0} uppercase letters", "{0} Großbuchstaben"); + + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaymentMethod", "Payment method", "Zahlart"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", "Browser User Agent", "Browser User-Agent"); + + builder.AddOrUpdate("Admin.Orders.NoOrdersSelected", + "No orders are selected. Please select the desired orders.", + "Es sind keine Aufträge ausgewählt. Bitte wählen Sie die gewünschten Aufträge aus."); + + builder.AddOrUpdate("Admin.Orders.ProcessSelectedOrders", + "There are {0} orders selected. Would you like to proceed?", + "Es sind {0} Aufträge ausgewählt. Möchten Sie fortfahren?"); + + builder.AddOrUpdate("Admin.Orders.ProcessingResult", + "
{0} of {1} orders were successfully processed.
{2} orders cannot be changed as desired due to their current status and were skipped.", + "
Es wurden {0} von {1} Aufträgen erfolgreich verarbeitet.
{2} Aufträge können aufgrund ihres aktuellen Status nicht wie gewünscht geändert werden und wurden übersprungen."); + + builder.AddOrUpdate("Admin.Configuration.Countries.Fields.DefaultCurrency", + "Default currency", + "Standardwährung", + "Specifies the default currency. Preselects the default currency in the shop according to the country to which the current IP address belongs.", + "Legt die Standardwährung fest. Bewirkt im Shop eine Vorauswahl der Standardwährung anhand des Landes, zu dem die aktuelle IP-Adresse gehört."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.CheckoutAttributes.Values.Fields.MediaFile", + "Picture", + "Bild", + "Specifies an image to be displayed as the selection element for the attribute.", + "Legt ein Bild fest, welches als Auswahlelement für das Attribut angezeigt werden soll."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.CheckoutAttributes.Values.Fields.Color", + "RGB color", + "RGB-Farbe", + "Specifies a color for the color squares control.", + "Legt eine Farbe für das Farbflächen-Steuerelement fest."); + + builder.AddOrUpdate("Common.Entity.CheckoutAttributeValue", "Checkout attribute option", "Checkout-Attribut-Option"); + + builder.AddOrUpdate("Checkout.MaxOrderSubtotalAmount", + "The maximum order value for the subtotal is {0}.", + "Der Höchstbestellwert für die Zwischensumme ist {0}."); + + builder.AddOrUpdate("Checkout.MaxOrderTotalAmount", + "The maximum order value for the total is {0}.", + "Der Höchstbestellwert der Gesamtsumme ist {0}."); + + builder.AddOrUpdate("Admin.Customers.CustomerRoles.Fields.MinOrderTotal", + "Minimum order total", + "Mindestbestellwert", + "Defines the minimum order total for customers in this group.", + "Legt den Mindestbestellwert für Kunden in dieser Gruppe fest."); + + builder.AddOrUpdate("Admin.Customers.CustomerRoles.Fields.MaxOrderTotal", + "Maximum order total", + "Höchstbestellwert", + "Defines the maximum order total for customers in this group.", + "Legt den Höchstbestellwert für Kunden in dieser Gruppe fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.MinOrderTotal", + "Minimum order total", + "Mindestbestellwert", + "Defines the default minimum order total for the shop. Overridden by customer group restrictions.", + "Legt den standardmäßigen Mindestbestellwert für den Shop fest. Wird von Beschränkungen in Kundengruppen überschrieben."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.MaxOrderTotal", + "Maximum order total", + "Höchstbestellwert", + "Defines the default maximum order total for the shop. Overridden by customer group restrictions.", + "Legt den standardmäßigen Höchstbestellwert für den Shop fest. Wird von Beschränkungen in Kundengruppen überschrieben."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.ApplyToSubtotal", + "Order total restriction in relation to subtotal", + "Beschränkung bezogen auf Zwischensumme", + "Determines whether the min/max order value refers to the order subtotal, otherwise it refers to the total amount.", + "Bestimmt, ob sich der Mindest-/Höchstbetrag auf die Auftragszwischensumme bezieht, andernfalls bezieht er sich auf den Gesamtbetrag."); + + builder.Delete("Admin.Configuration.Settings.Order.MaxOrderSubtotalAmount", + "Admin.Configuration.Settings.Order.MaxOrderSubtotalAmount.Hint", + "Admin.Configuration.Settings.Order.MaxOrderTotalAmount", + "Admin.Configuration.Settings.Order.MaxOrderTotalAmount.Hint", + "Admin.Configuration.Settings.Order.MinOrderSubtotalAmount", + "Admin.Configuration.Settings.Order.MinOrderSubtotalAmount.Hint", + "Admin.Configuration.Settings.Order.MinOrderTotalAmount", + "Admin.Configuration.Settings.Order.MinOrderTotalAmount.Hint"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.OrderTotalRestrictionType", + "Restrictions in customer groups are cumulative", + "Beschränkungen in Kundengruppen sind kumulativ", + "Determines whether multiple order total restrictions are cumulated by customer group assignments.", + "Bestimmt, ob mehrfache Bestellwertbeschränkungen durch Kundengruppenzuordnungen kumuliert werden."); + + builder.Delete("Products.AskQuestion.Question.Text"); + + builder.AddOrUpdate("Products.AskQuestion.Sent", + "Thank you. Your inquiry has been sent successfully.", + "Vielen Dank. Ihre Anfrage wurde erfolgreich gesendet."); + + builder.AddOrUpdate("Products.AskQuestion.Question.GeneralInquiry", + "I have following questions concerning the product {0}:", + "Ich habe folgende Fragen zum Artikel {0}:"); + + builder.AddOrUpdate("Products.AskQuestion.Question.QuoteRequest", + "I would like to request price information on the following article: {0}", + "Ich bitte um Preisinformationen zu folgendem Artikel: {0}"); + + builder.AddOrUpdate("Products.AskQuestion.CallHotline", "or call directly", "oder rufen Sie direkt an"); + + builder.AddOrUpdate("Products.AskQuestion.TitleQuoteRequest", "Request for quotation", "Angebotsanfrage"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerFormFields.Description", + "Manage form fields that are displayed during registration.
" + + "In order to ensure the address transfer from the registration form, " + + "it is necessary that the following fields are activated and filled in by the customer: " + + "
  • First name
  • Last name
  • E-mail
  • and all fields that are selected as required in the tab 'Addresses'
", + "Verwalten Sie Formularfelder, die während der Registrierung angezeigt werden.
" + + "Um die Adressübergabe aus dem Registrierungsformular zu gewährleisten, ist es notwending, " + + "dass folgende Felder aktiviert und vom Kunden ausgefüllt sind:" + + "
  • Vorname
  • Nachname
  • E-Mail
  • und alle Felder die im Tab \"Adressen\" als erforderlich ausgewählt sind
"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.Cookies.AddCookieInfo.Title", + "Add new cookie information", + "Neue Cookie-Informationen hinzufügen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.Cookies.EditCookieInfo.Title", + "Edit new cookie information", + "Cookie-Informationen bearbeiten"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.CookieInfo.Name", + "Name", + "Name", + "Specifies the display name of the cookie information.", + "Bestimmt den Anzeigenamen der Cookie-Information."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.CookieInfo.Description", + "Description", + "Beschreibung", + "Specifies the description of the cookie information.", + "Bestimmt die Beschreibung der Cookie-Information."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.CookieInfo.CookieType", + "Cookie type", + "Art des Cookies", + "Specifies the usage type of the cookie (Required, Analytics, ThirdParty).", + "Bestimmt die Verwendungsart des Cookies (Required, Analytics, ThirdParty)."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.SameSiteMode", + "'SameSite' mode", + "'SameSite' Modus"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.SameSiteMode.AdminInstruction", + @"
SameSite Mode
+

+ The SameSite setting for cookies specifies the level of security for cookies when they are requested by other sites (e.g. in a cross-site request or in an iframe). + The recommended setting is Lax, as cookies are sent with most secure actions. +
+ If the store should also be embedded in an iframe, it is necessary to select the setting None. Additionally SSL must be set up in the store. +

+
    +
  • Unspecified: No SameSite header is transmitted to requests.
  • +
  • None: No mode specified. This setting must be used if the store should also be embedded in an iframe (SSL must be set up).
  • +
  • Lax: Recommended setting. Cookies are sent at the highest level for requests on the same website and for cross-site requests.
  • +
  • Strict: Cookies are only sent to requests from the same website.
  • +
", + @"
SameSite Modus
+

+ Die SameSite-Einstellung für Cookies bestimmt, das Sicherheitslevel für Cookies, wenn diese von anderen Seiten (z.B. in einem Cross-Site-Request oder in einem IFrame) aufgerufen werden. + Die empfohlene Einstellung ist Lax, da hier Cookies bei den meisten gesicherten Aktionen mitgesendet werden. +
+ Soll der Shop auch in ein IFrame eingebettet werden, ist es notwendig die Einstellung None zu wählen. Zusätzlich muss SSL im Shop eingerichtet sein. +

+
    +
  • Unspecified: Es wird kein SameSite-Header an Requests übermittelt.
  • +
  • None: Kein Modus angegeben. Diese Einstellung muss verwendet werden, wenn der Shop auch in ein IFrame eingebettet werden soll (SSL muss eingerichtet sein).
  • +
  • Lax: Empfohlene Einstellung. Cookies werden bei Anfragen auf derselben Website und bei Cross-Site-Requests auf höchster Ebene gesendet.
  • +
  • Strict: Cookies werden nur an Anfragen derselben Website gesendet.
  • +
"); + + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.ModalCookieConsent", + "Modal Cookie Manager", + "Cookie-Manager modal anzeigen", + "Specifies whether the Cookie Manager is displayed in a modal dialog in the frontend. The modal mode requires a customer interaction with the dialog before the customer can navigate in the store.", + "Bestimmt, ob der Cookie-Manager im Frontend in einem modalen Dialog angezeigt wird. Der modale Modus macht eine Kundeninteraktion mit dem Dialog erfoderlich, bevor der Kunde im Shop navigieren kann."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.EnableCookieConsent", + "Enable Cookie Manager", + "Cookie-Manager aktivieren", + "Specifies whether the Cookie Manager will be displayed in the frontend.", + "Legt fest, ob ein Dialog für die Zustimmung zur Nutzung von Cookies im Frontend angezeigt wird."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.CookieInfo.IsPluginInfo", + "System cookie", + "System-Cookie"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.Cookies.CookieInfoNotFound", + "Cookie information wasn't found.", + "Cookie-Information wurde nicht gefunden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.Cookies.RegisterCookieInfo.Title", + "Register your own cookie information", + "Eigene Cookie-Informationen registrieren"); + + builder.AddOrUpdate("CookieManager.Dialog.Hide", + "Hide", + "Ausblenden"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Privacy.Cookies.AdminInstruction", + @"

+ If you have included third-party scripts in your shop that use cookies and are not integrated into Smartstore via a plugin, + you can provide your own information about these cookies for the Cookie Manager in the following table. + The added information is displayed in the Cookie Manager of the front end, corresponding to the type. + Cookie information provided to the Cookie Manager by active plug-ins is displayed in this table, + but cannot be edited. The texts of this information can be edited using the language resources + Admin > Configuration > Regional Settings > Languages. +

", + @"

+ Wenn Sie Skripte von Drittanbietern in Ihren Shop eingebunden haben, die Cookies nutzen und die nicht über ein Plugin in Smartstore integriert sind, + können Sie in folgender Tabelle eigene Informationen über diese Cookies für den Cookie-Manager bereitstellen. + Die zugefügten Informationen werden, dem Typ entsprechend, im Cookie-Manager des Frontends angezeigt. + Cookie-Informationen, die dem Cookie-Manager von aktiven Plugins zur Verfügung gestellt werden, werden in dieser Tabelle angezeigt, + können aber nicht bearbeitet werden. Die Texte dieser Informationen können über die Sprachresourcen bearbeitet werden + Admin > Konfiguration > Regionale Einstellungen > Sprachen. +

"); + + builder.AddOrUpdate("Admin.Catalog.Products.StockQuantityNotChanged", + "The stock quantity has not been updated because the value has changed since the page was loaded (e.g. due to a placed order). Current value is {0}.", + "Der Lagerbestand wurde nicht aktualisiert, weil sich der Wert seit Laden der Seite geändert hat (z.B. durch einen Auftragseingang). Aktueller Wert ist {0}."); + + builder.AddOrUpdate("Admin.Configuration.SeoTab.Title", + "SEO settings", + "SEO-Einstellungen"); + + builder.AddOrUpdate("Admin.Configuration.Seo.MetaTitle", + "Title tag", + "Title-Tag", + "Defines the title of the page, which is displayed in search engine results. If possible, enter a unique, concise title here.", + "Bestimmt den Titel der Seite, der unter anderem in den Ergebnissen von Suchmaschinen angezeigt wird. Geben Sie hier nach Möglichkeit einen einzigartigen, prägnanten Titel an."); + + builder.AddOrUpdate("Admin.Configuration.Seo.MetaDescription", + "Meta description", + "Meta Description", + "Defines the meta description of the site. An extract of this description is displayed in search engine results below the title.", + "Bestimmt die Meta Desciption der Seite. Ein Auszug dieser Beschreibung wird in den Ergebnissen von Suchmaschinen unterhalb des Titels dargestellt."); + + builder.AddOrUpdate("Admin.Configuration.Seo.MetaKeywords", + "Meta keywords", + "Meta Keywords", + "Defines the meta keywords of the site. Enter the keywords as a comma separated list.", + "Bestimmt die Meta Keywords der Seite. Geben Sie die Keywords als kommagetrennte Liste an."); + + builder.AddOrUpdate("Admin.System.MetaInfos", + "Meta tags", + "Meta-Elemente"); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.HomepageTitle", + "Title tag (Homepage)", + "Title-Tag (Startseite)", + "Defines the title tag for the homepage, which is displayed in search engine results.", + "Legt das Title-Tag für die Startseite fest, das unter anderem in den Ergebnissen von Suchmaschinen angezeigt wird."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.HomepageMetaDescription", + "Meta description (Homepage)", + "Meta Description (Startseite)", + "Defines the meta description for the homepage. An extract of this description is displayed in search engine results below the title.", + "Legt die Meta Description für die Startseite fest. Ein Auszug dieser Beschreibung wird in den Ergebnissen von Suchmaschinen unterhalb des Titels dargestellt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.HomepageMetaKeywords", + "Meta keywords (Homepage)", + "Meta Keywords (Startseite)", + "Defines the meta keywords for the homepage. Enter the keywords as a comma separated list.", + "Legt die Meta Keywords für die Startseite fest. Geben Sie die Keywords als kommagetrennte Liste an."); + + builder.AddOrUpdate("Admin.Configuration.DeliveryTimes.Fields.MinDays", + "Delivery not before (in days)", + "Lieferung frühestens (in Tagen)", + "Specifies the minimum number of days for the delivery.", + "Legt den frühesten Zeitpunkt der Lieferung in Tagen fest."); + + builder.AddOrUpdate("Admin.Configuration.DeliveryTimes.Fields.MaxDays", + "Delivery not later than (in days)", + "Lieferung spätestens (in Tagen)", + "Specifies the maximum number of days for the delivery.", + "Legt den spätesten Zeitpunkt der Lieferung in Tagen fest."); + + builder.AddOrUpdate("Products.DeliveryDate", "Arrives:", "Lieferung:"); + + builder.AddOrUpdate("DeliveryTimes.Dates.OnTomorrow", "Tomorrow", "Morgen"); // exactly tomorrow + builder.AddOrUpdate("DeliveryTimes.Dates.Tomorrow", "tomorrow", "Morgen"); // tomorrow + + builder.AddOrUpdate("DeliveryTimes.Dates.DeliveryOn", "{0}", "{0}"); // allow "on {0}" etc. + builder.AddOrUpdate("DeliveryTimes.Dates.NotBefore", "not before {0}", "frühestens {0}"); + builder.AddOrUpdate("DeliveryTimes.Dates.Until", "until {0}", "bis {0}"); + builder.AddOrUpdate("DeliveryTimes.Dates.Between", "{0} - {1}", "{0} - {1}"); // allow "{0} till {1}" etc. + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.DeliveryTimesPresentation.None", "Do not display", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.DeliveryTimesPresentation.DateOnly", "Date only (if possible)", "Nur Datum (sofern möglich)"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.DeliveryTimesPresentation.LabelOnly", "Label only", "Nur Label"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.DeliveryTimesPresentation.LabelAndDate", "Label and date", "Label und Datum"); + + builder.Delete( + "Admin.Configuration.Settings.Catalog.ShowDeliveryTimesInProductLists", + "Admin.Configuration.Settings.Catalog.ShowDeliveryTimesInProductLists.Hint", + "Admin.Configuration.Settings.Catalog.ShowDeliveryTimesInProductDetail", + "Admin.Configuration.Settings.Catalog.ShowDeliveryTimesInProductDetail.Hint", + "Admin.Configuration.Settings.ShoppingCart.ShowDeliveryTimes", + "Admin.Configuration.Settings.ShoppingCart.ShowDeliveryTimes.Hint"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.DeliveryTimesInProductDetail", + "Presentation of delivery times", + "Darstellung von Lieferzeiten", + "Specifies the way delivery times are displayed on product detail pages.", + "Legt die Darstellungsart von Lieferzeiten auf Produktdetailseiten fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.DeliveryTimesInLists", + "Presentation of delivery times", + "Darstellung von Lieferzeiten", + "Specifies the way delivery times are displayed in product lists. Due to lack of space, the grid view does not show a date for the delivery time.", + "Legt die Darstellungsart von Lieferzeiten in Produktlisten fest. Aus Platzgründen wird in der Rasteransicht kein Datum zur Lieferzeit angezeigt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.DeliveryTimesInShoppingCart", + "Presentation of delivery times", + "Darstellung von Lieferzeiten", + "Specifies the way delivery times are displayed in shopping cart.", + "Legt die Darstellungsart von Lieferzeiten im Warenkorb fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Shipping.TodayShipmentHour", + "Order before x o'clock for same day shipment", + "Bis x Uhr bestellt, heute verschickt", + "Specifies the hour by which the order will be shipped the same day.", + "Legt die Stunde fest, bis zu der die Bestellung noch am selben Tag verschickt wird."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Shipping.DeliveryOnWorkweekDaysOnly", + "Delivery on workweek days only", + "Lieferungen nur an Werktagen", + "Specifies whether delivery takes place only on workweek days.", + "Legt fest, ob Lieferungen nur an Werktagen stattfinden."); + + builder.AddOrUpdate("Admin.Configuration.Seo.SeName", + "URL alias", + "URL-Alias", + "Set a search engine friendly page name e.g. 'my-landing-page' to make the page URL 'http://www.yourStore.com/my-landing-page'. Leave empty to generate it automatically based on the name of the entity.", + "Legen Sie einen suchmaschinenfreundlichen Seitennamen fest, z.B. 'meine-landing-page', um die URL zu 'http://www.yourStore.com/meine-landing-page' zu machen. Lassen Sie das Feld leer, um den Pfad automatisch auf Basis des Namens der Entität zu generieren."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.PageTitleSeparator", + "Page title separator", + "Titel-Trennzeichen", + "Specify page title tag separator.", + "Legt das Trennzeichen für das Titel-Tag fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.PageTitleSeoAdjustment", + "Title tag order", + "Titel-Tag Reihenfolge", + "Select the page title order. The generated title tag can be (PAGE NAME | YOURSTORE.COM) or (YOURSTORE.COM | PAGE NAME).", + "Legen Sie hier die Seitentitel-Reihenfolge fest. Das erzeugte Title-Tag könnte z.B. (MEINSHOP.DE | SEITE NAME) oder (SEITE NAME | MEINSHOP.DE) lauten."); + + builder.Delete("Admin.Catalog.Categories.Fields.SeName", + "Admin.Catalog.Categories.Fields.SeName.Hint", + "Admin.Catalog.Manufacturers.Fields.SeName", + "Admin.Catalog.Manufacturers.Fields.SeName.Hint", + "Admin.Catalog.Products.Fields.SeName", + "Admin.Catalog.Products.Fields.SeName.Hint", + "Admin.ContentManagement.Blog.BlogPosts.Fields.SeName", + "Admin.ContentManagement.Blog.BlogPosts.Fields.SeName.Hint", + "Admin.ContentManagement.Forums.Forum.Fields.SeName", + "Admin.ContentManagement.Forums.Forum.Fields.SeName.Hint", + "Admin.ContentManagement.Forums.ForumGroup.Fields.SeName", + "Admin.ContentManagement.Forums.ForumGroup.Fields.SeName.Hint", + "Admin.ContentManagement.News.NewsItems.Fields.SeName", + "Admin.ContentManagement.News.NewsItems.Fields.SeName.Hint", + "Admin.ContentManagement.Topics.Fields.SeName", + "Admin.ContentManagement.Topics.Fields.SeName.Hint"); + + builder.Delete("Admin.Catalog.Categories.Fields.MetaTitle", + "Admin.Catalog.Categories.Fields.MetaTitle.Hint", + "Admin.Catalog.Manufacturers.Fields.MetaTitle", + "Admin.Catalog.Manufacturers.Fields.MetaTitle.Hint", + "Admin.Catalog.Products.Fields.MetaTitle", + "Admin.Catalog.Products.Fields.MetaTitle.Hint", + "Admin.ContentManagement.Blog.BlogPosts.Fields.MetaTitle", + "Admin.ContentManagement.Blog.BlogPosts.Fields.MetaTitle.Hint", + "Admin.ContentManagement.News.NewsItems.Fields.MetaTitle", + "Admin.ContentManagement.News.NewsItems.Fields.MetaTitle.Hint", + "Admin.ContentManagement.Topics.Fields.MetaTitle", + "Admin.ContentManagement.Topics.Fields.MetaTitle.Hint"); + + builder.Delete("Admin.Catalog.Categories.Fields.MetaDescription", + "Admin.Catalog.Categories.Fields.MetaDescription.Hint", + "Admin.Catalog.Manufacturers.Fields.MetaDescription", + "Admin.Catalog.Manufacturers.Fields.MetaDescription.Hint", + "Admin.Catalog.Products.Fields.MetaDescription", + "Admin.Catalog.Products.Fields.MetaDescription.Hint", + "Admin.ContentManagement.Blog.BlogPosts.Fields.MetaDescription", + "Admin.ContentManagement.Blog.BlogPosts.Fields.MetaDescription.Hint", + "Admin.ContentManagement.News.NewsItems.Fields.MetaDescription", + "Admin.ContentManagement.News.NewsItems.Fields.MetaDescription.Hint", + "Admin.ContentManagement.Topics.Fields.MetaDescription", + "Admin.ContentManagement.Topics.Fields.MetaDescription.Hint"); + + builder.Delete("Admin.Catalog.Categories.Fields.MetaKeywords", + "Admin.Catalog.Categories.Fields.MetaKeywords.Hint", + "Admin.Catalog.Manufacturers.Fields.MetaKeywords", + "Admin.Catalog.Manufacturers.Fields.MetaKeywords.Hint", + "Admin.Catalog.Products.Fields.MetaKeywords", + "Admin.Catalog.Products.Fields.MetaKeywords.Hint", + "Admin.ContentManagement.Blog.BlogPosts.Fields.MetaKeywords", + "Admin.ContentManagement.Blog.BlogPosts.Fields.MetaKeywords.Hint", + "Admin.ContentManagement.News.NewsItems.Fields.MetaKeywords", + "Admin.ContentManagement.News.NewsItems.Fields.MetaKeywords.Hint", + "Admin.ContentManagement.Topics.Fields.MetaKeywords", + "Admin.ContentManagement.Topics.Fields.MetaKeywords.Hint", + "Admin.ContentManagement.Blog.BlogPosts.Fields.Language", + "Admin.ContentManagement.News.NewsItems.Fields.Language"); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.DefaultTitle", + "Default title tag", + "Standard-Titel-Tag", + "Defines the default title for pages in your store. You can override this for individual categories, products, manufacturer and topic pages.", + "Legt das Standard-Titel-Tag für Seiten im Shop fest. Es kann für Warengruppen, Produkte, Hersteller und Seiten individuell angegeben werden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.DefaultMetaDescription", + "Default meta description", + "Standard Meta Beschreibung", + "Defines the default meta description for pages in your store. You can override this for individual categories, products, manufacturer and topic pages.", + "Legt die Meta Beschreibung für Seiten im Shop fest. Diese kann für Warengruppen, Produkte, Hersteller und Seiten individuell angegeben werden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.DefaultMetaKeywords", + "Default meta keywords", + "Standard Meta Keywords", + "Defines the default meta keywords for pages in your store. You can override these for individual categories, products, manufacturer and topic pages.", + "Legt die Meta Keywords für Seiten im Shop fest. Für Warengruppen, Produkte, Hersteller und Seiten können diese individuell angegeben werden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowDiscountSign", + "Show discount sign", + "Rabattzeichen anzeigen", + "Specifies whether a discount sign should be displayed on product pictures when discounts were applied.", + "Legt fest, ob ein Rabattzeichen auf dem Produktbild angezeigt werden soll, wenn Rabatte angewendet wurden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StateProvinceRequired", + "'State/province' required", + "'Bundesland' erferderlich", + "Check the box if 'State/province' is required.", + "Legt fest, ob Angaben im Eingabefeld 'Adresszusatz' erforderlich sind."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.AttributeChoiceBehaviour.GrayOutUnavailable", + "Gray out unavailable attributes", + "Nicht verfügbare Attribute ausgrauen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.AttributeChoiceBehaviour.None", + "None", + "Keines"); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.AttributeChoiceBehaviour", + "Attribute choice behaviour", + "Attribut-Auswahlverhalten", + "Specifies the behaviour when selecting attributes.", + "Legt das Verhalten bei der Auswahl von Attributen fest."); + + builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.CustomData", + "Custom data", + "Benutzerdefinierte Daten", + "Specifies user-defined data. For free usage, e.g. for individual shop extensions.", + "Legt benutzerdefinierte Daten fest. Zur freien Verwendung, z.B. bei individuellen Shop-Erweiterungen."); + + #region Media + + var mediaRoot = "Admin.Configuration.Settings.Media."; + + builder.AddOrUpdate(mediaRoot + "ImageSizes", "Image/thumbnail sizes", "Bild-/Thumbnailgrößen"); + + builder.AddOrUpdate(mediaRoot + "DefaultPictureZoomEnabled", + "Enable image zoom", + "Bildzoom aktivieren", + "Adds 'zoom on hover' functionality to product detail images.", + "Aktiviert 'Zoom bei Hover'-Funktion für Produktdetail-Bilder."); + + builder.AddOrUpdate(mediaRoot + "AssociatedProductPictureSize", "Associated (grouped) product", "Verknüpftes (Gruppen)-Produkt"); + builder.AddOrUpdate(mediaRoot + "AvatarPictureSize", "Customer avatar", "Kundenavatar"); + builder.AddOrUpdate(mediaRoot + "BundledProductPictureSize", "Bundle items", "Bundle Stückliste"); + builder.AddOrUpdate(mediaRoot + "CartThumbBundleItemPictureSize", "Bundle items in cart", "Bundle Stückliste im Warenkorb"); + builder.AddOrUpdate(mediaRoot + "CartThumbPictureSize", "Cart", "Warenkorb"); + builder.AddOrUpdate(mediaRoot + "CategoryThumbPictureSize", "Categories", "Warengruppen"); + builder.AddOrUpdate(mediaRoot + "ManufacturerThumbPictureSize", "Brands", "Marken"); + builder.AddOrUpdate(mediaRoot + "MessageProductThumbPictureSize", "Products in e-mails", "Produkte in E-Mails"); + builder.AddOrUpdate(mediaRoot + "MiniCartThumbPictureSize", "Mini cart", "Mini-Warenkorb"); + builder.AddOrUpdate(mediaRoot + "ProductDetailsPictureSize", "Product detail", "Produktdetail"); + builder.AddOrUpdate(mediaRoot + "ProductThumbPictureSize", "Product list", "Produktliste"); + builder.AddOrUpdate(mediaRoot + "ProductThumbPictureSizeOnProductDetailsPage", "Gallery thumbnail", "Gallerie-Thumbnail"); + + builder.Delete( + mediaRoot + "AssociatedProductPictureSize.Hint", + mediaRoot + "AvatarPictureSize.Hint", + mediaRoot + "BundledProductPictureSize.Hint", + mediaRoot + "CartThumbBundleItemPictureSize.Hint", + mediaRoot + "CartThumbPictureSize.Hint", + mediaRoot + "CategoryThumbPictureSize.Hint", + mediaRoot + "ManufacturerThumbPictureSize.Hint", + mediaRoot + "MessageProductThumbPictureSize.Hint", + mediaRoot + "MiniCartThumbPictureSize.Hint", + mediaRoot + "ProductDetailsPictureSize.Hint", + mediaRoot + "ProductThumbPictureSize.Hint", + mediaRoot + "ProductThumbPictureSizeOnProductDetailsPage.Hint"); + + #endregion + + #region Customer settings + + builder.Delete("Admin.Configuration.Settings.CustomerUser.AddressFormFields.TitleEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.TitleEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CompanyEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CompanyEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CompanyRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CompanyRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddressEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddressEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddressRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddressRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.StreetAddress2Enabled", + "Admin.Configuration.Settings.CustomerUser.StreetAddress2Enabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddress2Enabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddress2Enabled.Hint", + "Admin.Configuration.Settings.CustomerUser.StreetAddress2Required", + "Admin.Configuration.Settings.CustomerUser.StreetAddress2Required.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddress2Required", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StreetAddress2Required.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.ZipPostalCodeEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.ZipPostalCodeEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.ZipPostalCodeRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.ZipPostalCodeRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CityEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CityEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CityRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CityRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.PhoneEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.PhoneEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.PhoneRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.PhoneRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.FaxEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.FaxEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.FaxRequired", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.FaxRequired.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.SalutationEnabled", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.SalutationEnabled.Hint", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.Salutations", + "Admin.Configuration.Settings.CustomerUser.AddressFormFields.Salutations.Hint"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.GenderEnabled", + "'Gender' enabled", + "\"Geschlecht\" aktiv", + "Defines whether the input of 'Gender' is enabled.", + "Legt fest, ob das Eingabefeld \"Geschlecht\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.TitleEnabled", + "'Title' enabled", + "\"Titel\" aktiv", + "Defines whether the input of 'Title' is enabled.", + "Legt fest, ob das Eingabefeld \"Titel\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CompanyEnabled", + "'Company' enabled", + "\"Firma\" aktiv", + "Defines whether the input of 'Company' is enabled.", + "Legt fest, ob das Eingabefeld \"Firma\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CompanyRequired", + "'Company' required", + "\"Firma\" ist erforderlich", + "Defines whether the input of 'Company' is required.", + "Legt fest, ob die Eingabe von \"Firma\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StreetAddressEnabled", + "'Street address' enabled", + "\"Straße\" aktiv", + "Defines whether the input of 'Street adress' is enabled.", + "Legt fest, ob das Eingabefeld \"Straße\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StreetAddressRequired", + "'Street address' required", + "\"Straße\" ist erforderlich", + "Defines whether the input of 'Street adress' is required.", + "Legt fest, ob die Eingabe von \"Straße\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StreetAddress2Enabled", + "'Street address addition' enabled", + "\"Adressszusatz\" aktiv", + "Defines whether the input of 'street address addition' is enabled.", + "Legt fest, ob das Eingabefeld \"Adressszusatz\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StreetAddress2Required", + "'Street address addition' required", + "\"Adressszusatz\" ist erforderlich", + "Defines whether the input of 'street address addition' is required.", + "Legt fest, ob die Eingabe von \"Adressszusatz\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.ZipPostalCodeEnabled", + "'Zip / postal code' enabled", + "\"Postleitzahl\" aktiv", + "Defines whether the input of 'Zip / postal code' is enabled.", + "Legt fest, ob das Eingabefeld \"Postleitzahl\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.ZipPostalCodeRequired", + "'Zip / postal code' required", + "\"Postleitzahl\" ist erforderlich", + "Defines whether the input of 'Zip / postal code' is required.", + "Legt fest, ob die Eingabe von \"Postleitzahl\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CityEnabled", + "'City' enabled", + "\"Stadt\" aktiv", + "Defines whether the input of 'City' is enabled.", + "Legt fest, ob das Eingabefeld \"Stadt\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CityRequired", + "'City' required", + "\"Stadt\" ist erforderlich", + "Defines whether the input of 'City' is required.", + "Legt fest, ob die Eingabe von \"Stadt\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CountryEnabled", + "'Country' enabled", + "\"Land\" aktiv", + "Defines whether the input of 'Country' is enabled.", + "Legt fest, ob das Eingabefeld \"Land\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CountryRequired", + "'Country' required", + "\"Land\" ist erforderlich", + "Defines whether the input of 'Country' is required.", + "Legt fest, ob die Eingabe von \"Land\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StateProvinceEnabled", + "'State / province' enabled", + "\"Bundesland\" aktiv", + "Defines whether the input of 'State / province' is enabled.", + "Legt fest, ob das Eingabefeld \"Bundesland\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StateProvinceRequired", + "'State / province' required", + "\"Bundesland\" ist erforderlich", + "Defines whether the input of 'State / province' is required.", + "Legt fest, ob die Eingabe von \"Bundesland\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.PhoneEnabled", + "'Phone number' enabled", + "\"Telefon\" aktiv", + "Defines whether the input of 'Phone number' is enabled.", + "Legt fest, ob das Eingabefeld \"Telefon\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.PhoneRequired", + "'Phone number' required", + "\"Telefon\" ist erforderlich", + "Defines whether the input of 'Phone number' is required.", + "Legt fest, ob die Eingabe von \"Telefon\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.FaxEnabled", + "'Fax number' enabled", + "\"Fax\" aktiv", + "Defines whether the input of 'Fax number' is enabled.", + "Legt fest, ob das Eingabefeld \"Fax\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.FaxRequired", + "'Fax number' required", + "\"Fax\" ist erforderlich", + "Defines whether the input of 'Fax number' is required.", + "Legt fest, ob die Eingabe von \"Fax\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.NewsletterEnabled", + "'Newsletter' enabled", + "\"Newsletter\" aktiv", + "Displays the newsletter registration form.", + "Zeigt das Newsletter Anmeldeformular an."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.SalutationEnabled", + "'Salutation' enabled", + "\"Anrede\" aktiv", + "Defines whether the input of 'Salutation' is enabled.", + "Legt fest, ob das Eingabefeld \"Anrede\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.Salutations", + "Salutations", + "Anreden", + "Comma separated list of salutations (e.g. Mr., Mrs). Define the entries which will populate the dropdown list for salutation when entering addresses.", + "Komma getrennte Liste (z.B. Herr, Frau). Bestimmen Sie die Einträge für die Auswahl der Anrede, bei der Adresserfassung."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.DateOfBirthEnabled", + "'Date of birth' enabled", + "\"Geburtsdatum\" aktiv", + "Defines whether the input of 'Date of birth' is enabled.", + "Legt fest, ob das Eingabefeld \"Geburtsdatum\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.LastNameRequired", + "'Last name' required", + "\"Nachname\" ist erforderlich", + "Defines whether the input of 'Last name' is required.", + "Legt fest, ob die Eingabe von \"Nachname\" erforderlich ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.FirstNameRequired", + "'First name' required", + "\"Vorname\" ist erforderlich", + "Defines whether the input of 'First name' is required.", + "Legt fest, ob die Eingabe von \"Vorname\" erforderlich ist."); + + #endregion Customer settings + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SocialSettings.FacebookAppId", + "Facebook App ID", + "Facebook App ID", + "A Facebook App ID is a unique number that identifies your application and connects it to your Facebook account. It is used when sharing products, category and manufacturer pages on Facebook.", + "Eine Facebook App ID ist eine eindeutige Nummer, die Ihre Anwendung identifiziert und mit Ihrem Facebook-Account verknüpft. Wird beim Teilen von Produkten, Warengruppen- und Herstellerseiten auf Facebook genutzt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SocialSettings.TwitterSite", + "Twitter Username", + "Twitter-Benutzername", + "Twitter Username that gets displayed on Twitter cards when a product, category and manufacturer page is shared on Twitter. Starts with a '@'.", + "Twitter-Benutzername, der auf Twitter-Karten angezeigt wird, wenn ein Produkt, eine Kategorie oder eine Herstellerseite auf Twitter geteilt wird. Beginnt mit einem '@'."); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Validation.NoEmptyPassword", + "Password must have a value.", + "Passwort muss einen Wert haben."); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Validation.NoPasswordAllowed", + "HTML widgets cannot have a password protection.", + "HTML Widgets können keinen Passwortschutz haben."); + + builder.AddOrUpdate("Order.CompletePayment.Hint", + "Click 'Complete payment' if you have not yet paid and wish to start the payment process again.", + "Klicken Sie 'Zahlung veranlassen', falls Sie noch nicht gezahlt haben und den Zahlungsvorgang erneut starten möchten."); + + builder.AddOrUpdate("Admin.ContentManagement.Forums.ForumGroup.Updated") + .Value("de", "Die Forengruppe wurde erfolgreich aktualisiert."); + + builder.AddOrUpdate("Account.Login.NotRegisteredYet", + "Not registered yet?", + "Noch nicht registriert?"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.resx b/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.resx new file mode 100644 index 0000000000..aa24204390 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202011091154314_V410Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + H4sIAAAAAAAEAOy923LcOrIo+D4R8w8OP81M7G2vtbo7ok9H95yQdbEV27bUkmyf3S8KigWp2GKR1bzo0r82D/NJ8wsD8IpL4g6SVV71YpeIRAJIZCYSiUTi//t//t+//s+XTfrmCRVlkmd/e/vru1/evkFZnK+S7OFvb+vq/j///PZ//t//+//219PV5uXN9x7uDwQO18zKv71dV9X2L+/fl/EabaLy3SaJi7zM76t3cb55H63y97/98sv/eP/rr+8RRvEW43rz5q9XdVYlG9T8gf88zrMYbas6Sr/kK5SW3Xdcct1gffM12qByG8Xob2+vN1FRXVd5gd6dRFX09s1RmkS4G9covX/7JsqyvIoq3Mm/fCvRdVXk2cP1Fn+I0pvXLcJw91Faoq7zfxnBTcfxy29kHO/Hij2quC6rfGOJ8Nc/dIR5z1d3Iu/bgXCYdKeYxNUrGXVDvr+9vcm3Sfz2Dd/SX47TgkAxpD1u6IvBkuxdU69s//uPNxzQfwxM8cd3mHve/fIfb47rtKoL9LcM1VURpf/x5rK+S5P4v9DrTf6Isr9ldZrSPcV9xWXMB/zpssi3qKher9B91//z1ds379l67/mKQzWqTju486z6w29v33zFjUd3KRoYgSJEM6qPKENFVKHVZVRVqMgIDtSQUmida+v6tazQhvzu28T8h+Xo7Zsv0ctnlD1U67+9xT/fvjlLXtCq/9L141uWYLHDlaqiRrqmzsu2sW5K29Y+5HmKogwYoxrZp2qTjnSarM8f8tXrcVkep1FZTk+gLE7rFTrPrhNMpmjrS6Pz8hJ3+zkvVrigQjHmD1+UPcLJaXGTVOn0LHm9zotK19Sffgkyt1itKxr57U9/CsSukxPtC6oirMIIG0wvE6SxE1TGRbJtV5gZ2puH9z4nGyzmq5u80eClr2ReoWyFiqPyR7J6QJUvthbLP/Jsejq0Tf0ooi22qCq8agl9N6mPJfmZmTe/kX/AzI2KAPqySPKiWYbVC7qBMryJHqbXh/XdP/E6cZMfxWmA1YeYUOXan4rHef6YIIJjoAA2nN/Rn2kEf30/mpFq4zJ6OcZW00NevLqYmNHLOwrDwcqUt6WxL//4i9kSa8k2J0m5TaPXCyLKNgJozD/XqKqasVjzDlZ198lDXTTQ7zo8Bw7aNQ76HqV1iBXQstmGVnrqOvHs5zyO0uTfaNW36cC9HY6WeQWEBzaWt9VOh93UAqZjlD3U0YMliwB4yNQhTJ+PRV5v5xevof2lmp5Lvr9GT8lDw0CSmXz75gqlDUC5TradfSNI1u0Iflbkm6s8hQR6gLq9zusiJuPLtaA3UdFsG9x0ytAtT1XS4TloEHlbmoXw14nEpZuZjuoqR8Yk7ePa/6rRNcqPMQ5V6wE2QWdp9HC+wYM9S1KkIXcQ39BV5b3ZCuxMCLZzczXBlTqzUd3tZFyhstFxpVyBcpByHSoDHHQjo0al0L3SdbfOONRBDDQO50HD6nWdr3XV01qjQn6bRmP3rS+0hTkviXRdpvVDkglKRFf1Jq9jSPmEs6r8lQJoW2lViJNSuETFJimJcF6huDl8sVYI1yiuiR/yHY/roAjkbRmdjZoe3Dgvd9SMYUb7Em23uAvwegeC3orsM/K4WQ1hBTSs5rUUgm0EYv0R4YH/5W0dpWn+7G1Kckzhu6oeN/OPCjKH1rikQkZjBUWLBriVieQoVwbgglCZ1IEkylB5dLI/p9rg10hLbeOkNtiZtHfMd9VLZsYPWkLe1kzrI79JLxC6xizcLgp+Kuomejl9QZut96k5RtRteOmzym4KNLo2rpIn78PrPsSqZX4/XEGtH3VTjXvgBvNj+iXJkk296Vs8QXGyidK3by4L/KsL9/zz2zfXxNZuvD3GiKMXL8QhzLMJ1hBew9qsO6YjvKrxvgZVBmMaISWj6AHU/R6gvBaCDstpp8SMVgJSp+yXAgbBYQWQtxXIwaHxJBoFgZmeFunUaBh1fB3nXMhKw1PtV3vVXt8x53TOvuL8IcFq7mJLpjsvmP51ZaSbY7mthV4gwkYX2bdqCPM9wV9uko2D43+7Cofsc1RW+FOMylKK0njH0Ib9JBLXcx8VBOtEvlDcCwgQtpY/rVGDqm7ltsW5u9huivM6k6wyfSnYTaFQ6KIIYb2Pil43WBl+QdU6X0mcLzQI2FEYQnSuwGC2XW7WMrCnpKRd1XrsdC/FUqGHAIht73rbXUVRFgYkqQRE6LEMzstD5RE7iKtGaf7w7hA8uKuhX2cYPFDLIQybUNdZKsync7Z4+kImO0o/J9nj9NcfotUDukEv1fSEJC1dV6/jNtvNm9ncAwwQzqGxyDotc4M2W6xlvU82DW+AhJGPXbwBEmZkl1GBNWA/Of6TskoiErIiINL1AnNy8m9LdxU5Ghh8pTc5Xq9RXPGoDA98+2oXzQSrOCrMVvKySGJ0FWUPaHruJfdSLrJPmEyXzfGy39btU1QOhvLRdpsm/hE6Ia987GwUEubOAJcfPYKZ9maLfILuI2xnfk/Q8xd1qJ2jtpXuCDp+NtwIdtA3Ob0PB/aEAJx8ewgB225sBjWsdgtQYIBfYCgVHaUiSFh/rp3fQtq9MD7cfmYcdlgDHw2/DnsseVvLxKj1M0Pa9TV/vpXoEmFezKoA62zfMRGj5uDoj85NHW1aRp+omesKsz5ZFUycnsKllZVjzSv0rzrBlsBxXm/zjI7ddr8KymOabNfUz0xj1TRS5n3fZsBEiFdaItOtnOxaONXiyWt8o5XWeiRfoqy+j2KiiwvTwXB1VONhQA2GxMI7jwp/X9WxsWkzgqvG0kMZDGMADWoyWDrnpd0MYzLQU+XhmKXRHAwHeVvLOGd/bp8pzXsHl91OuewOTrZddLIF9juFdIkdfFjz+7BCeElDu6mcjVO9swo2ZsP4q2jcsM8KhhCsPAmYn6k39sfazmvqvhswHEw8eVtneYqVheWCp7EL/xBE7x+lqpPeMGvLPHYFOSbPSrWtFOSu+ResUOng9PlSCDSypml6muQBgOFlu4rjPqXW9hsxS1dNut+J7d8fyapaW3XtE0oe1pVVld1d78/LmyLC0gPlDlzGyvqUrLBy9cXSrUABwgLICgDdaDc1EtolALYQhrW8B6JsA65MtAp4AFvHFD06TfdYUKiTNISiqwyYbYc7F9yATRJLykHBppccSowplYPaGow3keR6C906e5OFLVGQtim2JSkW/vhR0aWmXGK8AuUC7UAgr8DRFk0nLl6ma4PjYLzK22pjrMIarxNdabxOa1WG1zAhksdRdoJILvBeavyWqNlMnEZXHtNHpN5XvtdJuirIKi3VZe2a1LKQqM/oUlhpsCC2ila+PFituXDPgEXZ/C5ES48QZIOXApCy7pr2KL0jNz99FG2DIpieffMhKhF164peDzTn6aU6W2IYJdE9RICV7dpXQUCORveZHMw3n7nskBxWTXlbJ5RK/5BkEbnjYqLSw6So5Y1Vj2NcHtVh0uVtdbSaLg59B3IHqr3d3tsuIeGJfodmuXE06/cArOh190Hf5x7QL4dZ3yNfUT5IsFaCQ4RTtsZXc9+8SRccQjN8T8oEQ59nq+QpWdVRmr56u8gIyiSlXtigBYIutY4zxL1kghcovFShJdplwmWaZ47mDMgglyfnbO9otUmy43yzoVzAkzXWS1mgyJzAN4Q6POHO+Q+RQ4HOKqlgniv0lKBnb+/L0XZb5E9odRWR50Oux/RGbjP9Na8CY+zRNSmYuEF79zEc0h2+A3f9WE8vdFRsBl73q6/15m7UGpM1+7FKVAIeKpXPx+S+Oo4K7zO9Hk8I66q7F3FRrTHFhyhvv/51OEdjTXOvPIhSq4ktX5GUQenr0WrF9cF7TOflSf6cpXnkH/fW4fG+ZpSlrYD3CP1d6NFLK3AX9wJOx41zh+b0ZZu0L0ydRK88TjMUTR6s9sZKALb/FJXXEbaaUKhZZbFZnvLg3nwrUXH0UCC0CRA7wCCbJQvFeXlFkhsXAXJgDoiOX+MUtZ3y1XE0xktUJLm39A04m7W/QewpK+clSQ10mpEq3uxIkt6HS0uK9WmzyYzSHuPxmrhgJrsjeF4Gy4R6Xp6W3uSkHlf0dkpGGYl/yZ6waGJ0bRoo751blcePf6+jzjcTwNfZYDx6ihJcl/FqeIZ7gz11XrCSLODIP+fP7ajJShPgbTS8QUjuXxuHwFle9H38gKhU5m6IP0TxY/MqLMmL4NvLZjdIMJ63tMQ7kGHT621RNJv+LpNumElqMbYpdMNg7LFcV2gbAtMreTy4yFMmwJS47PqGmHL7cD7E4fFW7oQB0KrDmigvyQQy1rEuIB38UL9+qKtqdK546BYC/SMp12lSVmGQdsovRVh48brG+K+c74zj3UmDLom9/WsMkuAr8EW6mraBwYFdTphqYIvxRKnlQMxxDqkMSLCwQ1ICGleX2sARU+/OO80qVJRB+KtT2wxmNDFTdIp91jbx5usmQa1Mei942IZAZXVUYdV5V1foON/cJVl3iBqQCccG1jlG+wGto6ckr9kMynKg5XN9/WCi/oPPKbtrDI6eueUQniGnpc2wrvlqrwGRn+oKdz60azdc+zcwhogDTxeTyTMYYXzVu3vDBk9N8oSKV1LZ0svW282YErZh2O2CVN5ERXJ/rz2U+EOQKSDqtni9uL8okodETPij2yKWqDMrgviTBnxfUFTWBSI0VFAgyAXBoU3bhFSGaMkPFrUZaetslaLmqFbjWg0ji217l6g4x9IfyqPHICXUCI3zep23jkK8xvifSyTZZdKcCto71jGXkHPZrr51V0Lfvr+0y6Skv3M/gFo/y8AqGFU4360AK0Tz8SCyYD4Bzjb6kF4AlH1mAcUO0+XS3jJAjoGSrVwQoVBesRvBbjtJkoZOykBll+2k8NYXLrpNuzq7Gwv1qogB5UFkAxDgHDuuT+YGAKpiWAEw6Y1HCNZxHJ02M7yyqei/NLRWG4Pr2PMhSkbe8RZE0WsWQNZlDsqxv42L5j6JG5BhR63sP1xFMR51Bdn4NLUcxyu9U9vjZeBErUoVS5UqDeOoU79jSxxb85CjRdl9RT3FBBnUks2SSVXHqeJRWw3cYrS2Q3QeF71BU/IgCygyIV0u5UIGyJYN+eARRWd5ULG7LIS0wxyY9eV0yskJdHcohjhDKBR4QYTwe9CotRKdsm3jbSE5WX/X4Thc05C3pXEmTZR/pzlxLD8kaRoiNqVBFuwB3uf8MyJEPC9zTWrn30KkdFoXCJk2+IcADX6tNwhLKdeYZ6zy98h7ix/OdRvMm9xhOs7zxwS1UTve57+BQ7KPViuMpTzLi02kckSFifHrHuQ4rosCZbEYHmO+g2fwwI8/tLr7VoClnoCAQcSHICRw1ks+Vrlk4/yUZLI1lAG5HdYw6pFACEB8IhCE8ltPBwL6LKgdksOKqlCw6hXV0DFuG1bSTYxmAZkkV8wVJpOha/7Prjr4c04QTH3e0B7ytMq0omyJqdrb2TeqfgeJeVvddpqt2qfupw54u8I6fNUQtHEDBzqUa7Bii64TO8/487GPTUh7yD6OWE9Qlm+I/2N87Tt4gAPXJHnMgYmL6bVlA4cnvAUIkzCEWbq9FlsG02HFVSiZ1j7yjtZeZit8d0fOJyPNFeMwzwjtwgqhO44MaVfzOwG19e0k7+who4e404gO0u4sphPZ13maF5/Qy/coredvfVYjOJzxd152O25B22jjjq0viWIqaOrYpgcaj8z9EwWNuA6SLW/rMkxqHybAwRdZsCt1ccgINvmbkK4L+ixBpfPn/klSzIL0+agflcjlq5t1vbnLosT72lWXguqw659y1y+P3uuZouURw8AsrtYtvUgogrTk1fQBW4q6tsfrXFTZtJFokkNsbeSaZQyE4TCMBmDedVWn3Q0NbqpD2h0c6oMZIm9rJJr3zeqe6MEQNfsPX2znJCS6fcAv5GtktC5cRpdq5dZAD/tIch+k6i+4PaaDnMrbCmThh0rrcV6eYaunHjNO7Y491l9D+xK5hcjIXXYDwxsEgY/A8ijw4YdElEXACcwGl7B1XX9DGgyBnpUGsB3UzeTqhib3z6Ry5GnOGW41vHDBVlLfumD+kGUQl1aYIve5x9URk/6H1CXtHQl/LdLimexVkj4fBMl4Q9LjzSGpuly8YVxULeFmSdHXJtL1I8onlG7v6/S/UdkcrQdB9jV3weUjobYXjCRSKbmG5HQhqqNFhsoSy6ruZp1Q55aTZ8mIpDXUd6jk1TxjElupdlE//VP0w6+D7SJvq6fRxzoZWm1/2zpjS9yu7gA2yCPEG8rDPlkrl1FZkmTyszXUxhb6P4jRYruOlI9Oh+n6rO8ZBEz7eXR/n6RJgEcRmmyQ3sc0gTPKHMXMgeKkGWXC8NHnqKzOt92FhckjnYIeQZGuf84fSFhC5ZLgiNTvc4pKUdjOXJTWlS5qLEhAiM4ODjNhZ0lRVhpG/O23EC8KktmYpSHy8ozuXD1M3Hq+2UbZ9G8x9maENv9RGJb4kBTV+oS+XWAob9hQWin7F2ax+h51z1OQsMK69N9vbtA/8oxarybrOV5kO1cPaci340SgsGFTb0hEQ+WiHNt09XMYGH1rJ3j7EjcehvBNymMO2sVPsrXrpeuWAqMuuwml4j03EcR2S9pdyR1WaVU3eVigryyIvMMcnG2vee+QqtsdzO249xT7zcMIm2MpoHUKpv4RJoy2O51RU50CVA4BgpMOAwS2HcrpC9mTRulRXa3JrrRNS3KF4vaxLmBMqhrg4IwqCKM0q2X9IjZReJd5Kcn7NRSDAxFLhV4DIE5dvMm3SazoY1Mu7yRbDPeSg7HtZnPaAPewKQI7x5YI/eKKbbt0haq6yMgLQkg2wQwI2EUYQuiqBMy+y89RsbrMk6wqPyVlJTuVBeAkvZfDAWNQANuOpM/kYLQICcDAKsTByJchHtD6gjiV8lCe7Y2HAskvBRLvikshbQn/I0pTVKmYh4EA+w1DCJ2WgHl5kwcWsHcm55tNnr3rEBwcyfK2jLwOgfYosxy/mbgdwu08ZmlI5zoP04re5xCqHfhyp4ZP6Yt+tqme6Recp3sluNE1v87V0G+TN/SPZEvM1SjV5KAI9PDxOs+Q1g8VSEf0ryJOz+xeXmunm7W9rQPcqeWKBLuJL/fKp6PsHAcpdpG92CvrKAvlFT4juBwCnGB3uA72h0JAOlJ5R84ZHET9FujR3/6NaN+zun24VTVuCUK74GSbNqmvzkmsP6T5w3DWbS3SpHb5jsKxG/FwXWdmiTEjgydmgLWAyl3SHUaQpSha346AIztB5QIrgUDebNR2xYOHCILDWqDSrMGuvobaa1q/gNcE0VFHcUMf2lg/sdg22rzP2m65B+qadq1+jWIywx9UGdQCpaTsiBM9lOdZ12vvGBvyqub0mjJfhdhmuiTL7ZSdf/bRzrLp8B0DL+nYpiHIq/BI20z1E0/n8JaaQ3xQ93yaQ80vqIqwTn5uz9jkYSZBTFnS2JzZG0h7Os0cZmSBEzFOtI2n7BT4ZMHJGuJd80qTyfjST79wSPvZoKXA2F6ypaDFxoHYB9+zK5y6oyI00F8eSN5tATLcVR4qVD/UrR4K5W5saJje+boCfkRlN8Bw92K5yxhz3uNQXlXRX/9wYsAhVuF7ToL1rLmuqV++Y9DsBqcNXfLlspYyofhr6JY69oQ0ekvBAgEoLIjAPTK4MOwSgFUOG2R5W01cjnd6hEAe10DeJ9ut3OVeXjgImuna3Uvi5ua1Dn2TKh21U9dIQRK9pYnQs9eS0lg9iTa1D9jTEJYGhEhLheXJiUsB+avzrjNe+rzBcVDo8rYaKu2MQiezFeL6QPeKUIBbMrbOpiGA18uya1+z9L1AEcLI7PGEmuIeX7vs2HmmDuuhR5g1qLQlodhWi4uugx0Q2Lvmp6prLYD/WuK5jBxWEHlbDYE+Fnm9nfi1BNPgCetb1PO5n7+Olye8CBVklSGqOMh+7mdca35fmYtHGZZr81saiNPmVBmszWkAf23edcJLpTc4DnpdoWR+em38U8r4tMeQw/OsR3HAs41WLv1VD+hEgXSTk+r5ijdl7oF8pPY7CsVuHEb0cXyzXIyZM2iQUNop37aUSXuMIJtSE3s7Ao6sCpULKyUI5LVajl1x59fDEzM7GejXPHwyf7NnjcqZPaoxVFTmUsGLi0Ry/R4D83Yq7OoQWRdkZI4hYcMSCoZYiaXgesyB2HpIqSUdtm+dDAfezlVaF0Gj1wbsqug1KZCcwoGj1/I0/Z6TzMRtShYH84egKN/xiHbDaCe9OsrKZwcPmXz2B5xwbBlHiFsangook4OJUWQKWO/Z77vmMe8tioPlq+bDiX3+E9mT7d3ri/sulMNnAFO8b0AIKxXDTkRaGFb0qCJQ3Ohy6+BmTlwlucT99YSQNdxApzjrCS8NcdAN8rY+R9lDHT14R3Isox/aHMadrT656Rxsd3u9zp8vsk/YkLgM8H5rs3382KQru8mdgo0n87cvs5/23VtKdWsvLFKVejtCsEp0KAD1/Vjqou1bHSvX84YLEaTPoYXKSYcD6SaD5M2g8B20vLytUCf1NNn9X7Ca6mU6k0QUXslUZQkplJlXbbtPkFkPoa1kNgzyzWooTQWvLZ8qH6uXPlAhPiiG6RXDPInn+lluR3efzJCWq2+ys4lmbPmC8HLDffM0dRTHqCznafCSJOLC1qXRGyShU7Er14eJElPzatYunbWTru3Md2ul2uZkbv87qE6F0BACzZQcsGnL+wWvZsC7coeEzfvvfZGETd9seezbkDfMSx59R8Jgu4xeyWFRUGRfULXOV1NqXgnHHNdFgbL4dZYkoX1jV/QbMihONuSxycsC/2q02Ntf//z2zXUcESL+2VkWwr6pMrwtM49aua7vKvIG53kWpzcErRGx/mg9LKax05d5GmteF8Vz07yYNtcImUbnGWmndeYZYdfYrCPDDVmIsn1jjHLEq0hCTIQoPUNoaprKW56awPKWp6Z2h3/61FwND03OpL2sT9bKcYHw7HyI0qhJgjzlWBrldYVHs6KcchM2NVkTeCeBB4FWR5tJp4Z+/+QHKhCWowD5ftcofszr6qjCgnFXV2jOCDGh8fJ/bWZ4VKCzpUIdSAZ7A3bY8Gwnp0Fzkkd2aeRNl0bkjzFvscagM0thTATD9BNJujzLvkKgzXRBk1H5iFayKZl0hMdPT9M/4EAaOn3ZJkXjGfmSZ9V65jb/G0XT05OWr5MELwDVCbobn610Py8fUHVvM3/K0zleIRUbnokxqYY/RNnjLDt6rs1ZVAzd5vnxnM0Nb3LM1+T5XTSDcdGtpo0BONfT8sTLjoX+342muSmirIyaJNUzPLcra3oWkZE1foXKOp2e7MfRlpwMzEtwsdGZRntd3w02+rxDvqyLeB2VaM4TicsocQ3P6l06rd9h+nnpmiMOB/JyWfeI3exu8BOUouqnegpGsRO+QuQskfIgGJ3DfIrKr+i5c0t9zclBe8y8OOgcIhnHaFvdrBPcvwh/bsIVPkXZ6uLJYWfl84R2+zqt/P1sqFw4PQaBXOOM/F7fhbvmHPzUe9K+ldEDUj1HCgHedkfeY2flUEK4owLU9obWx+S+2SVqBwEB3n4r0epHUq2FweihhUEZVHF6sln+wu1QLE4GVwQ/3EyVO/XsqzTp51As6dlYBPeMKrd/oHmF0AataA15Kn2WD3pRWcoUWmCjB5v9WIKssPKbk32pSHa2BHjdmCme4i3pDrP8IWkQQKL0fJ+Qtn2DWaAnUKx5fVlOWeMwI1hjW0cd9WhKULMf4pDkbQ2n255+7lb9+jrLp3n+ZDxgMzUFxhoaa6D/KAi1GtpWuDthncKSMeo4qw2cJB02a1zjCyFsBzGXt9XTy1dCmaU+BLLvUVpPdyY9kULpqWlulo81NBZ5/1GQSzW0rULhLMI5dhdGA4KNTy+F469kDopF3lbvPaS0ApSPzUw7hYh/nDhg47zsO3sUV8lTFMAn1yM8zuvtTK79K0yMbULcZHP4LofW5rnnc40ysuGeY2RtU/MM6wveFVL3vSdr57wcuaNxoi7tdZ7IU2a/JBv5x4QF3HQ0sCZVjudWVkcckQRUuirL4P0vFTlmqqQvFh2SVZpsxjGRvC/9BLHzL9sHA33R/L2OutF7bTqypLosknjyyOyhockDsecYzSwjmTpwvPdttDbi1CRjW5uadosE+84b5HuSP2dpHq0CJBA9L3tkwcz4z1hAshL1eC23QERb/0DJw9pi66K5IllnqxRhMyuaPpyjVfDHzSt4U0kTSpMnVLwSm9GStt3tOhpDuFP7o7LMYxIpvupNJ/iwKKDNJrNDdTaev7fZ4nwWPOUCzm+NzeOWxzTdGqCgjnWFiq71EP5W7le3B41pK/dryMeMf1or1zfzHJXxbL7Mc/1933yMkdnJHbZGE5jFQ4DiBsRLOAkbEI/glQoIwHeQQQXfBcrn0BI8BA7umqP7ydTEjuS53HkTaQZl/B8UJARFA6rgBLWhBA5//hU4jspkNAEPv/roJwdd2AciDaFXB/0nbyuIDXJTRPEjpvhMwfd9c98K1Zb9t1+CPFDR3NIOu7FtGBS53h/od4GO1Wc3tIwDIHkNI4mPtA3LlAfr0hC3o8IRO8gASAM1WSivqEIaZQgNeDhv0EtkQ6cgQYQu77mFOSrQCkNIOZDJKywtTnLAxQR7bIsYTAdRUIjCLmXJg62TReNnWvRT56JhWrkk2fnwrOxolKEQLBOV1MnNGLhP3rhunsdoy313l7+Gse+OVpsk09/E/qNja277T/aaArTzhCEEnSwBCxe+7XAnQ93HIC41rLmzK/Qv8iiEuw+bQXNYMuRtBVkygplOodaeMNEarbY7y4uWm+b31Hf8i5pz4iAn6n4dUN+5Npta/rHQ6cIjq+j+/ms+R6q+idchYVIo3RYmv26bcO4mb9W5cB40owXid63f1WNMEVTiK4YgAL8qCObpTM0bf8AxXsH8ohd5TIdlUaU8AiyLl1HRbaktt0Ft9IxDRXqKQ9xnCHbCFiYIc5m0jlgkUYF5iAQhTraNC2Ox7IMqHplb4tBitdQtDU+7taRggHNLDhs09YjQELSWSIH0/XbeA6ril4RWgDAmGYy+y0GCmjok11ssa31enUEXuLgWoypK84d3SryH5XFyfQ4T/mLLJh9zXCtIwsyzJMUDg55o066jzxdZN0rwoUujECuXZyidZFjJyJBAG1UQH3w0qmV92KfgAofRqtGZkkCFxZIuSlR+ewPlUJ3VogrtQSvaqrP9fJ74KE0ilePkDyE8GeFe622jV6bN5/AFrZIIryj+u6o8zVWRNr8GPq5QqifJ47tLq1jhSV9/be23HsG2s7JdCR7KrLavLlrcDjgmWHSCLzeHheawJPhtLrQ2fNgdhSW2syhG1XVeVBSuRqewBS5Yb9AG07BCnxLqOGJAzZbaXl3MVuilVS7kQ8AH6lV6TBICOL/yFcIHPRS4jyfkJnJ6g551e2AkByXrrGT/aGik2XrKCXHLtcONYJ35pzT0bkeeKAELji6WmWYMTAgm/x4VSZSNbzkd55u7JIs8N7d65AehkLd1/VgHkAl1Gx/xyjd5I1+irL6PYjKJBcmrrr3+EKZZy0MkNbK2I6Gw/UhW4ZB9CptNICpRQznb+FBDtOQHi9osWrUsk4cMrSjHgMo+DhT06JGLoD9kJNlhbFNElE2uCm8jei9PgrHSjB/DnNA2m4iLurq4b5C2oRnh1no2zYR8vVcsf7csDsEWMK0q85Ib1w95yGnQtvyUxKCWy2Bdz0toKXYdLIvDasR0VZdhM/W9PFHsIBzytpP3w/Li9R2N6GD9ydvSbIn+NMmOiPx7mdZFpNLz0zRtls3KsG0jv9fnnNguMzUWwMl2Xp6g+4h6o8xsCfPdCobf/x3EXt5WIJutQxPsYPQGvVT402Y7w3OADbfNks6MZMD9V42XJu+ccNTqm1VFnoYICF0osIWXVgtjzdZCczbLePZ2GQ/oqDatYztC9XmgwVB5xM3xu9LNCNa4lRQa0EBZX+aotEMSwpMZcN06LFgGC5bupNIxwGInTmFnzbAa9MQznNW586ecHL7Tl21eVF+iJjR6gutoxmtSd5x6jdSnQQC8yQplUk2mlI3qBtXFY0MBtfKI9KCfnTVnqKMWz+1GEEtvSlGS2HtWYuipSSyGLvSBFkAdARSVDTWKCsMEeiW4TjnoE3NRH6fW/xbJDliSge6N73r8sjCp5MB39U8sRvS9+uBh3W1+wBkaOi8xJqwgURwg4X84Y7rZ/obwDX1OskdyTXbJ91YcLOFdWMLMVnKTRTCkl727ThHa1d6gPSxnKs0H0u2nWMtCPUd3WMqAdg5L2e9oKRPd5bvidzc8hzBz3jstaP07SA6LV6Na3vUIDsuUQm47Gnm/8/dtfLiKSgvurE5EVBMe1t4UUVYmlK517bZfthXj9VHjqG4m3lfhtVg0hkCYlZWMsRPIqW+mHa+j7AFhu3ZG//0wj6By7zn9lgIbVbdYKihmAMRL7d4kqOgi3J03DQOOg+qVtxXIMgmaL5qwlGM8um9c0ZSJuY6jNK5b0fuCqnU+jLARwYFbATDvZIWEoqDgD63esqCj8MMQggKQgIWMvB6bGKCgbl5KonRECC8N1Sg6t9uTrWHYIzhoJ3lbgVZ+Z/kZVjP4eIpe7NjbjmyJcLDEFXudGrV8RB7D8WRFguLAjPK2grmKjtK7ejN7CPqMFvlvf/rTJDeauz8DtBzGbB7lBjacoXJhXQKB/Jzw0Svx2fXWg7VS6OqX7xhEB9Wg4EyaUNevZYU2oU5ibQPaMLhZYF+oHOJ5na0aj2vzRtppRjob4BXyTYIn4iZvJibgtcarOkXyoDV6Gm9HUMqLCUKIfkoYzO89C4zltONGI5EmFcpepsfaBzlWMHM7U95J8DEaUn8G05Vr+WJLRq48iPptkoaZhHmTeUmnuDTSzTmcz3+QmtsBjMrlL5SKefxFEO8HUUnor/PazjwKOGI6KAV5W4FiTHc4Oj/gLc6HDNP6eE143DvYfRkjgBUM0AqQgIBvIUJwXirgeI3ix7z2ugrUveMhoDqogV1TAzNeTQ13X7ThewLfeVpD4T0vb6KX0xdEUcMVFUZ0jNngIS+8XSI7djtWoFmYdDuTKWNBCSkuf8Kwt4BKHJW1aR3BSWtc0ct9C7cSUKcfgiS1x/EcxSbOsz5VvsdDaOAsCtU1ImbuXO2AUpxbo/L2sLUqDuOmlzQLuux1sKZjCuTKv0JxXRBW6TyK7o+gD8/ZgggP64O8LZ5k0zxnbhtM4vU+m3xnzI1V8solyEO3YmX63UuzOsBLmIYVg8pZOAE7SJaCiV/jFLG5Zx2lgSC6REWSr7xzQZFTrAZf6YfousLTTkQyRBhuqF3cCTanAlhT51lSJVG6y5qM7qKRFrtla8hVFwOo1VcstK1hI13/51bL/ObYWp87qeWG6fGnz7lLnB2u9UCOOigsB20sb4siUwgXVqgXf/WvoU+UH2kixcSRGRRlCuZWgB+lVwEm6CUVbNCna+mGoFdroXJlb8O8fi6QMYQ6IfAHlSJvqw2Fwj16zotVAN9HWEfcb9O06hQCZczHfkvhYQlUH3LkD5/RE0q9r8OsSZY2VJbUY2EzBwHqWw90arg9Wq0K3FgAQXM0KHSxmQ9IfYPz1z+FiYRE96goUDFLY37Gicj2D0GerLFs91NVbbmrUNDaEIBc30pUBLpMEsRIkhlHSqMooDF0f5+kSeQUOzLULcefhzVF3lanHr0jAsJ4bhzdSPLdTK/8oZ1Azx63AxS1DeALxT2AAOG5AUivUNzYofZRk8SrgdG9G5AcOF7e1ow3jSZamJQ3ka1dt+erNKDEae/0DjwqvdMLQwCbcBDMSwqxceQhhfm7of5BAH9mAbxOa1V2jIlaDRYrF2UPNd522M2AsQw1jJHEXm8B5JtNnr3jMR2EamqhwiP6WOT1dn7mxi3P32ioa0HTpyMxT02zRhvUJBm7S11Er6lfvmPQHORO3lZDqACcO0vMpp80/BbmSu6U3N/gdrHdmqjx9r8Dt0/Oh7Yp45Quy6lsvDLUbfFme65x8oaR4LMc75Bwx/H/R2lKXMvet9w+5WU1w2PPn/OHPFgGk7Po6Twm+ancQqEvswef6kfbbYpu8jpe+2D5Ut6QShs8h34odIHdu3ZH81O1ST/kK8qUne61wzyrsN7rH2P+iqrnvHicXEoviwQvMK+NKj6uiwJlsbfh3uE8fYmb7I1XWLs7o1akP5M2At9VJSO8Vdaibq3qgMX7q9oa9vndxJnRj4wDlwyJgVKPhQX1u5FPUHUPcnnaRR2Wg3kkb+sn8GxNaZzHa7QiuUei8tH9pgipjTlSxHVgTMW0UuTyPzG4qrMswDuJX6J4nWRoKUbHvBIqQuEsyZJyLcWm6UkdxwitHGufFsUM5h3+84GccV4ivGqN0a5m5nRXVx/y47qXkK7nNNvD67ioRm7ZStRqroEV13RdBb+VnelmADV60J/ytjQ6yjRoyjoTp/ohm1CxWnl2+rIlIqrO5hNoq9/GN0tb+fMOhb2C+6y8oDJ5t6mfsfSMBfYW1/YiY9S4a+e+opcKr80Oq8h5+SlZYVnw7QFuHS8R3cIeLqgAtBynVuf8nSJj/e+kzv9eoxqtTrFuTo+qChPQ8Z5nt8qW70CEBx0vb4siWIj3BvAkfM7jiMmQRniUmxEB1FYhRePt1Ol0tqsLcpkrDl+wxtOsMlO1TOjUTamOVI7pIkC5hrNFaEAFg1UH75T3oqOG7ViGakbD6aBtRtRXsR0UhdJiTEwt3ZCor4Yjomt47SqYfgZZfA5LjkJ5c9ak43EXZqf5Nyc3+fxtXqFt+hqkYc2W6HjyJj7E8eRtXNd3/0Sx6m5yIPOAnNdNf1oX8k7RNRbQmyLxzqKB0bi57RoFHsd57Z+xB3di9SXK6ihNX8PtucbVBb4+EXid43db5iuj6YBokutGdMsCgwNhYFTrNAvotTyz3XJfn2k8hwVaI6aqbcQ0T8l00RS63ZPbazJAgM/8I7zMC/4UwTYorMQzGYZA1lvustRkDJio5daJeV16vx1LXqJF9xGWaryqNrKAa02UEuA42myj5CHz0Vc9joOukrel0RZTxbJrbcyJGg5kc86adYcPevRMWg1PxU1+FFsqCIvX/BqBvEEbvD45Xe0ZRJpDdZBsZ8meyJf5M2/wv2DIJhP+9Mern6OyapsrUAj9rHUrtLZ9eyddOb4weU9+LidDqDuWIff3oZcJH3eB1Enwq0uk/FD7N6/af9DWNl7gvqLn8jMi+hnzNfXEkfs6B2M8LHfytmCKfayToQ/tbxeRnH/PFkafhHV+2gf5ikh+5MUjJuHEl8nP8qLeeIpig6N8J6A6yKCCRUJKXqhcq80M+mfQ3ibxjmaFViblEvgXTNElhxJc1ApQvweei+QJ02KI93UVWBbPQVoV0hpCoRMeCiWpN3koTPrtxR+neZCePG03v3+JvHIXBXiJrsvH9uH1qK7W/pGlFMIrFCfbhLoEsIxJI9WhNBvDz2gzauWWhafe0paDiQ9qK2Bt44tG0THpPQ0t7fsIpOs5Bem1BHxBUYkVcfsYl8MKcJIUWOjz4vUdg+mwBsjb0jgJJ8phvXAK7StCukF5qJ94+7PrMaz1pXZbMTnBGi8r3TY3gqQMyA7CchCWn0lYzjfbvCCvDd8nTpmtmPoH4dg14TjL01WwLNjW93fT5sJAmJv7YTAFua7W8nxjRqIVNq4j78ODx2TruTWMHpEfhm/bFR7OReZ/fIGl7ixB6Yr8NUOSo57NjvPsPnmoC/Zy0FRHWqcvWHvRcz9hPp203mRDtpGJW7tCJVbQ59m96rg4TFPdNT+M3CmBgvv9dGbNkl5llEMJ+z0FqN9t9Ncsdk8zQ7izT+DzjkJ1WKUVYh0k10zLH+rkq79O42AzSnMz0YLfZP56qRbaBjQ0/xSVqoubfwz3Js25ZcKOtlbfp6nXjKYxsoALi7luM5XFbt5D82j7F6IsT9A2zV2f3eVRHDSavK1uVfJVacsIdSCDfUajZmTKEBsWg3D8QNkT9dH3YRrSJmwN0Qh5Y+qmiLJykzTpV0JMBYRzGEmTgqBRSjCYw865dWtpEhGEmZPr+q71E0zeknEUZSBGaNozOOwMOLgw0YlEHpMnREJcA9xKcbnbokjn2fkKgZ0VvzDfDsDjvkoGI+yqpIB+l/1e/ByeTP2D4SFva4cdnhNlPyRqm/zqSDW93m6WiEBj1VqRTwkmrGmTEz3V0y2KvfB6RpOHMS5bhRDSG+znFxOZEjPjHJtO/CdZZ+fwvvbs2Pp8Z3HDzriHuLi/L5HnRd7meoMfig9RFa+vk397HixcYq3RPiDiKWcBzavjfLNtIq9sDNJg2Tb+kWyPingdIoCd1BoflvI37kZrC85P4WXg8dkotJZgMI8/Y7RJPf5yKIltGt7j/yGKH88zLC/xo2d8/jFWimn+8E6C8WC5ytsKEviL/1zVsb+mChTxu0RsvYT1wAh7HawggdoK9s9UNBNmNZKhjn4gHajxOHp4v4hRvDrcRzGR4sLjenmvSyB0B0Uib2uZbej3BD0H8hvuWrQaZkT0kBevAXiZR3Xg4wMfz8bHnXIPwMYcpgMXH7h4Ni7ufF6DEeTMxCyiAw/L2xp2Fb8G2p385odn+hW/yMsS2+CpP5fxqA58tqt8ZnHVJ6uvUNwELtjzxYbky+oRHLhB3lbQgybHQx8yU77u0tHemtjP+yNZ4U37P/JM01SQl3+SKp1+RA2nk9fYfOcg3Ju2O5PoT+VVOsesC7vSCUOT0lb93Lb8PfqRgGLBgQ7BeL2XwyL00KkjkoNelbfVUMo7UqjAjRF6B/BYN4eqgQI3NEMnyfsmP0mdRzder5tjLOq0ZmpljIouvJD8rTq2DHTcvnvK/wN6SLKPRe5w+CnO3vPpyzbKVv7j+5qf5WmaP3vjQc8/kmzlj+g81j0+GIL/q1ellP0aohEykuM8VT7LGmY4n6oN9VbZZM0cl+VxGpWaG5IhcqXug53Umj9hzCT+iE1lSpmZSfWGdknk+WPSPYV2Xp6l0UM5DNfcYLpMa6zByncjtmCWEmYPrF/TVxJBRqlYdia+oM0dKgZP3r/qpCD673uU1vjDL8LMMfBHWZS+VklcDhV+VVe4WScFto8KYhN1NYBkoi2ZFaS/qlN0HefOlCf1y3cDlgUpfhyRpxrMqD2el8Ok4+EHV1kH/gcHSn/OH5I4SgmpLrbEvCWK14PmAL4FqX+UGbP6RSHjcSM6dnPxPSmTuyRt9gTuOgM++xlxL0jRs6aaGUmvUVTE6zagktIgmlrdaC+b7Ie9IGjq9O/v9oLwi/sMHufZKmmt+9ATOKBecP6wxWk6fXh/XRd33X7AcPK+lRSwbtZOog2e5JXftP29jprFnFxKLvLUd7nmZg7AvuDkXW+TLKPWBx19semF9xaZl2I7qrC9eldX6HidJzH6gNbYtMOGV0Aiy5pYkNIfC7x7ratvWfQUJY2la0r0r40z2Iejj+uiQFn8epXXWF9kD2QtDUDtMdcZ1MCSRinpxpdktc2TrDqh+VWnoOiK37amNupR+hy9lk1lpjWNtUVVo9pysbkuC/SUoOfOSRJIY33AkkSy/vKoF5zYD1FhLDbdg1oWVkIzUPO1psV/3V5c+fAgFVFJQ2LFP2qtH77Gn1y0wY8oTVF1mZcVkVQUlWEsk26rUb4D8C9psK82ibH8X+Lt/zoqkangY4MmoaxXjbwT84faPvxB0xdMsATve9hKf9Qx5V0ysvyfHLjjLIpRdZ0XVeNScmeL1lR/16Ar39FYF2SGT0lVEne7KT80UEcUvIYfPkd3KKXhNRzBerU9FoCGvv0h9aeE5EYJOnU06iV9H2sUP97lL6g0ncFWLZnO39caa7AkviKJr7zcTTcJKi4xJnQcpXHduia/oGqdrwJaufJGltzOE6ezsb5FRYyyKkplKkvU5v/E1GqT/0jWdbP5icrH8bV37xkh6Mp3NNIlN+R5sYmMXSqfkoe1sXrLR/PoP122ftz70YQ6eL/+OY+jQP4R7kVlWUtLmrBpfmc6NyQtAF7/EdlbGdskbciwh+5SpaPx3zPSqQXlLS3pEknIDd/L5hlms2n6gjuSbHF3z7DkkQEylY3k5Kgs8zhpSDjIGsn83B7tXaGyOZi67V934vp/mq3etAdWylrj8dbrkHsIqvD2TTsiTEysyf729v8Shm/a4BB2RDXYD4Fr5Fd2TLiRi6x95OLNUbPtIScfZRytxKNBTNEV+6UTGmI/H+dZidkD2y1i1E+SxXjeUpuhcEgMg4dIJ4fm+JITtEUZifuxmUOTftBvgYn9GZrliKmj3V/fU8xqwMPJv5v4/KZvhgwMVpFyLw1tzbpwU/vHt8pxzMW0ynnbC47tvRhkULdjsBT5s8uBW0pZ16AuxMN0tUmZy66ngAiAkHqJc2JqC2LOwN0WBDPpDY1uMVY/ScomI8rt0RZPHony6S62JkjO46pKEHP38DZ6WdkGwJb9fVxNGxak+YJWSdTmA7tto2SlBBFBITJQUDaUAJAD41cg/+XdO3G34CSP8q7MIH5yIps03sU5LyVnbedJspdujtSzPQLKGSkRVwo9G1GIl2UisSOzsZBIXJOm2xo7wEDNr86VYcBGNPgEzMSgl7FU39sZeArqz6ycBdHbpAN0vcW4rIso4kbTcIeMFeRVIG7joW2YTtGSVJcBTSy/V9MPZAaO1c+bMdeeJQtasMfESixeb7uT+D4IRb5Bg+HBTVkLasOjMuyQxdr3dDKtqOnMHFsmNbFNesBVXYzPriu8w2juuGUxuu1ZQ8YHIDTEYwygDafBLUB8BnPx8mpQOYIZmFM5R0Y7+LbK0qt13/WL+4sieUgy3VLNwyvWaYflWcBuzpLhNJ+mL/MtrzJaW7BXX3VxNsPaIXlCxWuT81HHBTRwYAZjUAPcxfRzchaDejMjf0F0NltWx3pLc9aHOlthC7JCmyFku007fjuW6BjOBIeCD6XVHRjUqCuQZ52vvntLtsMI55MFGxYw6dVYa3cEpBtKrz2NOZGtZyQIXpzPtSfn9j3gcXgsS/A1PIvmvDyk09oZdrZn5PlY2IV5Q5oW8q4swnsOXLcj/NafDd4Oh4QaJhAqKLjN8ODRrBmLg82dUZTSUczHpdL5MtpmdXV2hksNdSIPPzGP7vFSLhvDAgy6v0qUTt5/S/+hPRmSVVSdD9H4HY6IpE1CB0WKtnaGhbUjmo+XtfNp0hW63k5xtqHyherMxM97rIhV41iIgfdQIXfvjWnPoDg4iD07EBuW5LEu4d6X9GEGFpLQ1MKdvzjbsCeOumlmD8nCshCLG2Ak5eFoeHYC+zMjU4G0Nmmfqbh42Pzt+CCijANEUFVQvFUAhogZYCwzpnUZ+4ckJU+v9g1ou8nCB6cCh96cFKJ0OVCDpDcicbrDEHT95Suo6NHBupBFaAYMz4HJvrwxpRvFDBpLN19G6yH1NOEi+oqktiGXNA2CHEVQiDN7KBuWBDDbBDOGWwTlHZmBn+T0NdpMLh6EOPS/S+tjwVF8jeCMJTSwMH/J+jMnm8mIbrb9Y+suynTH+aZ5+njgEBU78MAyVuvgbLlNQA8wmoyTl19VVSOYiTVl82PSfF9naWfaVSMcn1C6va/TjNicTIHOr6aprnCxCTUd/Gy61uUut250O8fVdgObzwdnOM8W7ri24mLsf5YX9aYRWu1mRwSF2HqAsuFjALXV1ibISi/vxAz8JSfuXuxJmu7f5NskNuQjFlbKSA2YNSdxyBdiJbgXc/ESTOD9Yabb5t/2qRvlZFOAUjay5iAaKcA+VN92bumU9X8uxgPmw6TpsdYuKLGWawyUTDvkKdRXi1nGfDvKd0DX51V4zHwY890OmF8Uv+itJGrA4U0wCrmM+0C+3hEWlIxhVhtOnB+T5psKy7Pi97xCtyMPaRmGhVcyJAF1YkquDRlf7qaLRjOKOTkTnitjPbmop+Yrei6biG69l1oEhdiyh7LhSADzMn5peUdm4Cc5fU0aX94FPfTf+NxDWiM4Y+3IuYe2P3Oy2U9x7kFG07vKBw5RsQMPLGM1h3MPED3AaDJOXn5RVY1gJtaUzY9J832d5c498jTVZ7RloMBTjJzkqbY4sWAQ7lWqWrDrM7AaOAcm7S6ekJb0/Cgrn0m8ci68Zc8yBQUn47MWxJbbaMTQQRjAwLvBa0DHZ+I2YC6MVloMvyiv4d1Nk3a8ee+Y4hgVe0jqyHiQBrflRFlTEq6E2X03eFMzlJn4VDN3pjzb1lo8/piMq0vIbB55ClXSJeo2Tn5t2NxeRqGqRjID+5rMn0k3Fj+sUw2EfHPiYPJtZi5umlRwcjOWveJmekQLczQ9nzZcTeotxtmnLxUqsig9qqs1wdi+vtStNloFbVQb4nFVRRtmN+vAXuluqyHNwPJWc7wX2rx5zNH0MhAEDHF090KkOeuCiM3vAAXygKp6MQNzqahr0jxbc2GO0qpLFiwgF+2npoP7PhvX7aXu6p9e+UYeUfyUlORx99vhDRcZlyhrqV6FoSu4vBADNwjlBZU8Q7M8lxoNZQamNZpDk370dXaLh1vNZ8VPrRjPxb1tawDrgjp7R/mWGcRSTMvMm0knmgrLLu7N0ZeaRzk46fJue2jO490fFpT0fK4VXpyLfWI2XbIjAXIChtvHdEbS3s/KdnuYuuhjcl8dR+SQAXd5HZVo9SOp1iMHydhFUw9iy76KDVfqmpGpRYj7w0UQGfZqBt4znAYjTgQxLM6YjBExsJCOX8BaKqZ0tRrVDQLsKZOC5XWo0VBm5GnlHJr0o6+zWzz8jRYxO0Zmqs7GzWyr+2OImg9mKaYG59OkM0zFZc3WryTu3mCPNMJJTdavlncoeLz7w5qSns9lrIpzsft7pCv0jMXnMscIyl5+tL53VSWIDQF4G4ZUNrdXXnqTkczArSbztxcefGggZoaAtqax2gP3PzYNuQmMV97C63WybSLR1URiwcBXHTsIqwcdWaz7s7zAHZ9BXuF52P3Fpe93u2fuWUXHFwy0iulsfXFwA1A6XglX7w4LgkOYkRPBOTJpf0CwbHgB6cbWOGKFgw4YbMBj9slb62U1y3oyl+0so7EpT213IHLlR5RiXWBsRcPgEHMxkDZMJmlir2xm9Rhm4E/1PO2FncwOQW30AbAT8mQAWzsAV81t0ClovPtW3RWq6iK7Qv+qkUkWQBgc3vVQkHYOArCJvVJz6jHM4hRQzdNeqLmx04bmnaxC8OcJQtp5VhuwvGn2OCoq6l1V5RG0og68GWPB7TZk8qZc3o0OtzwY9GyW3ZZ2Kkx6sQPvOwsj0S4b0hqTM+F+rh/aYSzBr3u5igij0EWPySpMzqn7GFKmG8QSbLqHAWZsFnEdh4LQ2hzzzonl95IxlSOYgSuVc7RHLHm9RXFy311tPKqqIrmrK2TKouraCpaFKzqwsKYHe8jSZiOaj8XN5ngfWB4eycWW/Hkr4Ujp6m6PCzQwFGisjA2H7kBHaUZiubyoeAx3DovFnTeMDlBADDu6kCgZ3E23q8m72JKj7JaxrMGiv7zE+Y998RXLhG/c5a/Fs7QU9svyTfQg96MCsAq5cZAQBrPcBMPF4Z667XB+j4okyqphWo7zzV2SNYC3WGKSJ1S83iQbRUpMOzwKwilQONDUuEPQFXN64JN5YR07Op9esJ1Tk57R9ZYWftX4DDd0Bih2guP3eH9nMazdEI093OkZjOrvNS7BnPEtS7ykgsazE6LBdAiQD2bgSy4GUEd3g+OhOTXpGV1v13jfdQVwUPsBGPpnUvC7o9X3WJVz+6zyGg27DL2zzgKHgs2B6g6cbtQJOdfvsFPOYYDziYLN3FuIxc6433h/hoJn7RhUgclcVvzlRNUNA2mRi+yuyo3BgBeTHgOecJGhEcvS0iRbOI2XGi2CZcypn2OFMR7d8pbWT7G28IP7HqX1MEKBWy1ZU4nMQkoaPAFERd0fudjopHZnpcdowMtJkhF/WEgVj2Jp4bJyRJl6m1zObHbKb7Swc2i/PUAn+XOW5tHK4CVVERTMKttBWWWSFTEv89KlvCMz8JOcviaNL/+o5eAwijbbFA18oNMlLHhgRcUhh847JfwaXk3BfZlRUcGUNukAW3MxDrtJUIFHHSOzZ4VgcIjDBkgbHpOgt3w4qOGzpc099VBmYFH1VJl0YCfeCRqHoTtBESAnYMt9PBeR9n5WJtzfs4/RdjA8wxMqKFZgqQVmsBSL7ewTW+oGMd8qLp2vfWBS2qxWBB2yYBBDOnEih1a2xQgaathiLKL40WCHBQFLR9/AWQ+fx26zz1peDFVDmEEEVfOzH3u0L1FW30dxVReoMGFIEBxkSQrSiinhFpbZ/qs7MweDKem9Hyw2PLZ0tMVThFY3OT0qudLX1FO9YOTyapGsGYjzzHnbi07d6mxDor7KdNQZWjA31wLQ5KpO0TUyIUUPGZwCA2Jg4F3ZaeO/DTf846hCDyRvlF4zi6BgZpsOyiqzjYh5GW0s78gMmlhO3/3QwkP/tZIkQAZnpAUk6TJ6JblIv6Bqna/0NIDBwZ0wDWm1DYabmI8kBG2Ls29dnthNAAWTug1QVhndRNy2NFh+HyIfxAyqST47Jo13VRbNHE1StJiKpgRelj16BLXNHw01MuPST7u99TobglaltoNOHkzS27ENzKi/UbFJyhJ/ID34EjWzc0t9bR5Wl+tzo+qgfodqWul5s6Yha5of3c6pPbuxzeETtZpnIw8pV3c5A26N4se85oObhM9yHWGIAFQbYF0rBWLaOnRcKoxx5yTBdnhz7Fgs59ukS0LlXRMHg42ypuKs7L/4ltqwW8ux655ut/FiURd46A/dNu/2PEsIXt1TRYpacB5ttoJdKm1VY+Zv9QRhRKPOzLGXMpkBo35Q9XaGC/s09ALbmPKIDIEJbzo9B2fYPLQR0YjG8jaD7egWYH/dfBs5GLi6i0kDmdYnPPef84db6jfhGakAKOpAPE+B2PC5qhUofzzX+Z3jbIPxzMDMBnNn0guu6k6wrzanOgQ8EcPuZxJ11Qhm5s29TJ1uxIU67rPkOldu24mQ6oUYbW8Z7Oj+PknxF3Sre9pFgAQVXQ9kpeYEzOavuQTaGEm7MIeSkhHWaNlc+Im+ozjtvL1Gd0NgcHjJTGF/vHrBBNFb3g1ZXoupxzHLwqmaJxvNtujFkGu8rSIR28kmKl5PX+J1lD2gKyxqx3ivhLL4VX7+qasJnoSSSlYHoNpWQNbt+j6NKjTu0wxsaDwLJn1RoNkNBm3+sONMpkp4lmTRL8yLYGfmZkKQ4Bbcx9Rfju3iNVrVKbqJysfe+UR/kzOfpiLIgmIdK4bUNQklbqfHsnOru+mI5uBtw/k06QpdbzHO/nuNarQ63URJelRVUbxu/Kj680pNPYivwSo2nK1rc5nDSsNezcCchnNi0pPljyoVgyFrQvTgxJtd1bnZs29WxqH9iHbRQ2Q+rGVZnJtaYy7v6u0Co9+2A4vb2yAGfMZU0DC1IyuzTQAMzPR552wH3Ujm5Vlwvky6QNfbBU6lhI9mMTvVSBNmPoVMtwrmBJNKzE5xs3pEi6liYE5N+kJVW4y9z/Ki3lzXd2VcJG1OWO1JkrwKxNACtA0zK5qyOuEMYvPqOzMDA+qJb+N3XTAjSvIUVegLKktivpwV+UbLd4o6cF4UGtwuK4q8ofnZzqA3M/CdAfFNekHX2xXmu8ltWW+sMSnjUc0sznZiX+ZnOpHsJn0Yay3GcOebbV5UuGv3jUfCxIcqrwKxHANtw3GKZqw9pkHYTt+hGfhOT3wLV2eSPSzq7Dx9sWY+eRWI+RhoG+ZTNLMM8+k7NAPz6Ym/d8yHG0rzNli5ZxM1T4gV5Iw3wtrzHtAO5OxRMfjy+2PdUGbjWfmsmXShq7IYq36I4sfz7LrK40e7/bCuIsS6kjo2HKxtdq9igE1HMwMzm87nXmy0ZYPRZSDV1JuZp/cxLanhWBZk6L1MUprV5xXadHGH5E9Fxk4BFk7YSYPZJe0UGwAPO7N6V7NGKIYwA2MqZsik9ab6PJx42uRQwXUqXAMVvZG9iYrq4u6fKK5IEXrBEx83Gj/KsrxqsPzlW4mO04LwQ/m3t1VRi7YvQX2NqsFfsU3i8u2b9judcZwUAOzIVY9euuReCQKxDOWvWlz4V4V3EBCarkiL4nMeR2nyb7Tqpg/uFA+l79rnKHuoowcYW1dm1jl0XZEbkVeobDhP2j0OToucz1YCIRbTypgjHZOqaDDTuXI06Ol4bAgrGxCvQUanH4KnnUtQpOuciq2NebrPIQkhGXNSapCwWVNFRGxWUh2yPuwIxDTGchmhydOVpEtjsRmqLkREimsI39GxLJcbHeRWIYe9GVIFLj07kZmWcVNTZsCSbYyuBMkQ9qxTs3ilQLjbT3jZBDExAHoOR2nyhIrXm2QDYqPLTQn9oc5WeG+O12oFyUcge7RDqhE8/RXMwbo6po0q9QgLo5+7nnM1CgEAM1bGKkWsRdJd/QLHOtyWM+xIY+PAmpMD0aL8kOYPl3kJ4urLtEias3AZlqHQDI3U6hpLTRBFMgxmvfhY5PVW2oumVIvoK3ouZYLal+nZOk/To6x8lnDzUGqESIbCyhxR2DoAmBb16QvWGFmUHtXVmmwI4sZYl1tqKnhtY03mGQhrl8bH0GT5Rk5B2wB8WHkBgHoN9jG5x+quWGmxA4Dm2FUIzegn4+mh0AzN17wCRzcU6u1a9Iw7fZnjrWOpopcIZ2AKrJPtRqJj+zJjJDKK0eVaZD+iNEWVapw0hH6EVwivflhw/lUjWHEzAAZjzRuBx3ykGC8LY7qoX29RnNx3oj4YG4rlHa6gHwNYrXtFHRqPAt6xMfNmTInXPCIkpVTzjo8ZIv6h3+N8c5dkkYw4+lrahunHX6Em2Bd03UZh0XXTJkxwuyPt2LFJfaxHP0K7NmTciOsMNDkNLaahyzOpW6u7dzjB5Xl4zlTnw+sfuwMdeOPDgybbeIkcjq9pGSEh7yrJ0bSvXemmgU6yD9KdTd9v4F3SuJYM/UpsgnHZgkknL9eZrXz6TNBoFRPp2qKVcrAsR6p2aYYyzsmMG0lGQcs2THDrN7ljBidwn0sn1jJH1bjs1ejalG9aLzPYK6Pe9FlfwH6MaXS0g0rlOxsqh4oGzbdCgWYo1G8JUIaw/lJKBw+jV5lrtEGNyr6DXYwMgIFLLi8krrgmVYJJdcWOlS7XIxOuX8MiCV6ht8Ctw2lgNwGXY2ADCrzkZI5eg1S/+6duloG7feZ6n/ZcYrONkgfQZOnLDJbZJsb5Bm22qUTeORAjR9BnVFWooE/9ZW4hEdLMZaXDDdz/0dprdNQ3bKGxEfZa2kZlXaAfKHlYg9PNAJiiO0kw15aSUfMwWqRMyDGEkQvx1sn1axar9M9YbOApY2P7YO8YH4lphFQxXC7mUudOhiNcQO+yLErJ4shPKaUAnOnR5qsKMQ9jvBFX4ORADKw3ArZSnL6xEPqBF3mJd4lpqkDJwxgIaB9wA4vmGI5jgGiMT5Eho4OHOIRUNIk66uB2jFmg6ijCD8YKfPALnaNTUW8IhRoGLY2HEOJrTJvog6HoJsa4DT6iiSWWKSHpqBE9FWFozfjASlL68cEuOurB2CcmHZOwTxZUItLQpJp8uAa1IapyQSkKepo0ABBWNn5vKosvttJnzyJ5lfDyYauqQQSlQmAUxFRiBahID82bclS0yu1lVDQmhUgvAEo+HhEYog0bRaMgD4AOIIoKnTNZyMWKPpJHRhQKRjeGEVROkEQnegKqOYnBxiOpSCJPpSQZDZhAyYc8YGokHuEYe+VNJT60ikpzBlBKAS0fnLwSRDEgHkxBOAVuKYdBSF3WyDYI7PYE3Ud1Wo3hXMC6KAFVLFVwDXD9GwPVVGufBCGkqamoNW8qMbFpt0PgnEgjGFA+IBAeog8fPqcgEowTIpGM5u4S2Dd3cX9RJA9JphA/AVQrH3wNheAZyZuAbyYasbGJcgIxcPrR0ODepGGQAXTh4i9D0UYa93hLx11KSWZUXTt4EywKAqviPfWUN2ocsu+BGNbws9J1YnBSGMwEV8WCAGxNI4pbkphrQU7WaYhpRUYXAk5JulmJ1vsFb8eYZSnNRFjtsIQqCorJt4BmmKfeVfIN65lMADUflZ7BHMk1K3fR7uxbNnxdvnuQ1tHb+bKqqp0EG3tvsJmQNgJtKVTYA9FVz4cguN1A9fzoR8h52LJ//Uax0eBB5MPgICHSUPcnFPTgEU1sLQ9PALH3guSkYAH142D3Sd5kYdEBxFFv4Tz827dUR+XubApK71wegVXOajNHNYULoIqUyD4E+ZCkJCvQcPdHQRUO1GA4bI0A9OEQzkSk7hbTmPpFQSUBVj8qvoqKTuOdKwNyCYhB549kAhzo1d/UUvsUASj5UERgiDrU9TEFWQBcU/sMhyYvC/SUoGczwgjABmPi6wQgk4ByDmod55smY9R46w8mlACnHhAP7i1jIFKAQFKau9uKV820fELp9r5OM6IHmQKF2airqbXwNAi8qWrYjEKPyZpxIPdwZVSp+AEo+fhEYIhk9EVWBbEAZBMr+PHyq54kHJhmGCy0lCj9tVwdVTh0c5Dllr6TK6EJDaMZAQUqoUakJwONBKABc8c4JHN0V5yVnNHCGM1jO5YwPNHigokhUNRLadA3xVVqg4IzkXVqLCFUB4VOxiEwgX1o8z2v0C11I19BHg7UYEhsDe8lSYZXRq1Qi31/N19tTANQ8qGIwBB1qIQBCrIAuKY2D4cmTYxpObDBmEyMaUsyzW5Mk5Z7A3XM8wATSoBTD4gH95YxEClAICnNXYzpPE2VAaIsgMJmpeFAd2qb80Jl9jIoJo71HLN13LZZN+Ch0yDqnlOQsuEPuUM0RKBRQY5jiJKOJMDqvAlSblIN0hlMYHLIwNXjkdTy3ycpcEvoJpkCz2DhLjrWyDUGwuu9WFA1XVywLGjXvIGJtwyqtttsenZ0bOq4DbWJjp6ank0jCpqCwdwOdFXl5VEyqVlF+eCN6kNk1mQeUtDbrMmJGbnJk2NwigDCyccGgUPU61MpKcgEopr45KBtU8VvHISu+yoOMqbBXDwB5Z26HZNpyi86wBXkw1LWU1114JNbGdx7gNuA4v1k1ylCUbLLEmZIxhbacnwtv0xIwLYBgHowM7uKYLORkNKLB9HIzwgplUL9hpDHNA8JFBElIpBJ5xXRI9aEmCdSpM8ud3uJe7yOSrT6kVRrKlucSBpdFfngNDUhslGZ8RRU0yGWsVOoXTOU9O92TNwnpyFcQT9QsJ6KfhaaSd0GQErpHIWi5Dd6Qo3JydayHC9TeUrCsg1NrvdI7kaN6qdANHpqhJRqvC6RpE7jUZimJAGQbFJpjirh5UNSVYMoJcmVqSCasoWJ7Vioba2E6ivZDdZRbCzxus6Vyw2xLt2oXDg5CPm4WEDwMtiYQFV1D4zFM6Vg0slWb8cUrnIisID6MTDwKpLoTTQYJRSvKaOy89a5T7Gn9SjwgLo9MAfvtZ/mcU3sVGAS6yq1uQRSPh64AkQcIf+vgkoSrBNrbbZVqZKBwEyHIlU37tSZwySgEilrjAEQUrW+QBXgRYVN96xctECkMznmTTSQFFbvIzfRQ5ax0TNrIz6LtubaqQpctQJJa8FLm5D9W7m8yZHPdW1U6INKLuXAFoNUSacn/eaSUaFhhXtLCmsxLoWzy5Nis97d64Jn9bfLOEDFMTQEHzYIeF4SwTnuDUimqagdrLq+4pKeNH+/nsCaNqcluOrRglvZUwOAeDugUcinPTZQKagfcFApCIcOQPsxM6YILifqlyhshUeJzZW71RSeUcyUHTGeUwlTeaRc6f5v3wmQThkDpiUBDe2db4XGJddRbf9DUUXxfIhpjhpjFFoKmGJSUFr9iIp+Eoy7MF8+HFWX9Ou3SW0vquhX8innZFYjStUR9q0cp+lgUHhRhca00MQwXQBmh3t7aKopcpAQd7FwkAUnOs/L9cBbR7d8kYK8RtW1JDDBoiA7/LqTnvJGzcpnYQoLlbeo5D01nxQVElsaKXBZTJDD5KgaNpgiBWeEV0cmgqOv66w4TEQmqKZaSljAt81uJYUWc6HGY00mJTqLOeofyrKfKHUP5JOm5RKPDZuhSWVpN5kaR2Z7trnNnP5pPfXlSQBKPhQRGIyPHd/7U4XEirimvg442FzRZpui8elBOb9wkPpJZit48wyHDtpJyojtQJ/hGUXtNRwJpHxAcAWIPvRDjwoKSRDOcL1mbFmxYRCBTIai2BRYk2VWw3+UYf0mSoTVyoFQZYIk6vPQi1Z2sHORg5CPgAUMkohf5k+kXk0NQ4HmeVT1wgTCaQbBgUtp0r/dqiMKj2/qBYrNwqmkDgypGA9YAaSQccpLCc6pqSS+wsJ1WX4rSFZFYaaoa4Z8S4ZFPEPeVbELo6YzoeEAbTPKvlJIyg04p1XeQ8PkJeV2968g0wBkMJIeNgBRBlQALbqy5kuYQOYhA7NSWwFQimAuARiMpjBLFA0gm/yNlL5JFZeIQAZjUHGJLT3mZRPmHXMlYSSQCkMOrAAaiNxr6yrrEEY6C63GZ9r7hsH4VBFKPhwRGIxLZZ6PV0WliuhmoQz75rySjWSgqrg0sIbs9sAIqr8/AKGdRzvTW2OlOgIBFWoEgg/4UOHM2gl67JB+LLF7yRTQVmY1FYrGCAGozYxeaHRpTP0cJJyUw4U51yh+zGvesSx8BvnVtK6C3wxRgFwN1tXwt2l7kAuLB5qQ/mpDTlfHevxqIy8cneez/7CE1AV5/LczIm7PM6yoolRxW1JVQbEMq+rBF0/YCrq7Jyr0097SYVvu7wfx302IKa1rPnAZChMSm17wNWwRWg51k+rymEZcJU+4gc/5wy31mzzyDT6poQCXD1pRC3xeYwRRE1OFF7oPxIKEJp/qLgsIZzYw1Q0WJ0rNdW9FRxNDWuhooB37nGM+ur9PUvwR3Sou0olAionjYUE26IE0TCDgmvjS3FGccvnZZKeMEkgVQ0MVYBlJTdLLSRDOcMp4jRcAciiVbKLi9fQlXkfZA7rC06R6c1dfSbFR1dWF35nFlTQbYC3e6Z/jpXrQ/GFMQxbacJBMpRBUYxFOTq54jVZ4j30TlY+9IUJ/A4mmq6MYqaYqSEChjs7C0rYC3TahykMQ9u81qtHqdBMlKd6cRPG6MemU+y1dFfmANTUhqoJV1GTVtTL1XkvR/nX3or0dVftaTkPuKk9P274hGXm78sAUvm37EktTWEphjQbJVNHQ0JhyLFKAXnT5dBxJfTVnSLqSLZvQJJiSHel2wJhG+ZS5Pp5xXd+VcZG0gdzaJ3hgaPkw5ZWkT43Q0GoaKpBPvAXBlsQTtrm+YCOeKI6zIt+oSKcCV8VUSWvBAVs0uC5cS456XtLd5BaEo4CNxzbWCUw0CvHEJDvfbPOiuizy+8bU0BiPCmj52OSVIKox0GqiKRDPYCOevthQTgEtH6C8Epya3ZhyCsSzUe4EbdO89ZV2HZHTDYDVDU6sIqfZCGtCNgAzZK0oZ8PluckofjzPsL0YPxqvq9o68rHqqoIvdsJ11ETVNjT1M7CS9hXh0boq9oNVBEsHJepcodNZTRIGdQ458iccNyyCyYcEQMNRwzSYLnJYRAlu1LLa/jD9r+9bFCSJUJRkqBjK/vqeaNRN1H3463sMEqNtVUfpl3yF0rIv6EMFxprdlzfX2ygmJ6j/ef32zcsmzcq/vV1X1fYv79+XDery3SaJi7zM76t3cb55H63y97/98sv/eP/rr+83LY73MeO3/ivX26GldoPKlZIX4VfoLCnK6iSqoruoxLNwvNoIYNebqKgu7v6J4qrJpvTCScdfBzr3DXZXN9pX/MS5I9Dk9KcHJ787bxhpqnHAvSN9egc+UjjS8AwPiyjxZoSImm5JPVzzOo7SqMCCskVF9dpbUCs88jytN9n4N89z8trXryVmQPKbxUJ/N8d2Xrb1unccmW6xReY4P1WblB9j/80cy4d89XpclsdpRM5UaFxsicVYszitVwhruQQPK9pywxVKbah4ifvyjOUdF1SYcxE/xRCAOf6+Mot0/GqO6SapUo5xuk8WHLjGdgqAiP5uMytVkfNT0Xyy4xWRR2wwfEFV9F/o9blV2TQmtsQO4wkaFlIRKVNohxcgPvXZHNfnZIOZfXWT96cWNEah0BzvFcpWqDgqfySrZmmk0fJl5ljbGv/IM27o9HdbbD+KaNsl7YOQMsW2uLE8PAMzJRTa4v2Qk2AcXsXwZRbapUjyAq9lnHYZvlpql5voAVAwzVcLHVM3FsBNfhSnnJZhSqx0dH2XJuUa0M1jgTm+4zx/TFAbNkOjo7+L2P76njMmeHvlvWCwcOYjb/6YGUfRC3WJwMtGGjAJZ6YmlpKq9jT2kmgp2dpIJ0m5TaPXLsaPxsSW7Mxs4w9VEzDsM9EdEodJltbc1QluokhZFN0nC4VFiMAPZPi4M6zxOcedT/6NVl33fdUBj89FKRjgmIZz2j7wOMavFoZU934xj4v+boGNEARho/NjkdfcNoUvc8AqQWiPC5AbpmB3uH54XtqL1yVvZZuwuLTqrurEvsfHdUruJMNsPRSa4/2WJf+q0TXKiTuIxcoVmeM8S6OH8w3uDwk9EYcOFFtsZSrO/mw+LL/FkhizTqbs3hk4rZa5rormgfCy8ekGWMc4jK5LmRbNNDIfdg3qey+KE1tijxFYNbgim00diRa4TOuHJON3dXSJDcabvI6BTeLweWekQLy45yMBuouNBtyvR7FLLvHlZ42+2xlo6kaUfvOnwjPNJB6laf7MIug+WSyKHAPyPYLKLTw+1DUAHjNftjPsxt5d8OEy1T0NA+ZSV99VM/isQKi/5s9ZlUyJhY80ejl9QZst52+mPlvh6iw00Q/Jl1lIIrk6xmHrv9mfLLYXgKCDxbZkqZPPxqa9yaso/ZJkyabesEiBYifc0YsSd1+8M/qCz8DgozFoXC6OIU39XdUZ0lM/xxO/8xKSyPGrhQTFOa8muk9Wcl3fAY4p+ruNI+khwd8v8LcIzz7vTOIKLdbqAkV4Z32Rfau4sAa2xMJxsV1JMLIlNhu2koT7xeTyJYQXKt8ZPRHoOMnjLGn/DpLO6jQF3FPD1+VUzIe8wsaZFCtQbOFlf6lQkUXp5yR75DztTIlFb6PVA7ohkWFsL8fPlriuq1feSKG/22xikqjkNzHNJwvV1fH1DbYGU/xb2GoA5YdAFzWuy6jAKqSnnLAtFEptetldeeSRMgU2PcVsl/wb8T3sv1puqIe455v8GqUormD8Olj7/g8PeEDDkL7uocBLEl9fkfvlPE66wCqI7fki+4THe9ncJOUC2Zgyi9DHqBxSenYZTLk4SAhgybCYvTifwNwohFQOH+c459hnM/ME3Ud1Wn1P0PMX4cxNKNwZA3NMjetjX/ZYHOxLedVdtS/7HhNy8H3hyyw4skSXqIgxiQRFyRXZ91SGGCq3x360aW+zQ5j7MpsIH8xamKmQIJlsiU38yQrER3+3OV/7V51gLX2Mt+B5Jh6vQ+U24Y4wVjds/Sw0C0xzFUbGryyE5brW1Ove4RTWNbpwZ7SeLnW9ueajMTloP3X1ndWAe7Qvpiks2+vJYA77PR2uw65sH3dlU+1Gwu+bDvubXdnfhNpzL7XijzmzvJZ7Wd4wk7VeXneahf4sTzEj8jjGr3OaDEdpxWtduzP4ADcqiSM8K4XFlPpssfRhi1aMBhi/Wi6iAKrxs4UCFhY36yUseUEpsAaPn+1MDaxsItHSaL+aY/qRrKo1i6b7ZKHAUPKw5piw//Zzqerz8qaIMEcLty2ZgvkXzk/JaoU44eu/mWP5jo00QYaHj5Zy12VRAO1XqmzHlrFGg9stZHjq8e+L+/8DXtEajP+n86LWVJ9rWWvPkeDTpbmXteu05gLW2i8255DZCSK5CvoXFdlDSK5wCeXbWE3Hoo+P/h5eQFwMs6P0rt7sBxNfoVK4I9Z/s9D0bTaNy4hfHZkCy4g6IIpOguM4z1YJ2c2+aa4UdIRsZ0FwMxZ4X5nJAoyW4BbZhC/MLjBRu+bmparPIjUk0PWeJ0lCYdOJklafRrBPBJ17ItG3S92MEB559pkhHpvLfQgtionsiLZdwZAYPy/nCd27u4KX8odsrZnJnYdmZh3oBJgrsjVrm9De5o4+yJsSGIvdU1Imd5gb8RrzlKzqKE05IwQEsMSfpEKeG/q7zXFotxbyp6HD51mN/XWTZVRyJiOW2oWsShELhRZetxW2Do7zzUZwBbAl9pwvOUkDihcPFuvqyFUqDHE4/NPwFn1sdoWeEvQsxORCEBYtbLdF/oRWVxFJdHPNbyqAYgttkFca9DCEfe+bG0gwfUAIpzHIG5EC/YzhkNePNddB8sEtWACvtdXXenPHawsZjHkrHyv+Gn37xcbD+zG5r44jPlcl/d2iP10dyJzhy6wDpS6qNSoG6xAKleIgrFsYDSEYP1NuIb811ltY8GNiAh2tVhw2Xpa10Daze5I/Z2keCbM7frfYSnR1hNAv6rvFaUWWtuLZV+foAJXbyN9LK04X9xL8MIQ9NU5ftknRxKadRK8lTBkexr6V5u5gGwEHBYtKoayO5a8jbGwhmGWAYgttytQUsq4JpVa9/lai4uihQEi0TcVSqzDasaJ4eQkotpHL4aVYXjCpAhv91VU6fo1T9BllD7wbF4ZwbeESFUkuzKMMxqGVxsBo0AiaGIKwoTzJAXCaRXhnKihFpsgGpzzrAF9ms+Nqd6ZR2tc+XpPM9fzuSwZldcoL5zhgCmzwnZYCbZtPFlEaY+JRns24IiubjJzCZk9YYnFlvFdZi0wsBbIJPM/jx7/XUXsNnos8Z4qs3XhN/aOnKEkjyDcih3JrCR4EDGET8ZIpsIulFruB/Lkde/eYspBaCyi32iUl96+Nv+MsL/r+fUBCHhwloEUwcxQ/NumhyU0ffihCoeV+W/7oiLDxNn+fRN5m4wrpMnPA8w5D2LbQ5udQtcBDmLfQ17muEJdLgS2xxdi8plHkqRi6BZVbRcmgvmcdCiFkRgSw5CO06jAk/FINFFtpIbIOf6hfP9RVJdwOEEqtMf9IynWalJUCPQ9iQZlW96YIi/8l3peKnkIYwibYI02bqknMX/BhSixDw4GgcCsZTFcAmvGrtXf4OC8rvk9UgcWavEVxEqVA79gSN4zDhTJyPUm8bqaEdGuxu3KmbY+Hs+Cwzt16mlWoKCFGgwCsrACiiBksCGIfJaCVR8CwPRWg1c70JkGtHHKakSuysm9QWR1VVZHc1RU6zjd3JGYDL8bAOLTAFvpsQLLOcdUPaB09JXnN6zQp1PyXA2T4fwCxuz+sY3ehHbb9nnp3YpGH1UityBRgDm2pVJgUaI7AB+matINXiPrcfH3YAu/RYAst1pqgufv2IdYdEz95QsUrYTXBv8qV2e8ZvmWJEIPBl9muzeVNVCT399DREghgEy2B1WvxenF/USQPiXDXGyi22dWWqDNNACecWOqA+QuKyrpAhK4S7AyEQwtQpgKh0AEv+aHETQNY4K+zVYrad53ENFx8oS3eS1SQNzZhB6gExLENQgN1EwOE8yjy1o2KVzjlSGgwGys0yS6T5sxX9DwyRVY2E95CksP4rrpgMfHFOxPs12oS76SErT5yyUkoqzlNtJ+4ntqupI1TpfyQpKkgBlyRLU5YfPkyCw/+c/4ZVXjvdF4Cb4eIpRaY1wVCKtxAucUc1XjTl8QgZr7MIfLle8SpFbZkIWs0uK3c1Wsf12vPVGDMHMTykT9HqxWuUZ7lxYafKq7IxrZssnYd10WBslg4lgCKd0dDt53yVtHd2JwS0cuq7qqS7nsMZF9iSiwOxvGWhjsIb75YC2T74A0oiX2R7TakFYdKWDrE0uWFe7eV5l5sk/MNtlNPs1XzCgoXhcQUWXA3tsJWDbWIaV2CG0MZjGUreAU/QXGyiVI+sEQodel/E5Wi6T8L49zKCcry4caeoiUGzrk1ktpf2UwLsDPL1nWFuRsP6SnJfJ8aY1C5PK+qrj/NItZtbCSem9l3LXd3ZAcaiVcQ2JJ9U+RLpVql3JGe6VYpTC4pV5XVd9Y8y9O8+IRegIftuKJlzavwBsZ52W01hHDw/rPV+a0Y8Dx8tDqXBfD0H3dG5rozlNYJ2JggIS6ojujcr6qqcEwjgpcB7zt3XlUYo1Bof/ACH7lYimEsz0vsgulykizK/opxwmupSVqhYggQ4KRdLLULcrtZ15u7LEqA8DaqyBxnd3OZxTZ83DcjZR92m7ui1AcebFkysI7nsIdQ+VqU06wAYwdgld2XOIQY8QiZAgd8jSknRTqU2hhRlwVqU/uKV0iYol3j80BvgLHYXFy6Wgy7b7fIbqe4XU05L8+wyq3Ha548XwnFy24j2jCg/sVhMOhoKNw1KQiXsh9A6JG1aIEE/iEFgu6/Ki3+zyAYC78NTe5qIN+XdjqCtVkc3NlWVn/3GTZEIuh29GJMFf3d6pRPUKf9N5uQ4HR7X6f/jcrGMc/HBnOF1ni/5nK0Q1l4kdFwtzQrLSczLplpORSzufC7ZmVP11sq0m0XvcB1afxso5T7RDC8Nh6//z73oZY6+EOaP/Sps+z5Ull7qmOlDZwOgSmw2EfiIVzmpej4o74vPkvMStdpuoyIjOeiqUQ16frZti9ZRcdCi6ssUdmNhrvPQn1ffB7P8qLeEK76nlcup12a+tPM2NCo+PoGVWDh28z5SKL2y+Kz8xU9l+66UFl7Wl0ompFsyXLalRAF8tHR3xef98s8TTELYqArFOfFykWralFMpE1xu0dZ+SzaaGzJzm0gfWM5WywepvTcNvTHOpFY0W2JhW1ZoiITTtrGr+aYTjfCiVX3yYIDo7IkOUE57hu+2mNq4zXFdMF8qT3m64iPO2BLbEK0QyejDZ0C6ej+PkkTIK0tU2CBjySs4VM/dd9swtDDXjo9ioGDcaHQ4vw56KXTzxG2jeBdL1e05F6VdOVz/kDiZoCXhcVSO8x9oiMpcgHAYraitK6AkD76+7zeuLOkKCuRfajPdtQTUY1fLXpVpynQqeGrlX24jbJXwTZsP9qvivKbw2OZxV4+Kar1iXBRgfpsjusjygSnfP/NYq8TdTl1SQhuXfK6GCi24dcN+keeCQqe/m6BLXrpzhyg9J5iqR0fN1tFEr9SidxMl9lhbTNgCkswV2SP8wQ9JTHwJCJUvjOWdbeWeJ6s9yuSvWEtrTmNXX3Q/WaYQtj4wbR+wCsBzJ0GIZ8vX2jRRyFi89gyWrMThF/5fUH/1RrTbyCm32ww/SPZEkddlIoXArkii13VGi8y0ALOFFjIT58Om5Of8fMcVvJC2rs/bvDT3j0Wx6McuOo0+vu8lMSmMgXzavAuSQVl6fDHE2K5RXhK4He1ut5I0ULlFvoVxWQR/cA/CTp+tg5zuYkeyvOs6xYY7sJBWHATSR3K8VH7yWL3kK84zd9+sdDXzSMxre8HfD9mKLLA2Z1nd3WBt0thCAsf/fiqirwRKZDNDoO/rtt+sVrz26RvYsJJpsTCOmqTuwn46O82Ev57fDxpqpvye7eGD+ehfov4gMbngHiuZfwm3yax4C3oPy4VaiSeX1q/j3EJBytd7oTLNnxSBZeLQkuKWcNiAeSsweMqaJLKE4ZhgCEYi0oaIQHoMaQLLJb4NhcVt7r3Hy2Mml4Ts1bM8NXChSu+Q/fd9tU54riBImjo7/bYZDMJldtjb27Ug3jbkoP+W07/Rf6Kz1XnzarumteBQZ03lFioBMGfautLlVrijlb412Ed47VU/3kJhUfEHDTrmIKD8jPkmcOt6YlMwEYFBDABGzyu6lBSeRqduHsa7Ofm7rk9Hu6PEy8kh30Yr58U9lgcg67nzDsT4pShyWLCTW/7yS6iR4zmmdrg/UlONnbci3w4RVC1s3ve5t/nacBSGRSGaw2ed88HPI63PWSVp1l3SIvQDY+57cf+vXJyY0vcatJFc9iQC/KgP/c58t1cHPc5yh5q8g6xsL8ev8/JeW0Efqc1oeD8oWgZGwibcM8X2ad8gy6FrHp8maUt8LFGZVXe5OLFSaB4yb3brltWvrbDwnfmrvK0Rxvm+hyF0OMmnRLLNMop7BkWPRAZxr7MJsBtL1NUnb5U5CJhelRXa9xoEjex1O2tVj+uU2F2YD87dPvAhyFCtXuqnK8ITe4TXrVD5fbYuwVC1wgAZt7WBZnYm/wRcfsc+rsltqM4RmUpw8mU2jgr8qcEL5Syq4NQ+c5Ie7PE+4l1ayXYy6+k3jSC2jQGRZczBZb4xCvd1GcbqykXn+IbPi6ljLpX3LpwJyE7jlBqYym3T7lJUAPFlvMC37Xjiuz7C6MVSy1UR/RKRANGLBRa4/2CqnW+kuolGZA9x6lem4IgbFpo64mvT7El9n1W33eUQ1kEDvWXO7ngofGzLV/XdxVJbneexSnuGMTePIRjC6cvuhYGCPsW2jeBukTzyrHAkJ4tKscGQ9q22KkExdh4CMcWFGPhIRxbwHVF2YMhHPUT1vMJsdyj9AwhkGQG4CHaBolpAB6ibZDMBuA2ty+aKsINjP6rJX/AXOfCafDjFlyR1TkOJtQHXELeuuIPcugi2xE3b4yRJ8eE3TxU7oJdhtUG2xW6x11AK+gNb77MButzVKwu8ySryh+oQJgbheflYBCLmVuj+DGvqyEzvvSoSw3p0WL5vzappqkWxN7ekPnToXIL/3DgLD/DXmEr2UNsrb3XZCeDJaIVvmPMIpBRpIa06D+uJd7bHL/aYRKN5vGrJSYoz4nTCL9E5SNaqakpg7Hr8/HT029ij9uvdphOX7ZJ0bjnvuRZtRaRCgCu+P8bRQCV+XI3Dj5JChRXJ+iOT2IiBbI5ixmqdVmrPuUpcC4jg/JpCeIgOZRTSx+i7FHcHIIAzvhFYQUB3PCfH8tRkzInrF3eCynmodwJ+/ldxMd48oX260Jjk4B572AIC0mrsZVbJP9uxPSmiLIyam6aCwubAs6/NZFJ1ZD+LV6hUngNUQdrox235G0KBT1hCJ8WoBHJoayiYwcrTzEgBZhNZEARr6MSSf3GIIDNTjCBD7aZAnsfZbtXhD2UfZk9VrJFJOl+usRKar+icSWbqPUwWSP3Ibac3kZdIfKcubDdlICYt/EpKr+i58698DUnZ3MxkEpLAWah9+IYbaubdYJ7HOHPzYnmpyhbXTwJmwA16M4cmvVuiW8l3q99SsrK/0EzAKXLq2ZmaKY5Yhv8tLycUt8tnSLgEY7l8rRvSSQ+JvfNni0gcwEoXZjLDM00zNW3zWOhv1to7RKtfiTVGmQyodAOL/CkN/X5d8C4YXjVgz9ni8nuTECKW4CXDuRQ9twPnVTyZRYrM+AhtvcMn5d9D5pEzhHwIikAYD92vBneQvszqNzG2oqTbULMG8GO5YoccALxXHyZhS3e5B4GzG3quy02oINMgc0NkrIUwp6HjzbcNJK9MThFbgIAfmKtOmiMAEFajncsFXUnDNYiDUoCrMai+e3Jy4DPP/69jhoqs6jGrxaGR5ZUl0USw+fnYqkDZvB0XCy1oaSsv659lffTtY/gsbzD4Xu/B2qXV3DQEhDXNkAySEAsTAbt0azvkaziKNb5CPYkf87SPFoBF1K5IpuFqq8qNXsAAJurKjHKStTjEG5hicUWfcf68wdKHtb8uynUd4tA0TpbpYisMlyIKPXdWr8ekxSGkIZtC6xch8kTKl5J2hbBM8GVWckaiQmkEQiSJgLsllHxtbmn6W1UfHV7yVBRd0qjwt8A+Cpcwvvqdu/uJh+eIoNYhy7+ie1b2pcdxtslYnRxdhlhmYZTw8bxt0PgdGn3zRYLGFXHFdl5yyAfBP19/h3o3kkQOW9r84H4SE2PxUFU5FV3W5PfFFH8iAcGHe/yZfZYvxUpjLIpsMBHAkAha40psDzTRfDhM19mb2aBaIXC34E0+rtsaEweUjmn42ZoE4hA6L87uIFgWbf2otu7WxbioR9RmqIqjDVE43KxgzT1J+KjnbwRGWrdCXPu0tYALTKuyBHnJbktiUmuwD2CLBmDc4WikndD9d/mtx/dXwZebAdW1UV2hf7VpJHx3HpRqJw2Xcr6u61sQi9XYZVXuLOGVrTO8qKdLUjuqEIbvM20o8ZfKsozV+iMVx6xpwS0nzc4g6NYasOp0f39VzHzGv19DkUlpzQlvvCldgmITRvkCttN3tolPHK2bD8DQxfbuOTNn8dYpYfYvLDYnDYwOhS7vR5cRkW3/RIzO9AltictEEa2xGbDNtIYCi6CyhdzZAY8eZ/ilLEfFWZhVKBVc+QMj5uFWGINP+i7nm+utyge4trHuffL7qlC7ZL20w7fNGoxpPjBI7nYgtd5NLAWQk+uaZ4lKRY+4UoFX2alQp8vso4MstSfTLH1SeH+ZMZVTZanQaHA7GJcWKGbyNAA+2DG/5Zn1pFv9t2jNImEXPTNpzm4WTqu5nQGiLhnCmx8QGHfMcAocv46evtpxyV2ClkNJqX789rObkrNlCvWNGvsWRSj6jovKgEnW2KJ8QZtMAEr9CnhPSBAsUV4XbZCL+1SQj7wL8gKpTujC7o5b17lDmD/Yjzuxi5YeVflfI8ej+zI+z0qErxtHHTqcb65S7LI307T43fnCSukE9lsjzWnLckHi9tMWFFx95eaLxY2SpTV91FMsikU5JY0nP8HhrEMpQdi6K1CmFH2wCf76b+ZY/mRrHgk3SdzHJ+A8JlP9oHOUYkaIkAHuEKhA17yQ4mbBrBYk8syecjQijJieaMEhFg6yLp3bJFLJjxWvswmcL+Jx+eYe/z68zgksRaNH2GfIVdkad9d1NXFfYOiSx8umnkiyM6sgDTr+K11NCaHVU1dfVdtHfLvZVoXfLZK+ruN5pjkPdLPOS6Er2T0RUvuwM7LE3QfCUmSqM87Iy0S02sSIzGcZbiPTvGuktQRCJVbhBGjlwp/2mz5KGLqu+1Zl3jni/5uIw4kICApoLve/XeHpfQ4z6oiT6FzTRnMT+ya59kniAQHEN3ZZTaEU85/GQ298E3jeAu/9O22402G+fRlmxe9QHF4+bKdlfjuGA7DhZX9EW8ALaBCtqsmcZiVe7e4ZAIOCcYdM1t3AG/qZpiF3Le1acljUKVD8mj1T2xgigGrQqGFe7FxA8oQi6U2Vu1lga5RimIgNwRTtOR63JyRQyYyU2Dh9E2yRxLaBu6LhMKf8PKWZEfakHOSXXMb5RBs6yxBN6mG5fsgUa8Q2EG3HnTrQbf+TnRrn+7IM9dyh8Ulv7K06jT6sW9PTITHlljEaY9Jo4QMBXyZfT8FlI74zssmc38iKA2mYI7o9Ln0eMtkPLLxqy0mcaWiv1v4a/BIvqOiFPxUTIHFerWOsgeU5pyLivq8M9rmJkFFs/r5qZsBjYO+UdTd/QON0Lf4r3JR2viyJS7ThIiVOY7SuE6phzK4YYrFOyMmjcLzjhnssTgIibzq0g7DRWeEpBsKMScEj/OswJWnmZfQS/JReldv+A1j82n/l/X+T0HNd193hpeZN3A9/Sc0KheHibr+RAvyTE+5n9VpKj2TFArN8TYP4o4v2Z5m0V0qvBwrgbHZGW8SbMXf5M2clfzOmCvcGea+qtOWwbxzXvaInFJdKipPw9SkReAUhfpsh0t88XX8ao7pAv8RVbw3bfxq6fgBXD4/dYAJ+8ycf+67EZdj9jsVgqUtQ+msTxMPGTJ28SHDpMb79eJBiLBgi3569S2+De7F9AI6B743wLGrrB82QjFsTGGjTohV0jlCYNxyKCvPZ/Ry+oIEMjAFFlSNXo6jCj3khbAL4Yp+nghLOW1DXf34WRRWgFNpGGcI1TXrSbTQuuBphADmjTI7nNJOoRV+h0kfrlBcFyQWuHMchHp4AcLq9viCIaaJtqdc82ISQ7HcYpv5e33ZlidbWG4LwGazrTWvcYqg29FMgSW+S1QkufBOFFdkYTcST1xTm7NumAKbw0A8ffCjA0zJEtZcqOflz7OkSqIUFHC+7CeW82YCcMHn3PNMkELkIN3K2tMINtUkuCcSi+1PwcMk0wTz/jqk/N1n5iSYgzFo42z2YlIYwzSM2p4R/Rd6fc4L3onBFs25wwEPhxSnQQuxkLdec9NnM+ox3NRn9IRSIcaU+m7jPCP346DnFdgSu8NKECFTYLFwb49WqwLX44g2fl5KT5P8a0JQ5fDRZktzj4oCFQIupmAOva/guQd+S99/M8fyqaq2UAAV/d0qWBZ4cX38ujMq6ej+PkmTyPcEYkDjspLJ605kbLWyKRhZ4+f5dwDQjkS+H1nMBErxzhev7r7GT4fGyeyR1p2GWXY1Dmua2NZzzP58+pv+286wIV5xQrDhgMaBDRV1f19seJ3WXEB++2WRs7Uoe6ixhSMYn9T3nWHijyhDRRIHigDgsTmwtB7FrnM23np+LPJ6y2Iav1phEpDMHcZlewliqbsua7RBzf3au9T3vguNyuXOi7r+NOzbNMo5uttPczpAfkcM18VG+DBa+/SkPYNJ6k3DWP5MIV7TtNuqX5dw1DX93QIbOTMTXRLUZws3Tl7ECHcD/3+UpsSrwTlgQAALZ0Be8s/vdZ+s3BK5NEJAKLQYe/R0HpPbXBLUULmF2yh7UGKHyi12udttim7yOl4rG1GAWURnlDekzgbPvTxQQwJj2woQucEULBml8qnapB/ylWBh0d9tjn+yCqvGPjfzV1Q958UjfxoEw1hFLGGV/dro2+O6KFAWC/2XwVi3cvoSN3eIr6JK15gMdLcWx+5TgDWyA3NdKqXVd30nEXiPvCeGVbxGK3L7JSofw8R1iRhdYrqMsEx02kk1LUwgV2YVYV5nWcIn26Q+W6w/UbxOMiSyKlNgGW0DHs+wJRb2SpI1T5kAKLkii17WcYzQCu4nV2Yh9UXBr+LdJ6vrsQ/kFOESYQNUjLXlCu3xgoeXQuFO6pRwusRTiezPDi5E5rHjIs9OX7aEP4S7YXyZhdtDuDhpe2nSNmZDYcXhlUq8kN5/tVqntxcZoAWYAov5Ry8VVumChqK/26wan5LVCmX8otF/tTjSrzOsgboFgjvUZ4t2Ro/8vUY1Wp1uoiQ9qircPf8wYBCl02McRnimUTFU4zwirsjOXMWrCHnuQrxNKhTaRKTwUcvtl+UuXPgr6C/JBrhDPn61HFtHXXB4VNkuimUwYfQTwfnSXYVadc6KnMsV036xibjnA+3tAry26SuPYvhoYWccc7bFsU3tDzG3SDYfbHYEd/9EccVvBbqPFv3IV9yctl+WDHC7xjx/02zGOc/58NkOF7RpGj9bWHDNmhfH5Fk5wevClVn1cEWeIYzSlH97jCnZGSVID9VPC9KYHNSguvpEPrdG6YpT7+DzFldjpsDu9EY8vLHS7nnBb92bL3YBoJkwoPGrjdlUlmKw/fjVdsd1XfLzNX62Gl/35BjWanj/Qa4olcJgIZCdkdvjaLONkgfPFyt6LC731qVVd9Uf8TOvsr6ZGTQ0u8mPYv7omynZGbnovIj9W0q+mSAZZE7pIDUYdlVYdsMs/5KvUNpeWGP3lNR3mxDL/7+9b22OG0cS/CsOf7q72Bi33T0RexPdF6Fnu+JsS6OS7d35UkGxoBJXLLKGDz3mYv/7AeALIB7Ek2RV6YutIoBEJpBIJBKJzLyoWmagNzy9Ii3Vv9JcKv/7PqKc4n0UWO7cXP2o3d7En7E6LzSKt1a2j1znEbbYBPYnOexPNrB/lcP+VQx7oi3hG3jOv4CiABlcO204ObudgQ/TYINQBeRpn+D2zqZdkNUb97il99RoRDOHo6jvP9PsEfKL6DUAp3g26+wyzcqtuyXGgDNYXQow/Cws+XKyWURuH9riAWKcMJuPOuriLgr7cNqPYyzMyfKeRU8Qr1rPt810RsIyym0mB+CJ1x2JPnRzIOLufpkOZ4pg0iXjq+UowmcPI/xFyxcLBP25q7/pQKmf/J6+npTFQ99/gFNsBPsGhNGOl9qIV+OAJcZXEORlBqoIg7b2CQKUkXVC2n6utgkfYVxukEdAz15RfRrD/X1aVjyPYJe5tcLWh2bOkBIQbzx54Dy52KK88pCK+8j2SSYFyoAbB9rPlRUv03jNC+BCftfxgI65yTzJ77pvE3jw6JLxPTGryb4ByEy/RuzTm0S2WEM4PEa9d93VFw1dMXjse6ziLxp3njjj41XSN2SS39WhQWl2GYF4jX71zK29In1OO0uT+2hTZhwHPkEVDW55KbKAnV3is85rMgSgeSbUe0RGFelcnORlXCySe+bupPuu/QIE4iB5A0KUzmYTWL4moZuHYB0gE797WWs/4t/dM7BlWmYhYIJBEJ91sWK3FLMnZfiJ5UvBgqMKdCn9HOQPPFKr77oGtgUTJbT7rAtrWWSshKBKdCGepmnMg1d919Fak5D/DoksmI1YuHhBm/A52MWpg1DafWgmTmuDIPxIiVonZR/6tp/HVDhdqWBuN79uVnjqJlu63+5xtuEyUKRInGN8G+HnVLwxE9Wx62W4D10lsjpwsy82+mVaJt/q9MQYfZvPuh4QfHcRc18R3JJrmaZLpvbUQLwdPQHka8MyfVugtRYZn9Dm28z2LSc2DQqU8Y71ZtPAwwvFQAGFNislqEJdIyCLpVluWvjzKYLUCdPfcsp1bmZqUVizQu9Splc4vjJQMarQHsMpHvNcLOEsyDSMtk981pp/JLMZIwj5XZ+bKtMJO6C88qlUt6v7+xz0trHmm6ZTIMcVUMt1MijCh2X0r976ID5rzABcqjgGGz3u7dept+azdLvDN7EyDUVYSffG9h/R7iQLHxhnLrZUA3IMgqQfQrP9OBt14DQIHxcJnPXw0Z27lACogYqgDGne7iTXVTZSzhm5+TyV09X+uSzABXQfhEWZgczRywoORJP7YiUwc1Vqf0TgmT2ldl8P+Ja3ycDrhpv60Iyesw2BeOOi2XFRLcvdMFEPmJH75QCENxaaHQvVJ8h66mxzc5KwjDJzygF4u0yoNKKPAk3poxG0TwJonw6an86yNM+XII6dcFQfmsnGNgjicLlqMrfKpHSR46WDY+RKKW7s6eDm1PrZmCARHXzjZFWi429f78s9n/vmq8YjrWi9AcU/0n4AOvK7Bl5R0be/1p801kd5F+NopL3l0X0eQ+ZKLXFv0QmqRbmArOtKOnSwDCWEDIAfKYF7ZULEVd90rhEzSBLCnrHvUCX69nFWftElGpSieAI9QqtP48oGnG70HHSGPHqlMKVaFu36mh797q1rpvBQpNkp2EQJJ1cT+V1rdp4vXnZBsmYylVAlGufB9DKN4/S5dyZsv2pAAs8/o2TNgOo+a+znYZ/zqi86huHX/lqoP+lhccbmuyA+6zizbJm4pc03DWNwnp/FQT8Hb/f1bZcV77IneZ6GEXYCZ7ZauLrxHdjqZLeLI0R5beLDMQfF26q8XX8PbWo35kPOjr5mBZKwg1XlssrZzJV24hY0jwXRYLa4WKJ5G2RQ0TVFsxksTTR//8Cdb3WWaG5rVsS1peTszanNHK7rOk3C7OFhZaFaznkL0MGcc5Czm+kazmQTfRrFyI+jzWeuMNv9JqIp15lrGqblmPaAuZz2Hmg7xtRG7CxN1hGaz3eL/FsZx3+8vw/ivO95MUS9NfPcgOcgW1+nUVLkdcaW1fccrH9GxUOtK8pMwIONWaMv00SBtwY7suQyGpYDJhtGeI7sNjQM7kTV8iHC33VkFdPGhbDqAbXkoz40l+KqD3uODDRMvzUL1TbwVXP9GGyk3MOtLrjK7OoocBAHsOWUNBAdMA0POTvO7iCNrd60CSNWQ3Pdr8maAOty/IfaNNMwLWe4A+ZgjnuY2U1vMyJjTy577iKdnDSPrr2motMrWc3oBEv1M+tDLI2pJYsQsKZnk/bCWYtDulYi5hDcGyuNdgN91izRIulkP5iMEW7KGMBStfnvKoumva5RlehMfgN6jnPe4mY31dTYjG7caKxnKhPOqSzyfNSdcAa0rSHLzCqoiNseT/h1gN8pfwXFQ7pWmnVRC0bLJ+vpzj+/E1uNnwLqQu/nY7nH7NCcKDX4QdiECXhCVdTlCEE3lizRg+qAJ0R47jFTNDaRmxSefpQ2Bn4DkQEJVdPeIHhdOLrtwDAd2pJoHOfMCM2dKIqVE0QJyndKV2kvXesv7e+8+bCskjFiZ5S8a4fevW4DTFS+C0I0OrDGZZTlBWKZuyAHVZX37xqXmMa5rxYz/4zP4ip+a1MBHpGie5AXt+kjSP54/+mXj5/ev8NZgFEKhfj+/buXbZzkfwvxPARJkhaY9D/ePxTF7m8fPuS4x/wv2yjM0jy9L/4SptsPwTr9AGH9+uHjxw9gvf3Qb16DVYLyy/9uoOT5mkrBSLhjNe44KKL1+3f97v62SNbg5Y/3/+/df9NM8/v/BQwTNvN+A+4JBurbBPsNf+cwHULsj/cRGm+8bv8EkB2wA3n1dB3VAtVqfYcMlOgNdmuk/CAFT/psVt0kT0EWPgTZ/9gGL/+ThFdk5SC4xmWzHr8K4h16iquJV+Nf4QAnlAOlc7NwQWQSxuUaLJJlBEkNdjZ0LvImCAwsKEBYgLUNuC6ijAM6awc0B5Cw7xkX3Pt3X4OXLyDZFA9/vP/rL/pTUWSpFOanv/7VhGGckA31jqAOB+uG8xBAyoXPEUx3M824H5nz8g2AwjY7yStnaxtIpLu2AxorcD+zYIfj2CUi3NRhIQ9Aag7MqTxN0Q2mpRRpEwQTO48mDEwOumhwIj8o/zMradu5lpqDOUvTx6jOnc0dIZYm0gNOrnkEL62P2RHoHzzNg5Lfv/3yizZQ2uFXlYeVpwiq6wUO8XKs0yOmG9X/27vFf6zqMVqhD3A0bGf0RxCXw8JbTeNtIkwQw6ZBUN0c0eSGm76k8KwZ/Qu058Rj4Ksuzq3eLDCDtUJxatIyWf/bO7zc//bu4+DUcLQWIs2Wc4R+M0EIgUXBe+tnBgYrUAGzX9/9t7QBjYWDZdwC9EXRJ2dIGksc9YVfM90xrPfBbf6jyUTVA3hWxuhqXn4M1Af/PYn+WYIlSM9wqEoZcF199jIONostRL2JQej4WHxTWKnJDg9xTvRtz/pcteCrEN03IK/s1kewKC12vbZlK3l/MdjlmsEeXAGfDKRDA9yh6rjIUXq667jcRImAn9WMpLdpGYrXhMXp8bp9i1k/9j0CNhabsOXUS6yENtKEmII0bl67HchQn1QPSy22gx5/6oseFoKVCCLvBfWRoVtrIKLMTWQPx7CYh7eCwZXKUbcyABpfABvuvQ1eLl7AdmdljIZAan1Gy4DIWYx1NmwCFam4097sKsFacZ5aJ4KlsepE9IoESx/bxhH7pih+lO8YKhOGId1ClOKvURJty22D+BqE0TaI0W06/CvH1+If/x0uLqSXGhwviI6CF3cdKQstiQPJAQumX7SPaZKrPJkVQ03H1BIP7ZqgfVqWIQRqtb9iCLrbqgwJE/MeXNDlHWVJcy4tv6SbCC6iqx3it1TjlMpRSajYwPXChV+KCHGgrhEDZ/1zBOxLkKNXAiF6MT4M0kKAvN2AWdowL2F1XcCWEsvQN6eA0+sa6sULmpMg/hIlj86dQ4L1BlT5213Qj6DVwW3MJQb2s9Mxr6qg1g+U3FsGmijKXGFsmXJUrxhbZKuwZc3g2g5q/Q5S/+RKNBXt72rUQPbF6THMjzHIptCo4vltugQxCIs+YAMzWwPiqk664Fh5u86iENwEyQY4Z2jkDnSVfE5RbqKNlU/Q5yBv3kTl9Ts4G3Cu/HBmd8EAcKoTb9qh2QXGPqmG5+A+KOMChRL/yrkuU5DBysphw9BvyqHhzUgzgAis3Qb0PQfXIAshcZZyqn2DzEAbMFn8ZtzVybZiIk/dLIsgQ88pgPYpiects3YE6Qb8s4ygxD2DZ2EUCXFtNWssFFfKZzNJeKPAa9uOUTs4t9G223AcXiJToRXeJJOh4+ZeHC95yYjeTkeOkH070Qzjtn8nGocKv/AM4lJhfztVzOJUYXV8Vd+6G8lxDPt2lUjZULyukGU1QdoxKWKZWk0f0kpV3K1X+n6JqYV+SitcRy8gRkJRWmuwAsm2NtuH7n20jNz+lfOv+nL3JGbs1Lai3Mu+3/KVxdixvPlRie36PuP67rZfoWwjnTmceTi3yFoMC0sw48qgjxijISmjQ61Ek8vcdsG7kmCflLjkVyXZ81FfYiBdeo2zcDtQzH9Ga5THz1h1/QyizUNhAWC+usQiv80COPPi97oelLpB5uOxFbN9qsmx34a2Tx6jsyzMqzVYgbt9mnh1fY7WULOxmaAf8MxFbCTG92p18BRDTaltra5MaOqrmEmOQWOtEz1pzwP6o2nbzcK/vVvk1cuev727hXMwoorHRegjDyFLf9dlXDLv7m39Hs6C5BygACVQioaPVid4pzseOrchc3Yi2rVUH4twXyXajVkdKKbKrWts9ZGd71Vt6Vm0jRLSMY2von76d6dn6loIHoOQOif4GQ7tHRzt7NXpc9HrKuzsURkrrpusr9qvRJqGVs6rfq3Rc3j6WI/TEfGS/aV3tYVjd+I2hbcdxB9RHsHacMijp2hdBnH8aqUBI3BR3A/g49L03KbEsCHbyxUim+bShZIBa7uGebKGW/JZut0SB1IrgA2DO7mRdOiCVsNwdVvxdlfKu2a8AU8ReLZSy092uyx9AuubAAW6WXavhozWdlo4hdcAw6+aesRa4ucK5AydI5ePpfMlQjg/wJ2w+FZu7wDnWKH68toAhT+LSOuewLrDRf5ndF+cBZnVdXADw14Dqb24rooHkF1T2UBMIy1ieJ0yM/iYQF9YlWhyCvRGKn49Wa97XVqhv8jP0+ckTgO72/oahqVHZBJXy7cBZ2cpCV6q9XV1z8Azwa4BcvGyizK8Ss6DVxFEJdtCDRA/Nqz866y5+3OQLwOowwAXs0pDMgj71mtvczcMCfueg+xkkwGwtbyGoAA5e460yG9AWGaZ5Tv5FsjZaxiDSmzYyTsS3jXIotRymbYQ8eaPwVotrEWO4gtcJKiJFb+iEDNughVAIYtPaEHcQDt7QOHuvXk8L3In8REW+UVuNYREBFVLr8wgQVc4yRNcYhBYlUXC8lhVpOHj38ugNmFYSPLqNIXhnTwFEWxLnfgtwmxxcTTavaLEGb1f0ueKVrTRsKEj9bX/6P4Vn8Av06zB7xQQ0W1MwJ4G4SMOv4yewNhhiA93CN6iGkN4uGiPpVYqBd6+6vgTLiamgleFmXABr4GxLMDOHg7OnpGlMelDY6STRGvQYFaDtJLNaHbBuoYYDfjbmujbcC2jyqfl62lZFKnVjXoN62eUP8RRXtgDrAVWDODig7sPZRIyeocCDxUYVBRamaooAM73x6t47beD+ix1luYenzXtIJwgNiZE6fkr0Uf7jAq923HwAIqEXT+rcgS5scRdJAXIcmterEU0BRV4ZqBajI/aJzwU3UagWrtWGxvUEEBenBRFFt2VBThLt+gSFu2XXpm16/Ahhd2cgofgKUpLKxO341fbPymnPuczSJ/vnIOnXBrds5/fsWl3OtdyrAXsVoi5uqThv59Rv7hftQBWtcNlG5WtrmHlQ6z5MscKPxMvyB6sUfD8ldueExbPBYFmwftMsOs7uxuY/+fr0wznNHoC2Sta/fq2Tbq1jWWzORp9TyIDZx26tQ0eleaS3wZZdH8vupmiXtRo94A25ez16v4qizZR/1W6SsjYPgAbek+DHNSqrLXlsYX1FQR5mQE0G9LB038n03ZhHntBqxv0B92VwRXHaZmsY1Cls+LY2W2FSwX+GmQLKLxc2H0pgGgYXMJbPqSVKRlqM3YXW1FyHeFbZKGdTPFKBTINurqvoakjpR6MsFqxx+AjN+iRZfKIDRu78tMojm1vNDAgJ4Gcn9MvAA3WIveQ3eP2IQNAHf6vuvDhxgbguu7BtnJZ+RFYLecJs3wI4VRJ+qo7HCvrokN/nJP1GkLIL9NsGzA7idGNbR1366zMMpCEBhm1GAAeHiw1wN9EKKM2qdm36/EbFCX6T4VuINWKetjQ2xDhSqzy6zjWJStVv1pJBbEdOAI/uwiFBx/r4xzKhSi5SNYog73z+7AblCYOjx5SY3MH5yUMEW7F59WqsXIf6bDD/iiusOsgnoMk7b0Ac27r7HWJgrirj4l68lW4ccDTXfoUJceRpas+fhhbGiwfIPk5EtzdoVNbMJj5wCB04D5knCMtb8fAw16UojROs8/ghZvfzRa4T83F1Va+yGvdXcDoapFrbD2A4VBoQdB9mliZn9CmfQwL5XraB6+1rc8UiV5zK1QcOUmGPg3N4qjFJnvOoJA0iyTv4TVkFEM2bx0frA5HyLHu9qHc3iVBZOVSV7+hfTuo8YBZHNTM5XXLIBW/HIP47og3FZ5VWyvJ2Y67pQdyAwYrWHawFuiWpgqD6yWCZ+P5eERpoybWFAQvLFSOpW1LKwQW+SUUkGX3hFDVSWYyid54qDQpgd3gqx0/JjiuOPUTrxNyuA2iyFCtD3q9aGdERs8wwHFkgWlINk9Qbcs8u/rmUn5RbpSmtI7CYPX+Y9905WZajol5/yyjtqMSxzuMMMj7CI2D7iTlKMHjoFFRPw71ljiGugJ6HeQ5ClfjC251CWob1KqCtQwGIoYb4Ok80pGjl8Yn9/dRHFmHSuIkWh7NHb1ZXIx390mILVO2aZNJWD7RGzEBvC03oyTIkv1QY1NeUaB6mJlkbJVugzqItZBMMmFDolA27MRRyjIEr3n1LQSpO+oNQBO2C+KyGL651L8XGg4SZsATl1GWF4OW5U+ftGPbolH0AVecuVpnjhso/ccfBn5G6XYXJJzQwFrrqQIiD+2s88hUP+oWBykKlr34OY2y4uGc9CczWp4tGP3nEFBrXbOjYqRi/AjqyGbI36TM7TQEdM3/jzQh9Awr3KDyU5+xETDLxIhwGUP9sdyi+5PChayuIhO5UvUaiOfgKQq5SVbMEgNW3uj11rsysNb3Adg842lc2s3RYSF4cONtdZ7DP7SKN3mzFcvb2g3vXgX7ufHqcgaMe3Y2giTYcw1h+XKfU1rZpLeiwbKmm1s9jSR8KSzjFmMp8NElsE9OgP0j2l2nOYrnZpaBnGMVeYC7Nl/XMlu9TTxHN6xtbvZUlvmncbq5xgFlDl/oL3InHiBqwl7x8gq/bSS0PRtdbwbJdSt6LBBhIViJZxCifk6Z1yNmb7HqaQo2+SKpEbVhpAWKM+ZG3U3Xw8Jf/Q1mbct1Em28hmWdwYcI6O0KJJpIJ8PfBn9xYA6r4704gPQWSL/Rgt299RpjS8bH9WPZk2/TXRTqbxN1MzsXp6lv3ZXCOqvtJNcq9/cjXjnoD4bTt4a64amcOWHhtYuZ8xgWL6ZWf/nUzbrYVXu+jNF0cyy1qmKsaoqvrZDUR8ZkK3zqoA/ml6a/GTyo+lZtWbniCOhT9cM2fUszvvb2dARFwHWG0Kp3gga3KoMMZJCw+khkvoI4Gnaq0NsYjmZPwMnzDDeGuu2KdDP1+8L5k8ELZ9eHn2+12mCX9IonhA1EEk8pP3hpa2QLYp2gnbC5iTfX3jnJdsNwDHJxL6SQAVfvL6PqGYpcjoAwk+AIJ9RvUI0+ltf2Dm9q8ENrN6CQ75qj6yMX11lvt0b7Yc4/ppuZeVjs365UGrDKb1DTOD5J8ufjePmEqDWQVbiV8xOt2Q7SZB/9kRZ2CRI9PwpFg3YMLPUlSDYlSkqozVZdy5mwVvV8p5aWbry1XKg77hLf4w35zxLkRX6bogVkA8xVEIJ56i7G+7n2i9ubNAZNAIbDEAmT32+RQ2uORNXaMsTBpAE2Ll7QTAbxSVk8oLkMcUc3IMTy7fD3pskZ0Z0DejOVC+KFukOwtTB3DP0KMd5t+gjcnA0wuJMwBHnuDug18mWHEyx+u6sAVnlF1rvl4S89TKhDz3IMz2nABkyx3fXM5PKFfvNl+2bMEpneiy/rJ2N2JjHc0sVTyQYrF7Cug1dkbXIIqkoBbyW8xHwty/JgCLgC6DXHQ4O9y0ep7eNbd8JsWd4VKCr9IgljiKricPxmJombzi5exumsCvFfB6Mdi0Kq03EorWXDOBTWnY1KGexIY7Hqd0aJMSj4I7Q3BPElAL7HVNyz7wEW9+x7tGv4bt6QVIk1fDOiTcRs1esWOAOnQRwkHpOKE1lIUFISwhjgsStvXcCjByQCrM2zZip28xxk6+sU7tj5T5ABuFYsw/I9gPAxLbuc7K6vr5gO8v/YOnqBXms1Anu7riXYTaCx9hizc0Ijtk+jcxnK54mX5Rmcf1rxMpp2CAW1djMRCCVnmjZDn93taZA/grVo6KwxPXt6cvMuHgG7eNlFGT4Kfk2T4sED3P8EgRvaSb48j6BwK87BXRebxuz2pAVTR577nMauQgWxwB0yAgH8NEgenZ0Ne3CdLTES7uLMNcg2Fa9bsIu7wNGGVEtorBS4jHiJbLFwQfwLr7TbLEjyAL9cdxRRSgTeGbuJOrgBeckGHDWURjsU3tr94LCAHWIND9KtTuQe9esStc2Ba/vwdRC5uvptjr7V+czNmNYg0eELhRKqIzl5Md5phm49IGdo8rhwA1DuTuKYZZbK/Rt4rk/r31J0zxBSEbiMfDDCEOyK24cIYhrAz/iK8HOQrK+edJRc9cR+9fn5ew4PDZ8jyAfHkY2kNT7q559uW1pdh1SpXbV7r5vZ+T+MEPPhz+geHzGOja8auvVntmtpNbnfc7D+GRUPhvzVa26NCpXs0odxbjROPgbubdQvggWMUoOJ4Njc2DbzYH9b59ksucgbVHHQ7kArg5iYbniU3Dk84NxAYncR0lxcaZYtRHduTUscH9kZhhU4d+h9BXlO+NxapojoZgTrkpNlmNHzVjqWp3ctsU69jCbUAq+nzTPmIl3u9yQqrrMo9H4Z3Hbk/e53DGpGocT3XXVzDKs2c99DRvfme+y83T+6v3c8T5+TOA3W1i8kF3kDyonW9AWyeJKDBqZQYVTamqHU/wmizYORzqgU7hSn0j0PisCV0zLOLotDLvpaESCOnkD2imKPWI1u7YRHwvNg56oMyvhN07HoKpNoFd+IZ2N2z+0a58y0yz04b3WYtC8fkcVrci/3asStnKUxhJ6Pl5FCmnt3enJ55nR7V0OaewwywvTbe8gHg+7ZtkeSgnfCPeA2C8LHKNk4vL9tQH7P5MlOP/2iH6cC+0L61e/wBS9wdQPdqEqOwI2xNzYr71isRS29Rq+cqpYOrhdZ4713K40yS/wM4hgUR6Qo8V4UKqVUws26FKf7HVzaz76kZPzwq5hV4H0/FKB6uUZPGCF3THS/asW8JhFBb0CQd5YwA1uDQHnuJxVwmjC8H5nKVWiqG1CUWXID/okCthyr/JxmV5xciLq4PKkW02WaVYzkxmhTsyPARlwFo7UJUFWnQK2nzL0whLbZk+7vv6WuHtI5FC68sSWkiIuH59Xbr9u0Uu8ENrvxjRMjhH9ePqTYi/csyI7nnGMvk6+DrD4YWl0jVFcoZu5KZFu7tMgdB9i7KU2+y0zsIeDvkSRcuiCD7I/uwb3pzi726L2TgfXML3dwDBt3+HYej0EgTrxm+AN/taOf6CibHqTQ7BY3ejN5GcVwuPuR77Rl7vNVUo+eJAKmVbYARwqCZDiPYW3w6XfFlrOJE3sSR4E8mPav2mq6q2iq1ZWMX6d8Udh73b0vjVPm8kgv/aPlyjyGNXkcbK+6SdhuXS4fUl4GISiWaVaIXymqw7kFWziQBfgcWXrpIXqqHQt98BH4uJ6l22BzrOtPlstA8RmPYebAbuy75IHu4g3X0H8EWQSPRq2YPUu3d1ESHI0W9FjqTLjS0ygoI1iYytlRDVJ7BEl5H4QoykCGninzPT28omB8eFcK747x8gX9Z7T2B/yzX0flIAd45M0vU7W6QX/QXRmYBk/yPNokYE0opoN5xfVVFbE/tMqjcqq1jQ2yMTuhJyv6eNCtbfBY5NiPH3jTjw7OOgk3mvCRazVUu2jvGq+winpVFlf3+DsmM7e6eudC1FUvdJD85E7vIJn6GDSMQZ3SJB06/Pc6LrNA7n9pAFnyxkkCWuP0+CVFu5Af2NYn00V+Du4DIjiT+5NUX98+hiVguzk0/7eDtqpztHS7h7tEx/2+jLF2ZQu9BS8F/LTdMf4GFtduzl63oZfy/yyjzO6BIHH8TIosjTn3tS61FZv02rY86XAr7aNwDLJk2LA5aIj2aXd1/kZ4HPPmKJm5Z2L7ZGFdvOzSrKjXr4mbmPGKrQjIl+BNFTa2PE26XVvO/DHMupjn7eeKhGXpATHStmLtIzqfC12BGfhk/V9wIkn/WOeX2tX7wBE6WuQQ0hLEILSMgOHqhIqdBey9Gb9EySNyueOdy+bzKE1wZK4dJo5GcvbpNxab3IEUFbo8U4+ttCtqrW/i9ujE7ciLZfiwry7iXR7F9kP+N/GijkHWN7TKAxq65IHvXUAuIpCD0WpjwVjaFHEug4gQNaqXWq1b/YoAYrV3yR31rVEyOayI9iQFZIimNvfJ1VLV775pV/+Bvtvl7G7B6Li7KKHzsecVY7A/o3H+AbJ86DJN3xXz7CFINiBONwbrTVn83kKRUzv2HL78ndiBwcHTveY91U1qIhjo1i58XqyeIXp9DHYWxGEZE1ldPOguWNAetdus6m6E/FvRF7m8tZ8LFLTqGGbDVjvAA7VCmY/SPCqI/fnf3i3y71gl/du7WygIjAwBd+XWiFVwyx6LGPi3mCou0pH56GBkLDUZKX6fePiRcdr++leT/RL/dI/tr3rY2tzGkbmdj0E0UASLU5lJ7yjUrnphdVVXMqNAGSgvc5c7+SJBTS1DVG8hE65vUzzmPp7S3JQxqD4eCDchgoxu8dqGViozgoL6NZJA6NnZaRmhLK6rBpC2AsLidLVDgztg6f2kD5d6I2qZt9TCMkoMGgnH3S15P53j4YtkXZ+ISbyQnPnXbhI4emcQjY2dx45/ac0kh39jRkNmdOy/6sbhFAsaVL8237iAuchvg5eLF0BQagIGAjmD07lJs/45xb1T7UReDNznSPMXAUfjeMBQbmBPZEE4jwhi/zT77QLd80qf/qpqzMglNyAsM+SoXR+0jyiedp90g1MhA8F5aGtdMThKnhqa6mPglbPXMAb0q36j6UFgrkEWpeKMYmoaF7IhYWhWiWuWRZAVCmkgRlORzuFGYLkPLJKoiILYME483Xr2adLxiMNPX9KjuLYjyNVyN+vuZPoA9jsZgiCqs9lDsJH5tTKEHj7PVpcWEInnNFvraHVuDhmfDIBqX04oz/+RyClI5hfwBGJ9wQBbrnBT8krxEvVp4qLzgB4JKuSsML4xc5rBfHeyXmcQoA4/64Rt9iGmlZze4QgNJR77+FeT2Pv3IMtA5gO208jRkKk3euG96PWAm3MXhKU15XNR7Gh/KoFg1h2+7znIBkWzu1P8yf19FEfBcZj8aylhoPw1Da10LgdHFN1zkoZ6FcPTOdYzDp8PuI5RDn2YLGXLGM6tiufyxTr2wW1w33njtplw2zIuGc96a6BOLr2CZFNCBUhn5JQ5EM9vFB7VjbcLRoQk/Jml5c45x0DARmpefyJXEJAD/U7NCcnPcwf1RysPYAvw88y7+Cg4GBOsw3quIlfowuSyj3W+aH+ctKySeR4+B3nZQoeO0Ubbcu7C03eJbtwGTQgGDH6ZZiGAOML/T+IY2SmsfNw+pzmbAtE6MjI8/qeOYhpcBk+LED2hGoKmZNZJNu6Anex2MbhNy/DBHcyv+S0CsYXT6hIg1z1hUifNz8U2Pk3XhFJkF28yTQoomJqY2t9A8Zxmj67X3nUWbYPsFYvGszLLQBIavPLhQ7FM24dBXryE+MnuDRTZ1vjxgfmIWoZHog6Mdwxb4dzPpf6UnfABrNE7iCB/PCKvJZJsg9RnVGtLs9JNmSTqIT5bHL4G4UOUAPR71QKxCnhBQDR73CfF6BcHB1HsaGMTkqMGsLqMEpxVx2rqGiAe0Pmofz22LMMQgLWjjNwXWeZIO4E/N+i+4BpAJdkqgUgDSeUqdlhDNpKQxyAaBzewvxqs3OFAcEa3q2ly8bJDPDH01tTkxFS51hgIQsw8tLT7d0f+Iwq9fwMvBRS+q7q5ZUz8KM0sQ3pA/txdJZRAMTkV12SZilrRqBjl1PkcreHqsqEGogIFYr1jerhU+nsJSrC+2AZRfFIUsJtjcTEmCDfJu0Q0tg7rA/colPSEfIxpFBIn6Lylncm26Z9nDG4zJi53X6E44IttW8CI6HpSDYesba0+aibL/RgWuYtt6TJLOQFpLNWc29Q5yBuwi1/14CrpTmeuIZ6GoWuQy/Luv0DIOGZbSz9k5XRj43Tp37eEq+E2iyzfokAgbk6AWHcIQ5T10SDeI9XabisFyRplOg3i+NWDpkRiegyys94kZPujQVSo+g5ieFMfiuHEvw5zju91molMEWq3nDmcBU1iFbW9PB984mACuDr4LHOrgL8odHCVKA9KPsyBsJWHRXkWbHdBtDmKNCm8NWPoXMPfLs2AKe2Rvl9Eca6xTaM/CIfrNj0JNRaFRvBJbKZs8l4dKytbH732R6f+mq4BDrbi3NL5JciLCnoGhoWForZeaT6VN/8AygbPhWavu7twy5UoyRPKNSOFmWOzbw2HH+0dc1pYnxzC+lUTlrLs/gae8y8AiUPIxkRItSMQ4VzK5ckp3JxAlIz4z3nMIFcdNle1j4Qwjr+J0syREC4zcbg1H3B8RFyPqUlam59p9hglG68vGC7TrNwe21L1tkAnD8iAp9M2tMsuCucfBeg6i54gyNal4Qi41j7hBTLdm/Mo3douQ3tqjgbZ1s4+qqBo/2YQMR+F73RjAECROwPLCJv1e+HT15OyeLDzJCCA3YAw2jHppmYoJ76CIC8zUMUTPAYxMWhKMAnz4jeIzA2aipYB5BEe/93Uwq7p5a/LYOeQZ5P8SBSoNx4bl8cW212aoZDQ99FxPNH0wmCXKYrD7wd0FDvIMd3Nuy0c1v9Rd6vHHHcDkAF+fR4UgZVd7THaWR1Jgkdg077KJ3mV2Fn14JK4jEC8Rr/cvzJsOOgsTe6jTZlR3m9W5tqLlyILyCm0fJwWl9ukfeDkAOINyMu4WCT3zLWCEbja6xxiZ/VEpW3v5ZHYaxIe+xsxlanohml1+lpBcZpcbZmWWQhMg0bQ6FWw5OjRQdEMx5G/gdkOJycjmyqp3GRulhsqfgP7UvihlZPdTZXWocRwRoov7uJzkMu9lX8zDDy4sHo7VMFYFpkrcV8BPE1TkeuAkpCHc+TZcHDxgjSgc7CL06MJM16fMIxS+t5LHdun0/odKMOONZSOp+xVfSV3OpPoACredAZwFYKIaMNEUTRxmvhthN+z2Y8qD6KV3R2ujcq8MfgmxWBMl+VddbR1DVjDz8Vk3jB4JSO8Keou/EvQSoieAHIisnQA1XIh1dyn3mxDo9iGTN7eo8WP/qopd776lZPSau8BWfoUwVHxmfZ2kddyseFfC5cy+42+WkuurF7TGSK4TAiZz5UOD38ioe3KQtUwWmX7cmaqcqy2Xd3f58Dq7QN2VbQBcBoU4cMy+peVMnINV3QVqG8O7psomzm+U9ZTNrRPnfiq+R/R7gSCs/RdPYtBkHTBSx3u5adB+LhI4OyEj0fnk2Xv3XJdJSY1Or1WDfc8G88Y7hSQ8++DsCgzkL090LANDxyBZ90D4Ryuo5tku28McKQMUAvMt/k/0vmvT0g1GxzD9LcqwkcnGsonGyi+pXuW5vkSxPHb9I4wvRp+hklZpzs5jDGXmZWUQgMl5aqDsWosSei709iZJGDFB016SJrEl+u2Xrfn5J/RegOKf6TJIGT9qIBRETvH97q8i3EEUKOZaVub5OFyFDxc9n5VlY4eDPO3EOyTfmUmJwC4c5RCoBdwvRyU4MNEGYRCQ63sIqsHGUQZjadh97DligRiFBizsvXq3uIohxFwYuv1IqtwOtRzQJj1HMtCkNX31eg3Y0I1M83byNeKX3zIWB0ESCAGOJyCTZRQ2aqMLoMe0ueLl10A0bS6ofqWXqZxnD5bwQDPP6NkbQcEpQlxvX6XxevAqtPPhIrw5GYMsUUWJf1gE37YQj3L87M4GMhBbADXgZaBF9P0mgZGw4+2ASUFvtFq7Jsm+kYDY8UqHtz6TV+c+sOKCtWZnngkmtpdOpD4a157dE19ONk3typNVm+DyWxAqE5m3ZXRXFJ9md0f2c4lib4eBl1LHzNZ21Uaq3OwMZnLurXqVHadGc0m2ZvRZaTtXPbwN8KhauxjRttQ2PgPwxltgajOadOZ0YzSvenqol1bq1mlKDDAwd+MNvsJeUE8xv5J9reveyhDg+bM0s19zm5zJTDCxIrl9V7M6cgbgPZM3pQxWIKiKh9jPqkO93VWWSL08Oi396L31mq19QxLjiaeZnj0w8R+zvB1gF8PfQXFQ7q2nmYK2nhzzXarKSZ77Q9+1pcPEX6U6mjaaXDjzTunX02/+D6Ag5/55nx/k8bAXqoTwEaU7P1ezSwcdfODn/HrNI5/pAXk8klvPhVGw1ZNhoSeJPmzids02dbHJKAAr2fpFj/+PtTxr+nj3ncaxoepAKrEQFTKdgYnwezGumvpgzlO43RzLMzhai7RmF2nucELja6lj7nEUXMReChywcHOZkulQeTXrqkVCnh83T+bqi0mN+ApAs+fQby7L+PE8MpnL6aSItjYwtQ0t0LlZ5DXI+57Yg99Nqd6t+Zu76/mydl2gUJ2dg+WjeJ4VJz5nyC/haMYOwD1LdWEJOL1kzxPwwjPbOOikaLoo1WkpRuQ46BQqyb4f4/5L5L1O3QWQglu6go1RksQ3/+l+/i1jItoF0chROGP9x/f95fMVVKFdH53gp9zIxNcHgZrdjggGWshDhzMaXy4FWjc/hfTJVzGAPkaRkF8liZ5kQVwuNk1HyVhtAvi/nj0KiqKB0RpC7Jfcg52IEFenzK6VfolUzqw/bfd9GZgaDx+/0AwlQKvRf/CT3gwbnvEaCTaLJfRpYfBYhRNe8FflL2m80tFP+vQen3lrJtksi01v3TBKAzHRZ1CSlDDC+NRAzAC8/FpU+mYxHQyJmwv/U52cCaQ02J9SRSBfLXkSGti4pu21Fx3H0dhPsYdkYdLV+iF5VqSR2A3ufuloE/5NfTkbHYbZBvQtxARso43taIpPWA205ryKdhMfhc+BptVPmQ4TFn9BkfIVURVajKp7/Q8/vKXv3xkppILEUFSheqIO8geRmCQPrEqXdavoqZlDuRgyBumEVgj6ulqxNfDYYlIUe2q8JoBM+C/0EsN2cGOqsTMYVuwL0zBJXcOXEFiNxlv1CbDHouwNJjPpa+TWA9x+hDGFHoUOqrzbXv46tOkzGNNbt9pTv5ItYJ60jm4D+AcnJUZBBz2PVXI035dgT7pNx+1pE7dNw2q+ebpRM4jzw9DNJQonYro0Z+MG5YF1Juv0ZPvJAQrLgVm8+dJylD4Unj0Svxwk8YMWzITTY7SCahCbuqdq2Giq/urLNpEiQtmGhArzaMHzo6z/4zAfdEhZ4Fm6CdnBbjgoyeQvd6iAJdC8wZZiTJtUAUz5wgxqdOzBYnb1DxxWiZrqP0UYHtSFFl0VxagCn296kqErMLA4E0wWTqm5iukTI4kU9kLe7JjMx6PimlVwaFrPR/WrXmV/9jOSAqNxZp6C8Yt803Ccho8Rrj5zITNnDLY4G75xiXyTmfCH+1NhyAKyrwur3pI8zjrgK6u+hQpKe115dnw1d5saxMy1fjCSounZiKqqMAHksAThC2brESZs6mCUS3aIpS45X7s2sKh88py2t2SDWbFe3sj0mbAbOOLNm0+m1q8NdG0HNrLBwxaTYw0ElL7bd9NnNwAcIKuprZyN1Mvsc/bX1fMjhn0byNGZAkKuck9qldtirQhF9amBdeHeqRtpx+DkYeKZznTEDwCV0kjTgq6lIeYnIizBrxWNQTEwfKVzhxPwFbiSKRjc9VpFKMcpvworEY8NeQFoiP39pcb1CzP1OBPzw0QTgEhrvhEzHTzqpHmotKWHczm1VCks3lNxldNXAj3bowDQqbpmALWfdx/98OWFiXDzORuhy0jXGf4EfobP0zJD/1JmIwt2sAlznccMSu0fVKQiK97v1V0tOzFJoHRvU13UTg2F+BOWTaoPx8GH1TE7A8jVMGMqoQ+0smjcv50k1d/HkUDxR2yKHhiHdHAeGQexedUDV5zECEcvPVnzCe/TCB01OdyXImDq89A+RChbTN1PllodO1Fc15H1V8wWpMxURPFcvQTbtMxBaz7uP8nmpaW/Tjhtoww0Qn3jR9mecJFQZD3I3gXwpT2OcEf9j5EFyZDpZ/Jo3J1AbNXLNK6M+WRSyocGQyaz37cklRn0QG31IQoyRlYffJbG4R5Hf1rv25uCMSFkeTGCdk25g0OSdVe2GVkfIa+7UWowPnxHDt08+Q71G4y3rt4KUCWBPFJWTwgiCEurLJu7Iewk1FA4SWvuPfiT0reXshBHMTbg8OM7FiH+6SA1F/23U+mIkOlI3rAJ579vRA54zLNmEJEnWsmlxZNHMvvOTxTfo4gNijsEjde60wD25KYc/GhKxxMgFuKLJV+m4bz4jXOWtGXEUfAYuoyZSr+whhOu/Fh0/acOarFksXAowV8NN7pCNknhpn/w8tp2GbER5Z6jDP108o/o/viLIBH++sSJSbKwfpnVDwIaDCfxoG7tAYLClj30Z8kGSs0U0uLEk9wp2JyFqF0HT5BplPqSdbwMOfiM4LSo8UBjrhNW+9pGs6L176TS2Gm2tCcWG00HcmYz6gZnVZt+pYWYP569jciUWiHQfV1v3moI2T+evYNeIbcfp1CAHkjnPbCPslBnEKHW773tkseVXthyeTxmdNNcEAdnw+7jCaGTHmFmpbp3hMvH6IdSk0+652sQZIOzNJ+3G8GaumY/zbWoIotRny8TWfNM+cwBge6wE8sHp2JdcREymaJtsG0l7QIjd2oQS3erunf9wZ9Mhb4GcRwge+VPkyhTCHSK9l7HZimZy+0X5qfxtB3p2WH0VQUfV6Y+rhdlFlyA/5ZAg8BCzwdtAmUe2cmqmTvBQtNz14IljYE1/jqylsMLqHeMuWhOcWInAVZQaQZ8ZkSZ4BN+hj1zjr9wsNJXcPQptLnDFLVMCy0F7vUHNhszL3KiLsm364Y3pq/c88cGGtEVx8jvpra46fuf7kDYXRfPwppE4XtAY9J8efhJaq579wnJ28fWJGP+tUO/Vyp0EWKnmF+UGMEX6JRQqsClk1FPyZucxaylaAyYpUOE1wAMxWu6tT6ZpyjlM3WzDa5wJZRMDXPN6rDbbAZTMUwGy2iw5mHC1l6INoCQZKGhjCdjYbHXAPZGORzypnMY2Ax1Qmfkstg9ckZ7UeQRUFStLL1LN3eRQmuOHl6dQluPNaSVj+kpOwyQlWwmFO+dhn/7c3ZfPaMOv6+a8ujUx/VFdjz7yUsgbP0PYnEPEpVInmBLjhI8SgeoFmzHon23Phvf2WiCkserPTbJ5HXO2bnS9CeRoatkkxFzqyPbYkU0yXFjqzmky1HtT9KiNRg0dnYHfs2dFXyfHLHqFysgONI3KvJSV742IiHO8Sn5mbRZr/XYnd+esAUAtdGIZiNtO0T8SOIy5ZJ5RT64YtxOReTq4JmXdEnDxvxkx9WrqjV4Oc+gKnZevKDvMphbb+P5Ptz7j5Pn5M4Ddajp1tpOqZt6O3H/U+v0dKi0tcM8mrUwmEZbHcx4CNvOn2zEwtaczOiSKAHfzJeuI1ABukLgceo9JgnvKgzLfYULsTXgwhC39Gj0tksQs93bDV/q+40TDSi5VaPf2Ziq223yT3goD7KPFSEytP+8RND0T6wVcdPKl5tmgqwJ75q+8N/9N2OOKX7r0qzRGnp1NM5HfX4a8CxTTyh4zm1zYi7RnJps2CuSR3aKgyyIHx0f3b3yVwYZQ6LV58PRFhV1OzHyf9rkJT3QViUGchGNwORndMAqYIDYAuSnv1gjCa9wupkB4cerG9TkohBdWlOmVeEbMavcDCZV7S5rp3z2XLdkBJlIlCOgPu0OWEqDqS2oxlxYX1W3Suxx7OCMGUHI+x0rB1zlHMth6k9gJrQtjU9f41o2TJhr+Y2bXruuiljsAT7JbZqnC/w9TgXoV6NgxFhNF17JshaVhuQX+LplU7rUTCdPgNMxnlUi+nY7ywowAZFEh3bdNF0TF95tx/332TR0rIf5oqWEVS3PK0J9CR9mu7E0kdQw48Tg86MO+IufcHTTvSMWG2vtrx5Md3oW545581ky7sOXlFSgK+geEjXyuKOakUfHumScdwjyD7FjCir5udgSY3FCNwooVDppEnxwtxYcq/E4lxZcnQBacmSM5GSCI0ahxqjPeHDDnEGlcPiM4IiDb6aNK8SCgerue/SzZj0RmTRaMmWuk7FrC+t5y0VEzEcI3CgjEaV7nssMTvW3Kv9d76MObpktOXLmWzB5LMDdbOMyXMSXydlokvJaVlc6yDemojpUzLXkEwwM1bcK/k4T2Yc33xjxY0zkYzXINtGeQ4/IDK+Bljar4ivIEwz8fNLpiJ1QGUKxzk+80gSIUbW8GPHkQ6lH87k06dkxelhO52YfADhY1r2g14wn8W7N1OTEk5s6Tiik0uWHDWfYS0GxtMPewooVNrD+03nxqCjX/7OjKHGvRg2Z6Tpr4mhgC0zSOmmtnKuFkmE4DrKrC5mmX7HvfSR/cL9zkXK0KPUJzETs+GPJlGtnCJX8+wtbymXKClqfrPimjGIW6bUSZXbbzsZfyI2eIIz8SXdrIi/0UQKWbJfj0p42i8bhSGJXkXY+EqbKhszP3xHEqXSXQ/FWbDaXiS+nI6rxkx3qctOk2e6dM8//qIH9VnnMFhmb1jl5P4+iuEXsHKYvFumlbcd0oC6r/ueu7sjRWnrqRCbcMuJKwuYx9Bj3jafmGOMJb4exF1QR4+OPEFtp7sch0o+ihgVbYPs9eIlfAiSDbiBK+IMavQgCV8l7FVXoFmr+aguZTAK9CV29cUTT/Do8sMPFR0qHUkmYB6sgX+88cQEPEGN/HTMED6AdRmD2yB/bKw+5DexhxdZiZpQqmAcJxqWCCFKfi084qHzxHUcylS6JdtNxnt/L0EJ1hfbIIpPiiIIH7BpeuxbDS4Wvejf3Br7f6fBJ0yl4+mvNCTMgyRrsBngn6YSw0JtwVin7xkyIHcEZ8mDNaZzYMNVRUTIeUBPzDZViZxkumCUfZNAXsRvnrhMPFTeuUypOxK/OfAWIeKEpNhN7FEIN102mEy2ES0nY7/LNCu3y/IuD7OoSr3m+j5ErJQxfVMQOaV7b61madKxNU0YpDx6CgrwFeQ53IZXl1m6HY9L6M57saHoor3njx5BKj2SkzEXBrlN39hjJuzRTcVkzLHY7tIMBRm7R/HCfdqdxFxC4UBB7JUchMGIpknDVBQlm0mNRRcv07MKhQN9eHs5PFahado7VoEdxWnl7cqlwX5ePR2Z+uhzECIL/ZzPtefeCbMRdKn0WeM3GaOdBuHjIlkWafjo9Zzkhc0EyFMoCevsvb4komwvDl0ivpt/gqj5Md2IQXVteG76rFFJuSjAtnZUQj8l9ytJyfEKIj+PlHqFxJnBhSzydJXSEjwCd/VIUukRN5nMMJDG8Y+0gMpizVNoejqg2JTU7qTwe4E0udt01W83uMnWbbl7bVumYVXo909JU6ZQ4S7aarNsKBjDxiAdea0+p+Av9OEkyZ8lehlRpT+rzedxnpXb8JirjVEwXDPirQ7FybjsG3jOz9ItPmYqCjCiydiyi+yaBEl9PyCJJRxqre5GZiP0N9rIhczRVuhPYfVxFAllwEmO5BJ/eObBPw1u050M43SjKY6IJmOLI7Jr6gRIfj8gcSQcaq3uRmYj9Pd1mouZo63Qn8Lq4zh2BX1OciSO+MMzD/5pcJvWhQRhAFU7oCqQqEZjiyS6c8bppCs5ILEkGXCtDkdnqPaXkEu6GtypHEk6GTKVIwklGKW58FLbYkIXFWxbvQFPEXj+DOLdfRknIM+VLVKC9qNbpkR4cOz9nEoHJNHUZkSr7yn5kCoYut2pawnnfDKOcnthwxuMGTIT1XgenGUk1iaVZerMfDhSa49E1R7cOxuy0/7dMevz0Lg3yxc4BjFaVLAFyBqPinQNLqMsL86DIrgLcvbYh1otQdG6ru6i8P276jMxmfV35Ie2Df54v75L4VwHd3HXhBE4PcDBS5cMjwVPlnI7ISsMdAX/KnAoYKabtoTXRVs4AP5LGgZx9C+wbmac0xGnDq9LTrWhzoNkU2LHarbPtojbVVuqQh5YFhm+Mcqr6PoCCplqQiKZmgNYsFGnGQzYKrze2VrqPadEWGlZ92S9ARzIqgOI0EFnmP7pYl63dI2B3nrB3ZnueuW8/pq8SIOEieWAXAgoS4Au0zTTQ1fE66ErHejha5CU90FYlBm6umZ6oYt5PdE1hnrrXuCzXXVl3H66YqVO0njNp4gsFXdUV1Dpqn0Ozu+rLRZ21tYYWtHVPiwbRLYKdx3XedjbaivVtdzqZKKepR0OryjEs/wF1ZRw11NTOCiJmng0HCnUFPElUFM6tGUXcGlDap+gMsabn145d/umqwxJCBBHTyB7vY22vP7oYq6koGqoTf9pmaxjUN0lixiBrCNhCbKabudtoG3IwgV3sQ83UUKNaaWGqWR/YGrIlmmXy1tvlQ7Idm4tqbggairjwpzwhQpAW0OmA7SVFLuV9CfvaLCHNqoi00FbwoPfFg6A765HGfhdEa+DrnSgB+KOg+mCKOP1gYvzlXo3ouMYWSjpSO1ohiuLOpHCr2uowP8zS8udqJO6UNJTXWOgp85Vh+mnK+L1gkqHxQPhrMhKBaKQKwyIcoV+BD2IYWsdJ8SnGm6tocOFqky7eIG7QBLEJ2XxgEwcITZiCA948uo8pOQtBrCrUzAwaNTfef3VRYqnke/oAXEbJU14MqGryU4pdM0BLP6M7uG+ma0HsOBX42HBr6mIhaRneW9q0ygQAkSZcDqVdCpc8xu+WeZ3UpUJO6mKh47k4BkSfJ1GSZGLJ4xbi3s851Uc0ssfot2Wr350RVxtvC1V7EEwY3SxrCelefsZxDEoxIPZK+f116syOIdQ7YMS6J8l4CoMvXL+vFFVBsczxcIYrhXxmPar8Me1X0tNe17uQBjd16KXyMwl0qNF9WUaNb+Nsm7Nb35VP4Zjx0tanTt20hZG2CnjpYOR6pzeBlwLKFEomS1crtbPjyCLgqTLBHWWbu+iJBDMi0ojCV7SdgP4/r0M8JfvScRb1XQxDwe6htnoqA+JfDXV/+uvo35DMUJqmGjzZW9p5dj6PIgDWVkFG7K+EV7KOOngY8o1dd44Vdapq2uso7rFkFKcPidxGvAUwa6Iq/y2pUM3jhHIrrOIa08kyri3jV2xiiWbLx67IqEFW0U0VhWzIHwU9lEVinupyofYpcpEVeUI5/EHXc5lCLqKwo3They6qSkU3TUp6ZZd3nOBdklWEOmXZJ2hYzabk5Q9ZLN1uEdstppu56LFLqqohIbaAhcmgOPowIKafG1YUFkTHQU81BAYNniS6aJYoydZyjV8khXUu0JdyLuragx0WVUadBngUSakSImSLmUOS0NXxsW+Kx4crlhofCLK+EMUK5qVvmfiTogyXidE8ZBZBSQA7lcyocNW4ZpZmFpDuyyEAbAKcMe9We2Vc3dbusrgVSFOa8HK8uo7/2oQF6kAFltG6WJhN8pnUF7mBLZTXi1u37yKGigM9D3cqcKRiRuNmHN24tbjH6K4VdURkXc/2OmwwZuKPM4auKlirkGbqjHoV7PdBdGGd9zoivh+NU3poC6Kg2Degu0u5stmpgZfK+1VUrjW+QKKAmR02CKmd1FF0ZUPr67KZdYAGpw6kqstre774VU5xzi6guwasld1cPKDvMzATxBtHnjc3CvnTzxVRa3D8wgu65w/0mwVSbdErYGee/FJmW575bw+e1WGRPFrEkq2HrKUK4jJCoOXcP04h6xcYqrwL9v6tZR6Fo9qr1zcp+qoCuOdMV0La3I9BUSVNXwGZSKUW23Ih1BZmDZeMRIM2Coyb0zlnht7sLhjpobMrKza7Q1A1dZiF7x+Bf6pj64zNMhZmucQeCzula3CHWSm1qDQ7GLBccRlV8gXlF25Qj9knDduX2QFUX9kHcWLdImDGFtFdoG+Otnt4gisb9O6fgSG3UAazwexIxNTQ+ZAUVdS6LhmAZU7GcWrmVVXT9no3zq/Dpk+6TpS3+SVDgLN5A246fGrqXED2WZ4Xpr24rXO1FBDo66ujsGQE7+gnsSZf9Vipey4WK+8IVwE9WS46Dt0kgbpIYRklWVYUe3U76Aps/IQbtLaMuTohtrep8i7a3AixXWlk0k0U59QJkIfO41MFZHLHF1L4QzaxrvhHjzbUtFps62g4MIq7ooqFTmyqnbVi5LBP8l25cLDZFdFbQ/jPGkXbWWcqpIdjVNbB6MhNBT6Zjok3muSJnP2Udyqe6NHtGmt6bIGVI/vyPe5/QeDEAXxY0C6peQhYAVF5WHfB5p81aEhH0UOjwu/trdB4T3+7EZE+p5TfzgoOcl9SZjzxkWlmZhM3ttDTKHsTSEFQfqMEoNSexWpP2Ay5X61rLmUHTCVZmJy+y8OMYWi14Tclv1DDQVB+PLR8fDcBtkGubloDk/dTMJPPOJERE06PMQzRqjZZXgTZUeDU0tMAudhJcZe8mJS1B51rgrDmHh0IGtAikgn6vgmPOqJIPGLVhuC6RelMrJleXz7yPfeuHb4C96ujkx+/9krkSKcMwSS2s6IoPcQwcPdavtgCu232uo97Ooc3AdlXHSvVznbq6CqbEulH9HW2yn/gSzdkn7dWzUUPN3VJ5p6N7tq4bIk8ys6QZtqx30LjFvLn/gas39Dy9X9VRZtokTC+0xV9+T3rEgkrzskmX6+LKaXqifZpTnPqasdWvJMenyyhU+TV+R7auFoKDUfJJF9BU4SK37YLYcleNTNBz30MNvBANdD0k6hwqD2mjjhFZcT4GRYtAbkAIeiteF2OrtwJNi6Pk4YgqgD5EA4PF/0SRvmB6aqN24Ydwh6YRHIWxqx7ilsI9FAOfdElRIqCcHD1UNFgLjlXoZpmFu41b1xzMhDUt+VyrTUfhX3Clrv3he3E0WnMCexp+uKCaUrOlerRyS9sS2u2itxiblOXHnYqsm1aMqPYPw7fwqA14EQG+bElZ3M4xyG4TSKUerjFrJkEHpV/Q2BEg9ZEN2EC+5Ai6lm6vpcAr2ARBQAUZQh/WFoovHILVKcWn4sUf2oQripMGKQBbnXGb5EVKOaqbyfxLc3yFJm59TyweZMdCfclPjqhlwcmWmY3l41bwRTcaY6ivnxowxJXpGhngT0knUGUKYCSnUocwNFsa3Zhm5ntgYpndaqjjWi08wmXokkWNmKJeo5RH3kddtEEJNvSZxafqRyP9YZbtp9dEeuypYkrryfxCP3MKnbCV1BjKi5pwkZBa46UvOivJmRVgWiW1UQ+cSRVSyRZFrQQfTadoLweHYuM7VfiZIeza3vU5cWucaoBNdzOyyVP4/e0OA2akSa+RBNM0yyIIJSNlJr6IOfVAIrYmhaARL1hw7HtlM4rnPruT+rUxEdcSt+wEZTQmXM0KvhY9b9kccLMLnqvNnE3mH8BpJ7Y2u3OV44SwqKNDylo6Gpx11xXKra1vM6i+FoQ2aKx6BfxS3hTLjPrp0jXbDDX3L7wlZyf+fin9QmyOrquszChyAH659R8UD0wBI+1MQZOVTbfhhZ3FQYItZ8IKh128EXDwO/gSNCuC2FK14pPq+joflOzrvy+NCt3MqFqYanDe87IBCJKh4EIhmauGvHjTqsTyInkLBUFZLW96EYSQInYyAqkZDdDMvguhhu5JY7phmaJkazeE30argluh/Furr1FkWoNicP7/8dXDGVdEVHiHNbMvurNFy26UGoeWU6eOTrV9ynMx8V+Fsq8AQ1fYg6bkBz3Fwep9yWfOFC5lVzu5rHI5mKvD6wxXFr+tncOBHla9ktCxRv4X+hsLyFdd2vcN8eGP3A9wO+4bLqg2dAJ77gopD/tbwfCOPvYIBkK0Nc2cfimHwoJCYCYV33loKxh+FalspBwWl1oKH7AZL2y4OmmD/BgIMkqSIEeS64zGUARsI/w+OiNiAKUOkQ+RLYgrD3rrlV3rkuC0uhjTxUM10BbCwqmd+vpLY3McFG3CIhkKVeBkPs+yupPUiUgBoOGfMYEkm6FtXXjMogJPcsrh46KqS6IUdUJ1mNlyEe3sRVWntbovs2nHT+H6MxpUCIB4iXqggPhSwF0b4OsQGbjs+bKqPlbog4aYyYjE+S4VJqPjgEUtrVtEcxPlKYkuRNroaUGQ+yT9WBlQEZcVAUoCtA9r/AVVh3uO2IfDutEODm6FoJu1UeUjmcUQZFBS6VBkkGnJ/ayFyNV9zmR9/LPYXZaHKzyV3kObXECNt4ifdzzFWKuih/nPksL4PtLgYdYPE892o6Qn3EGW4z4w16DgtqDlt9zf2FmYx/uLk4mZ8N+RJFk63kXrX0T2rzgr+N9qUQe4Cp602nHi0mGBWjXGaY4lf0I9iEMdhpGMIck9bDIDZJ8SsOkMKlYMgSNdEg4Gya8g2OW88jJ1DZQYkB5Ob9NCCbCroiJZxf0xPpJnFdnARMpdIDaIWU5baUaAHW7vHCQeJX8D5YOgFmuS0d88NMB63eyPSYq9/IJ1/xtm+mzOfA6DBSv5F7rWSiYalzKSixSb+uT+7gpoygwPRqeBgSBQbp1xUTJiZISshMBqfLmyLbvDm1/GzcWqH6bMhVWB7Cuo7Q57YUc4CghochkQSUEtX1uzymGxw6gY8C08gbSHYXsh29x9AlsvPvYEojFuBIAybxU5A28MtXcxgwBK8C1hDPdSJma/kdmq5DBoAz0nsJqBRW10ALMTl0Q+bVA1k08HJiIDEXB6RrjhENgniNDbTwy0nzGDQq543K5i+r79NQPZhejQE3zlANR5nk1vesGEw+WNxMRWRSpTp+BmfjU2sp2b2YTsg9iymU7YOTJWQ6ewDhY1r2b42Zz1y+U20r4R+2H5Jv2FIZL3LRkQN0dacsGAr5mW6ojacT3oTDBJdCmaGUdLVGt1okUREFseQJrqyBmEiz57d0X733W/1C54PRvMtjuxoeF2FbT+RKIfHfLgvqWA/jSVhET7CbL+lmRfyNskRyQ3NLqotJZAATpDFlkqEi6opgOB4S2fsvbr3hzV7/6dc4ZA+R653MPnmOZvP+PorhR7CSvPFkK0lmw/BxZ9sH3bL76oBx415INpEHi6Cmz4NB22WPhWNnIdqWUCQi/4xoG2SvFy/hQ5BswA0cWlm2s+FGskExzXuGe6WPj9UXt4OAfyhTT9feM7LDB7CGZ6PbIH9sdnPyG5f4oTYSQijQJD1UgWwo2M6FgNzt9n8vQQnWF9sgiqF2GoQPWCuU6tlDTfyo2dxee36h3Bo+h2gpzqGp0GpgoFrg/bFqC+Y+XKsKeigMpSisKyaMBknQQxeoDY1oQLxxDdWHKtOQjZySNROOwYHLl+VdHmZR9T5kMFcAv7YPHZTpjgLBKXXgjBo9QXXjK1Qf4SpfXWbpVjYesuo+BoTur+c9Qhe5HorbVGMgiMr7PgyL7S7NkE/NPfI1HdJiJLV96i9UtxSIXon1cFy86AyHpLbP4aC6pTeqFx/DcQ52cVrZ9FrQosHg1HVOCAdG1y0HDFloPSSnQfi4SKCeFD4q7yuDbXxIEUGnFCBhHW/DJHn9MNREJjDN3A2nHaKvIClR4KDaOIJ+8r3g2Woy9T4pOaYP8rPUDZ7si4FAFtlvwGkc/0iLKNnUlCGL7Fma5EUWRFB8IG2jXSFNarbbdNVvJ8q84QT28MI0yONG8y5DDsnETKH7YSdThiiMJFFdTpRWdpJJhwQlvDlLt3jTUmQUookoj48FRP9cR+FPwKC+Ox3YLquQfLjaenLklbMWTUA2yumnOftEE1GqQguI/vmJwp/cPcnvTge2S5woH662nhx55cSME5DdZl/7gYKdK84/1UiaVs4cqn++6lHRN490JY4HmUh4NzRyXVUFMvgkzGQIapX5Bqee+wzi3X0ZJyg3tqr2JGgveersvq8RNDYhmezxg1PJ3zRRBVqDTrdUJF1M7/4MnxG7DQ6tGdSROVd9+qwHeTjkQa+i+6O+e9J//1ABQQMPZxlkbdnvH5ApbxvUH+DP6lbta7oGcY6//v7hpoStt6D6dQ7yaNOB+B3CTECI+uyANnUWyT16ibkDGSaAxKip0hS3hoYiWAdFcJIVEXpuC4tDuJawVyp2F0Q3XHdgvUiuymJXFpBksL2LqTvg3z/I+//9A4Pz73V4LBckQDQjSAK4Sk7LKF63eF8Gcd7b+kQgzuDo/wng92ou4dIswOa1hfQtTRQB1cN3DnYgWcMldwu2uxgCy6+SZfAETHD7noMvYBOEr/D7U4S9H0VAhieCHvbfz6NgkwXbvIbRtYc/IQ+vty//5/8DsJwqXj7xCgA= + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.Designer.cs new file mode 100644 index 0000000000..05f0cd1fa7 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class AddBlogAndNewsLanguage : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddBlogAndNewsLanguage)); + + string IMigrationMetadata.Id + { + get { return "202012051645539_AddBlogAndNewsLanguage"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.cs b/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.cs new file mode 100644 index 0000000000..0312293126 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.cs @@ -0,0 +1,28 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddBlogAndNewsLanguage : DbMigration + { + public override void Up() + { + AddColumn("dbo.BlogPost", "LanguageId", c => c.Int()); + AddColumn("dbo.News", "LanguageId", c => c.Int()); + CreateIndex("dbo.BlogPost", "LanguageId"); + CreateIndex("dbo.News", "LanguageId"); + AddForeignKey("dbo.BlogPost", "LanguageId", "dbo.Language", "Id"); + AddForeignKey("dbo.News", "LanguageId", "dbo.Language", "Id"); + } + + public override void Down() + { + DropForeignKey("dbo.News", "LanguageId", "dbo.Language"); + DropForeignKey("dbo.BlogPost", "LanguageId", "dbo.Language"); + DropIndex("dbo.News", new[] { "LanguageId" }); + DropIndex("dbo.BlogPost", new[] { "LanguageId" }); + DropColumn("dbo.News", "LanguageId"); + DropColumn("dbo.BlogPost", "LanguageId"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.resx b/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.resx new file mode 100644 index 0000000000..9e6a6c4fa3 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202012051645539_AddBlogAndNewsLanguage.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.Designer.cs new file mode 100644 index 0000000000..7c33223814 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class V411Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V411Resources)); + + string IMigrationMetadata.Id + { + get { return "202101251149352_V411Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.cs b/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.cs new file mode 100644 index 0000000000..fd19393cf6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.cs @@ -0,0 +1,92 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V411Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CartItemQuantity", + "Product quantity is in range", + "Produktmenge liegt in folgendem Bereich"); + + builder.AddOrUpdate("Newsletter.SubscriptionFailed", + "The subscription or unsubscription has failed.", + "Die Abonnierung bzw. Abbestellung ist fehlgeschlagen."); + + builder.AddOrUpdate("Common.UnsupportedBrowser", + "You are using an unsupported browser! Please consider switching to a modern browser such as Google Chrome, Firefox or Opera to fully enjoy your shopping experience.", + "Sie verwenden einen nicht unterstützten Browser! Bitte ziehen Sie in Betracht, zu einem modernen Browser wie Google Chrome, Firefox oder Opera zu wechseln, um Ihr Einkaufserlebnis in vollen Zügen genießen zu können."); + + builder.Delete("Admin.Configuration.Settings.Order.ApplyToSubtotal"); + builder.Delete("Checkout.MaxOrderTotalAmount"); + builder.Delete("Checkout.MinOrderTotalAmount"); + + builder.AddOrUpdate("Checkout.MaxOrderSubtotalAmount", + "Your maximum order total allowed is {0}.", + "Ihr zulässiger Höchstbestellwert beträgt {0}."); + + builder.AddOrUpdate("Checkout.MinOrderSubtotalAmount", + "Your minimum order total allowed is {0}.", + "Ihr zulässiger Mindestbestellwert beträgt {0}."); + + builder.Delete("Admin.Configuration.Settings.Order.OrderTotalRestrictionType"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.MultipleOrderTotalRestrictionsExpandRange", + "Customer groups extend the value range", + "Kundengruppen erweitern den Wertebereich", + "Specifies whether multiple order total restrictions through customer group assignments extend the allowed order value range.", + "Legt fest, ob mehrfache Bestellwertbeschränkungen durch Kundengruppenzuordnungen den erlaubten Bestellwertbereich erweitern."); + + builder.AddOrUpdate("ActivityLog.EditOrder", + "Edited order {0}", + "Auftrag {0} bearbeitet"); + + builder.AddOrUpdate("Admin.ContentManagement.Blog.BlogPosts.Fields.Language", + "Regional relevance", + "Regionale Relevanz", + "Specifies the language for which the post is displayed. If limited to one language, blog contents need only be edited in that language (no multilingualism).", + "Legt fest, für welche Sprache der Beitrag angezeigt wird. Bei einer Begrenzung auf eine Sprache brauchen Blog-Inhalte nur in dieser Sprache eingegeben zu werden (keine Mehrsprachigkeit)."); + + builder.AddOrUpdate("Admin.ContentManagement.News.NewsItems.Fields.Language", + "Regional relevance", + "Regionale Relevanz", + "Specifies the language for which the news is displayed. If limited to one language, news contents need only be edited in that language (no multilingualism).", + "Legt fest, für welche Sprache die News angezeigt wird. Bei einer Begrenzung auf eine Sprache brauchen News-Inhalte nur in dieser Sprache eingegeben zu werden (keine Mehrsprachigkeit)."); + + builder.AddOrUpdate("Common.International", "International", "International"); + + builder.AddOrUpdate("Admin.Plugins.KnownGroup.B2B", "B2B", "B2B"); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.ReCaptchaTypeChangeWarning", + "When changing the reCAPTCHA type, the public and private key must also be changed.", + "Beim Ändern des reCAPTCHA Typs müssen auch die beiden Zugangsschlüssel (public\\private key) geändert werden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.ExtraRobotsAllows", + "Extra Allows for robots.txt", + "Extra Allows für robots.txt", + "Enter additional paths that should be included as Allow entries in your robots.txt. Each entry has to be entered in a new line.", + "Geben Sie hier zusätzliche Pfade an, die als Allow-Einträge zur robots.txt hinzugefügt werden sollen. Jeder Eintrag muss in einer neuen Zeile erfolgen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.DisplayAllows", "Show items for 'Allow'", "Einträge für 'Allow' anzeigen"); + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.DisplayDisallows", "Show items for 'Disallow'", "Einträge für 'Disallow' anzeigen"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.resx b/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.resx new file mode 100644 index 0000000000..45481ac4d2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202101251149352_V411Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.Designer.cs new file mode 100644 index 0000000000..db9463e650 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.4")] + public sealed partial class V420Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V420Resources)); + + string IMigrationMetadata.Id + { + get { return "202112171231491_V420Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.cs b/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.cs new file mode 100644 index 0000000000..9dae67f2b9 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.cs @@ -0,0 +1,46 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Core.Data; + using SmartStore.Data.Setup; + + public partial class V420Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + + public bool RollbackOnFailure => false; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + if (DataSettings.DatabaseIsInstalled()) + { + var logTypeMigrator = new ActivityLogTypeMigrator(context); + logTypeMigrator.AddActivityLogType("EditOrder", "Edit an order", "Auftrag bearbeitet"); + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SocialSettings.TwitterSite.Error", + "The Twitter username must begin with an '@'.", + "Der Twitter-Benutzername muss mit einem '@' beginnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.AddProductsToBasketInSinglePositions", + "Add products to cart in single positions", + "Produkte in einzelnen Positionen in den Warenkorb legen", + "Enable this option if you want products with different quantities to be added to the shopping cart in single position.", + "Aktivieren Sie diese Option, wenn Produkte mit verschiedenen Mengenangaben als Einzelpositionen in den Warenkorb gelegt werden sollen."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.resx b/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.resx new file mode 100644 index 0000000000..a16bcc4ba7 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/202112171231491_V420Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 588bc2a5ee..9afb1372cc 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -1,24 +1,19 @@ namespace SmartStore.Data.Migrations { - using System; + using System; + using System.Data.Entity.Migrations; using System.Linq; - using System.Data.Entity.Migrations; - using Setup; + using Setup; using SmartStore.Core.Data; - using SmartStore.Core.Domain.Catalog; - using SmartStore.Core.Domain.Common; - using SmartStore.Core.Domain.Configuration; - using SmartStore.Core.Domain.Media; - using SmartStore.Core.Domain.Tasks; using SmartStore.Utilities; - public sealed class MigrationsConfiguration : DbMigrationsConfiguration - { - public MigrationsConfiguration() - { - AutomaticMigrationsEnabled = false; - AutomaticMigrationDataLossAllowed = true; - ContextKey = "SmartStore.Core"; + public sealed class MigrationsConfiguration : DbMigrationsConfiguration + { + public MigrationsConfiguration() + { + AutomaticMigrationsEnabled = false; + AutomaticMigrationDataLossAllowed = true; + ContextKey = "SmartStore.Core"; if (DataSettings.Current.IsSqlServer) { @@ -30,608 +25,35 @@ public MigrationsConfiguration() CommandTimeout = 9999999; } - } - - public void SeedDatabase(SmartObjectContext context) - { - using (var scope = new DbContextScope(context, hooksEnabled: false)) - { - Seed(context); - scope.Commit(); - } - } - - protected override void Seed(SmartObjectContext context) - { - context.MigrateLocaleResources(MigrateLocaleResources); - MigrateSettings(context); } - public void MigrateSettings(SmartObjectContext context) - { - var prefix = nameof(MediaSettings) + "."; - - ChangeMediaSetting(nameof(MediaSettings.AvatarPictureSize), "256", x => x == 250); - ChangeMediaSetting(nameof(MediaSettings.ProductThumbPictureSize), "256", x => x == 250); - ChangeMediaSetting(nameof(MediaSettings.CategoryThumbPictureSize), "256", x => x == 250); - ChangeMediaSetting(nameof(MediaSettings.ManufacturerThumbPictureSize), "256", x => x == 250); - ChangeMediaSetting(nameof(MediaSettings.CartThumbPictureSize), "256", x => x == 250); - ChangeMediaSetting(nameof(MediaSettings.MiniCartThumbPictureSize), "256", x => x == 250); - ChangeMediaSetting(nameof(MediaSettings.ProductThumbPictureSizeOnProductDetailsPage), "72", x => x == 70); - ChangeMediaSetting(nameof(MediaSettings.MessageProductThumbPictureSize), "72", x => x == 70); - ChangeMediaSetting(nameof(MediaSettings.BundledProductPictureSize), "72", x => x == 70); - ChangeMediaSetting(nameof(MediaSettings.VariantValueThumbPictureSize), "72", x => x == 70); - ChangeMediaSetting(nameof(MediaSettings.AttributeOptionThumbPictureSize), "72", x => x == 70); - - void ChangeMediaSetting(string propName, string newVal, Func predicate) + public void SeedDatabase(SmartObjectContext context) + { + using (var scope = new DbContextScope(context, hooksEnabled: false)) { - var name = prefix + propName; - var settings = context.Set().Where(x => x.Name == name).ToList(); - foreach (var setting in settings) - { - if (predicate(setting.Value.Convert())) - { - setting.Value = newVal; - } - } + Seed(context); + scope.Commit(); } } - public void MigrateLocaleResources(LocaleResourcesBuilder builder) - { - // FluentValidation splits messages by dot for client validation. Don't use abbrevs. - builder.AddOrUpdate("Validation.MinimumLengthValidator") - .Value("de", "'{PropertyName}' muss mindestens {MinLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); - builder.AddOrUpdate("Validation.MaximumLengthValidator") - .Value("de", "'{PropertyName}' darf maximal {MaxLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); - - builder.AddOrUpdate("Admin.Configuration.Measures.Weights.AddWeight", "Add weight", "Gewicht hinzufügen"); - builder.AddOrUpdate("Admin.Configuration.Measures.Weights.EditWeight", "Edit weight", "Gewicht bearbeiten"); - - builder.AddOrUpdate("Admin.Configuration.Measures.Dimensions.AddDimension", "Add dimension", "Abmessung hinzufügen"); - builder.AddOrUpdate("Admin.Configuration.Measures.Dimensions.EditDimension", "Edit dimension", "Abmessung bearbeiten"); - - builder.AddOrUpdate("Admin.Configuration.QuantityUnit.AddQuantityUnit", "Add quantity unit", "Verpackungseinheit hinzufügen"); - builder.AddOrUpdate("Admin.Configuration.QuantityUnit.EditQuantityUnit", "Edit quantity unit", "Verpackungseinheit bearbeiten"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ApplyPercentageDiscountOnTierPrice", - "Apply percentage discounts on tier prices", - "Prozentuale Rabatte auf Staffelpreise anwenden", - "Specifies whether to apply percentage discounts also on tier prices.", - "Legt fest, ob prozentuale Rabatte auch auf Staffelpreise angewendet werden sollen."); - - builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ShowProductBundleImagesOnShoppingCart", - "Show product images of bundle items", - "Produktbilder von Bundle-Bestandteilen anzeigen", - "Specifies whether to show product images of bundle items.", - "Legt fest, ob Produktbilder von Bundle-Bestandteilen angezeigt werden sollen."); - - builder.AddOrUpdate("Admin.Common.CustomerRole.LimitedTo", - "Limited to customer roles", - "Auf Kundengruppen begrenzt", - "Specifies whether the object is only available to certain customer groups.", - "Legt fest, ob das Objekt nur für bestimmte Kundengruppen verfügbar ist."); - - builder.AddOrUpdate("Admin.Permissions.AllowInherited", "Allow (inherited)", "Erlaubt (geerbt)"); - builder.AddOrUpdate("Admin.Permissions.DenyInherited", "Deny (inherited)", "Verweigert (geerbt)"); - - builder.AddOrUpdate("Admin.AccessDenied.DetailedDescription", - "
You do not have permission to perform the selected operation.
Access right: {0}
System name: {1}
", - "
Sie haben keine Berechtigung, diesen Vorgang durchzuführen.
Zugriffsrecht: {0}
Systemname: {1}
"); - - builder.Delete( - "Admin.Configuration.Measures.Weights.Fields.MarkAsPrimaryWeight", - "Admin.Configuration.Measures.Dimensions.Fields.MarkAsPrimaryDimension", - "Admin.Customers.Customers.Addresses.AddButton", - "Admin.Address", - "Admin.Catalog.Products.Acl", - "Admin.Configuration.ACL.Updated", - "Admin.Configuration.Stores.NoStoresDefined", - "Admin.Configuration.Acl.NoRolesDefined", - "Admin.Common.Acl.AvailableFor", - "Admin.Catalog.Attributes.SpecificationAttributes.OptionsCount"); - - builder.AddOrUpdate("Admin.ContentManagement.Blog.BlogPosts.Fields.Tags.Hint", - "Tags are keywords that this blog post can also be identified by. Enter a comma separated list of the tags to be associated with this blog post.", - "Dieser Blog-Eintrag kann durch die Verwendung von Tags (Stichwörter) gekennzeichnet und kategorisiert werden. Mehrere Tags können als kommagetrennte Liste eingegeben werden."); - - // Granular permission. - builder.AddOrUpdate("Common.Read", "Read", "Lesen"); - builder.AddOrUpdate("Common.Create", "Create", "Erstellen"); - builder.AddOrUpdate("Common.Notify", "Notify", "Benachrichtigen"); - builder.AddOrUpdate("Common.Approve", "Approve", "Genehmigen"); - builder.AddOrUpdate("Common.Rules", "Rules", "Regeln"); - builder.AddOrUpdate("Common.Allow", "Allow", "Erlaubt"); - builder.AddOrUpdate("Common.Deny", "Deny", "Verweigert"); - builder.AddOrUpdate("Common.ExpandCollapseAll", @"Expand\collapse all", @"Alle auf-\zuklappen"); - builder.AddOrUpdate("Common.Trash", "Trash", "Papierkorb"); - builder.AddOrUpdate("Common.Cut", "Cut", "Ausschneiden"); - builder.AddOrUpdate("Common.Copy", "Copy", "Kopieren"); - builder.AddOrUpdate("Common.Paste", "Paste", "Einfügen"); - builder.AddOrUpdate("Common.SelectAll", "Select all", "Alles auswählen"); - builder.AddOrUpdate("Common.Rename", "Rename", "Umbenennen"); - - builder.AddOrUpdate("Common.CtrlKey", "Ctrl", "Strg"); - builder.AddOrUpdate("Common.ShiftKey", "Shift", "Umschalt"); - builder.AddOrUpdate("Common.AltKey", "Alt", "Alt"); - builder.AddOrUpdate("Common.DelKey", "Del", "Entf"); - builder.AddOrUpdate("Common.EnterKey", "Enter", "Eingabe"); - builder.AddOrUpdate("Common.EscKey", "Esc", "Esc"); - - builder.AddOrUpdate("Admin.Customers.PermissionViewNote", - "The view shows the permissions that apply to this customer based on the customer roles assigned to him. To change permissions, switch to the relevant customer role.", - "Die Ansicht zeigt die Rechte, die für diesen Kunden auf Basis der ihm zugeordneten Kundengruppen gelten. Um Rechte zu ändern, wechseln Sie bitte zur betreffenden Kundengruppe."); - - builder.AddOrUpdate("Admin.Permissions.AddedPermissions", - "Added permissions: {0}", - "Hinzugefügte Zugriffsrechte: {0}"); - - builder.AddOrUpdate("Admin.Permissions.RemovedPermissions", - "Permissions have been removed: {0}", - "Zugriffsrechte wurden entfernt: {0}"); - - builder.AddOrUpdate("Permissions.DisplayName.DisplayPrice", "Display prices", "Preise anzeigen"); - builder.AddOrUpdate("Permissions.DisplayName.AccessShop", "Access shop", "Zugang zum Shop"); - builder.AddOrUpdate("Permissions.DisplayName.AccessShoppingCart", "Access shoppping cart", "Auf Warenkorb zugreifen"); - builder.AddOrUpdate("Permissions.DisplayName.AccessWishlist", "Access wishlist", "Auf Wunschliste zugreifen"); - builder.AddOrUpdate("Permissions.DisplayName.AccessBackend", "Access backend", "Auf Backend zugreifen"); - builder.AddOrUpdate("Permissions.DisplayName.EditOrderItem", "Edit order items", "Auftragspositionen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditShipment", "Edit shipment", "Sendungen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditAnswer", "Edit answers", "Antworten bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditOptionSet", "Edit options sets", "Options-Sets bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditCategory", "Edit categories", "Warengruppen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditManufacturer", "Edit manufacturers", "Hersteller bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditAssociatedProduct", "Edit associated products", "Verknüpfte Produkte bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditBundle", "Edit bundles", "Bundles bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditPromotion", "Edit promotion", "Promotion bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditPicture", "Edit pictures", "Bilder bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditTag", "Edit tags", "Tags bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditAttribute", "Edit attributes", "Attribute bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditVariant", "Edit variants", "Varianten bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditTierPrice", "Edit tier prices", "Staffelpreise bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditRecurringPayment", "Edit recurring payment", "Wiederkehrende Zahlungen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditOption", "Edit options", "Optionen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditAddress", "Edit addresses", "Adressen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditCustomerRole", "Edit customer roles", "Kundengruppen bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.SendPM", "Send private messages", "Private Nachrichten senden"); - builder.AddOrUpdate("Permissions.DisplayName.EditProduct", "Edit products", "Produkte bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditComment", "Edit comments", "Kommentare bearbeiten"); - builder.AddOrUpdate("Permissions.DisplayName.EditResource", "Edit resources", "Ressourcen bearbeiten"); - - builder.AddOrUpdate("Admin.ContentManagement.Blog.Heading.Publish", "Publishing", "Veröffentlichung"); - builder.AddOrUpdate("Admin.ContentManagement.Blog.Heading.Display", "Display", "Darstellung"); - builder.AddOrUpdate("Admin.ContentManagement.News.Heading.Publish", "Publishing", "Veröffentlichung"); - - builder.AddOrUpdate("Admin.Validation.Url", "Please enter a valid URL.", "Bitte geben Sie eine gültige URL ein."); - - builder.AddOrUpdate("Common.WrongInvisibleCaptcha", - "The reCAPTCHA failed. Please try it again.", - "reCAPTCHA ist fehlgeschlagen. Bitte versuchen Sie es erneut."); - - builder.AddOrUpdate("Common.CannotDisplayView", - "The view \"{0}\" could not be displayed.", - "Die Ansicht \"{0}\" konnte nicht angezeigt werden."); - - builder.AddOrUpdate("Admin.Catalog.Products.Fields.Visibility", - "Visibility", - "Sichtbarkeit", - "Limits the visibility of the product. In the case of \"Not visible\", the product only appears as an associated product on the parent product detail page, but without a link to an individual page.", - "Schränkt die Sichtbarkeit des Produktes ein. Bei \"Nicht sichtbar\" erscheint das Produkt nur noch als verknüpftes Produkt auf der übergeordneten Produktdetailseite, jedoch ohne Verlinkung auf eine eigenständige Seite."); - - builder.AddOrUpdate("Admin.DataExchange.Export.Filter.Visibility", - "Visibility", - "Sichtbarkeit", - "Filter by visibility. \"In search results\" includes fully visible products.", - "Nach Sichtbarkeit filter. \"In Suchergebnissen\" schließt überall sichtbare Produkte mit ein."); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.Full", "Fully visible", "Überall sichtbar"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.SearchResults", "In search results", "In Suchergebnissen"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.ProductPage", "On product detail pages", "Auf Produktdetailseiten"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductVisibility.Hidden", "Not visible", "Nicht sichtbar"); - - builder.Delete( - "Admin.Catalog.Products.Fields.VisibleIndividually", - "Admin.Catalog.Products.Fields.VisibleIndividually.Hint", - "Admin.Promotions.Discounts.NoDiscountsAvailable", - "Admin.Orders.Shipments.TrackingNumber.Button", - "Admin.Catalog.Products.Copy.CopyImages"); - - // Rule - builder.AddOrUpdate("Admin.Rules.SystemName", "System name", "Systemname"); - builder.AddOrUpdate("Admin.Rules.Title", "Title", "Titel"); - builder.AddOrUpdate("Admin.Rules.Execute", "{0} Execute {1} Rules", "Bedingungen {0} Ausführen {1}"); - builder.AddOrUpdate("Admin.Rules.AddGroup", "Add group", "Gruppe hinzufügen"); - builder.AddOrUpdate("Admin.Rules.DeleteGroup", "Delete group", "Gruppe löschen"); - builder.AddOrUpdate("Admin.Rules.AddCondition", "Add condition", "Bedingung hinzufügen"); - builder.AddOrUpdate("Admin.Rules.SaveConditions", "Save all conditions", "Alle Bedingungen speichern"); - builder.AddOrUpdate("Admin.Rules.OpenRule", "Open rule", "Regel öffnen"); - - builder.AddOrUpdate("Admin.Rules.EditRule", "Edit rule", "Regel bearbeiten"); - builder.AddOrUpdate("Admin.Rules.AddRule", "Add rule", "Regel hinzufügen"); - builder.AddOrUpdate("Admin.Rules.RuleSet.Added", "The rule has been successfully added.", "Die Regel wurde erfolgreich hinzugefügt."); - builder.AddOrUpdate("Admin.Rules.RuleSet.Deleted", "The rule has been successfully deleted.", "Die Regel wurde erfolgreich gelöscht."); - builder.AddOrUpdate("Admin.Rules.Operator.All", "ALL", "ALLE"); - builder.AddOrUpdate("Admin.Rules.Operator.One", "ONE", "EINE"); - - builder.AddOrUpdate("Admin.Rules.OneCondition", - "If at least {0} of the following conditions is true.", - "Wenn mindestens {0} der folgenden Bedingungen zutrifft."); - - builder.AddOrUpdate("Admin.Rules.AllConditions", - "If {0} of the following conditions are true.", - "Wenn {0} der folgenden Bedingungen erfüllt sind."); - - builder.AddOrUpdate("Admin.Rules.NotFound", "The rule with ID {0} was not found.", "Die Regel mit der ID {0} wurde nicht gefunden."); - builder.AddOrUpdate("Admin.Rules.GroupNotFound", "The group with ID {0} was not found.", "Die Gruppe mit der ID {0} wurde nicht gefunden."); - builder.AddOrUpdate("Admin.Rules.NumberOfRules", "Number of rules", "Anzahl an Regeln"); - builder.AddOrUpdate("Admin.Rules.OperatorNotSupported", "The rule scope does not support this operator.", "Die Regelart unterstützt diesen Operator nicht."); - - builder.AddOrUpdate("Admin.Rules.InvalidDescriptor", - "Invalid rule. This rule is no longer supported and should be deleted.", - "Ungültige Regel. Diese Regel wird nicht mehr unterstützt und sollte gelöscht werden."); - - builder.AddOrUpdate("Admin.Rules.Execute.MatchCustomers", - "{0} customers match the rule conditions.", - "{0} Kunden entsprechen den Regelbedingungen."); - builder.AddOrUpdate("Admin.Rules.Execute.MatchProducts", - "{0} products match the rule conditions.", - "{0} Produkte entsprechen den Regelbedingungen."); - builder.AddOrUpdate("Admin.Rules.Execute.MatchCart", - "The rule conditions are true for the current customer {0}.", - "Die Regelbedingungen sind für den aktuellen Kunden {0} wahr."); - builder.AddOrUpdate("Admin.Rules.Execute.DoesNotMatchCart", - "The rule conditions are false for the current customer {0}.", - "Die Regelbedingungen sind für den aktuellen Kunden {0} falsch."); - - // Rule fields - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.Name", "Name", "Name"); - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.Description", "Description", "Beschreibung"); - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.IsActive", "Is active", "Ist aktiv"); - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.Scope", "Scope", "Art"); - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.IsSubGroup", "Is sub group", "Ist Untergruppe"); - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.LogicalOperator", "Logical operator", "Logischer Operator"); - - // Rule descriptors - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.TaxExempt", "Tax exempt", "Steuerbefreit"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BillingCountry", "Billing country", "Rechnungsland"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingCountry", "Shipping country", "Lieferland"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastActivityDays", "Days since last activity", "Tage seit letztem Besuch"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CompletedOrderCount", "Completed order count", "Anzahl abgeschlossener Bestellungen"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CancelledOrderCount", "Cancelled order count", "Anzahl stornierter Bestellungen"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.NewOrderCount", "New order count", "Anzahl neuer Bestellungen"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderInStore", "Orders in store", "Bestellungen im Shop"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PurchasedProduct", "Purchased product", "Gekauftes Produkt"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PurchasedFromManufacturer", "Purchased from manufacturer", "Gekauft von Hersteller"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.HasPurchasedProduct", "Has purchased product", "Hat eines der folgenden Produkte gekauft"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.HasPurchasedAllProducts", "Has purchased all products", "Hat alle folgenden Produkte gekauft"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.RuleSet", "Other rule set", "Anderer Regelsatz"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Active", "Is active", "Ist aktiv"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastLoginDays", "Days since last login", "Tage seit letztem Login"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CreatedDays", "Days since registration", "Tage seit Registrierung"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Salutation", "Salutation", "Anrede"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Title", "Title", "Titel"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Company", "Company", "Firma"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CustomerNumber", "Customer number", "Kundennummer"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BirthDate", "Days since date of birth", "Tage seit dem Geburtsdatum"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Gender", "Gender", "Geschlecht"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ZipPostalCode", "Zip postal code", "PLZ"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.VatNumberStatus", "Vat number status", "Steuernummerstatus"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.TimeZone", "Time zone", "Zeitzone"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.TaxDisplayType", "Tax display type", "Steueranzeigetyp"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.IPCountry", "IP associated with country", "IP gehört zu Land"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Currency", "Currency", "Währung"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.MobileDevice", "Mobile device", "Mobiles Endgerät"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.DeviceFamily", "Device family", "Endgerätefamilie"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OperatingSystem", "Operating system", "Betriebssystem"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BrowserName", "Browser name", "Browser Name"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BrowserMajorVersion", "Browser major version", "Browser Hauptversionsnummer"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.BrowserMinorVersion", "Browser minor version", "Browser Nebenversionsnummer"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Language", "Language", "Sprache"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastForumVisit", "Days since last forum visit", "Tage seit letztem Forenbesuch"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastUserAgent", "Last user agent", "Zuletzt genutzter User-Agent"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.IsInCustomerRole", "In customer role", "In Kundengruppe"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Store", "Store", "Shop"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.LastOrderDateDays", "Days since last order", "Tage seit letzter Bestellung"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.AcceptThirdPartyEmailHandOver", "Accept third party email handover", "Akzeptiert Weitergabe der E-Mail Adresse an Dritte"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CartTotal", "Total amount of cart", "Gesamtbetrag des Warenkorbes"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.CartSubtotal", "Subtotal amount of cart", "Zwischensumme des Warenkorbes"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductInCart", "Product in cart", "Produkt im Warenkorb"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductFromCategoryInCart", "Product from category in cart", "Produkt aus Kategorie im Warenkorb"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductFromManufacturerInCart", "Product from manufacturer in cart", "Produkt von Hersteller im Warenkorb"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductOnWishlist", "Product on wishlist", "Produkt auf der Wunschliste"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ProductReviewCount", "Number of product reviews", "Anzahl der Produkt Rezensionen"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.RewardPointsBalance", "Number of reward points", "Anzahl der Bonuspunkte"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderTotal", "Order total", "Gesamtbetrag der Bestellung"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderSubtotalInclTax", "Order subtotal incl. tax", "Gesamtbetrag der Bestellung (Brutto)"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderSubtotalExclTax", "Order subtotal excl tax", "Gesamtbetrag der Bestellung (Netto)"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.OrderCount", "Number of orders", "Anzahl der Aufträge"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.SpentAmount", "Amount spent", "Ausgegebener Betrag"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaymentMethod", "Selected payment method", "Gewählte Zahlart"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaymentStatus", "Payment status", "Zahlungsstatus"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.PaidBy", "Paid by", "Bezahlt mit"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingRateComputationMethod", "Shipping rate computation method", "Berechnungsmethode für Versandkosten"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingMethod", "Shipping method", "Versandart"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ShippingStatus", "Shipping status", "Lieferstatus"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.ReturnRequestCount", "Number of return requests", "Anzahl Rücksendeaufträge"); - builder.AddOrUpdate("Admin.Rules.FilterDescriptor.AvailableByDate", "Available by date", "Nach Datum verfügbar"); - - // Rule operators - builder.AddOrUpdate("Admin.Rules.RuleOperator.ContainsOperator", "Contains", "Enthält"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.EndsWithOperator", "Ends with", "Endet auf"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.GreaterThanOperator", "Greater than", "Größer als"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.GreaterThanOrEqualOperator", "Greater than or equal to", "Größer oder gleich"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.IsEmptyOperator", "Is empty", "Ist leer"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.EqualOperator", "Is equal to", "Gleich"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.IsNotEmptyOperator", "Is not empty", "Ist nicht leer"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.NotEqualOperator", "Is not equal to", "Ungleich"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.IsNotNullOperator", "Is not null", "Ist nicht NULL"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.IsNullOperator", "Is null", "Ist NULL"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.LessThanOperator", "Less than", "Kleiner als"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.LessThanOrEqualOperator", "Less than or equal to", "Kleiner oder gleich"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.NotContainsOperator", "Not contains", "Enthält nicht"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.StartsWithOperator", "Starts with", "Beginnt mit"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.InOperator", "In", "Ist eine von"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.NotInOperator", "Not in", "Ist KEINE von"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.AllInOperator", "All in", "Sind alle von"); - builder.AddOrUpdate("Admin.Rules.RuleOperator.NotAllInOperator", "Not all in", "Sind nicht alle von"); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.InOperator", - "At least one value of the left list is contained in the right list", - "Mind. ein Wert der linken Liste ist in der rechten Liste enthalten", - "True for left {1,2,3} and right {5,4,3}. False for left {1,2,3} and right {6,5,4}.", - "Wahr für links {1,2,3} und rechts {5,4,3}. Falsch für links {1,2,3} und rechts {6,5,4}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotInOperator", - "At least one value of the left list is missing in the right list", - "Mind. ein Wert der linken Liste fehlt in der rechten Liste", - "True for left {1,2,3} and right {3,4,5,6}. False for left {1,2,3} and right {5,4,3,2,1}.", - "Wahr für links {1,2,3} und rechts {3,4,5,6}. Falsch für links {1,2,3} und rechts {5,4,3,2,1}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.AllInOperator", - "Right list contains ALL values of the left list", - "Rechte Liste enthält ALLE Werte der linken Liste", - "True for left {3,2,1} and right {0,1,2,3}. False for left {1,2,9} and right {9,8,2}.", - "Wahr für links {3,2,1} und rechts {0,1,2,3}. Falsch für links {1,2,9} und rechts {9,8,2}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotAllInOperator", - "Right list contains NO value of the left list", - "Rechte Liste enthält KEINEN Wert der linken Liste", - "True for left {1,2,3} and right {4,5}. False for left {1,2,3} and right {3,4,5}.", - "Wahr für links {1,2,3} und rechts {4,5}. Falsch für links {1,2,3} und rechts {3,4,5}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.ContainsOperator", - "Left list contains ALL values of the right list", - "Linke Liste enthält ALLE Werte der rechten Liste", - "True for left {3,2,1,0} and right {2,3}. False for left {3,2,1} and right {0,1,2,3}.", - "Wahr für links {3,2,1,0} und rechts {2,3}. Falsch für links {3,2,1} und rechts {0,1,2,3}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotContainsOperator", - "Left list contains NO value of the right list", - "Linke Liste enthält KEINEN Wert der rechten Liste", - "True for left {1,2,3} and right {9,8}. False for left {1,2,3} and right {9,8,2}.", - "Wahr für links {1,2,3} und rechts {9,8}. Falsch für links {1,2,3} und rechts {9,8,2}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.EqualOperator", - "Left and right list contain the same values", - "Linke und rechte Liste enthalten dieselben Werte", - "True for left {1,2,3} and right {3,1,2}. False for left {1,2,3} and right {1,2,3,4}.", - "Wahr für links {1,2,3} und rechts {3,1,2}. Falsch für links {1,2,3} und rechts {1,2,3,4}."); - - builder.AddOrUpdate("Admin.Rules.RuleOperator.Sequence.NotEqualOperator", - "Left and right list differ in at least one value", - "Linke und rechte Liste unterscheiden sich in mind. einem Wert", - "True for left {1,2,3} and right {1,2,3,4}. False for left {1,2,3} and right {3,1,2}.", - "Wahr für links {1,2,3} und rechts {1,2,3,4}. Falsch für links {1,2,3} und rechts {3,1,2}."); - - builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Cart", "Cart", "Warenkorb"); - builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.OrderItem", "Order item", "Bestellposition"); - builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Customer", "Customer", "Kunde"); - builder.AddOrUpdate("Enums.SmartStore.Rules.RuleScope.Product", "Product", "Produkt"); - - builder.AddOrUpdate("SmartStore.Blog.Button", "Visit our blog", "Zum Blog"); - builder.AddOrUpdate("SmartStore.Blog.Button.Hint", "Click here to be forwarded to our blog", "Klicken Sie hier um zu unserem Blog weitergeleitet zu werden"); - - builder.AddOrUpdate("Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.ColorSquaresRgb", - "RGB color", - "RGB-Farbe", - "Specifies a color for the color squares control.", - "Legt eine Farbe für das Farbquadrat-Steuerelement fest."); - - builder.AddOrUpdate("Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.Picture", - "Picture", - "Bild", - "Specifies an image as the selector element.", - "Legt ein Bild als Auswahlelement fest."); - - builder.AddOrUpdate("Admin.Catalog.Manufacturers.Fields.BottomDescription", - "Bottom description", - "Untere Beschreibung", - "Optional second description displayed below products on the category page.", - "Optionale zweite Beschreibung, die auf der Herstellerseite unterhalb der Produkte angezeigt wird."); - - builder.AddOrUpdate("Admin.Promotions.Discounts.AppliedDiscounts", - "Discounts", - "Rabatte", - "Specifies discounts to be applied to the object.", - "Legt auf das Objekt anzuwendende Rabatte fest."); - - builder.AddOrUpdate("Admin.Promotions.Discounts.RuleSetRequirements", - "Requirements", - "Voraussetzungen", - "Specifies requirements for the applying of the discount. The discount is applied when one of the selected rules is fulfilled.", - "Legt Voraussetzungen für die Anwendung des Rabatts fest. Der Rabatt wird gewährt, wenn eine der ausgewählten Regeln erfüllt ist."); - - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.AssignedToDiscounts", - "Assigned to discounts", - "Rabatten zugeordnet", - "A list of discounts to which the rule is assigned. The assignment can be made on the discount details page.", - "Eine Liste von Rabatten, denen die Regel zugeordnet ist. Die Zuordnung kann auf der Rabattdetailseite vorgenommen werden."); - - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.AssignedToShippingMethods", - "Assigned to shipping methods", - "Versandarten zugeordnet", - "A list of shipping methods to which the rule is assigned. The assignment can be made on the shipping method details page.", - "Eine Liste von Versandarten, denen die Regel zugeordnet ist. Die Zuordnung kann auf der Detailseite der Versandart vorgenommen werden."); - - builder.AddOrUpdate("Admin.Rules.RuleSet.Fields.AssignedToPaymentMethods", - "Assigned to payment methods", - "Zahlarten zugeordnet", - "A list of payment methods to which the rule is assigned. The assignment can be made on the payment method details page.", - "Eine Liste von Zahlarten, denen die Regel zugeordnet ist. Die Zuordnung kann auf der Detailseite der Zahlart vorgenommen werden."); - - builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.Fields.Requirements", - "Requirements", - "Voraussetzungen", - "Specifies requirements for the availability of the shipping method. The shipping method is offered if one of the selected rules is fulfilled.", - "Legt Voraussetzungen für die Verfügbarkeit der Versandart fest. Die Versandart wird angeboten, wenn eine der ausgewählten Regeln erfüllt ist."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.Requirements", - "Requirements", - "Voraussetzungen", - "Specifies requirements for the availability of the payment method. The payment method is offered if one of the selected rules is fulfilled.", - "Legt Voraussetzungen für die Verfügbarkeit der Zahlart fest. Die Zahlart wird angeboten, wenn eine der ausgewählten Regeln erfüllt ist."); - - builder.AddOrUpdate("Admin.Common.IsPublished", "Published", "Veröffentlicht"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToOrderTotal") - .Value("de", "Bezogen auf Gesamtsumme"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToSkus") - .Value("de", "Bezogen auf Produkte"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToCategories") - .Value("de", "Bezogen auf Warengruppen"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToManufacturers") - .Value("de", "Bezogen auf Hersteller"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToShipping") - .Value("de", "Bezogen auf Versandkosten"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Discounts.DiscountType.AssignedToOrderSubTotal") - .Value("de", "Bezogen auf Zwischensumme"); - - - builder.AddOrUpdate("ActivityLog.EditSettings", - "The setting {0} has been changed. The new value is {1}.", - "Die Einstellung {0} wurde geändert. Der neue Wert ist {1}."); - - builder.AddOrUpdate("Admin.Catalog.Products.Fields.Condition", - "Product condition", - "Artikelzustand", - "Specifies the product condition.", - "Legt den Artikelzustand fest."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowProductCondition", - "Show product condition", - "Artikelzustand anzeigen", - "Specifies whether to show the product condition.", - "Legt fest, ob der Artikelzustand im Shop angezeigt werden soll."); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.New", "New", "Neu"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.Refurbished", "Refurbished", "Generalüberholt"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.Used", "Used", "Gebraucht"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.ProductCondition.Damaged", "Damaged", "Defekt"); - - builder.AddOrUpdate("Products.Condition", "Product condition", "Artikelzustand"); - builder.AddOrUpdate("Common.OpenUrl", "Open URL", "URL öffnen"); - builder.AddOrUpdate("Common.NoPreview", "A preview is not available.", "Eine Vorschau ist nicht verfügbar."); - - builder.AddOrUpdate("Admin.Orders.Shipments.TrackingNumber", - "Tracking number", - "Tracking-Nummer", - "Specifies the tracking number for tracking the shipment.", - "Legt die Tracking-Nummer zur Sendungsverfolgung fest."); - - builder.AddOrUpdate("Admin.Orders.Shipments.TrackingUrl", - "Tracking URL", - "Tracking-URL", - "Specifies the URL for tracking the shipment.", - "Legt die URL zur Sendungsverfolgung fest."); - - builder.AddOrUpdate("Admin.Media.Album.Message", "Messages", "Nachrichten"); - builder.AddOrUpdate("Admin.Media.Album.File", "Files", "Dateien"); - - builder.AddOrUpdate("Admin.SalesReport.LatestOrders", "Latest orders", "Neueste Bestellungen"); - builder.AddOrUpdate("Admin.SalesReport.TopCustomers", "Top customers", "Top Kunden"); - - builder.AddOrUpdate("Admin.Customers.CustomerRoles.AutomatedAssignmentRules", - "Rules for automated assignment", - "Regeln für automatische Zuordnung", - "Customers are automatically assigned to this customer group by scheduled task if they fulfill one of the selected rules.", - "Kunden werden automatisch per geplanter Aufgabe dieser Kundengruppe zugeordnet, wenn sie eine der gewählten Regeln erfüllen."); - - builder.AddOrUpdate("Admin.Catalog.Categories.AutomatedAssignmentRules", - "Rules for automated assignment", - "Regeln für automatische Zuordnung", - "Products are automatically assigned to this category by scheduled task if they fulfill one of the selected rules.", - "Produkte werden automatisch per geplanter Aufgabe dieser Warengruppe zugeordnet, wenn sie eine der gewählten Regeln erfüllen."); - - builder.AddOrUpdate("Admin.Rules.AutomaticAssignment", "Automatic assignment", "Automatische Zuordnung"); - builder.AddOrUpdate("Admin.Rules.ReapplyRules", "Reapply rules", "Regeln neu anwenden"); - - builder.AddOrUpdate("Admin.CustomerRoleMapping.RoleMappingListDescription", - "The list shows customers who are assigned to this customer role. Customers are automatically assigned by scheduled task as long as the group is active and rules are specified for it. You can make a manual assignment using the customer role selection at the respective customer.", - "Die Liste zeigt Kunden, die dieser Kundengruppe zugeordnet sind. Kunden werden automatisch per geplanter Aufgabe zugeordnet, sofern die Gruppe aktiv ist und für sie Regeln festgelegt sind. Eine manuelle Zuordnung können Sie über die Kundengruppenauswahl beim jeweiligen Kunden vornehmen."); - - builder.AddOrUpdate("Admin.Catalog.Categories.ProductListDescription", - "The list shows products that are assigned to this category. Products are automatically assigned by scheduled task as long as the category is published and rules are specified for it.", - "Die Liste zeigt Produkte, die dieser Warenruppe zugeordnet sind. Produkte werden automatisch per geplanter Aufgabe zugeordnet, sofern die Warengruppe veröffentlicht ist und für sie Regeln festgelegt sind."); - - builder.AddOrUpdate("Admin.System.ScheduleTasks.TaskNotFound", - "The scheduled task \"{0}\" was not found.", - "Die geplante Aufgabe \"{0}\" wurde nicht gefunden."); - - builder.AddOrUpdate("Admin.SalesReport.ByQuantity", "By quantity", "Nach Anzahl"); - builder.AddOrUpdate("Admin.SalesReport.ByAmount", "By amount", "Nach Betrag"); - builder.AddOrUpdate("Admin.SalesReport.Attribute", "Attributes", "Attribute"); - builder.AddOrUpdate("Admin.SalesReport.Value", "Values", "Werte"); - builder.AddOrUpdate("Admin.SalesReport.NewOrders", "Total new orders", "Neue Aufträge"); - builder.AddOrUpdate("Admin.SalesReport.NoIncompleteOrders", "No incomplete orders", "Keine unvollständigen Aufträge"); - builder.AddOrUpdate("Admin.Report.StoreStatistics", "Statistics", "Statistiken"); - builder.AddOrUpdate("Admin.Report.CustomerRegistrations", "Customer registrations", "Kundenregistrierungen"); - builder.AddOrUpdate("Admin.Report.Today", "Today", "Heute"); - builder.AddOrUpdate("Admin.Report.Yesterday", "Yesterday", "Gestern"); - builder.AddOrUpdate("Admin.Report.LastWeek", "Last 7 days", "Letzte 7 Tage"); - builder.AddOrUpdate("Admin.Report.LastMonth", "Last 28 days", "Letzte 28 Tage"); - builder.AddOrUpdate("Admin.Report.ThisYear", "This year", "Dieses Jahr"); - builder.AddOrUpdate("Admin.Report.Overall", "Overall", "Insgesamt"); - builder.AddOrUpdate("Admin.Promotions.NewsLetterSubscriptions.Short", "Newsletter abos", "Newsletter Abos"); - builder.AddOrUpdate("Admin.Promotions.SalesReport.Sales", "Sales", "Verkäufe"); - builder.AddOrUpdate("Common.Amount", "Amount", "Betrag"); - builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.AttributeCombinations.Short", "Combinations", "Kombinationen"); - builder.AddOrUpdate("Admin.CurrentWishlists.Short", "Whishlists", "Wunschlisten"); - builder.AddOrUpdate("Admin.CurrentCarts.Short", "Shopping carts", "Warenkörbe"); - builder.AddOrUpdate("Admin.SalesReport.Sales", "Sales", "Umsatz"); - builder.AddOrUpdate("Admin.SalesReport.Sales.Hint", "Total value of all orders", "Gesamtwert aller Aufträge"); - builder.AddOrUpdate("Admin.Report.OnlineCustomers", "Customers online within last 15 minutes", "Kunden in den letzten 15 Minuten online"); - builder.AddOrUpdate("Admin.Orders.Overall", "Orders overall", "Aufträge insgesamt"); - builder.AddOrUpdate("Admin.Report.Registrations", "Registrations", "Registrierungen"); - - - builder.AddOrUpdate("FileUploader.Dropzone.Message", "To upload files drop them here or click.", "Zum Hochladen Dateien hier ablegen oder klicken."); - builder.AddOrUpdate("FileUploader.MultiFiles.MainMediaFile", "Main media file", "Hauptbild"); - builder.AddOrUpdate("FileUploader.Preview.SetMainMedia.Title", "Set as main picture", "Zum Hauptbild machen"); - builder.AddOrUpdate("FileUploader.Preview.DeleteEntityMedia.Title", "Remove assignment", "Zuordnung entfernen"); + protected override void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + MigrateSettings(context); + } - builder.AddOrUpdate("FileUploader.DuplicateDialog.Title", "Replace or skip", "Ersetzen oder überspringen"); - builder.AddOrUpdate("FileUploader.DuplicateDialog.Intro", "The file already exists.", "Die Datei ist bereits vorhanden."); - builder.AddOrUpdate("FileUploader.DuplicateDialog.DupeFile.Title", "Uploaded file", "Hochgeladene Datei"); - builder.AddOrUpdate("FileUploader.DuplicateDialog.ExistingFile.Title", "Existing files", "Existierende Datei"); - builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.Skip", "Skip file", "Datei überspringen"); - builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.Replace", "Upload and replace", "Hochladen und ersetzen"); - builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.Rename", "Upload and rename", "Hochladen und umbenennen"); - builder.AddOrUpdate("FileUploader.DuplicateDialog.Option.SaveSelection", "Remember this selection and apply it to the current queue.", "Diese Auswahl merken und auf die aktuelle Wartenschlange anwenden."); + public void MigrateSettings(SmartObjectContext context) + { - builder.AddOrUpdate("FileUploader.Dropzone.DictDefaultMessage", "Drop files here to upload", "Dateien zum Hochladen hier ablegen"); - builder.AddOrUpdate("FileUploader.Dropzone.DictFallbackMessage", "Your browser does not support drag'n'drop file uploads.", "Ihr Browser unterstützt keine Datei-Uploads per Drag'n'Drop."); - builder.AddOrUpdate("FileUploader.Dropzone.DictFallbackText", "Please use the fallback form below to upload your files like in the olden days.", "Bitte benutzen Sie das untenstehende Formular, um Ihre Dateien wie in längst vergangenen Zeiten hochzuladen."); - builder.AddOrUpdate("FileUploader.Dropzone.DictFileTooBig", "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", "Die Datei ist zu groß ({{filesize}}MB). Maximale Dateigröße: {{maxFilesize}}MB."); - builder.AddOrUpdate("FileUploader.Dropzone.DictInvalidFileType", "You can't upload files of this type.", "Dateien dieses Typs können nicht hochgeladen werden."); - builder.AddOrUpdate("FileUploader.Dropzone.DictResponseError", "Server responded with {{statusCode}} code.", "Der Server gab die Antwort {{statusCode}} zurück."); - builder.AddOrUpdate("FileUploader.Dropzone.DictCancelUpload", "Cancel upload", "Upload abbrechen"); - builder.AddOrUpdate("FileUploader.Dropzone.DictUploadCanceled", "Upload canceled.", "Upload abgebrochen."); - builder.AddOrUpdate("FileUploader.Dropzone.DictCancelUploadConfirmation", "Are you sure you want to cancel this upload?", "Sind Sie sicher, dass Sie den Upload abbrechen wollen?"); - builder.AddOrUpdate("FileUploader.Dropzone.DictRemoveFile", "Remove file", "Datei entfernen"); - builder.AddOrUpdate("FileUploader.Dropzone.DictMaxFilesExceeded", "You can not upload any more files.", "Sie können keine weiteren Dateien hochladen."); + } - builder.AddOrUpdate("Admin.Catalog.Products.ProductPictures.Delete.Success", "The assignment was successfully removed.", "Die Zuordnung wurde erfolgreich entfernt."); - builder.AddOrUpdate("Admin.Catalog.Products.ProductPictures.Sort.Success", "The new order of your product images has been saved successfully.", "Die neue Anordnung Ihrer Produktbilder wurde erfolgreich gespeichert."); + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.DisplayRegionInLanguageSelector", + "Display region in language selector", + "Region in der Sprachauswahl anzeigen", + "Whether to display region/country name in language selector (e.g. 'Deutsch (Deutschland)' instead of 'Deutsch')", + "Zeigt den Namen der Region/des Landes in der Sprachauswahl an (z. B. 'Deutsch (Deutschland)' statt 'Deutsch')"); } } -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs index 63d32c8111..8503451f1f 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs @@ -15,344 +15,326 @@ namespace SmartStore.Data { - public abstract partial class ObjectContextBase - { - private SaveChangesOperation _currentSaveOperation; - - private readonly static ConcurrentDictionary _hookableEntities = new ConcurrentDictionary(); - - private enum SaveStage - { - PreSave, - PostSave - } - - private IEnumerable GetChangedEntries() - { - return ChangeTracker.Entries().Where(x => x.State > EfState.Unchanged); - } - - public IDbHookHandler DbHookHandler - { - get; - set; - } - - public override int SaveChanges() - { - var op = _currentSaveOperation; - - if (op != null) - { - if (op.Stage == SaveStage.PreSave) - { - // This was called from within a PRE action hook. We must get out:... - // 1.) to prevent cyclic calls - // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. - return 0; - } - else if (op.Stage == SaveStage.PostSave) - { - // This was called from within a POST action hook. Core SaveChanges() has already been called, - // but new entities could have been added to the state tracker by hooks. - // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! - // DetectChanges() here is important, 'cause we turned it off for the save process. - base.ChangeTracker.DetectChanges(); - return SaveChangesCore(); - } - } - - _currentSaveOperation = new SaveChangesOperation(this, this.DbHookHandler); - - Action endExecute = () => - { - _currentSaveOperation?.Dispose(); - _currentSaveOperation = null; - }; - - using (new ActionDisposable(endExecute)) - { - return _currentSaveOperation.Execute(); - } - } - - public override Task SaveChangesAsync(CancellationToken cancellationToken) - { - var op = _currentSaveOperation; - - if (op != null) - { - if (op.Stage == SaveStage.PreSave) - { - // This was called from within a PRE action hook. We must get out:... - // 1.) to prevent cyclic calls - // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. - return Task.FromResult(0); - } - else if (op.Stage == SaveStage.PostSave) - { - // This was called from within a POST action hook. Core SaveChanges() has already been called, - // but new entities could have been added to the state tracker by hooks. - // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! - // DetectChanges() here is important, 'cause we turned it off for the save process. - base.ChangeTracker.DetectChanges(); - return SaveChangesCoreAsync(cancellationToken); - } - } - - _currentSaveOperation = new SaveChangesOperation(this, this.DbHookHandler); - - var result = _currentSaveOperation.ExecuteAsync(cancellationToken); - - result.ContinueWith(t => - { - _currentSaveOperation?.Dispose(); - _currentSaveOperation = null; - }); - - return result; - } - - /// - /// Just calls DbContext.SaveChanges() without any sugar - /// - /// The number of affected records - protected internal int SaveChangesCore() - { - return base.SaveChanges(); - } - - /// - /// Just calls DbContext.SaveChangesAsync() without any sugar - /// - /// The number of affected records - protected internal Task SaveChangesCoreAsync(CancellationToken cancellationToken) - { - return base.SaveChangesAsync(cancellationToken); - } - - private void IgnoreMergedData(IEnumerable entries, bool ignore) - { - foreach (var entry in entries) - { - entry.MergedDataIgnore = ignore; - } - } - - internal bool IsInSaveOperation - { - get { return _currentSaveOperation != null; } - } - - private bool IsHookableEntry(IHookedEntity entry) - { - var entity = entry.Entity; - if (entity == null) - { - return false; - } - - return IsHookableEntityType(entry.EntityType); - } - - private static bool IsHookableEntityType(Type entityType) - { - var isHookable = _hookableEntities.GetOrAdd(entityType, t => - { - var attr = t.GetAttribute(true); - if (attr != null) - { - return attr.IsHookable; - } - - // Entities are hookable by default - return true; - }); - - return isHookable; - } - - class SaveChangesOperation : IDisposable - { - private SaveStage _stage; - private IEnumerable _changedEntries; - private ObjectContextBase _ctx; - private IDbHookHandler _hookHandler; - - public SaveChangesOperation(ObjectContextBase ctx, IDbHookHandler hookHandler) - { - _ctx = ctx; - _hookHandler = hookHandler; - } - - public IEnumerable ChangedEntries - { - get { return _changedEntries; } - } - - public SaveStage Stage - { - get { return _stage; } - } - - private IDisposable ExecuteCore() - { - var autoDetectChanges = _ctx.Configuration.AutoDetectChangesEnabled; - IEnumerable mergeableEntities = null; - - // Suppress implicit DetectChanges() calls by EF, - // e.g. called by SaveChanges(), ChangeTracker.Entries() etc. - _ctx.Configuration.AutoDetectChangesEnabled = false; - - // Get all attached entries implementing IMergedData, - // we need to ignore merge on them. Otherwise - // EF's change detection may think that properties has changed - // where they actually didn't. - mergeableEntities = _ctx.GetMergeableEntitiesFromChangeTracker().ToArray(); - - // Now ignore merged data, otherwise merged data will be saved to database - _ctx.IgnoreMergedData(mergeableEntities, true); - - // We must detect changes earlier in the process - // before hooks are executed. Therefore we suppressed the - // implicit DetectChanges() call by EF and call it here explicitly. - _ctx.ChangeTracker.DetectChanges(); - - // Now get changed entries - _changedEntries = _ctx.GetChangedEntries(); - - // pre - PreExecute(out IEnumerable changedHookEntries); - - return new ActionDisposable(endExecute); - - void endExecute() - { - try - { - // post - PostExecute(changedHookEntries); - } - finally - { - _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; - _ctx.IgnoreMergedData(mergeableEntities, false); - } - } - } - - public int Execute() - { - using (ExecuteCore()) - { - return _ctx.SaveChangesCore(); - } - } - - public async Task ExecuteAsync(CancellationToken cancellationToken) - { - using (ExecuteCore()) - { - return await _ctx.SaveChangesCoreAsync(cancellationToken); - } - } - - private IEnumerable PreExecute(out IEnumerable changedHookEntries) - { - bool enableHooks = false; - bool importantHooksOnly = false; - bool anyStateChanged = false; - IEnumerable processedHooks = null; - - changedHookEntries = null; - - enableHooks = _changedEntries.Any(); // hooking is meaningless without hookable entries - if (enableHooks) - { - // despite the fact that hooking can be disabled, we MUST determine if any "important" pre hook exists. - // If yes, but hooking is disabled, we'll trigger only the important ones. - importantHooksOnly = !_ctx.HooksEnabled && _hookHandler.HasImportantSaveHooks(); - - // we'll enable hooking for this unit of work only when it's generally enabled, - // OR we have "important" hooks in the pipeline. - enableHooks = importantHooksOnly || _ctx.HooksEnabled; - } - - if (enableHooks) - { - changedHookEntries = _changedEntries - .Select(x => new HookedEntity(x)) - .Where(x => IsHookableEntityType(x.EntityType)) - .ToArray(); - - // Regardless of validation (possible fixing validation errors too) - processedHooks = _hookHandler.TriggerPreSaveHooks(changedHookEntries, importantHooksOnly, out anyStateChanged); - - if (processedHooks.Any() && changedHookEntries.Any(x => x.State == SmartStore.Core.Data.EntityState.Modified)) - { - // Because at least one pre action hook has been processed, - // we must assume that entity properties has been changed. - // We need to call DetectChanges() again. - _ctx.ChangeTracker.DetectChanges(); - } - } - - if (anyStateChanged) - { - // because the state of at least one entity has been changed during pre hooking - // we have to further reduce the set of hookable entities (for the POST hooks) - changedHookEntries = changedHookEntries.Where(x => x.InitialState > SmartStore.Core.Data.EntityState.Unchanged); - } - - return processedHooks ?? Enumerable.Empty(); - } - - private IEnumerable PostExecute(IEnumerable changedHookEntries) - { - if (changedHookEntries == null || !changedHookEntries.Any()) - return Enumerable.Empty(); - - // the existence of hook entries actually implies that hooking is enabled. - - _stage = SaveStage.PostSave; - - var importantHooksOnly = !_ctx.HooksEnabled && _hookHandler.HasImportantSaveHooks(); - - return _hookHandler.TriggerPostSaveHooks(changedHookEntries, importantHooksOnly); - } - - private string FormatValidationExceptionMessage(IEnumerable results) - { + public abstract partial class ObjectContextBase + { + private SaveChangesOperation _currentSaveOperation; + + private readonly static ConcurrentDictionary _hookableEntities = new ConcurrentDictionary(); + + private enum SaveStage + { + PreSave, + PostSave + } + + private IEnumerable GetChangedEntries() + { + return ChangeTracker.Entries().Where(x => x.State > EfState.Unchanged); + } + + public IDbHookHandler DbHookHandler + { + get; + set; + } + + public override int SaveChanges() + { + var op = _currentSaveOperation; + + if (op != null) + { + if (op.Stage == SaveStage.PreSave) + { + // This was called from within a PRE action hook. We must get out:... + // 1.) to prevent cyclic calls + // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. + return 0; + } + else if (op.Stage == SaveStage.PostSave) + { + // This was called from within a POST action hook. Core SaveChanges() has already been called, + // but new entities could have been added to the state tracker by hooks. + // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! + // DetectChanges() here is important, 'cause we turned it off for the save process. + base.ChangeTracker.DetectChanges(); + return SaveChangesCore(); + } + } + + _currentSaveOperation = new SaveChangesOperation(this, this.DbHookHandler); + + Action endExecute = () => + { + _currentSaveOperation?.Dispose(); + _currentSaveOperation = null; + }; + + using (new ActionDisposable(endExecute)) + { + return _currentSaveOperation.Execute(); + } + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken) + { + var op = _currentSaveOperation; + + if (op != null) + { + if (op.Stage == SaveStage.PreSave) + { + // This was called from within a PRE action hook. We must get out:... + // 1.) to prevent cyclic calls + // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. + return Task.FromResult(0); + } + else if (op.Stage == SaveStage.PostSave) + { + // This was called from within a POST action hook. Core SaveChanges() has already been called, + // but new entities could have been added to the state tracker by hooks. + // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! + // DetectChanges() here is important, 'cause we turned it off for the save process. + base.ChangeTracker.DetectChanges(); + return SaveChangesCoreAsync(cancellationToken); + } + } + + _currentSaveOperation = new SaveChangesOperation(this, this.DbHookHandler); + + var result = _currentSaveOperation.ExecuteAsync(cancellationToken); + + result.ContinueWith(t => + { + _currentSaveOperation?.Dispose(); + _currentSaveOperation = null; + }); + + return result; + } + + /// + /// Just calls DbContext.SaveChanges() without any sugar + /// + /// The number of affected records + protected internal int SaveChangesCore() + { + return base.SaveChanges(); + } + + /// + /// Just calls DbContext.SaveChangesAsync() without any sugar + /// + /// The number of affected records + protected internal Task SaveChangesCoreAsync(CancellationToken cancellationToken) + { + return base.SaveChangesAsync(cancellationToken); + } + + private void IgnoreMergedData(IEnumerable entries, bool ignore) + { + foreach (var entry in entries) + { + entry.MergedDataIgnore = ignore; + } + } + + internal bool IsInSaveOperation => _currentSaveOperation != null; + + private static bool IsHookableEntityType(Type entityType) + { + var isHookable = _hookableEntities.GetOrAdd(entityType, t => + { + var attr = t.GetAttribute(true); + if (attr != null) + { + return attr.IsHookable; + } + + // Entities are hookable by default + return true; + }); + + return isHookable; + } + + class SaveChangesOperation : IDisposable + { + private SaveStage _stage; + private IEnumerable _changedEntries; + private ObjectContextBase _ctx; + private IDbHookHandler _hookHandler; + + public SaveChangesOperation(ObjectContextBase ctx, IDbHookHandler hookHandler) + { + _ctx = ctx; + _hookHandler = hookHandler; + } + + public IEnumerable ChangedEntries => _changedEntries; + + public SaveStage Stage => _stage; + + private IDisposable ExecuteCore() + { + var autoDetectChanges = _ctx.Configuration.AutoDetectChangesEnabled; + IEnumerable mergeableEntities = null; + + // Suppress implicit DetectChanges() calls by EF, + // e.g. called by SaveChanges(), ChangeTracker.Entries() etc. + _ctx.Configuration.AutoDetectChangesEnabled = false; + + // Get all attached entries implementing IMergedData, + // we need to ignore merge on them. Otherwise + // EF's change detection may think that properties has changed + // where they actually didn't. + mergeableEntities = _ctx.GetMergeableEntitiesFromChangeTracker().ToArray(); + + // Now ignore merged data, otherwise merged data will be saved to database + _ctx.IgnoreMergedData(mergeableEntities, true); + + // We must detect changes earlier in the process + // before hooks are executed. Therefore we suppressed the + // implicit DetectChanges() call by EF and call it here explicitly. + _ctx.ChangeTracker.DetectChanges(); + + // Now get changed entries + _changedEntries = _ctx.GetChangedEntries(); + + // pre + PreExecute(out IEnumerable changedHookEntries); + + return new ActionDisposable(endExecute); + + void endExecute() + { + try + { + // post + PostExecute(changedHookEntries); + } + finally + { + _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; + _ctx.IgnoreMergedData(mergeableEntities, false); + } + } + } + + public int Execute() + { + using (ExecuteCore()) + { + return _ctx.SaveChangesCore(); + } + } + + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + using (ExecuteCore()) + { + return await _ctx.SaveChangesCoreAsync(cancellationToken); + } + } + + private IEnumerable PreExecute(out IEnumerable changedHookEntries) + { + bool enableHooks = false; + bool importantHooksOnly = false; + bool anyStateChanged = false; + IEnumerable processedHooks = null; + + changedHookEntries = null; + + enableHooks = _changedEntries.Any(); // hooking is meaningless without hookable entries + if (enableHooks) + { + // despite the fact that hooking can be disabled, we MUST determine if any "important" pre hook exists. + // If yes, but hooking is disabled, we'll trigger only the important ones. + importantHooksOnly = !_ctx.HooksEnabled && _hookHandler.HasImportantSaveHooks(); + + // we'll enable hooking for this unit of work only when it's generally enabled, + // OR we have "important" hooks in the pipeline. + enableHooks = importantHooksOnly || _ctx.HooksEnabled; + } + + if (enableHooks) + { + var contextType = _ctx.GetType(); + + changedHookEntries = _changedEntries + .Select(x => new HookedEntity(contextType, x)) + .Where(x => IsHookableEntityType(x.EntityType)) + .ToArray(); + + // Regardless of validation (possible fixing validation errors too) + processedHooks = _hookHandler.TriggerPreSaveHooks(changedHookEntries, importantHooksOnly, out anyStateChanged); + + if (processedHooks.Any() && changedHookEntries.Any(x => x.State == SmartStore.Core.Data.EntityState.Modified)) + { + // Because at least one pre action hook has been processed, + // we must assume that entity properties has been changed. + // We need to call DetectChanges() again. + _ctx.ChangeTracker.DetectChanges(); + } + } + + if (anyStateChanged) + { + // because the state of at least one entity has been changed during pre hooking + // we have to further reduce the set of hookable entities (for the POST hooks) + changedHookEntries = changedHookEntries.Where(x => x.InitialState > SmartStore.Core.Data.EntityState.Unchanged); + } + + return processedHooks ?? Enumerable.Empty(); + } + + private IEnumerable PostExecute(IEnumerable changedHookEntries) + { + if (changedHookEntries == null || !changedHookEntries.Any()) + return Enumerable.Empty(); + + // the existence of hook entries actually implies that hooking is enabled. + + _stage = SaveStage.PostSave; + + var importantHooksOnly = !_ctx.HooksEnabled && _hookHandler.HasImportantSaveHooks(); + + return _hookHandler.TriggerPostSaveHooks(changedHookEntries, importantHooksOnly); + } + + private string FormatValidationExceptionMessage(IEnumerable results) + { var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; sb.Append("Entity validation failed" + Environment.NewLine); - foreach (var res in results) - { - var baseEntity = res.Entry.Entity as BaseEntity; - sb.AppendFormat("Entity Name: {0} - Id: {0} - State: {1}", - res.Entry.Entity.GetType().Name, - baseEntity != null ? baseEntity.Id.ToString() : "N/A", - res.Entry.State.ToString()); - sb.AppendLine(); - - foreach (var validationError in res.ValidationErrors) - { - sb.AppendFormat("\tProperty: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); - sb.AppendLine(); - } - } + foreach (var res in results) + { + var baseEntity = res.Entry.Entity as BaseEntity; + sb.AppendFormat("Entity Name: {0} - Id: {0} - State: {1}", + res.Entry.Entity.GetType().Name, + baseEntity != null ? baseEntity.Id.ToString() : "N/A", + res.Entry.State.ToString()); + sb.AppendLine(); + + foreach (var validationError in res.ValidationErrors) + { + sb.AppendFormat("\tProperty: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage); + sb.AppendLine(); + } + } return psb.ToStringAndReturn(); } - public void Dispose() - { - _ctx = null; - _hookHandler = null; - _changedEntries = null; - } - } - } + public void Dispose() + { + _ctx = null; + _hookHandler = null; + _changedEntries = null; + } + } + } } diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.cs index 4d9a707a40..26d5a65685 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.cs @@ -10,148 +10,147 @@ using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; -using SmartStore.Data.Setup; using SmartStore.Utilities; using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data { - [DbConfigurationType(typeof(SmartDbConfiguration))] + [DbConfigurationType(typeof(SmartDbConfiguration))] public abstract partial class ObjectContextBase : DbContext, IDbContext { - private static bool? _isSqlServer2012OrHigher = null; - private static int? _commandTimeoutInSeconds = CommonHelper.GetAppSetting("sm:EfCommandTimeout"); - - // Instance of the internal ObjectStateManager.TransactionManager - // required for detecting if EF performs change detection - private object _transactionManager; - - /// - /// Parameterless constructor for tooling support, e.g. EF Migrations. - /// - protected ObjectContextBase() - : this(GetConnectionString(), null) - { - } - - protected ObjectContextBase(string nameOrConnectionString, string alias = null) + private static bool? _isSqlServer2012OrHigher = null; + private static int? _commandTimeoutInSeconds = CommonHelper.GetAppSetting("sm:EfCommandTimeout"); + + // Instance of the internal ObjectStateManager.TransactionManager + // required for detecting if EF performs change detection + private object _transactionManager; + + /// + /// Parameterless constructor for tooling support, e.g. EF Migrations. + /// + protected ObjectContextBase() + : this(GetConnectionString(), null) + { + } + + protected ObjectContextBase(string nameOrConnectionString, string alias = null) : base(nameOrConnectionString) { - this.HooksEnabled = true; - this.AutoCommitEnabled = true; + this.HooksEnabled = true; + this.AutoCommitEnabled = true; this.Alias = null; - this.DbHookHandler = NullDbHookHandler.Instance; + this.DbHookHandler = NullDbHookHandler.Instance; - if (_commandTimeoutInSeconds >= 0 && DataSettings.Current.IsSqlServer) - { - Database.CommandTimeout = _commandTimeoutInSeconds; - } - } + if (_commandTimeoutInSeconds >= 0 && DataSettings.Current.IsSqlServer) + { + Database.CommandTimeout = _commandTimeoutInSeconds; + } + } - public bool HooksEnabled - { - get; - set; - } + public bool HooksEnabled + { + get; + set; + } - #region IDbContext members + #region IDbContext members - public virtual string CreateDatabaseScript() + public virtual string CreateDatabaseScript() { return ((IObjectContextAdapter)this).ObjectContext.CreateDatabaseScript(); } - public new DbSet Set() where TEntity : BaseEntity - { - return base.Set(); - } - - private IEnumerable ToParameters(params object[] parameters) - { - if (parameters == null || parameters.Length == 0) - return Enumerable.Empty(); - - return parameters.Cast(); - } - - public virtual IList ExecuteStoredProcedureList(string commandText, params object[] parameters) where TEntity : BaseEntity, new() - { - // Add parameters to command - var commandText2 = commandText; - var dbParams = ToParameters(parameters); - bool firstParam = true; - bool hasOutputParams = false; - foreach (var p in dbParams) - { - commandText += firstParam ? " " : ", "; - firstParam = false; - - commandText += "@" + p.ParameterName; - if (p.Direction == ParameterDirection.InputOutput || p.Direction == ParameterDirection.Output) - { - // output parameter - hasOutputParams = true; - commandText += " output"; - } - } - - - var isLegacyDb = !this.IsSqlServer2012OrHigher(); - if (isLegacyDb && hasOutputParams) - { - // SQL Server 2008 or lower is not capable of handling - // stored procedures with output parameters - return ExecuteStoredProcedureListLegacy(commandText2, dbParams); - } - - var result = this.Database.SqlQuery(commandText, parameters).ToList(); - if (!ForceNoTracking) - { - using (var scope = new DbContextScope(this, autoDetectChanges: false)) - { - for (int i = 0; i < result.Count; i++) - { - result[i] = Attach(result[i]); - } - } - } - return result; - } - - private IList ExecuteStoredProcedureListLegacy(string commandText, IEnumerable parameters) where TEntity : BaseEntity, new() - { - var connection = this.Database.Connection; - // Don't close the connection after command execution - - // open the connection for use - if (connection.State == ConnectionState.Closed) - connection.Open(); - - // create a command object - using (var cmd = connection.CreateCommand()) - { - // command to execute - cmd.CommandText = commandText; - cmd.CommandType = CommandType.StoredProcedure; - - // move parameters to command object - cmd.Parameters.AddRange(parameters.ToArray()); - - // database call - var reader = cmd.ExecuteReader(); - var result = ((IObjectContextAdapter)(this)).ObjectContext.Translate(reader).ToList(); - if (!ForceNoTracking) - { - for (int i = 0; i < result.Count; i++) - { - result[i] = Attach(result[i]); - } - } - // close up the reader, we're done saving results - reader.Close(); - return result; - } - } + public new DbSet Set() where TEntity : BaseEntity + { + return base.Set(); + } + + private IEnumerable ToParameters(params object[] parameters) + { + if (parameters == null || parameters.Length == 0) + return Enumerable.Empty(); + + return parameters.Cast(); + } + + public virtual IList ExecuteStoredProcedureList(string commandText, params object[] parameters) where TEntity : BaseEntity, new() + { + // Add parameters to command + var commandText2 = commandText; + var dbParams = ToParameters(parameters); + bool firstParam = true; + bool hasOutputParams = false; + foreach (var p in dbParams) + { + commandText += firstParam ? " " : ", "; + firstParam = false; + + commandText += "@" + p.ParameterName; + if (p.Direction == ParameterDirection.InputOutput || p.Direction == ParameterDirection.Output) + { + // output parameter + hasOutputParams = true; + commandText += " output"; + } + } + + + var isLegacyDb = !this.IsSqlServer2012OrHigher(); + if (isLegacyDb && hasOutputParams) + { + // SQL Server 2008 or lower is not capable of handling + // stored procedures with output parameters + return ExecuteStoredProcedureListLegacy(commandText2, dbParams); + } + + var result = this.Database.SqlQuery(commandText, parameters).ToList(); + if (!ForceNoTracking) + { + using (var scope = new DbContextScope(this, autoDetectChanges: false)) + { + for (int i = 0; i < result.Count; i++) + { + result[i] = Attach(result[i]); + } + } + } + return result; + } + + private IList ExecuteStoredProcedureListLegacy(string commandText, IEnumerable parameters) where TEntity : BaseEntity, new() + { + var connection = this.Database.Connection; + // Don't close the connection after command execution + + // open the connection for use + if (connection.State == ConnectionState.Closed) + connection.Open(); + + // create a command object + using (var cmd = connection.CreateCommand()) + { + // command to execute + cmd.CommandText = commandText; + cmd.CommandType = CommandType.StoredProcedure; + + // move parameters to command object + cmd.Parameters.AddRange(parameters.ToArray()); + + // database call + var reader = cmd.ExecuteReader(); + var result = ((IObjectContextAdapter)(this)).ObjectContext.Translate(reader).ToList(); + if (!ForceNoTracking) + { + for (int i = 0; i < result.Count; i++) + { + result[i] = Attach(result[i]); + } + } + // close up the reader, we're done saving results + reader.Close(); + return result; + } + } /// /// Creates a raw SQL query that will return elements of the given generic type. @@ -211,69 +210,63 @@ public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, in /// /// public virtual bool IsDetectingChanges() - { - if (_transactionManager == null && DataSettings.DatabaseIsInstalled()) - { - var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager; - if (stateManager != null) - { - // Get the internal TransactionManager property instance from ObjectStateManager - _transactionManager = FastProperty.GetProperty(stateManager.GetType(), "TransactionManager")?.GetValue(stateManager); - } - } - - if (_transactionManager != null) - { - // Get the "IsDetectChanges" property of the internal TransactionManager - var prop = FastProperty.GetProperty(_transactionManager.GetType(), "IsDetectChanges"); - if (prop != null) - { - return (bool)prop.GetValue(_transactionManager); - } - } - - return false; - } - - public void DetectChanges() - { - base.ChangeTracker.DetectChanges(); - } - - public bool HasChanges - { - get + { + if (_transactionManager == null && DataSettings.DatabaseIsInstalled()) { - return GetChangedEntries().Any(); + var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager; + if (stateManager != null) + { + // Get the internal TransactionManager property instance from ObjectStateManager + _transactionManager = FastProperty.GetProperty(stateManager.GetType(), "TransactionManager")?.GetValue(stateManager); + } } + + if (_transactionManager != null) + { + // Get the "IsDetectChanges" property of the internal TransactionManager + var prop = FastProperty.GetProperty(_transactionManager.GetType(), "IsDetectChanges"); + if (prop != null) + { + return (bool)prop.GetValue(_transactionManager); + } + } + + return false; } - public virtual bool IsModified(BaseEntity entity) - { - Guard.NotNull(entity, nameof(entity)); + public void DetectChanges() + { + base.ChangeTracker.DetectChanges(); + } - var entry = this.Entry((object)entity); - return entry.HasChanges(this); - } + public bool HasChanges => GetChangedEntries().Any(); - public bool TryGetModifiedProperty(BaseEntity entity, string propertyName, out object originalValue) - { - Guard.NotNull(entity, nameof(entity)); + public virtual bool IsModified(BaseEntity entity) + { + Guard.NotNull(entity, nameof(entity)); - if (entity.IsTransientRecord()) - { - originalValue = null; - return false; - } + var entry = this.Entry((object)entity); + return entry.HasChanges(this); + } - var entry = this.Entry((object)entity); - return entry.TryGetModifiedProperty(this, propertyName, out originalValue); - } + public bool TryGetModifiedProperty(BaseEntity entity, string propertyName, out object originalValue) + { + Guard.NotNull(entity, nameof(entity)); - public IDictionary GetModifiedProperties(BaseEntity entity) - { - return this.Entry((object)entity).GetModifiedProperties(this); - } + if (entity.IsTransientRecord()) + { + originalValue = null; + return false; + } + + var entry = this.Entry((object)entity); + return entry.TryGetModifiedProperty(this, propertyName, out originalValue); + } + + public IDictionary GetModifiedProperties(BaseEntity entity) + { + return this.Entry((object)entity).GetModifiedProperties(this); + } // required for UoW implementation public string Alias { get; internal set; } @@ -281,285 +274,261 @@ public IDictionary GetModifiedProperties(BaseEntity entity) // performance on bulk inserts public bool AutoDetectChangesEnabled { - get + get => this.Configuration.AutoDetectChangesEnabled; + set => this.Configuration.AutoDetectChangesEnabled = value; + } + + // performance on bulk inserts + public bool ValidateOnSaveEnabled + { + get => this.Configuration.ValidateOnSaveEnabled; + set => this.Configuration.ValidateOnSaveEnabled = value; + } + + public bool ProxyCreationEnabled + { + get => this.Configuration.ProxyCreationEnabled; + set => this.Configuration.ProxyCreationEnabled = value; + } + + public bool LazyLoadingEnabled + { + get => this.Configuration.LazyLoadingEnabled; + set => this.Configuration.LazyLoadingEnabled = value; + } + + public bool ForceNoTracking { get; set; } + + public bool AutoCommitEnabled { get; set; } + + public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + { + var dbContextTransaction = this.Database.BeginTransaction(isolationLevel); + return new DbContextTransactionWrapper(dbContextTransaction); + } + + public void UseTransaction(DbTransaction transaction) + { + this.Database.UseTransaction(transaction); + } + + private IEnumerable GetMergeableEntitiesFromChangeTracker() + { + return base.ChangeTracker.Entries() + .Where(x => x.State > EfState.Detached) + .Select(x => x.Entity) + .OfType(); + } + + #endregion + + #region Utils + + /// + /// Resolves the connection string from the Settings.txt file + /// + /// The connection string + /// This helper is called from parameterless DbContext constructors which are required for EF tooling support or during installation. + public static string GetConnectionString() + { + if (DataSettings.Current.IsValid()) { - return this.Configuration.AutoDetectChangesEnabled; + return DataSettings.Current.DataConnectionString; } - set + + throw Error.Application("A connection string could not be resolved for the parameterless constructor of the derived DbContext. Either the database is not installed, or the file 'Settings.txt' does not exist or contains invalid content."); + } + + protected internal bool IsSqlServer2012OrHigher() + { + if (!_isSqlServer2012OrHigher.HasValue) { - this.Configuration.AutoDetectChangesEnabled = value; + try + { + // TODO: actually we should cache this value by connection (string). + // But fact is: it's quite unlikely that multiple DB versions are used within a single application scope. + var info = this.GetSqlServerInfo(); + string productVersion = info.ProductVersion; + int version = productVersion.Split(new char[] { '.' })[0].ToInt(); + _isSqlServer2012OrHigher = version >= 11; + } + catch + { + _isSqlServer2012OrHigher = false; + } } + + return _isSqlServer2012OrHigher.Value; } - // performance on bulk inserts - public bool ValidateOnSaveEnabled + public TEntity Attach(TEntity entity) where TEntity : BaseEntity { - get + var dbSet = Set(); + var alreadyAttached = dbSet.Local.FirstOrDefault(x => x.Id == entity.Id); + + if (alreadyAttached == null) { - return this.Configuration.ValidateOnSaveEnabled; + dbSet.Attach(entity); + return entity; } - set + + return alreadyAttached; + } + + public bool IsAttached(TEntity entity) where TEntity : BaseEntity + { + if (entity != null) { - this.Configuration.ValidateOnSaveEnabled = value; + return Set().Local.Any(x => x.Id == entity.Id); } + + return false; } - public bool ProxyCreationEnabled + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int DetachEntity(TEntity entity, bool deep = false) where TEntity : BaseEntity + { + return DetachInternal(entity, deep ? new HashSet() : null, deep); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int DetachEntities(bool unchangedEntitiesOnly = true, bool deep = false) where TEntity : BaseEntity + { + return DetachEntities(o => o is TEntity, unchangedEntitiesOnly, deep); + } + + public int DetachEntities(Func predicate, bool unchangedEntitiesOnly = true, bool deep = false) + { + Guard.NotNull(predicate, nameof(predicate)); + + var numDetached = 0; + + using (new DbContextScope(this, autoDetectChanges: false, lazyLoading: false)) + { + var entries = this.ChangeTracker.Entries().Where(Match).ToList(); + + HashSet objSet = deep ? new HashSet() : null; + + foreach (var entry in entries) + { + numDetached += DetachInternal(entry, objSet, deep); + } + + return numDetached; + } + + bool Match(DbEntityEntry entry) + { + if (entry.State > EfState.Detached && predicate(entry.Entity)) + { + return unchangedEntitiesOnly + ? entry.State == EfState.Unchanged + : true; + } + + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int DetachInternal(BaseEntity obj, ISet objSet, bool deep) { - get + if (obj == null) + return 0; + + return DetachInternal(this.Entry(obj), objSet, deep); + } + + internal int DetachInternal(DbEntityEntry entry, ISet objSet, bool deep) + { + var obj = entry.Entity; + int numDetached = 0; + + if (deep) { - return this.Configuration.ProxyCreationEnabled; + // This is to prevent an infinite recursion when the child object has a navigation property + // that points back to the parent + if (objSet != null && !objSet.Add(obj)) + return 0; + + // Recursively detach all navigation properties + foreach (var prop in FastProperty.GetProperties(obj.GetUnproxiedType()).Values) + { + if (typeof(BaseEntity).IsAssignableFrom(prop.Property.PropertyType)) + { + numDetached += DetachInternal(prop.GetValue(obj) as BaseEntity, objSet, deep); + } + else if (typeof(IEnumerable).IsAssignableFrom(prop.Property.PropertyType)) + { + var val = prop.GetValue(obj); + if (val is IEnumerable list) + { + foreach (var item in list.ToList()) + { + numDetached += DetachInternal(item, objSet, deep); + } + } + } + } } - set + + entry.State = EfState.Detached; + numDetached++; + + return numDetached; + } + + public void ChangeState(TEntity entity, EfState requestedState) where TEntity : BaseEntity + { + //Console.WriteLine("ChangeState ORIGINAL"); + var entry = this.Entry(entity); + + if (entry.State != requestedState) { - this.Configuration.ProxyCreationEnabled = value; + // Only change state when requested state differs, + // because EF internally sets all properties to modified + // if necessary, even when requested state equals current state. + entry.State = requestedState; } } - public bool LazyLoadingEnabled - { - get - { - return this.Configuration.LazyLoadingEnabled; - } - set - { - this.Configuration.LazyLoadingEnabled = value; - } - } - - public bool ForceNoTracking { get; set; } - - public bool AutoCommitEnabled { get; set; } - - public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) - { - var dbContextTransaction = this.Database.BeginTransaction(isolationLevel); - return new DbContextTransactionWrapper(dbContextTransaction); - } - - public void UseTransaction(DbTransaction transaction) - { - this.Database.UseTransaction(transaction); - } - - private IEnumerable GetMergeableEntitiesFromChangeTracker() - { - return base.ChangeTracker.Entries() - .Where(x => x.State > EfState.Detached) - .Select(x => x.Entity) - .OfType(); - } + public void ReloadEntity(TEntity entity) where TEntity : BaseEntity + { + this.Entry((object)entity).ReloadEntity(); + } #endregion - #region Utils + #region Nested classes - /// - /// Resolves the connection string from the Settings.txt file - /// - /// The connection string - /// This helper is called from parameterless DbContext constructors which are required for EF tooling support or during installation. - public static string GetConnectionString() - { - if (DataSettings.Current.IsValid()) - { - return DataSettings.Current.DataConnectionString; - } - - throw Error.Application("A connection string could not be resolved for the parameterless constructor of the derived DbContext. Either the database is not installed, or the file 'Settings.txt' does not exist or contains invalid content."); - } - - protected internal bool IsSqlServer2012OrHigher() - { - if (!_isSqlServer2012OrHigher.HasValue) - { - try - { - // TODO: actually we should cache this value by connection (string). - // But fact is: it's quite unlikely that multiple DB versions are used within a single application scope. - var info = this.GetSqlServerInfo(); - string productVersion = info.ProductVersion; - int version = productVersion.Split(new char[] { '.' })[0].ToInt(); - _isSqlServer2012OrHigher = version >= 11; - } - catch - { - _isSqlServer2012OrHigher = false; - } - } - - return _isSqlServer2012OrHigher.Value; - } - - public TEntity Attach(TEntity entity) where TEntity : BaseEntity - { - var dbSet = Set(); - var alreadyAttached = dbSet.Local.FirstOrDefault(x => x.Id == entity.Id); - - if (alreadyAttached == null) - { - dbSet.Attach(entity); - return entity; - } - - return alreadyAttached; - } - - public bool IsAttached(TEntity entity) where TEntity : BaseEntity - { - if (entity != null) - { - return Set().Local.Any(x => x.Id == entity.Id); - } - - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int DetachEntity(TEntity entity, bool deep = false) where TEntity : BaseEntity + private class DbContextTransactionWrapper : ITransaction { - return DetachInternal(entity, deep ? new HashSet() : null, deep); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int DetachEntities(bool unchangedEntitiesOnly = true, bool deep = false) where TEntity : BaseEntity - { - return DetachEntities(o => o is TEntity, unchangedEntitiesOnly, deep); - } - - public int DetachEntities(Func predicate, bool unchangedEntitiesOnly = true, bool deep = false) - { - Guard.NotNull(predicate, nameof(predicate)); - - var numDetached = 0; - - using (new DbContextScope(this, autoDetectChanges: false, lazyLoading: false)) - { - var entries = this.ChangeTracker.Entries().Where(Match).ToList(); - - HashSet objSet = deep ? new HashSet() : null; - - foreach (var entry in entries) - { - numDetached += DetachInternal(entry, objSet, deep); - } - - return numDetached; - } - - bool Match(DbEntityEntry entry) - { - if (entry.State > EfState.Detached && predicate(entry.Entity)) - { - return unchangedEntitiesOnly - ? entry.State == EfState.Unchanged - : true; - } - - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal int DetachInternal(BaseEntity obj, ISet objSet, bool deep) - { - if (obj == null) - return 0; - - return DetachInternal(this.Entry(obj), objSet, deep); - } - - internal int DetachInternal(DbEntityEntry entry, ISet objSet, bool deep) - { - var obj = entry.Entity; - int numDetached = 0; - - if (deep) - { - // This is to prevent an infinite recursion when the child object has a navigation property - // that points back to the parent - if (objSet != null && !objSet.Add(obj)) - return 0; - - // Recursively detach all navigation properties - foreach (var prop in FastProperty.GetProperties(obj.GetUnproxiedType()).Values) - { - if (typeof(BaseEntity).IsAssignableFrom(prop.Property.PropertyType)) - { - numDetached += DetachInternal(prop.GetValue(obj) as BaseEntity, objSet, deep); - } - else if (typeof(IEnumerable).IsAssignableFrom(prop.Property.PropertyType)) - { - var val = prop.GetValue(obj); - if (val is IEnumerable list) - { - foreach (var item in list.ToList()) - { - numDetached += DetachInternal(item, objSet, deep); - } - } - } - } - } - - entry.State = EfState.Detached; - - numDetached++; - - return numDetached; - } - - public void ChangeState(TEntity entity, EfState requestedState) where TEntity : BaseEntity - { - //Console.WriteLine("ChangeState ORIGINAL"); - var entry = this.Entry(entity); - - if (entry.State != requestedState) - { - // Only change state when requested state differs, - // because EF internally sets all properties to modified - // if necessary, even when requested state equals current state. - entry.State = requestedState; - } - } - - public void ReloadEntity(TEntity entity) where TEntity : BaseEntity - { - this.Entry((object)entity).ReloadEntity(); - } - - #endregion - - #region Nested classes - - private class DbContextTransactionWrapper : ITransaction - { - private readonly DbContextTransaction _tx; - - public DbContextTransactionWrapper(DbContextTransaction tx) - { - Guard.NotNull(tx, nameof(tx)); - - _tx = tx; - } - - public void Commit() - { - _tx.Commit(); - } - - public void Rollback() - { - _tx.Rollback(); - } - - public void Dispose() - { - _tx.Dispose(); - } - } - - #endregion - } + private readonly DbContextTransaction _tx; + + public DbContextTransactionWrapper(DbContextTransaction tx) + { + Guard.NotNull(tx, nameof(tx)); + + _tx = tx; + } + + public void Commit() + { + _tx.Commit(); + } + + public void Rollback() + { + if (_tx.UnderlyingTransaction.Connection != null) + _tx.Rollback(); + } + + public void Dispose() + { + _tx.Dispose(); + } + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs index 103513101e..c6419d7f3b 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs @@ -6,63 +6,63 @@ namespace SmartStore.Data.Setup { - internal class ActivityLogTypeMigrator - { - private readonly SmartObjectContext _ctx; - private readonly DbSet _activityLogTypeRecords; + internal class ActivityLogTypeMigrator + { + private readonly SmartObjectContext _ctx; + private readonly DbSet _activityLogTypeRecords; - public ActivityLogTypeMigrator(SmartObjectContext ctx) - { - Guard.NotNull(ctx, nameof(ctx)); + public ActivityLogTypeMigrator(SmartObjectContext ctx) + { + Guard.NotNull(ctx, nameof(ctx)); - _ctx = ctx; - _activityLogTypeRecords = _ctx.Set(); - } + _ctx = ctx; + _activityLogTypeRecords = _ctx.Set(); + } - private Language GetDefaultAdminLanguage(DbSet settingRecords, DbSet languageRecords) - { - const string settingKey = "LocalizationSettings.DefaultAdminLanguageId"; + private Language GetDefaultAdminLanguage(DbSet settingRecords, DbSet languageRecords) + { + const string settingKey = "LocalizationSettings.DefaultAdminLanguageId"; - var defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey && x.StoreId == 0); + var defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey && x.StoreId == 0); - if (defaultAdminLanguageSetting == null) - defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey); + if (defaultAdminLanguageSetting == null) + defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey); - if (defaultAdminLanguageSetting != null) - { - var defaultAdminLanguageId = defaultAdminLanguageSetting.Value.ToInt(); - if (defaultAdminLanguageId != 0) - { - var language = languageRecords.FirstOrDefault(x => x.Id == defaultAdminLanguageId); - if (language != null) - return language; - } - } + if (defaultAdminLanguageSetting != null) + { + var defaultAdminLanguageId = defaultAdminLanguageSetting.Value.ToInt(); + if (defaultAdminLanguageId != 0) + { + var language = languageRecords.FirstOrDefault(x => x.Id == defaultAdminLanguageId); + if (language != null) + return language; + } + } - return languageRecords.First(); - } + return languageRecords.First(); + } - public void AddActivityLogType(string systemKeyword, string enName, string deName) - { - Guard.NotEmpty(systemKeyword, nameof(systemKeyword)); - Guard.NotEmpty(enName, nameof(enName)); - Guard.NotEmpty(deName, nameof(deName)); + public void AddActivityLogType(string systemKeyword, string enName, string deName) + { + Guard.NotEmpty(systemKeyword, nameof(systemKeyword)); + Guard.NotEmpty(enName, nameof(enName)); + Guard.NotEmpty(deName, nameof(deName)); - var record = _activityLogTypeRecords.FirstOrDefault(x => x.SystemKeyword == systemKeyword); + var record = _activityLogTypeRecords.FirstOrDefault(x => x.SystemKeyword == systemKeyword); - if (record == null) - { - var language = GetDefaultAdminLanguage(_ctx.Set(), _ctx.Set()); + if (record == null) + { + var language = GetDefaultAdminLanguage(_ctx.Set(), _ctx.Set()); - _activityLogTypeRecords.Add(new ActivityLogType - { - Enabled = true, - SystemKeyword = systemKeyword, - Name = (language.UniqueSeoCode.IsCaseInsensitiveEqual("de") ? deName : enName) - }); + _activityLogTypeRecords.Add(new ActivityLogType + { + Enabled = true, + SystemKeyword = systemKeyword, + Name = (language.UniqueSeoCode.IsCaseInsensitiveEqual("de") ? deName : enName) + }); - _ctx.SaveChanges(); - } - } - } + _ctx.SaveChanges(); + } + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/BuilderDbContextExtensions.cs b/src/Libraries/SmartStore.Data/Setup/Builder/BuilderDbContextExtensions.cs index 91ad3c1149..fff96484f1 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/BuilderDbContextExtensions.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/BuilderDbContextExtensions.cs @@ -2,42 +2,40 @@ namespace SmartStore.Data.Setup { + public static class BuilderDbContextExtensions + { + #region Resource building - public static class BuilderDbContextExtensions - { - #region Resource building + public static void MigrateLocaleResources(this SmartObjectContext ctx, Action fn, bool updateTouchedResources = false) + { + Guard.NotNull(ctx, nameof(ctx)); + Guard.NotNull(fn, nameof(fn)); - public static void MigrateLocaleResources(this SmartObjectContext ctx, Action fn, bool updateTouchedResources = false) - { - Guard.NotNull(ctx, nameof(ctx)); - Guard.NotNull(fn, nameof(fn)); + var builder = new LocaleResourcesBuilder(); + fn(builder); + var entries = builder.Build(); - var builder = new LocaleResourcesBuilder(); - fn(builder); - var entries = builder.Build(); - - var migrator = new LocaleResourcesMigrator(ctx); - migrator.Migrate(entries, updateTouchedResources); - } + var migrator = new LocaleResourcesMigrator(ctx); + migrator.Migrate(entries, updateTouchedResources); + } - #endregion + #endregion - #region Settings building + #region Settings building - public static void MigrateSettings(this SmartObjectContext ctx, Action fn) - { - Guard.NotNull(ctx, nameof(ctx)); - Guard.NotNull(fn, nameof(fn)); + public static void MigrateSettings(this SmartObjectContext ctx, Action fn) + { + Guard.NotNull(ctx, nameof(ctx)); + Guard.NotNull(fn, nameof(fn)); - var builder = new SettingsBuilder(); - fn(builder); - var entries = builder.Build(); + var builder = new SettingsBuilder(); + fn(builder); + var entries = builder.Build(); - var migrator = new SettingsMigrator(ctx); - migrator.Migrate(entries); - } - - #endregion - } + var migrator = new SettingsMigrator(ctx); + migrator.Migrate(entries); + } + #endregion + } } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs index 2f90e53a5b..6459b02090 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs @@ -1,178 +1,177 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace SmartStore.Data.Setup { - internal class LocaleResourceEntry - { - public string Key { get; set; } - public string Value { get; set; } - public string Lang { get; set; } - - public bool UpdateOnly { get; set; } - public bool Important { get; set; } - } - - public interface IResourceAddBuilder : IHideObjectMembers - { - IResourceAddBuilder Value(string value); - IResourceAddBuilder Value(string lang, string value); - IResourceAddBuilder Hint(string value); - IResourceAddBuilder Hint(string lang, string value); - } - - public class LocaleResourcesBuilder : IHideObjectMembers - { - private readonly List _entries = new List(); - - /// - /// Deletes one or many locale resources in any language from the database - /// - /// The key(s) of the resources to delete - public void Delete(params string[] keys) - { - keys.Each(x => _entries.Add(new LocaleResourceEntry { Key = x, Important = true })); - } - - /// - /// Deletes one or many locale resources in the specified language from the database - /// - /// The language identifier - /// The key(s) of the resources to delete - public void DeleteFor(string lang, params string[] keys) - { - Guard.NotEmpty(lang, nameof(lang)); - lang = lang.NullEmpty(); - - keys.Each(x => _entries.Add(new LocaleResourceEntry { Key = x, Lang = lang, Important = true })); - } - - /// - /// Updates existing locale resources - /// - /// The key of the resource - /// IResourceAddBuilder - public IResourceAddBuilder Update(string key) - { - Guard.NotEmpty(key, nameof(key)); - - Action fn = (string v, string l, bool isHint) => - { - string k = key; - if (isHint) - { - k += ".Hint"; - } - - _entries.Add(new LocaleResourceEntry { Key = k, Value = v, Lang = l, UpdateOnly = true }); - }; - - return new ResourceAddBuilder(key, fn); - } - - /// - /// Adds or updates locale resources - /// - /// The key of the resource - /// IResourceAddBuilder - public IResourceAddBuilder AddOrUpdate(string key) - { - Guard.NotEmpty(key, nameof(key)); - - Action fn = (string v, string l, bool isHint) => - { - string k = key; - if (isHint) - { - k += ".Hint"; - } - - _entries.Add(new LocaleResourceEntry { Key = k, Value = v, Lang = l }); - }; - - return new ResourceAddBuilder(key, fn); - } - - /// - /// Adds or updates locale resources - /// - /// The key of the resource - /// Primary English (untranslated) value of the resource - /// German value of the resource - public void AddOrUpdate(string key, string value, string deValue) - { - Guard.NotEmpty(key, nameof(key)); - - _entries.Add(new LocaleResourceEntry { Key = key, Value = value }); - _entries.Add(new LocaleResourceEntry { Key = key, Value = deValue, Lang = "de" }); - } - - /// - /// Adds or updates locale resources - /// - /// The key of the resource - /// Primary English (untranslated) value of the resource - /// German value of the resource - /// Primary English (untranslated) hint resource - /// German hint resource - public void AddOrUpdate(string key, string value, string deValue, string hint, string deHint) - { - Guard.NotEmpty(key, nameof(key)); - - AddOrUpdate(key, value, deValue); - AddOrUpdate(key + ".Hint", hint, deHint); - } - - internal void Reset() - { - _entries.Clear(); - } - - internal IEnumerable Build() - { - return _entries.OrderByDescending(x => x.Important).ThenBy(x => x.Lang).ToList(); - } - - #region Nested builder for AddOrUpdate - - internal class ResourceAddBuilder : IResourceAddBuilder - { - private readonly Action _fn; - - public ResourceAddBuilder(string key, Action fn) - { - _fn = fn; - } - - public IResourceAddBuilder Value(string value) - { - return Value(null, value); - } - - public IResourceAddBuilder Value(string lang, string value) - { - Guard.NotEmpty(value, nameof(value)); - _fn(value, lang.NullEmpty(), false); - return this; - } - - - public IResourceAddBuilder Hint(string value) - { - return Hint(null, value); - } - - public IResourceAddBuilder Hint(string lang, string value) - { - Guard.NotEmpty(value, nameof(value)); - _fn(value, lang.NullEmpty(), true); - return this; - } - } - - #endregion - - } + internal class LocaleResourceEntry + { + public string Key { get; set; } + public string Value { get; set; } + public string Lang { get; set; } + + public bool UpdateOnly { get; set; } + public bool Important { get; set; } + } + + public interface IResourceAddBuilder : IHideObjectMembers + { + IResourceAddBuilder Value(string value); + IResourceAddBuilder Value(string lang, string value); + IResourceAddBuilder Hint(string value); + IResourceAddBuilder Hint(string lang, string value); + } + + public class LocaleResourcesBuilder : IHideObjectMembers + { + private readonly List _entries = new List(); + + /// + /// Deletes one or many locale resources in any language from the database + /// + /// The key(s) of the resources to delete + public void Delete(params string[] keys) + { + keys.Each(x => _entries.Add(new LocaleResourceEntry { Key = x, Important = true })); + } + + /// + /// Deletes one or many locale resources in the specified language from the database + /// + /// The language identifier + /// The key(s) of the resources to delete + public void DeleteFor(string lang, params string[] keys) + { + Guard.NotEmpty(lang, nameof(lang)); + lang = lang.NullEmpty(); + + keys.Each(x => _entries.Add(new LocaleResourceEntry { Key = x, Lang = lang, Important = true })); + } + + /// + /// Updates existing locale resources + /// + /// The key of the resource + /// IResourceAddBuilder + public IResourceAddBuilder Update(string key) + { + Guard.NotEmpty(key, nameof(key)); + + Action fn = (string v, string l, bool isHint) => + { + string k = key; + if (isHint) + { + k += ".Hint"; + } + + _entries.Add(new LocaleResourceEntry { Key = k, Value = v, Lang = l, UpdateOnly = true }); + }; + + return new ResourceAddBuilder(key, fn); + } + + /// + /// Adds or updates locale resources + /// + /// The key of the resource + /// IResourceAddBuilder + public IResourceAddBuilder AddOrUpdate(string key) + { + Guard.NotEmpty(key, nameof(key)); + + Action fn = (string v, string l, bool isHint) => + { + string k = key; + if (isHint) + { + k += ".Hint"; + } + + _entries.Add(new LocaleResourceEntry { Key = k, Value = v, Lang = l }); + }; + + return new ResourceAddBuilder(key, fn); + } + + /// + /// Adds or updates locale resources + /// + /// The key of the resource + /// Primary English (untranslated) value of the resource + /// German value of the resource + public void AddOrUpdate(string key, string value, string deValue) + { + Guard.NotEmpty(key, nameof(key)); + + _entries.Add(new LocaleResourceEntry { Key = key, Value = value }); + _entries.Add(new LocaleResourceEntry { Key = key, Value = deValue, Lang = "de" }); + } + + /// + /// Adds or updates locale resources + /// + /// The key of the resource + /// Primary English (untranslated) value of the resource + /// German value of the resource + /// Primary English (untranslated) hint resource + /// German hint resource + public void AddOrUpdate(string key, string value, string deValue, string hint, string deHint) + { + Guard.NotEmpty(key, nameof(key)); + + AddOrUpdate(key, value, deValue); + AddOrUpdate(key + ".Hint", hint, deHint); + } + + internal void Reset() + { + _entries.Clear(); + } + + internal IEnumerable Build() + { + return _entries.OrderByDescending(x => x.Important).ThenBy(x => x.Lang).ToList(); + } + + #region Nested builder for AddOrUpdate + + internal class ResourceAddBuilder : IResourceAddBuilder + { + private readonly Action _fn; + + public ResourceAddBuilder(string key, Action fn) + { + _fn = fn; + } + + public IResourceAddBuilder Value(string value) + { + return Value(null, value); + } + + public IResourceAddBuilder Value(string lang, string value) + { + Guard.NotEmpty(value, nameof(value)); + _fn(value, lang.NullEmpty(), false); + return this; + } + + + public IResourceAddBuilder Hint(string value) + { + return Hint(null, value); + } + + public IResourceAddBuilder Hint(string lang, string value) + { + Guard.NotEmpty(value, nameof(value)); + _fn(value, lang.NullEmpty(), true); + return this; + } + } + + #endregion + + } } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs index 7816eaca86..eadb6db49d 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs @@ -2,149 +2,147 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; namespace SmartStore.Data.Setup { - internal class LocaleResourcesMigrator - { - private readonly SmartObjectContext _ctx; - private readonly DbSet _languages; - private readonly DbSet _resources; - - public LocaleResourcesMigrator(SmartObjectContext ctx) - { - Guard.NotNull(ctx, nameof(ctx)); - - _ctx = ctx; - _languages = _ctx.Set(); - _resources = _ctx.Set(); - } - - public void Migrate(IEnumerable entries, bool updateTouchedResources = false) - { - Guard.NotNull(entries, nameof(entries)); - - if (!entries.Any() || !_languages.Any()) - return; - - using (var scope = new DbContextScope(_ctx, autoDetectChanges: false, hooksEnabled: false)) - { - var langMap = _languages.ToDictionarySafe(x => x.LanguageCulture.EmptyNull().ToLower()); - - var toDelete = new List(); - var toUpdate = new List(); - var toAdd = new List(); - - bool IsEntryValid(LocaleResourceEntry entry, Language targetLang) - { - if (entry.Lang == null) - return true; - - var sourceLangCode = entry.Lang.ToLower(); - - if (targetLang != null) - { - var culture = targetLang.LanguageCulture; - if (culture == sourceLangCode || culture.StartsWith(sourceLangCode + "-")) - { - return true; - } - } - else - { - if (langMap.ContainsKey(sourceLangCode)) - return true; - - if (langMap.Keys.Any(k => k.StartsWith(sourceLangCode + "-", StringComparison.OrdinalIgnoreCase))) - return true; - } - - return false; - } - - // Remove all entries with invalid lang identifier - var invalidEntries = entries.Where(x => !IsEntryValid(x, null)); - if (invalidEntries.Any()) - { - entries = entries.Except(invalidEntries).ToArray(); - } - - foreach (var lang in langMap) - { - var validEntries = entries.Where(x => IsEntryValid(x, lang.Value)).ToArray(); - foreach (var entry in validEntries) - { - var db = GetResource(entry.Key, lang.Value.Id, toAdd, out bool isLocal); - - if (db == null && entry.Value.HasValue() && !entry.UpdateOnly) - { - // ADD action - toAdd.Add(new LocaleStringResource { LanguageId = lang.Value.Id, ResourceName = entry.Key, ResourceValue = entry.Value }); - } - - if (db == null) - continue; - - if (entry.Value == null) - { - // DELETE action - if (isLocal) - toAdd.Remove(db); - else - toDelete.Add(db); - } - else - { - if (isLocal) - { - db.ResourceValue = entry.Value; - continue; - } - - // UPDATE action - if (updateTouchedResources || !db.IsTouched.GetValueOrDefault()) - { - db.ResourceValue = entry.Value; - toUpdate.Add(db); - if (toDelete.Contains(db)) - toDelete.Remove(db); - } - } - } - } - - // add new resources to context - _resources.AddRange(toAdd); - - // remove deleted resources - _resources.RemoveRange(toDelete); - - // save now - int affectedRows = _ctx.SaveChanges(); - - _ctx.DetachEntities(); - } - } - - private LocaleStringResource GetResource(string key, int langId, IList local, out bool isLocal) - { - var res = local.FirstOrDefault(x => x.ResourceName.Equals(key, StringComparison.InvariantCultureIgnoreCase) && x.LanguageId == langId); - isLocal = res != null; - - if (res == null) - { - var query = _resources.Where(x => x.ResourceName.Equals(key, StringComparison.InvariantCultureIgnoreCase) && x.LanguageId == langId); - res = query.FirstOrDefault(); - } - - return res; - } - - } + internal class LocaleResourcesMigrator + { + private readonly SmartObjectContext _ctx; + private readonly DbSet _languages; + private readonly DbSet _resources; + + public LocaleResourcesMigrator(SmartObjectContext ctx) + { + Guard.NotNull(ctx, nameof(ctx)); + + _ctx = ctx; + _languages = _ctx.Set(); + _resources = _ctx.Set(); + } + + public void Migrate(IEnumerable entries, bool updateTouchedResources = false) + { + Guard.NotNull(entries, nameof(entries)); + + if (!entries.Any() || !_languages.Any()) + return; + + using (var scope = new DbContextScope(_ctx, autoDetectChanges: false, hooksEnabled: false)) + { + var langMap = _languages.ToDictionarySafe(x => x.LanguageCulture.EmptyNull().ToLower()); + + var toDelete = new List(); + var toUpdate = new List(); + var toAdd = new List(); + + bool IsEntryValid(LocaleResourceEntry entry, Language targetLang) + { + if (entry.Lang == null) + return true; + + var sourceLangCode = entry.Lang.ToLower(); + + if (targetLang != null) + { + var culture = targetLang.LanguageCulture; + if (culture == sourceLangCode || culture.StartsWith(sourceLangCode + "-")) + { + return true; + } + } + else + { + if (langMap.ContainsKey(sourceLangCode)) + return true; + + if (langMap.Keys.Any(k => k.StartsWith(sourceLangCode + "-", StringComparison.OrdinalIgnoreCase))) + return true; + } + + return false; + } + + // Remove all entries with invalid lang identifier + var invalidEntries = entries.Where(x => !IsEntryValid(x, null)); + if (invalidEntries.Any()) + { + entries = entries.Except(invalidEntries).ToArray(); + } + + foreach (var lang in langMap) + { + var validEntries = entries.Where(x => IsEntryValid(x, lang.Value)).ToArray(); + foreach (var entry in validEntries) + { + var db = GetResource(entry.Key, lang.Value.Id, toAdd, out bool isLocal); + + if (db == null && entry.Value.HasValue() && !entry.UpdateOnly) + { + // ADD action + toAdd.Add(new LocaleStringResource { LanguageId = lang.Value.Id, ResourceName = entry.Key, ResourceValue = entry.Value }); + } + + if (db == null) + continue; + + if (entry.Value == null) + { + // DELETE action + if (isLocal) + toAdd.Remove(db); + else + toDelete.Add(db); + } + else + { + if (isLocal) + { + db.ResourceValue = entry.Value; + continue; + } + + // UPDATE action + if (updateTouchedResources || !db.IsTouched.GetValueOrDefault()) + { + db.ResourceValue = entry.Value; + toUpdate.Add(db); + if (toDelete.Contains(db)) + toDelete.Remove(db); + } + } + } + } + + // add new resources to context + _resources.AddRange(toAdd); + + // remove deleted resources + _resources.RemoveRange(toDelete); + + // save now + int affectedRows = _ctx.SaveChanges(); + + _ctx.DetachEntities(); + _ctx.DetachEntities(); + } + } + + private LocaleStringResource GetResource(string key, int langId, IList local, out bool isLocal) + { + var res = local.FirstOrDefault(x => x.ResourceName.Equals(key, StringComparison.InvariantCultureIgnoreCase) && x.LanguageId == langId); + isLocal = res != null; + + if (res == null) + { + var query = _resources.Where(x => x.ResourceName.Equals(key, StringComparison.InvariantCultureIgnoreCase) && x.LanguageId == langId); + res = query.FirstOrDefault(); + } + + return res; + } + + } } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/SettingsBuilder.cs b/src/Libraries/SmartStore.Data/Setup/Builder/SettingsBuilder.cs index 2a2d087e89..6496fc4d4e 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/SettingsBuilder.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/SettingsBuilder.cs @@ -1,59 +1,57 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; namespace SmartStore.Data.Setup { - internal class SettingEntry - { - public string Key { get; set; } - public string Value { get; set; } - public bool KeyIsGroup { get; set; } - } - - public class SettingsBuilder : IHideObjectMembers - { - private readonly List _entries = new List(); - - /// - /// Deletes one or many setting records from the database - /// - /// The key(s) of the settings to delete - public void Delete(params string[] keys) - { - keys.Each(x => _entries.Add(new SettingEntry { Key = x.TrimSafe() })); - } - - /// - /// Deletes all settings records prefixed with the specified group name from the database - /// - /// The group/prefix (actually the settings class name) - public void DeleteGroup(string group) - { - Guard.NotEmpty(group, nameof(group)); - _entries.Add(new SettingEntry { Key = group.Trim(), KeyIsGroup = true }); - } - - /// - /// Adds a setting if it doesn't exist yet. - /// - public void Add(string key, TValue value) - { - Guard.NotEmpty(key, nameof(key)); - - var valueStr = value.Convert(); - _entries.Add(new SettingEntry { Key = key.Trim(), Value = valueStr }); - } - - internal void Reset() - { - _entries.Clear(); - } - - internal IEnumerable Build() - { - return _entries.Where(x => x.Key.HasValue()); - } - } + internal class SettingEntry + { + public string Key { get; set; } + public string Value { get; set; } + public bool KeyIsGroup { get; set; } + } + + public class SettingsBuilder : IHideObjectMembers + { + private readonly List _entries = new List(); + + /// + /// Deletes one or many setting records from the database + /// + /// The key(s) of the settings to delete + public void Delete(params string[] keys) + { + keys.Each(x => _entries.Add(new SettingEntry { Key = x.TrimSafe() })); + } + + /// + /// Deletes all settings records prefixed with the specified group name from the database + /// + /// The group/prefix (actually the settings class name) + public void DeleteGroup(string group) + { + Guard.NotEmpty(group, nameof(group)); + _entries.Add(new SettingEntry { Key = group.Trim(), KeyIsGroup = true }); + } + + /// + /// Adds a setting if it doesn't exist yet. + /// + public void Add(string key, TValue value) + { + Guard.NotEmpty(key, nameof(key)); + + var valueStr = value.Convert(); + _entries.Add(new SettingEntry { Key = key.Trim(), Value = valueStr }); + } + + internal void Reset() + { + _entries.Clear(); + } + + internal IEnumerable Build() + { + return _entries.Where(x => x.Key.HasValue()); + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/SettingsMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/SettingsMigrator.cs index b3d707666e..16eccfdfd4 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/SettingsMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/SettingsMigrator.cs @@ -2,103 +2,101 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; -using System.Linq.Expressions; -using System.Text; using SmartStore.Core.Data; using SmartStore.Core.Domain.Configuration; namespace SmartStore.Data.Setup { - - internal class SettingsMigrator - { - private readonly SmartObjectContext _ctx; - private readonly DbSet _settings; - - public SettingsMigrator(SmartObjectContext ctx) - { - Guard.NotNull(ctx, nameof(ctx)); - - _ctx = ctx; - _settings = _ctx.Set(); - } - - public void Migrate(IEnumerable entries) - { - Guard.NotNull(entries, nameof(entries)); - - if (!entries.Any()) - return; - - using (var scope = new DbContextScope(_ctx, autoDetectChanges: false)) - { - var toDelete = new List(); - var toAdd = new List(); - - // First perform DELETE actions - foreach (var entry in entries.Where(x => x.Value == null)) - { - bool isPattern = entry.KeyIsGroup; - if (!HasSettings(entry.Key, isPattern)) - continue; // nothing to delete - - var db = GetSettings(entry.Key, isPattern); - _settings.RemoveRange(db); - } - - _ctx.SaveChanges(); - - // Then perform ADD actions - foreach (var entry in entries.Where(x => x.Value.HasValue())) - { - var existing = toAdd.FirstOrDefault(x => x.Name.Equals(entry.Key, StringComparison.InvariantCultureIgnoreCase)); - if (existing != null) - { - existing.Value = entry.Value; - continue; - } - - if (HasSettings(entry.Key, false)) - continue; // skip existing (we don't perform updates) - - _settings.Add(new Setting - { - Name = entry.Key, - Value = entry.Value, - StoreId = 0 - }); - } - - _ctx.SaveChanges(); - } - } - - private bool HasSettings(string key, bool isPattern = false) - { - var query = BuildQuery(key, isPattern); - return query.Any(); - } - - private IList GetSettings(string key, bool isPattern = false) - { - var query = BuildQuery(key, isPattern); - return query.ToList(); - } - - private IQueryable BuildQuery(string key, bool isPattern = false) - { - var query = _settings.AsQueryable(); - if (isPattern) - { - query = query.Where(x => x.Name.StartsWith(key)); - } - else - { - query = query.Where(x => x.Name.Equals(key)); - } - - return query; - } - } + + internal class SettingsMigrator + { + private readonly SmartObjectContext _ctx; + private readonly DbSet _settings; + + public SettingsMigrator(SmartObjectContext ctx) + { + Guard.NotNull(ctx, nameof(ctx)); + + _ctx = ctx; + _settings = _ctx.Set(); + } + + public void Migrate(IEnumerable entries) + { + Guard.NotNull(entries, nameof(entries)); + + if (!entries.Any()) + return; + + using (var scope = new DbContextScope(_ctx, autoDetectChanges: false)) + { + var toDelete = new List(); + var toAdd = new List(); + + // First perform DELETE actions + foreach (var entry in entries.Where(x => x.Value == null)) + { + bool isPattern = entry.KeyIsGroup; + if (!HasSettings(entry.Key, isPattern)) + continue; // nothing to delete + + var db = GetSettings(entry.Key, isPattern); + _settings.RemoveRange(db); + } + + _ctx.SaveChanges(); + + // Then perform ADD actions + foreach (var entry in entries.Where(x => x.Value.HasValue())) + { + var existing = toAdd.FirstOrDefault(x => x.Name.Equals(entry.Key, StringComparison.InvariantCultureIgnoreCase)); + if (existing != null) + { + existing.Value = entry.Value; + continue; + } + + if (HasSettings(entry.Key, false)) + continue; // skip existing (we don't perform updates) + + _settings.Add(new Setting + { + Name = entry.Key, + Value = entry.Value, + StoreId = 0 + }); + } + + _ctx.SaveChanges(); + } + } + + private bool HasSettings(string key, bool isPattern = false) + { + var query = BuildQuery(key, isPattern); + return query.Any(); + } + + private IList GetSettings(string key, bool isPattern = false) + { + var query = BuildQuery(key, isPattern); + return query.ToList(); + } + + private IQueryable BuildQuery(string key, bool isPattern = false) + { + var query = _settings.AsQueryable(); + if (isPattern) + { + query = query.Where(x => x.Name.StartsWith(key)); + } + else + { + query = query.Where(x => x.Name.Equals(key)); + } + + return query; + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/DbMigrationContext.cs b/src/Libraries/SmartStore.Data/Setup/DbMigrationContext.cs index a900f0a4e7..61c6c92d0d 100644 --- a/src/Libraries/SmartStore.Data/Setup/DbMigrationContext.cs +++ b/src/Libraries/SmartStore.Data/Setup/DbMigrationContext.cs @@ -3,45 +3,83 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; -using System.Text; +using SmartStore.Collections; namespace SmartStore.Data.Setup { - public class DbMigrationContext - { - private static DbMigrationContext _dbMigrationContext; - private readonly ConcurrentDictionary _map = new ConcurrentDictionary(); - - private DbMigrationContext() - { - } - - public static DbMigrationContext Current - { - get - { - if (_dbMigrationContext == null) - { - _dbMigrationContext = new DbMigrationContext(); - } - - return _dbMigrationContext; - } - } - - internal void SetSuppressInitialCreate(bool suppress) where TContext : DbContext - { - _map[typeof(TContext)] = suppress; - } - - public bool SuppressInitialCreate() where TContext : DbContext - { - if (_map.TryGetValue(typeof(TContext), out var value)) - { - return value; - } - - return false; - } - } + public class DbMigrationContext + { + private static DbMigrationContext _instance; + private readonly ConcurrentDictionary _map = new ConcurrentDictionary(); + private Multimap _appliedMigrations; + private readonly object _lock = new object(); + + private DbMigrationContext() + { + } + + public static DbMigrationContext Current + { + get + { + if (_instance == null) + { + _instance = new DbMigrationContext(); + } + + return _instance; + } + } + + internal void SetSuppressInitialCreate(bool suppress) where TContext : DbContext + { + _map[typeof(TContext)] = suppress; + } + + public bool SuppressInitialCreate() where TContext : DbContext + { + if (_map.TryGetValue(typeof(TContext), out var value)) + { + return value; + } + + return false; + } + + internal void AddAppliedMigration(Type contextType, string migrationName) + { + if (_appliedMigrations == null) + { + lock (_lock) + { + if (_appliedMigrations == null) + { + _appliedMigrations = new Multimap(); + } + } + } + + _appliedMigrations.Add(contextType, migrationName); + } + + public IEnumerable GetAppliedMigrations() + { + if (_appliedMigrations != null) + { + return _appliedMigrations.SelectMany(x => x.Value); + } + + return Enumerable.Empty(); + } + + public IEnumerable GetAppliedMigrations() where TContext : DbContext + { + if (_appliedMigrations != null && _appliedMigrations.ContainsKey(typeof(TContext))) + { + return _appliedMigrations[typeof(TContext)].AsReadOnly(); + } + + return Enumerable.Empty(); + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/DbMigrationException.cs b/src/Libraries/SmartStore.Data/Setup/DbMigrationException.cs index 64caf77d27..8552957fe7 100644 --- a/src/Libraries/SmartStore.Data/Setup/DbMigrationException.cs +++ b/src/Libraries/SmartStore.Data/Setup/DbMigrationException.cs @@ -1,27 +1,23 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Data.Setup { - - public class DbMigrationException : ApplicationException - { - private const string MSG_DDL = "Migration '{0}' could not be applied due to following error: '{1}'."; - private const string MSG_SEED = "Migration '{0}' could not be seeded due to following error: '{1}'."; - private const string MSG_SUFFIX = " All changes to the database were reversed to '{2}'. Please downgrade your application to a version which is known to be compatible with '{2}'."; - - public DbMigrationException(string initialMigration, string targetMigration, Exception inner, bool isSeed) - : base(((isSeed ? MSG_SEED : MSG_DDL) + MSG_SUFFIX).FormatCurrent(targetMigration, inner != null ? inner.Message : "", initialMigration), inner) - { - this.InitialMigration = initialMigration; - this.TargetMigration = targetMigration; - } - public string InitialMigration { get; private set; } - public string TargetMigration { get; private set; } - } + public class DbMigrationException : ApplicationException + { + private const string MSG_DDL = "Migration '{0}' could not be applied due to following error: '{1}'."; + private const string MSG_SEED = "Migration '{0}' could not be seeded due to following error: '{1}'."; + private const string MSG_SUFFIX = " All changes to the database were reversed to '{2}'. Please downgrade your application to a version which is known to be compatible with '{2}'."; + + public DbMigrationException(string initialMigration, string targetMigration, Exception inner, bool isSeed) + : base(((isSeed ? MSG_SEED : MSG_DDL) + MSG_SUFFIX).FormatCurrent(targetMigration, inner != null ? inner.Message : "", initialMigration), inner) + { + this.InitialMigration = initialMigration; + this.TargetMigration = targetMigration; + } + + public string InitialMigration { get; private set; } + public string TargetMigration { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs b/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs index 9376d3c55c..633c5ff6b1 100644 --- a/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs @@ -4,9 +4,6 @@ using System.Data.Entity.Migrations; using System.Data.Entity.Migrations.Infrastructure; using System.Linq; -using System.Net; -using System.Threading; -using System.Web; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; using SmartStore.Core.Logging; @@ -14,238 +11,234 @@ namespace SmartStore.Data.Setup { - /// - /// Provides advanced migrations by providing a seeding platform for each migration. - /// This allows for initial seed data after each new database version (for example when - /// deploying new features and you want to include initial data). Seeders will be executing - /// in the correct order after all migrations have been completed. - /// - public class DbSeedingMigrator : DbMigrator where TContext : DbContext - { - private ILogger _logger; - private static bool _isMigrating; - private static Exception _lastSeedException; - - /// - /// Initializes a new instance of the DbMigrator class with the default (core db) configuration. - /// - public DbSeedingMigrator() - : this(new MigrationsConfiguration()) - { - } - - /// - /// Initializes a new instance of the DbMigrator class. - /// - /// Configuration to be used for the migration process. - public DbSeedingMigrator(DbMigrationsConfiguration configuration) - : base(configuration) - { - } - - public ILogger Logger - { - get - { - if (_logger == null) - { - try - { - _logger = EngineContext.Current.Resolve().GetLogger(this.GetType()); - } - catch - { - _logger = NullLogger.Instance; - } - } - - return _logger; - } - } - - public static bool IsMigrating - { - get - { - return _isMigrating; - } - } - - /// - /// Migrates the database to the latest version - /// - /// The number of applied migrations - public int RunPendingMigrations(TContext context) - { - if (_lastSeedException != null) - { - // This can happen when a previous migration attempt failed with a rollback. - //return 0; - throw _lastSeedException; - } - - var pendingMigrations = GetPendingMigrations().ToList(); - if (!pendingMigrations.Any()) - return 0; - - // There are pending migrations and the app version is 3.0.0 at least. - // Earlier versions are totally blocked by AppUpdater. - // We have to ensure that the initial migration - which is '201705281903241_MoreIndexes' - // after squashing old 2.x and 1.x migrations - does not run if current head is '201705102339006_V3Final'. - DbMigrationContext.Current.SetSuppressInitialCreate(true); - - var coreSeeders = new List(); - var externalSeeders = new List(); - var isCoreMigration = context is SmartObjectContext; - var databaseMigrations = this.GetDatabaseMigrations().ToArray(); - var initialMigration = databaseMigrations.LastOrDefault() ?? "[Initial]"; - var lastSuccessfulMigration = databaseMigrations.FirstOrDefault(); - int result = 0; - - _isMigrating = true; - - // Apply migrations - foreach (var migrationId in pendingMigrations) - { - if (MigratorUtils.IsAutomaticMigration(migrationId)) - continue; - - if (!MigratorUtils.IsValidMigrationId(migrationId)) - continue; - - // Resolve and instantiate the DbMigration instance from the assembly - var migration = MigratorUtils.CreateMigrationInstanceByMigrationId(migrationId, Configuration); - - // Seeders for the core DbContext must be run in any case - // (e.g. for Resource or Setting updates even from external plugins) - var coreSeeder = migration as IDataSeeder; - IDataSeeder externalSeeder = null; - - if (!isCoreMigration) - { - // Context specific seeders should only be resolved - // when origin is external (e.g. a Plugin) - externalSeeder = migration as IDataSeeder; - } - - try - { - // Call the actual Update() to execute this migration - Update(migrationId); - result++; - } - catch (AutomaticMigrationsDisabledException) - { - if (context is SmartObjectContext) - { - _isMigrating = false; - throw; - } - - // DbContexts in plugin assemblies tend to produce - // this error, but obviously without any negative side-effect. - // Therefore catch and forget! - // TODO: (MC) investigate this and implement a cleaner solution - } - catch (Exception ex) - { - result = 0; - _isMigrating = false; - throw new DbMigrationException(lastSuccessfulMigration, migrationId, ex.InnerException ?? ex, false); - } - - var migrationName = MigratorUtils.GetMigrationClassName(migrationId); - - if (coreSeeder != null) - coreSeeders.Add(new SeederEntry - { - DataSeeder = coreSeeder, - MigrationId = migrationId, - MigrationName = migrationName, - PreviousMigrationId = lastSuccessfulMigration, - }); - - if (externalSeeder != null) - externalSeeders.Add(new SeederEntry - { - DataSeeder = externalSeeder, - MigrationId = migrationId, - MigrationName = migrationName, - PreviousMigrationId = lastSuccessfulMigration, - }); - - lastSuccessfulMigration = migrationId; - } - - if (coreSeeders.Any()) - { - // Apply core data seeders first - var coreContext = isCoreMigration ? context as SmartObjectContext : new SmartObjectContext(); - RunSeeders(coreSeeders, coreContext); - } - - // Apply external data seeders - RunSeeders(externalSeeders, context); - - _isMigrating = false; - - Logger.Info("Database migration successful: {0} >> {1}".FormatInvariant(initialMigration, lastSuccessfulMigration)); - - return result; - } - - private void RunSeeders(IEnumerable seederEntries, T ctx) where T : DbContext - { - foreach (var seederEntry in seederEntries) - { - var seeder = (IDataSeeder)seederEntry.DataSeeder; - - try - { - var eventPublisher = EngineContext.Current.Resolve(); - - // Pre seed event - eventPublisher.Publish(new SeedingDbMigrationEvent { MigrationName = seederEntry.MigrationName, DbContext = ctx }); - - // Seed - seeder.Seed(ctx); - - // Post seed event - eventPublisher.Publish(new SeededDbMigrationEvent { MigrationName = seederEntry.MigrationName, DbContext = ctx }); - } - catch (Exception ex) - { - if (seeder.RollbackOnFailure) - { - _lastSeedException = new DbMigrationException(seederEntry.PreviousMigrationId, seederEntry.MigrationId, ex.InnerException ?? ex, true); - - try - { - Update(seederEntry.PreviousMigrationId); - } catch { } - - _isMigrating = false; - - throw _lastSeedException; - } - - Logger.WarnFormat(ex, "Seed error in migration '{0}'. The error was ignored because no rollback was requested.", seederEntry.MigrationId); - } - } - } - - private void LogError(string initialMigration, string targetMigration, Exception exception) - { - Logger.ErrorFormat(exception, "Database migration error: {0} >> {1}", initialMigration, targetMigration); - } - - private class SeederEntry - { - public string PreviousMigrationId { get; set; } - public string MigrationId { get; set; } - public string MigrationName { get; set; } - public object DataSeeder { get; set; } - } - } + /// + /// Provides advanced migrations by providing a seeding platform for each migration. + /// This allows for initial seed data after each new database version (for example when + /// deploying new features and you want to include initial data). Seeders will be executing + /// in the correct order after all migrations have been completed. + /// + public class DbSeedingMigrator : DbMigrator where TContext : DbContext + { + private ILogger _logger; + private static bool _isMigrating; + private static Exception _lastSeedException; + + /// + /// Initializes a new instance of the DbMigrator class with the default (core db) configuration. + /// + public DbSeedingMigrator() + : this(new MigrationsConfiguration()) + { + } + + /// + /// Initializes a new instance of the DbMigrator class. + /// + /// Configuration to be used for the migration process. + public DbSeedingMigrator(DbMigrationsConfiguration configuration) + : base(configuration) + { + } + + public ILogger Logger + { + get + { + if (_logger == null) + { + try + { + _logger = EngineContext.Current.Resolve().GetLogger(this.GetType()); + } + catch + { + _logger = NullLogger.Instance; + } + } + + return _logger; + } + } + + public static bool IsMigrating => _isMigrating; + + /// + /// Migrates the database to the latest version + /// + /// The number of applied migrations + public int RunPendingMigrations(TContext context) + { + if (_lastSeedException != null) + { + // This can happen when a previous migration attempt failed with a rollback. + //return 0; + throw _lastSeedException; + } + + var pendingMigrations = GetPendingMigrations().ToList(); + if (!pendingMigrations.Any()) + return 0; + + // There are pending migrations and the app version is 3.0.0 at least. + // Earlier versions are totally blocked by AppUpdater. + // We have to ensure that the initial migration - which is '201705281903241_MoreIndexes' + // after squashing old 2.x and 1.x migrations - does not run if current head is '201705102339006_V3Final'. + DbMigrationContext.Current.SetSuppressInitialCreate(true); + + var coreSeeders = new List(); + var externalSeeders = new List(); + var isCoreMigration = context is SmartObjectContext; + var databaseMigrations = this.GetDatabaseMigrations().ToArray(); + var initialMigration = databaseMigrations.LastOrDefault() ?? "[Initial]"; + var lastSuccessfulMigration = databaseMigrations.FirstOrDefault(); + int result = 0; + + _isMigrating = true; + + // Apply migrations + foreach (var migrationId in pendingMigrations) + { + if (MigratorUtils.IsAutomaticMigration(migrationId)) + continue; + + if (!MigratorUtils.IsValidMigrationId(migrationId)) + continue; + + // Resolve and instantiate the DbMigration instance from the assembly + var migration = MigratorUtils.CreateMigrationInstanceByMigrationId(migrationId, Configuration); + + // Seeders for the core DbContext must be run in any case + // (e.g. for Resource or Setting updates even from external plugins) + var coreSeeder = migration as IDataSeeder; + IDataSeeder externalSeeder = null; + + if (!isCoreMigration) + { + // Context specific seeders should only be resolved + // when origin is external (e.g. a Plugin) + externalSeeder = migration as IDataSeeder; + } + + try + { + // Call the actual Update() to execute this migration + Update(migrationId); + result++; + } + catch (AutomaticMigrationsDisabledException) + { + if (context is SmartObjectContext) + { + _isMigrating = false; + throw; + } + + // DbContexts in plugin assemblies tend to produce + // this error, but obviously without any negative side-effect. + // Therefore catch and forget! + // TODO: (MC) investigate this and implement a cleaner solution + } + catch (Exception ex) + { + result = 0; + _isMigrating = false; + throw new DbMigrationException(lastSuccessfulMigration, migrationId, ex.InnerException ?? ex, false); + } + + var migrationName = MigratorUtils.GetMigrationClassName(migrationId); + + if (coreSeeder != null) + coreSeeders.Add(new SeederEntry + { + DataSeeder = coreSeeder, + MigrationId = migrationId, + MigrationName = migrationName, + PreviousMigrationId = lastSuccessfulMigration, + }); + + if (externalSeeder != null) + externalSeeders.Add(new SeederEntry + { + DataSeeder = externalSeeder, + MigrationId = migrationId, + MigrationName = migrationName, + PreviousMigrationId = lastSuccessfulMigration, + }); + + lastSuccessfulMigration = migrationId; + DbMigrationContext.Current.AddAppliedMigration(typeof(TContext), migrationId); + } + + if (coreSeeders.Any()) + { + // Apply core data seeders first + var coreContext = isCoreMigration ? context as SmartObjectContext : new SmartObjectContext(); + RunSeeders(coreSeeders, coreContext); + } + + // Apply external data seeders + RunSeeders(externalSeeders, context); + + _isMigrating = false; + + Logger.Info("Database migration successful: {0} >> {1}".FormatInvariant(initialMigration, lastSuccessfulMigration)); + + return result; + } + + private void RunSeeders(IEnumerable seederEntries, T ctx) where T : DbContext + { + var eventPublisher = EngineContext.Current.Resolve(); + + foreach (var seederEntry in seederEntries) + { + var seeder = (IDataSeeder)seederEntry.DataSeeder; + + try + { + // Pre seed event + eventPublisher.Publish(new SeedingDbMigrationEvent { MigrationName = seederEntry.MigrationName, DbContext = ctx }); + + // Seed + seeder.Seed(ctx); + + // Post seed event + eventPublisher.Publish(new SeededDbMigrationEvent { MigrationName = seederEntry.MigrationName, DbContext = ctx }); + } + catch (Exception ex) + { + if (seeder.RollbackOnFailure) + { + _lastSeedException = new DbMigrationException(seederEntry.PreviousMigrationId, seederEntry.MigrationId, ex.InnerException ?? ex, true); + + try + { + Update(seederEntry.PreviousMigrationId); + } + catch { } + + _isMigrating = false; + + throw _lastSeedException; + } + + Logger.WarnFormat(ex, "Seed error in migration '{0}'. The error was ignored because no rollback was requested.", seederEntry.MigrationId); + } + } + } + + private void LogError(string initialMigration, string targetMigration, Exception exception) + { + Logger.ErrorFormat(exception, "Database migration error: {0} >> {1}", initialMigration, targetMigration); + } + + private class SeederEntry + { + public string PreviousMigrationId { get; set; } + public string MigrationId { get; set; } + public string MigrationName { get; set; } + public object DataSeeder { get; set; } + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/IDataSeeder.cs b/src/Libraries/SmartStore.Data/Setup/IDataSeeder.cs index bb30dd802e..72f5a511a0 100644 --- a/src/Libraries/SmartStore.Data/Setup/IDataSeeder.cs +++ b/src/Libraries/SmartStore.Data/Setup/IDataSeeder.cs @@ -1,21 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; -using System.Linq; +using System.Data.Entity; namespace SmartStore.Data.Setup { - public interface IDataSeeder where TContext : DbContext - { - /// - /// Seeds data - /// - void Seed(TContext context); + public interface IDataSeeder where TContext : DbContext + { + /// + /// Seeds data + /// + void Seed(TContext context); - /// - /// Gets a value indicating whether migration should be completely rolled back - /// when an error occurs during migration seeding. - /// - bool RollbackOnFailure { get; } - } + /// + /// Gets a value indicating whether migration should be completely rolled back + /// when an error occurs during migration seeding. + /// + bool RollbackOnFailure { get; } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs b/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs index c0c842e39c..8513c8352e 100644 --- a/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs +++ b/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs @@ -1,44 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity.Migrations.Infrastructure; +using System.Data.Entity.Migrations.Infrastructure; using System.Data.Entity.Migrations.Model; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Reflection; -using System.Text; using SmartStore.Core.Data; -using SmartStore.Utilities; namespace SmartStore.Data.Setup { - - public static class IDbMigrationExtensions - { + public static class IDbMigrationExtensions + { + public static void SqlFileOrResource(this IDbMigration migration, string fileName, Assembly assembly = null, string location = null) + { + Guard.NotEmpty(fileName, nameof(fileName)); - public static void SqlFileOrResource(this IDbMigration migration, string fileName, Assembly assembly = null, string location = null) - { - Guard.NotEmpty(fileName, nameof(fileName)); - - var tokenizer = new SqlFileTokenizer(fileName, assembly ?? Assembly.GetExecutingAssembly(), location); - foreach (var cmd in tokenizer.Tokenize()) - { - if (cmd.HasValue()) - { - migration.AddSqlOperation(cmd); - } - } - } - - private static void AddSqlOperation(this IDbMigration migration, string sql, bool suppressTransaction = false, object anonymousArguments = null) - { - var operation = new SqlOperation(sql, anonymousArguments) - { - SuppressTransaction = suppressTransaction - }; - migration.AddOperation(operation); - } - - } + var tokenizer = new SqlFileTokenizer(fileName, assembly ?? Assembly.GetExecutingAssembly(), location); + foreach (var cmd in tokenizer.Tokenize()) + { + if (cmd.HasValue()) + { + migration.AddSqlOperation(cmd); + } + } + } + private static void AddSqlOperation(this IDbMigration migration, string sql, bool suppressTransaction = false, object anonymousArguments = null) + { + var operation = new SqlOperation(sql, anonymousArguments) + { + SuppressTransaction = suppressTransaction + }; + migration.AddOperation(operation); + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/ILocaleResourcesProvider.cs b/src/Libraries/SmartStore.Data/Setup/ILocaleResourcesProvider.cs index 95eeb90779..0ad4a5c81c 100644 --- a/src/Libraries/SmartStore.Data/Setup/ILocaleResourcesProvider.cs +++ b/src/Libraries/SmartStore.Data/Setup/ILocaleResourcesProvider.cs @@ -1,11 +1,9 @@ -using System; - -namespace SmartStore.Data.Setup +namespace SmartStore.Data.Setup { - - public interface ILocaleResourcesProvider - { - void MigrateLocaleResources(LocaleResourcesBuilder builder); - } + + public interface ILocaleResourcesProvider + { + void MigrateLocaleResources(LocaleResourcesBuilder builder); + } } diff --git a/src/Libraries/SmartStore.Data/Setup/InstallDatabaseInitializer.cs b/src/Libraries/SmartStore.Data/Setup/InstallDatabaseInitializer.cs index 3a8d7f115a..1e48290630 100644 --- a/src/Libraries/SmartStore.Data/Setup/InstallDatabaseInitializer.cs +++ b/src/Libraries/SmartStore.Data/Setup/InstallDatabaseInitializer.cs @@ -1,44 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; +using System.Data.Entity; using System.Data.Entity.Migrations; using SmartStore.Data.Migrations; namespace SmartStore.Data.Setup { - /// - /// An implementation of that will use Code First Migrations - /// to setup and seed the database. - /// - public class InstallDatabaseInitializer : MigrateDatabaseInitializer - { - public InstallDatabaseInitializer() - : base() - { - } + /// + /// An implementation of that will use Code First Migrations + /// to setup and seed the database. + /// + public class InstallDatabaseInitializer : MigrateDatabaseInitializer + { + public InstallDatabaseInitializer() + : base() + { + } - public InstallDatabaseInitializer(string connectionString) - : base(connectionString) - { - } + public InstallDatabaseInitializer(string connectionString) + : base(connectionString) + { + } - /// - /// Initializes the database. - /// - /// The context. - /// - public override void InitializeDatabase(SmartObjectContext context) - { - // we don't use DbSeedingMigrator here because we don't care - // about Migration seeds during installation. - // The installation seeder contains ALL required seed data already. - var migrator = new DbMigrator(base.CreateConfiguration()); + /// + /// Initializes the database. + /// + /// The context. + /// + public override void InitializeDatabase(SmartObjectContext context) + { + // we don't use DbSeedingMigrator here because we don't care + // about Migration seeds during installation. + // The installation seeder contains ALL required seed data already. + var migrator = new DbMigrator(base.CreateConfiguration()); - // Run all migrations including the initial one - migrator.Update(); + // Run all migrations including the initial one + migrator.Update(); - // seed install data - this.Seed(context); - } - } + // seed install data + this.Seed(context); + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs b/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs index 0bd1830c92..a4d10d520a 100644 --- a/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs +++ b/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs @@ -4,6 +4,7 @@ using System.Data.Entity.Infrastructure; using System.Data.Entity.Migrations; using System.Linq; +using SmartStore.Collections; using SmartStore.Core.Data; using SmartStore.Data.Caching; using SmartStore.Data.Migrations; @@ -11,146 +12,147 @@ namespace SmartStore.Data.Setup { - /// - /// An implementation of that will use Code First Migrations - /// to update the database to the latest version. - /// - public class MigrateDatabaseInitializer : IDatabaseInitializer - where TContext : DbContext, new() - where TConfig : DbMigrationsConfiguration, new() - { - #region Ctor - - public MigrateDatabaseInitializer() - { - } - - public MigrateDatabaseInitializer(string connectionString) - { - this.ConnectionString = connectionString; - } - - #endregion - - #region Properties - - public IEnumerable> DataSeeders - { - get; - set; - } - - public IEnumerable TablesToCheck - { - get; - set; - } - - public string ConnectionString - { - get; - private set; - } - - #endregion - - #region Interface members - - /// - /// Initializes the database. - /// - /// The context. - public virtual void InitializeDatabase(TContext context) - { - if (!context.Database.Exists()) - { - throw Error.InvalidOperation("Database migration failed because the target database does not exist. Ensure the database was initialized and seeded with the 'InstallDatabaseInitializer'."); - } - - var config = CreateConfiguration(); - var migrator = new DbSeedingMigrator(config); - - using (new DbContextScope(context as IDbContext, hooksEnabled: false)) - { - // run all pending migrations - var appliedCount = migrator.RunPendingMigrations(context); - - if (appliedCount > 0) - { - Seed(context); - } - else - { - // DB is up-to-date and no migration ran. - EfMappingViewCacheFactory.SetContext(context); - - if (config is MigrationsConfiguration coreConfig && context is SmartObjectContext ctx) - { - // Call the main Seed method anyway (on every startup), - // we could have locale resources or settings to add/update. - coreConfig.SeedDatabase(ctx); - } - } - - // not needed anymore - this.DataSeeders = null; - } - } - - #endregion - - #region Methods - - /// - /// Seeds the specified context. - /// - /// The context. - protected virtual void Seed(TContext context) - { - if (this.DataSeeders == null) - return; - - this.DataSeeders.Each((x) => x.Seed(context)); - } - - protected virtual TConfig CreateConfiguration() - { - var config = new TConfig(); - if (this.ConnectionString.HasValue()) - { - var dbContextInfo = new DbContextInfo(typeof(TContext)); - config.TargetDatabase = new DbConnectionInfo(this.ConnectionString, dbContextInfo.ConnectionProviderName); - } - - if (config.CommandTimeout == null && DataSettings.Current.IsSqlServer) - { - var commandTimeout = CommonHelper.GetAppSetting("sm:EfMigrationsCommandTimeout"); - if (commandTimeout.HasValue) - { - config.CommandTimeout = commandTimeout.Value; - } - } - - return config; - } - - /// - /// Checks tables existence - /// - /// - /// Returns true if the tables to check exist in the database or the check list is empty. - /// - protected bool CheckTables(TContext context) - { - if (this.TablesToCheck == null || !this.TablesToCheck.Any()) - return true; - - var existingTableNames = new List(context.Database.SqlQuery("SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_type = 'BASE TABLE'")); - var result = existingTableNames.Intersect(this.TablesToCheck, StringComparer.InvariantCultureIgnoreCase).Count() == 0; - return !result; - } - - #endregion - } + /// + /// An implementation of that will use Code First Migrations + /// to update the database to the latest version. + /// + public class MigrateDatabaseInitializer : IDatabaseInitializer + where TContext : DbContext, new() + where TConfig : DbMigrationsConfiguration, new() + { + private static readonly SyncedCollection _initializedContextTypes = new List().AsSynchronized(); + + public MigrateDatabaseInitializer() + { + } + + public MigrateDatabaseInitializer(string connectionString) + { + this.ConnectionString = connectionString; + } + + public IEnumerable> DataSeeders + { + get; + set; + } + + public IEnumerable TablesToCheck + { + get; + set; + } + + public string ConnectionString + { + get; + private set; + } + + #region Interface members + + /// + /// Initializes the database. + /// + /// The context. + public virtual void InitializeDatabase(TContext context) + { + if (_initializedContextTypes.Contains(context.GetType())) + { + return; + } + + if (!context.Database.Exists()) + { + throw Error.InvalidOperation("Database migration failed because the target database does not exist. Ensure the database was initialized and seeded with the 'InstallDatabaseInitializer'."); + } + + var config = CreateConfiguration(); + var migrator = new DbSeedingMigrator(config); + + using (new DbContextScope(context as IDbContext, hooksEnabled: false)) + { + // run all pending migrations + var appliedCount = migrator.RunPendingMigrations(context); + + if (appliedCount > 0) + { + Seed(context); + } + else + { + // DB is up-to-date and no migration ran. + EfMappingViewCacheFactory.SetContext(context); + + if (config is MigrationsConfiguration coreConfig && context is SmartObjectContext ctx) + { + // Call the main Seed method anyway (on every startup), + // we could have locale resources or settings to add/update. + coreConfig.SeedDatabase(ctx); + } + } + + // not needed anymore + this.DataSeeders = null; + + _initializedContextTypes.Add(context.GetType()); + } + } + + #endregion + + #region Methods + + /// + /// Seeds the specified context. + /// + /// The context. + protected virtual void Seed(TContext context) + { + if (this.DataSeeders == null) + return; + + this.DataSeeders.Each((x) => x.Seed(context)); + } + + protected virtual TConfig CreateConfiguration() + { + var config = new TConfig(); + if (this.ConnectionString.HasValue()) + { + var dbContextInfo = new DbContextInfo(typeof(TContext)); + config.TargetDatabase = new DbConnectionInfo(this.ConnectionString, dbContextInfo.ConnectionProviderName); + } + + if (config.CommandTimeout == null && DataSettings.Current.IsSqlServer) + { + var commandTimeout = CommonHelper.GetAppSetting("sm:EfMigrationsCommandTimeout"); + if (commandTimeout.HasValue) + { + config.CommandTimeout = commandTimeout.Value; + } + } + + return config; + } + + /// + /// Checks tables existence + /// + /// + /// Returns true if the tables to check exist in the database or the check list is empty. + /// + protected bool CheckTables(TContext context) + { + if (this.TablesToCheck == null || !this.TablesToCheck.Any()) + return true; + + var existingTableNames = new List(context.Database.SqlQuery("SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_type = 'BASE TABLE'")); + var result = existingTableNames.Intersect(this.TablesToCheck, StringComparer.InvariantCultureIgnoreCase).Count() == 0; + return !result; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Data/Setup/MigratorUtils.cs b/src/Libraries/SmartStore.Data/Setup/MigratorUtils.cs index a89c9fcb13..4ed6df0496 100644 --- a/src/Libraries/SmartStore.Data/Setup/MigratorUtils.cs +++ b/src/Libraries/SmartStore.Data/Setup/MigratorUtils.cs @@ -3,172 +3,170 @@ using System.Data.Entity.Migrations; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using SmartStore.Data.Migrations; namespace SmartStore.Data.Setup { - internal static class MigratorUtils - { - private static readonly Regex _migrationIdPattern = new Regex(@"\d{15}_.+"); - private const string _migrationTypeFormat = "{0}.{1}, {2}"; - private const string _automaticMigration = "AutomaticMigration"; - - /// - /// Creates a full type instance for the migration id by using the current migrations namespace - /// ie: SmartStore.Data.Migrations.34589734533_Initial - /// - /// The migrator context - /// The migration id from the migrations list of the migrator - /// The full DbMigration instance - public static DbMigration CreateMigrationInstanceByMigrationId(string migrationId, DbMigrationsConfiguration config) - { - string migrationTypeName = - string.Format(_migrationTypeFormat, - config.MigrationsNamespace, - GetMigrationClassName(migrationId), - config.MigrationsAssembly.FullName); - - return CreateTypeInstance(migrationTypeName); - } - - /// - /// Checks if the migration id is valid - /// - /// The migration id from the migrations list of the migrator - /// true if valid, otherwise false - /// - /// This snippet has been copied from the EntityFramework source (http://entityframework.codeplex.com/) - /// - public static bool IsValidMigrationId(string migrationId) - { - if (string.IsNullOrWhiteSpace(migrationId)) - return false; - - return _migrationIdPattern.IsMatch(migrationId) || migrationId == DbMigrator.InitialDatabase; - } - - /// - /// Checks if the the migration id belongs to an automatic migration - /// - /// The migration id from the migrations list of the migrator - /// true if automatic, otherwise false - /// - /// This snippet has been copied from the EntityFramework source (http://entityframework.codeplex.com/) - /// - public static bool IsAutomaticMigration(string migrationId) - { - if (string.IsNullOrWhiteSpace(migrationId)) - return false; - - return migrationId.EndsWith(_automaticMigration, StringComparison.Ordinal); - } - - /// - /// Gets the ClassName from a migration id - /// - /// The migration id from the migrations list of the migrator - /// The class name for this migration id - /// - /// This snippet has been copied from the EntityFramework source (http://entityframework.codeplex.com/) - /// - public static string GetMigrationClassName(string migrationId) - { - if (string.IsNullOrWhiteSpace(migrationId)) - return string.Empty; - - return migrationId.Substring(16); - } - - /// - /// Creates a new instance of a typename - /// - /// The type of the return instance - /// The full name (including assembly and namespaces) of the type to create - /// - /// A new instance of the type if it is (or boxable to) , - /// otherwise the default of - /// - private static TType CreateTypeInstance(string typeName) where TType : class - { - Type classType = Type.GetType(typeName, false); - - if (classType == null) - return default(TType); - - object newType = Activator.CreateInstance(classType); - - return newType as TType; - } - - - public static void ExecutePendingResourceMigrations(string resPath, SmartObjectContext dbContext) - { - Guard.NotNull(dbContext, nameof(dbContext)); - - string headPath = Path.Combine(resPath, "head.txt"); - if (!File.Exists(headPath)) - return; - - string resHead = File.ReadAllText(headPath).Trim(); - if (!MigratorUtils.IsValidMigrationId(resHead)) - return; - - var migrator = new DbMigrator(new MigrationsConfiguration()); - var migrations = GetPendingResourceMigrations(migrator, resHead); - - foreach (var id in migrations) - { - if (IsAutomaticMigration(id)) - continue; - - if (!IsValidMigrationId(id)) - continue; - - // Resolve and instantiate the DbMigration instance from the assembly - var migration = CreateMigrationInstanceByMigrationId(id, migrator.Configuration); - - var provider = migration as ILocaleResourcesProvider; - if (provider == null) - continue; - - var builder = new LocaleResourcesBuilder(); - provider.MigrateLocaleResources(builder); - - var resEntries = builder.Build(); - var resMigrator = new LocaleResourcesMigrator(dbContext); - resMigrator.Migrate(resEntries); - } - } - - private static IEnumerable GetPendingResourceMigrations(DbMigrator migrator, string resHead) - { - var local = migrator.GetLocalMigrations(); - var atHead = false; - - if (local.Last().IsCaseInsensitiveEqual(resHead)) - yield break; - - foreach (var id in local) - { - if (!atHead) - { - if (!id.IsCaseInsensitiveEqual(resHead)) - { - continue; - } - else - { - atHead = true; - continue; - } - } - - yield return id; - } - } - - } + internal static class MigratorUtils + { + private static readonly Regex _migrationIdPattern = new Regex(@"\d{15}_.+"); + private const string _migrationTypeFormat = "{0}.{1}, {2}"; + private const string _automaticMigration = "AutomaticMigration"; + + /// + /// Creates a full type instance for the migration id by using the current migrations namespace + /// ie: SmartStore.Data.Migrations.34589734533_Initial + /// + /// The migrator context + /// The migration id from the migrations list of the migrator + /// The full DbMigration instance + public static DbMigration CreateMigrationInstanceByMigrationId(string migrationId, DbMigrationsConfiguration config) + { + string migrationTypeName = + string.Format(_migrationTypeFormat, + config.MigrationsNamespace, + GetMigrationClassName(migrationId), + config.MigrationsAssembly.FullName); + + return CreateTypeInstance(migrationTypeName); + } + + /// + /// Checks if the migration id is valid + /// + /// The migration id from the migrations list of the migrator + /// true if valid, otherwise false + /// + /// This snippet has been copied from the EntityFramework source (http://entityframework.codeplex.com/) + /// + public static bool IsValidMigrationId(string migrationId) + { + if (string.IsNullOrWhiteSpace(migrationId)) + return false; + + return _migrationIdPattern.IsMatch(migrationId) || migrationId == DbMigrator.InitialDatabase; + } + + /// + /// Checks if the the migration id belongs to an automatic migration + /// + /// The migration id from the migrations list of the migrator + /// true if automatic, otherwise false + /// + /// This snippet has been copied from the EntityFramework source (http://entityframework.codeplex.com/) + /// + public static bool IsAutomaticMigration(string migrationId) + { + if (string.IsNullOrWhiteSpace(migrationId)) + return false; + + return migrationId.EndsWith(_automaticMigration, StringComparison.Ordinal); + } + + /// + /// Gets the ClassName from a migration id + /// + /// The migration id from the migrations list of the migrator + /// The class name for this migration id + /// + /// This snippet has been copied from the EntityFramework source (http://entityframework.codeplex.com/) + /// + public static string GetMigrationClassName(string migrationId) + { + if (string.IsNullOrWhiteSpace(migrationId)) + return string.Empty; + + return migrationId.Substring(16); + } + + /// + /// Creates a new instance of a typename + /// + /// The type of the return instance + /// The full name (including assembly and namespaces) of the type to create + /// + /// A new instance of the type if it is (or boxable to) , + /// otherwise the default of + /// + private static TType CreateTypeInstance(string typeName) where TType : class + { + Type classType = Type.GetType(typeName, false); + + if (classType == null) + return default(TType); + + object newType = Activator.CreateInstance(classType); + + return newType as TType; + } + + + public static void ExecutePendingResourceMigrations(string resPath, SmartObjectContext dbContext) + { + Guard.NotNull(dbContext, nameof(dbContext)); + + string headPath = Path.Combine(resPath, "head.txt"); + if (!File.Exists(headPath)) + return; + + string resHead = File.ReadAllText(headPath).Trim(); + if (!MigratorUtils.IsValidMigrationId(resHead)) + return; + + var migrator = new DbMigrator(new MigrationsConfiguration()); + var migrations = GetPendingResourceMigrations(migrator, resHead); + + foreach (var id in migrations) + { + if (IsAutomaticMigration(id)) + continue; + + if (!IsValidMigrationId(id)) + continue; + + // Resolve and instantiate the DbMigration instance from the assembly + var migration = CreateMigrationInstanceByMigrationId(id, migrator.Configuration); + + var provider = migration as ILocaleResourcesProvider; + if (provider == null) + continue; + + var builder = new LocaleResourcesBuilder(); + provider.MigrateLocaleResources(builder); + + var resEntries = builder.Build(); + var resMigrator = new LocaleResourcesMigrator(dbContext); + resMigrator.Migrate(resEntries); + } + } + + private static IEnumerable GetPendingResourceMigrations(DbMigrator migrator, string resHead) + { + var local = migrator.GetLocalMigrations(); + var atHead = false; + + if (local.Last().IsCaseInsensitiveEqual(resHead)) + yield break; + + foreach (var id in local) + { + if (!atHead) + { + if (!id.IsCaseInsensitiveEqual(resHead)) + { + continue; + } + else + { + atHead = true; + continue; + } + } + + yield return id; + } + } + + } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.ActivityLogTypes.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.ActivityLogTypes.cs index 5959f0810a..44ae838d99 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.ActivityLogTypes.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.ActivityLogTypes.cs @@ -1,396 +1,401 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Domain.Logging; namespace SmartStore.Data.Setup { public abstract partial class InvariantSeedData { - public IList ActivityLogTypes() - { - var entities = new List() - { + public IList ActivityLogTypes() + { + var entities = new List() + { //admin area activities new ActivityLogType - { - SystemKeyword = "AddNewCategory", - Enabled = true, - Name = "Add a new category" - }, - new ActivityLogType - { - SystemKeyword = "AddNewCheckoutAttribute", - Enabled = true, - Name = "Add a new checkout attribute" - }, - new ActivityLogType - { - SystemKeyword = "AddNewCustomer", - Enabled = true, - Name = "Add a new customer" - }, - new ActivityLogType - { - SystemKeyword = "AddNewCustomerRole", - Enabled = true, - Name = "Add a new customer role" - }, - new ActivityLogType - { - SystemKeyword = "AddNewDiscount", - Enabled = true, - Name = "Add a new discount" - }, - new ActivityLogType - { - SystemKeyword = "AddNewGiftCard", - Enabled = true, - Name = "Add a new gift card" - }, - new ActivityLogType - { - SystemKeyword = "AddNewManufacturer", - Enabled = true, - Name = "Add a new manufacturer" - }, - new ActivityLogType - { - SystemKeyword = "AddNewProduct", - Enabled = true, - Name = "Add a new product" - }, - new ActivityLogType - { - SystemKeyword = "AddNewProductAttribute", - Enabled = true, - Name = "Add a new product attribute" - }, - new ActivityLogType - { - SystemKeyword = "AddNewSetting", - Enabled = true, - Name = "Add a new setting" - }, - new ActivityLogType - { - SystemKeyword = "AddNewSpecAttribute", - Enabled = true, - Name = "Add a new specification attribute" - }, - new ActivityLogType - { - SystemKeyword = "AddNewWidget", - Enabled = true, - Name = "Add a new widget" - }, - new ActivityLogType - { - SystemKeyword = "DeleteCategory", - Enabled = true, - Name = "Delete category" - }, - new ActivityLogType - { - SystemKeyword = "DeleteCheckoutAttribute", - Enabled = true, - Name = "Delete a checkout attribute" - }, - new ActivityLogType - { - SystemKeyword = "DeleteCustomer", - Enabled = true, - Name = "Delete a customer" - }, - new ActivityLogType - { - SystemKeyword = "DeleteCustomerRole", - Enabled = true, - Name = "Delete a customer role" - }, - new ActivityLogType - { - SystemKeyword = "DeleteDiscount", - Enabled = true, - Name = "Delete a discount" - }, - new ActivityLogType - { - SystemKeyword = "DeleteGiftCard", - Enabled = true, - Name = "Delete a gift card" - }, - new ActivityLogType - { - SystemKeyword = "DeleteManufacturer", - Enabled = true, - Name = "Delete a manufacturer" - }, - new ActivityLogType - { - SystemKeyword = "DeleteProduct", - Enabled = true, - Name = "Delete a product" - }, - new ActivityLogType - { - SystemKeyword = "DeleteProductAttribute", - Enabled = true, - Name = "Delete a product attribute" - }, - new ActivityLogType - { - SystemKeyword = "DeleteReturnRequest", - Enabled = true, - Name = "Delete a return request" - }, - new ActivityLogType - { - SystemKeyword = "DeleteSetting", - Enabled = true, - Name = "Delete a setting" - }, - new ActivityLogType - { - SystemKeyword = "DeleteSpecAttribute", - Enabled = true, - Name = "Delete a specification attribute" - }, - new ActivityLogType - { - SystemKeyword = "DeleteWidget", - Enabled = true, - Name = "Delete a widget" - }, - new ActivityLogType - { - SystemKeyword = "EditCategory", - Enabled = true, - Name = "Edit category" - }, - new ActivityLogType - { - SystemKeyword = "EditCheckoutAttribute", - Enabled = true, - Name = "Edit a checkout attribute" - }, - new ActivityLogType - { - SystemKeyword = "EditCustomer", - Enabled = true, - Name = "Edit a customer" - }, - new ActivityLogType - { - SystemKeyword = "EditCustomerRole", - Enabled = true, - Name = "Edit a customer role" - }, - new ActivityLogType - { - SystemKeyword = "EditDiscount", - Enabled = true, - Name = "Edit a discount" - }, - new ActivityLogType - { - SystemKeyword = "EditGiftCard", - Enabled = true, - Name = "Edit a gift card" - }, - new ActivityLogType - { - SystemKeyword = "EditManufacturer", - Enabled = true, - Name = "Edit a manufacturer" - }, - new ActivityLogType - { - SystemKeyword = "EditProduct", - Enabled = true, - Name = "Edit a product" - }, - new ActivityLogType - { - SystemKeyword = "EditProductAttribute", - Enabled = true, - Name = "Edit a product attribute" - }, - new ActivityLogType - { - SystemKeyword = "EditPromotionProviders", - Enabled = true, - Name = "Edit promotion providers" - }, - new ActivityLogType - { - SystemKeyword = "EditReturnRequest", - Enabled = true, - Name = "Edit a return request" - }, - new ActivityLogType - { - SystemKeyword = "EditSettings", - Enabled = true, - Name = "Edit setting(s)" - }, - new ActivityLogType - { - SystemKeyword = "EditSpecAttribute", - Enabled = true, - Name = "Edit a specification attribute" - }, - new ActivityLogType - { - SystemKeyword = "EditWidget", - Enabled = true, - Name = "Edit a widget" - }, - new ActivityLogType - { - SystemKeyword = "EditThemeVars", - Enabled = true, - Name = "Edit theme variables" - }, - new ActivityLogType - { - SystemKeyword = "ResetThemeVars", - Enabled = true, - Name = "Reset theme variables to defaults" - }, - new ActivityLogType - { - SystemKeyword = "ImportThemeVars", - Enabled = true, - Name = "Import theme variables" - }, - new ActivityLogType - { - SystemKeyword = "ExportThemeVars", - Enabled = true, - Name = "Export theme variables" - }, + { + SystemKeyword = "AddNewCategory", + Enabled = true, + Name = "Add a new category" + }, + new ActivityLogType + { + SystemKeyword = "AddNewCheckoutAttribute", + Enabled = true, + Name = "Add a new checkout attribute" + }, + new ActivityLogType + { + SystemKeyword = "AddNewCustomer", + Enabled = true, + Name = "Add a new customer" + }, + new ActivityLogType + { + SystemKeyword = "AddNewCustomerRole", + Enabled = true, + Name = "Add a new customer role" + }, + new ActivityLogType + { + SystemKeyword = "AddNewDiscount", + Enabled = true, + Name = "Add a new discount" + }, + new ActivityLogType + { + SystemKeyword = "AddNewGiftCard", + Enabled = true, + Name = "Add a new gift card" + }, + new ActivityLogType + { + SystemKeyword = "AddNewManufacturer", + Enabled = true, + Name = "Add a new manufacturer" + }, + new ActivityLogType + { + SystemKeyword = "AddNewProduct", + Enabled = true, + Name = "Add a new product" + }, + new ActivityLogType + { + SystemKeyword = "AddNewProductAttribute", + Enabled = true, + Name = "Add a new product attribute" + }, + new ActivityLogType + { + SystemKeyword = "AddNewSetting", + Enabled = true, + Name = "Add a new setting" + }, + new ActivityLogType + { + SystemKeyword = "AddNewSpecAttribute", + Enabled = true, + Name = "Add a new specification attribute" + }, + new ActivityLogType + { + SystemKeyword = "AddNewWidget", + Enabled = true, + Name = "Add a new widget" + }, + new ActivityLogType + { + SystemKeyword = "DeleteCategory", + Enabled = true, + Name = "Delete category" + }, + new ActivityLogType + { + SystemKeyword = "DeleteCheckoutAttribute", + Enabled = true, + Name = "Delete a checkout attribute" + }, + new ActivityLogType + { + SystemKeyword = "DeleteCustomer", + Enabled = true, + Name = "Delete a customer" + }, + new ActivityLogType + { + SystemKeyword = "DeleteCustomerRole", + Enabled = true, + Name = "Delete a customer role" + }, + new ActivityLogType + { + SystemKeyword = "DeleteDiscount", + Enabled = true, + Name = "Delete a discount" + }, + new ActivityLogType + { + SystemKeyword = "DeleteGiftCard", + Enabled = true, + Name = "Delete a gift card" + }, + new ActivityLogType + { + SystemKeyword = "DeleteManufacturer", + Enabled = true, + Name = "Delete a manufacturer" + }, + new ActivityLogType + { + SystemKeyword = "DeleteProduct", + Enabled = true, + Name = "Delete a product" + }, + new ActivityLogType + { + SystemKeyword = "DeleteProductAttribute", + Enabled = true, + Name = "Delete a product attribute" + }, + new ActivityLogType + { + SystemKeyword = "DeleteReturnRequest", + Enabled = true, + Name = "Delete a return request" + }, + new ActivityLogType + { + SystemKeyword = "DeleteSetting", + Enabled = true, + Name = "Delete a setting" + }, + new ActivityLogType + { + SystemKeyword = "DeleteSpecAttribute", + Enabled = true, + Name = "Delete a specification attribute" + }, + new ActivityLogType + { + SystemKeyword = "DeleteWidget", + Enabled = true, + Name = "Delete a widget" + }, + new ActivityLogType + { + SystemKeyword = "EditCategory", + Enabled = true, + Name = "Edit category" + }, + new ActivityLogType + { + SystemKeyword = "EditCheckoutAttribute", + Enabled = true, + Name = "Edit a checkout attribute" + }, + new ActivityLogType + { + SystemKeyword = "EditCustomer", + Enabled = true, + Name = "Edit a customer" + }, + new ActivityLogType + { + SystemKeyword = "EditCustomerRole", + Enabled = true, + Name = "Edit a customer role" + }, + new ActivityLogType + { + SystemKeyword = "EditDiscount", + Enabled = true, + Name = "Edit a discount" + }, + new ActivityLogType + { + SystemKeyword = "EditGiftCard", + Enabled = true, + Name = "Edit a gift card" + }, + new ActivityLogType + { + SystemKeyword = "EditManufacturer", + Enabled = true, + Name = "Edit a manufacturer" + }, + new ActivityLogType + { + SystemKeyword = "EditProduct", + Enabled = true, + Name = "Edit a product" + }, + new ActivityLogType + { + SystemKeyword = "EditProductAttribute", + Enabled = true, + Name = "Edit a product attribute" + }, + new ActivityLogType + { + SystemKeyword = "EditPromotionProviders", + Enabled = true, + Name = "Edit promotion providers" + }, + new ActivityLogType + { + SystemKeyword = "EditReturnRequest", + Enabled = true, + Name = "Edit a return request" + }, + new ActivityLogType + { + SystemKeyword = "EditSettings", + Enabled = true, + Name = "Edit setting(s)" + }, + new ActivityLogType + { + SystemKeyword = "EditSpecAttribute", + Enabled = true, + Name = "Edit a specification attribute" + }, + new ActivityLogType + { + SystemKeyword = "EditWidget", + Enabled = true, + Name = "Edit a widget" + }, + new ActivityLogType + { + SystemKeyword = "EditThemeVars", + Enabled = true, + Name = "Edit theme variables" + }, + new ActivityLogType + { + SystemKeyword = "EditOrder", + Enabled = true, + Name = "Edit an order" + }, + new ActivityLogType + { + SystemKeyword = "ResetThemeVars", + Enabled = true, + Name = "Reset theme variables to defaults" + }, + new ActivityLogType + { + SystemKeyword = "ImportThemeVars", + Enabled = true, + Name = "Import theme variables" + }, + new ActivityLogType + { + SystemKeyword = "ExportThemeVars", + Enabled = true, + Name = "Export theme variables" + }, //public store activities new ActivityLogType - { - SystemKeyword = "PublicStore.ViewCategory", - Enabled = false, - Name = "Public store. View a category" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.ViewManufacturer", - Enabled = false, - Name = "Public store. View a manufacturer" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.ViewProduct", - Enabled = false, - Name = "Public store. View a product" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.PlaceOrder", - Enabled = false, - Name = "Public store. Place an order" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.SendPM", - Enabled = false, - Name = "Public store. Send PM" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.ContactUs", - Enabled = false, - Name = "Public store. Use contact us form" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddToCompareList", - Enabled = false, - Name = "Public store. Add to compare list" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddToShoppingCart", - Enabled = false, - Name = "Public store. Add to shopping cart" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddToWishlist", - Enabled = false, - Name = "Public store. Add to wishlist" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.Login", - Enabled = false, - Name = "Public store. Login" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.Logout", - Enabled = false, - Name = "Public store. Logout" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddProductReview", - Enabled = false, - Name = "Public store. Add product review" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddNewsComment", - Enabled = false, - Name = "Public store. Add news comment" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddBlogComment", - Enabled = false, - Name = "Public store. Add blog comment" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddForumTopic", - Enabled = false, - Name = "Public store. Add forum topic" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.EditForumTopic", - Enabled = false, - Name = "Public store. Edit forum topic" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.DeleteForumTopic", - Enabled = false, - Name = "Public store. Delete forum topic" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.AddForumPost", - Enabled = false, - Name = "Public store. Add forum post" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.EditForumPost", - Enabled = false, - Name = "Public store. Edit forum post" - }, - new ActivityLogType - { - SystemKeyword = "PublicStore.DeleteForumPost", - Enabled = false, - Name = "Public store. Delete forum post" - }, + { + SystemKeyword = "PublicStore.ViewCategory", + Enabled = false, + Name = "Public store. View a category" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.ViewManufacturer", + Enabled = false, + Name = "Public store. View a manufacturer" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.ViewProduct", + Enabled = false, + Name = "Public store. View a product" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.PlaceOrder", + Enabled = false, + Name = "Public store. Place an order" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.SendPM", + Enabled = false, + Name = "Public store. Send PM" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.ContactUs", + Enabled = false, + Name = "Public store. Use contact us form" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddToCompareList", + Enabled = false, + Name = "Public store. Add to compare list" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddToShoppingCart", + Enabled = false, + Name = "Public store. Add to shopping cart" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddToWishlist", + Enabled = false, + Name = "Public store. Add to wishlist" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.Login", + Enabled = false, + Name = "Public store. Login" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.Logout", + Enabled = false, + Name = "Public store. Logout" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddProductReview", + Enabled = false, + Name = "Public store. Add product review" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddNewsComment", + Enabled = false, + Name = "Public store. Add news comment" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddBlogComment", + Enabled = false, + Name = "Public store. Add blog comment" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddForumTopic", + Enabled = false, + Name = "Public store. Add forum topic" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.EditForumTopic", + Enabled = false, + Name = "Public store. Edit forum topic" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.DeleteForumTopic", + Enabled = false, + Name = "Public store. Delete forum topic" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.AddForumPost", + Enabled = false, + Name = "Public store. Add forum post" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.EditForumPost", + Enabled = false, + Name = "Public store. Edit forum post" + }, + new ActivityLogType + { + SystemKeyword = "PublicStore.DeleteForumPost", + Enabled = false, + Name = "Public store. Delete forum post" + }, //new ActivityLogType // { // SystemKeyword = "EditThemeVars", @@ -405,8 +410,8 @@ public IList ActivityLogTypes() // }, }; - this.Alter(entities); - return entities; - } - } + this.Alter(entities); + return entities; + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Categories.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Categories.cs index 42cd808c3a..1b27139014 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Categories.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Categories.cs @@ -1,7 +1,9 @@ -using System; +using System.Collections.Generic; using System.Linq; -using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Rules; +using SmartStore.Rules.Domain; namespace SmartStore.Data.Setup { @@ -9,16 +11,14 @@ public abstract partial class InvariantSeedData { public IList CategoriesFirstLevel() { - var sampleImagesPath = this._sampleImagesPath; - var categoryTemplateInGridAndLines = this.CategoryTemplates().Where(pt => pt.ViewPath == "CategoryTemplate.ProductsInGridOrLines").FirstOrDefault(); - - #region category definitions + var imagesPath = _sampleImagesPath; + var gridOrLinesTemplate = CategoryTemplates().Where(pt => pt.ViewPath == "CategoryTemplate.ProductsInGridOrLines").FirstOrDefault(); var categoryFurniture = new Category { Name = "Furniture", Alias = "Furniture", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/furniture.jpg"), Published = true, DisplayOrder = 1, @@ -30,7 +30,7 @@ public IList CategoriesFirstLevel() { Name = "Apple", Alias = "Apple", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/apple.png"), Published = true, DisplayOrder = 1, @@ -42,7 +42,7 @@ public IList CategoriesFirstLevel() { Name = "Sports", Alias = "Sports", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/sports.jpg"), Published = true, DisplayOrder = 1, @@ -54,35 +54,24 @@ public IList CategoriesFirstLevel() { Name = "Books", Alias = "Books", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/emblem_library.png", GetSeName("Books")), Published = true, DisplayOrder = 1, MetaTitle = "Books" }; - //var categoryComputers = new Category - //{ - // Name = "Computers", - // Alias = "Computers", - // CategoryTemplateId = categoryTemplateInGridAndLines.Id, - // Picture = CreatePicture("category/computers.png"), - // Published = true, - // DisplayOrder = 2, - // MetaTitle = "Computers" - //}; - var categoryFashion = new Category { Name = "Fashion", Alias = "Fashion", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/fashion.jpg"), Published = true, DisplayOrder = 2, MetaTitle = "Fashion", ShowOnHomePage = true, - BadgeText = "SALE", + BadgeText = "NEW", BadgeStyle = 4 }; @@ -90,7 +79,7 @@ public IList CategoriesFirstLevel() { Name = "Gaming", Alias = "Gaming", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("product/ps4_bundle_minecraft.jpg", GetSeName("Gaming")), Published = true, DisplayOrder = 3, @@ -98,23 +87,11 @@ public IList CategoriesFirstLevel() MetaTitle = "Gaming" }; - //var categoryCellPhones = new Category - //{ - // Name = "Cell phones", - // Alias = "Cell phones", - // CategoryTemplateId = categoryTemplateInGridAndLines.Id, - // //ParentCategoryId = categoryElectronics.Id, - // Picture = CreatePicture("category/cellphone.png"), - // Published = true, - // DisplayOrder = 4, - // MetaTitle = "Cell phones" - //}; - var categoryDigitalDownloads = new Category { Name = "Digital Products", Alias = "Digital Products", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/digitalproducts.jpg"), Published = true, DisplayOrder = 6, @@ -126,7 +103,7 @@ public IList CategoriesFirstLevel() { Name = "Gift Cards", Alias = "Gift Cards", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/gift-cards.png"), Published = true, DisplayOrder = 12, @@ -138,140 +115,151 @@ public IList CategoriesFirstLevel() { Name = "Watches", Alias = "Watches", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/watches.png"), Published = true, DisplayOrder = 10, MetaTitle = "Watches", - ShowOnHomePage = true, + ShowOnHomePage = true + }; + + var categorySale = new Category + { + Name = "Sale", + Alias = "Sale", + MetaTitle = "Sale", + CategoryTemplateId = gridOrLinesTemplate.Id, + Published = true, + DisplayOrder = 99, + ShowOnHomePage = false, BadgeText = "%", BadgeStyle = 5 }; - #endregion + var saleRuleSet = _ctx.Set().FirstOrDefault(x => x.Scope == RuleScope.Product && x.Name == "Sale"); + if (saleRuleSet != null) + { + categorySale.RuleSets.Add(saleRuleSet); + } + + //var categoryComputers = new Category + //{ + // Name = "Computers", + // Alias = "Computers", + // CategoryTemplateId = categoryTemplateInGridAndLines.Id, + // Picture = CreatePicture("category/computers.png"), + // Published = true, + // DisplayOrder = 2, + // MetaTitle = "Computers" + //}; + + //var categoryCellPhones = new Category + //{ + // Name = "Cell phones", + // Alias = "Cell phones", + // CategoryTemplateId = categoryTemplateInGridAndLines.Id, + // //ParentCategoryId = categoryElectronics.Id, + // Picture = CreatePicture("category/cellphone.png"), + // Published = true, + // DisplayOrder = 4, + // MetaTitle = "Cell phones" + //}; var entities = new List { - categoryApple, categorySports, categoryBooks, categoryFurniture, categoryDigitalDownloads, categoryGaming, - categoryGiftCards, categoryFashion, categoryWatches + categoryApple, categorySports, categoryBooks, categoryFurniture, categoryDigitalDownloads, + categoryGaming, categoryGiftCards, categoryFashion, categoryWatches, categorySale }; - this.Alter(entities); + Alter(entities); return entities; } public IList CategoriesSecondLevel() { - var sampleImagesPath = this._sampleImagesPath; - var categoryTemplateInGridAndLines = this.CategoryTemplates().Where(pt => pt.ViewPath == "CategoryTemplate.ProductsInGridOrLines").FirstOrDefault(); - - #region category definitions - - #region new + var imagesPath = _sampleImagesPath; + var gridOrLinesTemplate = CategoryTemplates().Where(pt => pt.ViewPath == "CategoryTemplate.ProductsInGridOrLines").FirstOrDefault(); + var categories = _ctx.Set().ToList().ToDictionarySafe(x => x.Alias, x => x); + var discounts = _ctx.Set().Where(x => x.DiscountTypeId == (int)DiscountType.AssignedToCategories).ToList(); var categoryFashionJackets = new Category { Name = "Jackets", Alias = "Jackets", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/jackets.jpg"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + ParentCategoryId = categories["Fashion"].Id, DisplayOrder = 1, MetaTitle = "Jackets", ShowOnHomePage = true }; - var categoryFashionLeatherJackets = new Category - { - Name = "Leather jackets", - Alias = "Leather jackets", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, - MediaFile = CreatePicture("category/leather_jackets.jpg"), - Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, - DisplayOrder = 1, - MetaTitle = "Leather jackets", - ShowOnHomePage = true - }; - var categoryFashionShoes = new Category { Name = "Shoes", Alias = "Shoes", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/shoes.png"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + ParentCategoryId = categories["Fashion"].Id, DisplayOrder = 1, MetaTitle = "Shoes", ShowOnHomePage = true }; - var categoryFashionTrousers = new Category + var categoryFashionSunglasses = new Category { - Name = "Trousers", - Alias = "Pants", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, - MediaFile = CreatePicture("category/trousers.png"), + Name = "Sunglasses", + Alias = "Sunglasses", + CategoryTemplateId = gridOrLinesTemplate.Id, + MediaFile = CreatePicture("category/glasses.png"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + ParentCategoryId = categories["Fashion"].Id, DisplayOrder = 1, - MetaTitle = "Trousers", + MetaTitle = "Sunglasses", ShowOnHomePage = true }; - var categoryFashionTracksuits = new Category + var categoryFashionTrousers = new Category { - Name = "Tracksuits", - Alias = "Tracksuits", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, - MediaFile = CreatePicture("category/tracksuit.png"), + Name = "Trousers", + Alias = "Trousers", + CategoryTemplateId = gridOrLinesTemplate.Id, + MediaFile = CreatePicture("category/trousers.png"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + ParentCategoryId = categories["Fashion"].Id, DisplayOrder = 1, - MetaTitle = "Tracksuits", + MetaTitle = "Trousers", ShowOnHomePage = true }; - - #endregion - - + if (discounts.Any()) + { + categoryFashionTrousers.HasDiscountsApplied = true; + categoryFashionTrousers.AppliedDiscounts.Add(discounts.First()); + } var categorySportsGolf = new Category { Name = "Golf", Alias = "Golf", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/golf.jpg"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Sports").First().Id, + ParentCategoryId = categories["Sports"].Id, DisplayOrder = 1, MetaTitle = "Golf", ShowOnHomePage = true }; - var categorySportsSunglasses = new Category - { - Name = "Sunglasses", - Alias = "Sunglasses", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, - MediaFile = CreatePicture("category/glasses.png"), - Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, - DisplayOrder = 1, - MetaTitle = "Sunglasses", - ShowOnHomePage = true - }; - var categorySportsSoccer = new Category { Name = "Soccer", Alias = "Soccer", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/soccer.png"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Sports").First().Id, + ParentCategoryId = categories["Sports"].Id, DisplayOrder = 1, MetaTitle = "Soccer", ShowOnHomePage = true @@ -281,10 +269,10 @@ public IList CategoriesSecondLevel() { Name = "Basketball", Alias = "Basketball", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/basketball.png"), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Sports").First().Id, + ParentCategoryId = categories["Sports"].Id, DisplayOrder = 1, MetaTitle = "Basketball", ShowOnHomePage = true @@ -294,10 +282,10 @@ public IList CategoriesSecondLevel() { Name = "SPIEGEL-Bestseller", Alias = "SPIEGEL-Bestseller", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/0000930_spiegel-bestseller.png", GetSeName("SPIEGEL-Bestseller")), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Books").First().Id, + ParentCategoryId = categories["Books"].Id, DisplayOrder = 1, MetaTitle = "SPIEGEL-Bestseller" }; @@ -306,14 +294,100 @@ public IList CategoriesSecondLevel() { Name = "Cook and enjoy", Alias = "Cook and enjoy", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, + CategoryTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("category/0000936_kochen-geniesen.jpeg", GetSeName("Cook and enjoy")), Published = true, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Books").First().Id, + ParentCategoryId = categories["Books"].Id, DisplayOrder = 2, MetaTitle = "Cook and enjoy" }; + var categoryGamingAccessories = new Category + { + Name = "Gaming Accessories", + Alias = "Gaming Accessories", + CategoryTemplateId = gridOrLinesTemplate.Id, + ParentCategoryId = categories["Gaming"].Id, + MediaFile = CreatePicture("category/gaming_accessories.png"), + Published = true, + DisplayOrder = 2, + MetaTitle = "Gaming Accessories" + }; + + var categoryGamingGames = new Category + { + Name = "Games", + Alias = "Games", + CategoryTemplateId = gridOrLinesTemplate.Id, + ParentCategoryId = categories["Gaming"].Id, + MediaFile = CreatePicture("category/games.jpg"), + Published = true, + DisplayOrder = 3, + MetaTitle = "Games" + }; + + var categoryFurnitureSofas = new Category + { + Name = "Sofas", + Alias = "Sofas", + CategoryTemplateId = gridOrLinesTemplate.Id, + ParentCategoryId = categories["Furniture"].Id, + MediaFile = CreatePicture("category/sofas.jpg"), + Published = true, + DisplayOrder = 3, + MetaTitle = "Sofas" + }; + + var categoryFurnitureChairs = new Category + { + Name = "Chairs", + Alias = "Chairs", + CategoryTemplateId = gridOrLinesTemplate.Id, + ParentCategoryId = categories["Furniture"].Id, + MediaFile = CreatePicture("category/furniture.jpg"), + Published = true, + DisplayOrder = 3, + MetaTitle = "Chairs" + }; + + var categoryFurnitureTables = new Category + { + Name = "Tables", + Alias = "Tables", + CategoryTemplateId = gridOrLinesTemplate.Id, + ParentCategoryId = categories["Furniture"].Id, + MediaFile = CreatePicture("category/tables.jpg"), + Published = true, + DisplayOrder = 3, + MetaTitle = "Tables" + }; + + //var categoryFashionLeatherJackets = new Category + //{ + // Name = "Leather jackets", + // Alias = "Leather jackets", + // CategoryTemplateId = categoryTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("category/leather_jackets.jpg"), + // Published = true, + // ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + // DisplayOrder = 1, + // MetaTitle = "Leather jackets", + // ShowOnHomePage = true + //}; + + //var categoryFashionTracksuits = new Category + //{ + // Name = "Tracksuits", + // Alias = "Tracksuits", + // CategoryTemplateId = categoryTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("category/tracksuit.png"), + // Published = true, + // ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + // DisplayOrder = 1, + // MetaTitle = "Tracksuits", + // ShowOnHomePage = true + //}; + //var categoryDesktops = new Category //{ // Name = "Desktops", @@ -338,40 +412,14 @@ public IList CategoriesSecondLevel() // MetaTitle = "Notebooks" //}; - var categoryGamingAccessories = new Category - { - Name = "Gaming Accessories", - Alias = "Gaming Accessories", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Gaming").First().Id, - MediaFile = CreatePicture("category/gaming_accessories.png"), - Published = true, - DisplayOrder = 2, - MetaTitle = "Gaming Accessories" - }; - - var categoryGamingGames = new Category - { - Name = "Games", - Alias = "Games", - CategoryTemplateId = categoryTemplateInGridAndLines.Id, - ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Gaming").First().Id, - MediaFile = CreatePicture("category/games.jpg"), - Published = true, - DisplayOrder = 3, - MetaTitle = "Games" - }; - - #endregion - var entities = new List { - categorySportsSunglasses,categorySportsSoccer, categorySportsBasketball,categorySportsGolf, categoryBooksSpiegel, categoryBooksCookAndEnjoy, - categoryGamingAccessories, categoryGamingGames, categoryFashionJackets, categoryFashionLeatherJackets, categoryFashionShoes, categoryFashionTrousers, - categoryFashionTracksuits + categorySportsSoccer, categorySportsBasketball, categorySportsGolf, categoryBooksSpiegel, + categoryBooksCookAndEnjoy, categoryGamingAccessories, categoryGamingGames, categoryFashionJackets, categoryFashionShoes, + categoryFashionTrousers, categoryFashionSunglasses, categoryFurnitureChairs, categoryFurnitureSofas, categoryFurnitureTables }; - this.Alter(entities); + Alter(entities); return entities; } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Countries.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Countries.cs index cddcaabbb8..6d977a5751 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Countries.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Countries.cs @@ -1,3403 +1,3402 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Domain.Directory; namespace SmartStore.Data.Setup { public abstract partial class InvariantSeedData { - public IList Countries() - { - var cUsa = new Country - { - Name = "United States", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "US", - ThreeLetterIsoCode = "USA", - NumericIsoCode = 840, - SubjectToVat = false, - DisplayOrder = 1, - Published = true, - }; + public IList Countries() + { + var cUsa = new Country + { + Name = "United States", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "US", + ThreeLetterIsoCode = "USA", + NumericIsoCode = 840, + SubjectToVat = false, + DisplayOrder = 1, + Published = true, + }; - #region US Regions + #region US Regions - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "AA (Armed Forces Americas)", - Abbreviation = "AA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "AE (Armed Forces Europe)", - Abbreviation = "AE", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Alabama", - Abbreviation = "AL", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Alaska", - Abbreviation = "AK", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "American Samoa", - Abbreviation = "AS", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "AP (Armed Forces Pacific)", - Abbreviation = "AP", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Arizona", - Abbreviation = "AZ", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Arkansas", - Abbreviation = "AR", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "California", - Abbreviation = "CA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Colorado", - Abbreviation = "CO", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Connecticut", - Abbreviation = "CT", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Delaware", - Abbreviation = "DE", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "District of Columbia", - Abbreviation = "DC", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Federated States of Micronesia", - Abbreviation = "FM", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Florida", - Abbreviation = "FL", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Georgia", - Abbreviation = "GA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Guam", - Abbreviation = "GU", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Hawaii", - Abbreviation = "HI", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Idaho", - Abbreviation = "ID", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Illinois", - Abbreviation = "IL", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Indiana", - Abbreviation = "IN", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Iowa", - Abbreviation = "IA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Kansas", - Abbreviation = "KS", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Kentucky", - Abbreviation = "KY", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Louisiana", - Abbreviation = "LA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Maine", - Abbreviation = "ME", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Marshall Islands", - Abbreviation = "MH", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Maryland", - Abbreviation = "MD", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Massachusetts", - Abbreviation = "MA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Michigan", - Abbreviation = "MI", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Minnesota", - Abbreviation = "MN", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Mississippi", - Abbreviation = "MS", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Missouri", - Abbreviation = "MO", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Montana", - Abbreviation = "MT", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Nebraska", - Abbreviation = "NE", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Nevada", - Abbreviation = "NV", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "New Hampshire", - Abbreviation = "NH", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "New Jersey", - Abbreviation = "NJ", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "New Mexico", - Abbreviation = "NM", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "New York", - Abbreviation = "NY", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "North Carolina", - Abbreviation = "NC", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "North Dakota", - Abbreviation = "ND", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Northern Mariana Islands", - Abbreviation = "MP", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Ohio", - Abbreviation = "OH", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Oklahoma", - Abbreviation = "OK", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Oregon", - Abbreviation = "OR", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Palau", - Abbreviation = "PW", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Pennsylvania", - Abbreviation = "PA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Puerto Rico", - Abbreviation = "PR", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Rhode Island", - Abbreviation = "RI", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "South Carolina", - Abbreviation = "SC", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "South Dakota", - Abbreviation = "SD", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Tennessee", - Abbreviation = "TN", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Texas", - Abbreviation = "TX", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Utah", - Abbreviation = "UT", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Vermont", - Abbreviation = "VT", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Virgin Islands", - Abbreviation = "VI", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Virginia", - Abbreviation = "VA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Washington", - Abbreviation = "WA", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "West Virginia", - Abbreviation = "WV", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Wisconsin", - Abbreviation = "WI", - Published = true, - DisplayOrder = 1, - }); - cUsa.StateProvinces.Add(new StateProvince() - { - Name = "Wyoming", - Abbreviation = "WY", - Published = true, - DisplayOrder = 1, - }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "AA (Armed Forces Americas)", + Abbreviation = "AA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "AE (Armed Forces Europe)", + Abbreviation = "AE", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Alabama", + Abbreviation = "AL", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Alaska", + Abbreviation = "AK", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "American Samoa", + Abbreviation = "AS", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "AP (Armed Forces Pacific)", + Abbreviation = "AP", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Arizona", + Abbreviation = "AZ", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Arkansas", + Abbreviation = "AR", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "California", + Abbreviation = "CA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Colorado", + Abbreviation = "CO", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Connecticut", + Abbreviation = "CT", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Delaware", + Abbreviation = "DE", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "District of Columbia", + Abbreviation = "DC", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Federated States of Micronesia", + Abbreviation = "FM", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Florida", + Abbreviation = "FL", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Georgia", + Abbreviation = "GA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Guam", + Abbreviation = "GU", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Hawaii", + Abbreviation = "HI", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Idaho", + Abbreviation = "ID", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Illinois", + Abbreviation = "IL", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Indiana", + Abbreviation = "IN", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Iowa", + Abbreviation = "IA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Kansas", + Abbreviation = "KS", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Kentucky", + Abbreviation = "KY", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Louisiana", + Abbreviation = "LA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Maine", + Abbreviation = "ME", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Marshall Islands", + Abbreviation = "MH", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Maryland", + Abbreviation = "MD", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Massachusetts", + Abbreviation = "MA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Michigan", + Abbreviation = "MI", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Minnesota", + Abbreviation = "MN", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Mississippi", + Abbreviation = "MS", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Missouri", + Abbreviation = "MO", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Montana", + Abbreviation = "MT", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Nebraska", + Abbreviation = "NE", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Nevada", + Abbreviation = "NV", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "New Hampshire", + Abbreviation = "NH", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "New Jersey", + Abbreviation = "NJ", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "New Mexico", + Abbreviation = "NM", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "New York", + Abbreviation = "NY", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "North Carolina", + Abbreviation = "NC", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "North Dakota", + Abbreviation = "ND", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Northern Mariana Islands", + Abbreviation = "MP", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Ohio", + Abbreviation = "OH", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Oklahoma", + Abbreviation = "OK", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Oregon", + Abbreviation = "OR", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Palau", + Abbreviation = "PW", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Pennsylvania", + Abbreviation = "PA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Puerto Rico", + Abbreviation = "PR", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Rhode Island", + Abbreviation = "RI", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "South Carolina", + Abbreviation = "SC", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "South Dakota", + Abbreviation = "SD", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Tennessee", + Abbreviation = "TN", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Texas", + Abbreviation = "TX", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Utah", + Abbreviation = "UT", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Vermont", + Abbreviation = "VT", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Virgin Islands", + Abbreviation = "VI", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Virginia", + Abbreviation = "VA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Washington", + Abbreviation = "WA", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "West Virginia", + Abbreviation = "WV", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Wisconsin", + Abbreviation = "WI", + Published = true, + DisplayOrder = 1, + }); + cUsa.StateProvinces.Add(new StateProvince() + { + Name = "Wyoming", + Abbreviation = "WY", + Published = true, + DisplayOrder = 1, + }); - #endregion + #endregion - var cCanada = new Country - { - Name = "Canada", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CA", - ThreeLetterIsoCode = "CAN", - NumericIsoCode = 124, - SubjectToVat = false, - DisplayOrder = 2, - Published = true, - }; + var cCanada = new Country + { + Name = "Canada", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CA", + ThreeLetterIsoCode = "CAN", + NumericIsoCode = 124, + SubjectToVat = false, + DisplayOrder = 2, + Published = true, + }; - #region CA Regions + #region CA Regions - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Alberta", - Abbreviation = "AB", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "British Columbia", - Abbreviation = "BC", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Manitoba", - Abbreviation = "MB", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "New Brunswick", - Abbreviation = "NB", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Newfoundland and Labrador", - Abbreviation = "NL", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Northwest Territories", - Abbreviation = "NT", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Nova Scotia", - Abbreviation = "NS", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Nunavut", - Abbreviation = "NU", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Ontario", - Abbreviation = "ON", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Prince Edward Island", - Abbreviation = "PE", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Quebec", - Abbreviation = "QC", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Saskatchewan", - Abbreviation = "SK", - Published = true, - DisplayOrder = 1, - }); - cCanada.StateProvinces.Add(new StateProvince() - { - Name = "Yukon Territory", - Abbreviation = "YU", - Published = true, - DisplayOrder = 1, - }); - #endregion + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Alberta", + Abbreviation = "AB", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "British Columbia", + Abbreviation = "BC", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Manitoba", + Abbreviation = "MB", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "New Brunswick", + Abbreviation = "NB", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Newfoundland and Labrador", + Abbreviation = "NL", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Northwest Territories", + Abbreviation = "NT", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Nova Scotia", + Abbreviation = "NS", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Nunavut", + Abbreviation = "NU", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Ontario", + Abbreviation = "ON", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Prince Edward Island", + Abbreviation = "PE", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Quebec", + Abbreviation = "QC", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Saskatchewan", + Abbreviation = "SK", + Published = true, + DisplayOrder = 1, + }); + cCanada.StateProvinces.Add(new StateProvince() + { + Name = "Yukon Territory", + Abbreviation = "YU", + Published = true, + DisplayOrder = 1, + }); + #endregion - var entities = new List() - { - new Country() - { - Name = "Germany", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "DE", - ThreeLetterIsoCode = "DEU", - NumericIsoCode = 276, - SubjectToVat = true, - DisplayOrder = -10, - Published = true - }, + var entities = new List() + { + new Country() + { + Name = "Germany", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "DE", + ThreeLetterIsoCode = "DEU", + NumericIsoCode = 276, + SubjectToVat = true, + DisplayOrder = -10, + Published = true + }, - new Country - { - Name = "Austria", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AT", - ThreeLetterIsoCode = "AUT", - NumericIsoCode = 40, - SubjectToVat = true, - DisplayOrder = -5, - Published = true - }, - new Country - { - Name = "Switzerland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CH", - ThreeLetterIsoCode = "CHE", - NumericIsoCode = 756, - SubjectToVat = false, - DisplayOrder = -1, - Published = true - }, - cUsa, - cCanada, + new Country + { + Name = "Austria", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AT", + ThreeLetterIsoCode = "AUT", + NumericIsoCode = 40, + SubjectToVat = true, + DisplayOrder = -5, + Published = true + }, + new Country + { + Name = "Switzerland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CH", + ThreeLetterIsoCode = "CHE", + NumericIsoCode = 756, + SubjectToVat = false, + DisplayOrder = -1, + Published = true + }, + cUsa, + cCanada, //other countries new Country - { - Name = "Argentina", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AR", - ThreeLetterIsoCode = "ARG", - NumericIsoCode = 32, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Armenia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AM", - ThreeLetterIsoCode = "ARM", - NumericIsoCode = 51, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Aruba", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AW", - ThreeLetterIsoCode = "ABW", - NumericIsoCode = 533, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Australia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AU", - ThreeLetterIsoCode = "AUS", - NumericIsoCode = 36, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Azerbaijan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AZ", - ThreeLetterIsoCode = "AZE", - NumericIsoCode = 31, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bahamas", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BS", - ThreeLetterIsoCode = "BHS", - NumericIsoCode = 44, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bangladesh", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BD", - ThreeLetterIsoCode = "BGD", - NumericIsoCode = 50, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Belarus", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BY", - ThreeLetterIsoCode = "BLR", - NumericIsoCode = 112, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Belgium", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BE", - ThreeLetterIsoCode = "BEL", - NumericIsoCode = 56, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Belize", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BZ", - ThreeLetterIsoCode = "BLZ", - NumericIsoCode = 84, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bermuda", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BM", - ThreeLetterIsoCode = "BMU", - NumericIsoCode = 60, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bolivia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BO", - ThreeLetterIsoCode = "BOL", - NumericIsoCode = 68, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bosnia and Herzegowina", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BA", - ThreeLetterIsoCode = "BIH", - NumericIsoCode = 70, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Brazil", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BR", - ThreeLetterIsoCode = "BRA", - NumericIsoCode = 76, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bulgaria", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BG", - ThreeLetterIsoCode = "BGR", - NumericIsoCode = 100, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cayman Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KY", - ThreeLetterIsoCode = "CYM", - NumericIsoCode = 136, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Chile", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CL", - ThreeLetterIsoCode = "CHL", - NumericIsoCode = 152, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "China", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CN", - ThreeLetterIsoCode = "CHN", - NumericIsoCode = 156, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Colombia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CO", - ThreeLetterIsoCode = "COL", - NumericIsoCode = 170, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Costa Rica", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CR", - ThreeLetterIsoCode = "CRI", - NumericIsoCode = 188, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Croatia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "HR", - ThreeLetterIsoCode = "HRV", - NumericIsoCode = 191, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cuba", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CU", - ThreeLetterIsoCode = "CUB", - NumericIsoCode = 192, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cyprus", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CY", - ThreeLetterIsoCode = "CYP", - NumericIsoCode = 196, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Czech Republic", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CZ", - ThreeLetterIsoCode = "CZE", - NumericIsoCode = 203, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Denmark", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "DK", - ThreeLetterIsoCode = "DNK", - NumericIsoCode = 208, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Dominican Republic", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "DO", - ThreeLetterIsoCode = "DOM", - NumericIsoCode = 214, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Ecuador", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "EC", - ThreeLetterIsoCode = "ECU", - NumericIsoCode = 218, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Egypt", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "EG", - ThreeLetterIsoCode = "EGY", - NumericIsoCode = 818, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Finland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "FI", - ThreeLetterIsoCode = "FIN", - NumericIsoCode = 246, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "France", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "FR", - ThreeLetterIsoCode = "FRA", - NumericIsoCode = 250, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Georgia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GE", - ThreeLetterIsoCode = "GEO", - NumericIsoCode = 268, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Gibraltar", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GI", - ThreeLetterIsoCode = "GIB", - NumericIsoCode = 292, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Greece", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GR", - ThreeLetterIsoCode = "GRC", - NumericIsoCode = 300, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Guatemala", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GT", - ThreeLetterIsoCode = "GTM", - NumericIsoCode = 320, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Hong Kong", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "HK", - ThreeLetterIsoCode = "HKG", - NumericIsoCode = 344, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Hungary", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "HU", - ThreeLetterIsoCode = "HUN", - NumericIsoCode = 348, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "India", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IN", - ThreeLetterIsoCode = "IND", - NumericIsoCode = 356, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Indonesia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ID", - ThreeLetterIsoCode = "IDN", - NumericIsoCode = 360, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Ireland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IE", - ThreeLetterIsoCode = "IRL", - NumericIsoCode = 372, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Israel", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IL", - ThreeLetterIsoCode = "ISR", - NumericIsoCode = 376, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Italy", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IT", - ThreeLetterIsoCode = "ITA", - NumericIsoCode = 380, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Jamaica", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "JM", - ThreeLetterIsoCode = "JAM", - NumericIsoCode = 388, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Japan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "JP", - ThreeLetterIsoCode = "JPN", - NumericIsoCode = 392, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Jordan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "JO", - ThreeLetterIsoCode = "JOR", - NumericIsoCode = 400, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Kazakhstan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KZ", - ThreeLetterIsoCode = "KAZ", - NumericIsoCode = 398, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Korea, Democratic People's Republic of", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KP", - ThreeLetterIsoCode = "PRK", - NumericIsoCode = 408, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Kuwait", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KW", - ThreeLetterIsoCode = "KWT", - NumericIsoCode = 414, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Malaysia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MY", - ThreeLetterIsoCode = "MYS", - NumericIsoCode = 458, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mexico", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MX", - ThreeLetterIsoCode = "MEX", - NumericIsoCode = 484, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Netherlands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NL", - ThreeLetterIsoCode = "NLD", - NumericIsoCode = 528, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "New Zealand", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NZ", - ThreeLetterIsoCode = "NZL", - NumericIsoCode = 554, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Norway", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NO", - ThreeLetterIsoCode = "NOR", - NumericIsoCode = 578, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Pakistan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PK", - ThreeLetterIsoCode = "PAK", - NumericIsoCode = 586, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Paraguay", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PY", - ThreeLetterIsoCode = "PRY", - NumericIsoCode = 600, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Peru", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PE", - ThreeLetterIsoCode = "PER", - NumericIsoCode = 604, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Philippines", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PH", - ThreeLetterIsoCode = "PHL", - NumericIsoCode = 608, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Poland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PL", - ThreeLetterIsoCode = "POL", - NumericIsoCode = 616, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Portugal", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PT", - ThreeLetterIsoCode = "PRT", - NumericIsoCode = 620, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Puerto Rico", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PR", - ThreeLetterIsoCode = "PRI", - NumericIsoCode = 630, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Qatar", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "QA", - ThreeLetterIsoCode = "QAT", - NumericIsoCode = 634, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Romania", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "RO", - ThreeLetterIsoCode = "ROM", - NumericIsoCode = 642, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Russia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "RU", - ThreeLetterIsoCode = "RUS", - NumericIsoCode = 643, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Saudi Arabia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SA", - ThreeLetterIsoCode = "SAU", - NumericIsoCode = 682, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Singapore", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SG", - ThreeLetterIsoCode = "SGP", - NumericIsoCode = 702, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Slovakia (Slovak Republic)", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SK", - ThreeLetterIsoCode = "SVK", - NumericIsoCode = 703, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Slovenia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SI", - ThreeLetterIsoCode = "SVN", - NumericIsoCode = 705, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "South Africa", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ZA", - ThreeLetterIsoCode = "ZAF", - NumericIsoCode = 710, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Spain", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ES", - ThreeLetterIsoCode = "ESP", - NumericIsoCode = 724, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Sweden", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SE", - ThreeLetterIsoCode = "SWE", - NumericIsoCode = 752, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Taiwan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TW", - ThreeLetterIsoCode = "TWN", - NumericIsoCode = 158, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Thailand", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TH", - ThreeLetterIsoCode = "THA", - NumericIsoCode = 764, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Turkey", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TR", - ThreeLetterIsoCode = "TUR", - NumericIsoCode = 792, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Ukraine", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "UA", - ThreeLetterIsoCode = "UKR", - NumericIsoCode = 804, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "United Arab Emirates", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AE", - ThreeLetterIsoCode = "ARE", - NumericIsoCode = 784, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "United Kingdom", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GB", - ThreeLetterIsoCode = "GBR", - NumericIsoCode = 826, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "United States minor outlying islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "UM", - ThreeLetterIsoCode = "UMI", - NumericIsoCode = 581, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Uruguay", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "UY", - ThreeLetterIsoCode = "URY", - NumericIsoCode = 858, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Uzbekistan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "UZ", - ThreeLetterIsoCode = "UZB", - NumericIsoCode = 860, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Venezuela", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VE", - ThreeLetterIsoCode = "VEN", - NumericIsoCode = 862, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Serbia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "RS", - ThreeLetterIsoCode = "SRB", - NumericIsoCode = 688, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Afghanistan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AF", - ThreeLetterIsoCode = "AFG", - NumericIsoCode = 4, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Albania", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AL", - ThreeLetterIsoCode = "ALB", - NumericIsoCode = 8, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Algeria", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "DZ", - ThreeLetterIsoCode = "DZA", - NumericIsoCode = 12, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "American Samoa", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AS", - ThreeLetterIsoCode = "ASM", - NumericIsoCode = 16, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Andorra", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AD", - ThreeLetterIsoCode = "AND", - NumericIsoCode = 20, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Angola", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AO", - ThreeLetterIsoCode = "AGO", - NumericIsoCode = 24, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Anguilla", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AI", - ThreeLetterIsoCode = "AIA", - NumericIsoCode = 660, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Antarctica", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AQ", - ThreeLetterIsoCode = "ATA", - NumericIsoCode = 10, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Antigua and Barbuda", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AG", - ThreeLetterIsoCode = "ATG", - NumericIsoCode = 28, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bahrain", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BH", - ThreeLetterIsoCode = "BHR", - NumericIsoCode = 48, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Barbados", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BB", - ThreeLetterIsoCode = "BRB", - NumericIsoCode = 52, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Benin", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BJ", - ThreeLetterIsoCode = "BEN", - NumericIsoCode = 204, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bhutan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BT", - ThreeLetterIsoCode = "BTN", - NumericIsoCode = 64, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Botswana", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BW", - ThreeLetterIsoCode = "BWA", - NumericIsoCode = 72, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Bouvet Island", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BV", - ThreeLetterIsoCode = "BVT", - NumericIsoCode = 74, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "British Indian Ocean Territory", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IO", - ThreeLetterIsoCode = "IOT", - NumericIsoCode = 86, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Brunei Darussalam", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BN", - ThreeLetterIsoCode = "BRN", - NumericIsoCode = 96, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Burkina Faso", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BF", - ThreeLetterIsoCode = "BFA", - NumericIsoCode = 854, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Burundi", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "BI", - ThreeLetterIsoCode = "BDI", - NumericIsoCode = 108, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cambodia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KH", - ThreeLetterIsoCode = "KHM", - NumericIsoCode = 116, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cameroon", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CM", - ThreeLetterIsoCode = "CMR", - NumericIsoCode = 120, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cape Verde", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CV", - ThreeLetterIsoCode = "CPV", - NumericIsoCode = 132, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Central African Republic", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CF", - ThreeLetterIsoCode = "CAF", - NumericIsoCode = 140, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Chad", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TD", - ThreeLetterIsoCode = "TCD", - NumericIsoCode = 148, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Christmas Island", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CX", - ThreeLetterIsoCode = "CXR", - NumericIsoCode = 162, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cocos (Keeling) Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CC", - ThreeLetterIsoCode = "CCK", - NumericIsoCode = 166, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Comoros", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KM", - ThreeLetterIsoCode = "COM", - NumericIsoCode = 174, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Congo", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CG", - ThreeLetterIsoCode = "COG", - NumericIsoCode = 178, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cook Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CK", - ThreeLetterIsoCode = "COK", - NumericIsoCode = 184, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Cote D'Ivoire", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "CI", - ThreeLetterIsoCode = "CIV", - NumericIsoCode = 384, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Djibouti", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "DJ", - ThreeLetterIsoCode = "DJI", - NumericIsoCode = 262, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Dominica", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "DM", - ThreeLetterIsoCode = "DMA", - NumericIsoCode = 212, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "El Salvador", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SV", - ThreeLetterIsoCode = "SLV", - NumericIsoCode = 222, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Equatorial Guinea", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GQ", - ThreeLetterIsoCode = "GNQ", - NumericIsoCode = 226, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Eritrea", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ER", - ThreeLetterIsoCode = "ERI", - NumericIsoCode = 232, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Estonia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "EE", - ThreeLetterIsoCode = "EST", - NumericIsoCode = 233, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Ethiopia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ET", - ThreeLetterIsoCode = "ETH", - NumericIsoCode = 231, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Falkland Islands (Malvinas)", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "FK", - ThreeLetterIsoCode = "FLK", - NumericIsoCode = 238, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Faroe Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "FO", - ThreeLetterIsoCode = "FRO", - NumericIsoCode = 234, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Fiji", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "FJ", - ThreeLetterIsoCode = "FJI", - NumericIsoCode = 242, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "French Guiana", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GF", - ThreeLetterIsoCode = "GUF", - NumericIsoCode = 254, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "French Polynesia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PF", - ThreeLetterIsoCode = "PYF", - NumericIsoCode = 258, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "French Southern Territories", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TF", - ThreeLetterIsoCode = "ATF", - NumericIsoCode = 260, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Gabon", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GA", - ThreeLetterIsoCode = "GAB", - NumericIsoCode = 266, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Gambia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GM", - ThreeLetterIsoCode = "GMB", - NumericIsoCode = 270, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Ghana", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GH", - ThreeLetterIsoCode = "GHA", - NumericIsoCode = 288, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Greenland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GL", - ThreeLetterIsoCode = "GRL", - NumericIsoCode = 304, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Grenada", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GD", - ThreeLetterIsoCode = "GRD", - NumericIsoCode = 308, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Guadeloupe", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GP", - ThreeLetterIsoCode = "GLP", - NumericIsoCode = 312, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Guam", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GU", - ThreeLetterIsoCode = "GUM", - NumericIsoCode = 316, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Guinea", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GN", - ThreeLetterIsoCode = "GIN", - NumericIsoCode = 324, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Guinea-bissau", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GW", - ThreeLetterIsoCode = "GNB", - NumericIsoCode = 624, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Guyana", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GY", - ThreeLetterIsoCode = "GUY", - NumericIsoCode = 328, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Haiti", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "HT", - ThreeLetterIsoCode = "HTI", - NumericIsoCode = 332, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Heard and Mc Donald Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "HM", - ThreeLetterIsoCode = "HMD", - NumericIsoCode = 334, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Honduras", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "HN", - ThreeLetterIsoCode = "HND", - NumericIsoCode = 340, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Iceland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IS", - ThreeLetterIsoCode = "ISL", - NumericIsoCode = 352, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Iran (Islamic Republic of)", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IR", - ThreeLetterIsoCode = "IRN", - NumericIsoCode = 364, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Iraq", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "IQ", - ThreeLetterIsoCode = "IRQ", - NumericIsoCode = 368, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Kenya", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KE", - ThreeLetterIsoCode = "KEN", - NumericIsoCode = 404, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Kiribati", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KI", - ThreeLetterIsoCode = "KIR", - NumericIsoCode = 296, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Korea", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KR", - ThreeLetterIsoCode = "KOR", - NumericIsoCode = 410, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Kyrgyzstan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KG", - ThreeLetterIsoCode = "KGZ", - NumericIsoCode = 417, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Lao People's Democratic Republic", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LA", - ThreeLetterIsoCode = "LAO", - NumericIsoCode = 418, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Latvia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LV", - ThreeLetterIsoCode = "LVA", - NumericIsoCode = 428, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Lebanon", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LB", - ThreeLetterIsoCode = "LBN", - NumericIsoCode = 422, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Lesotho", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LS", - ThreeLetterIsoCode = "LSO", - NumericIsoCode = 426, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Liberia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LR", - ThreeLetterIsoCode = "LBR", - NumericIsoCode = 430, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Libyan Arab Jamahiriya", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LY", - ThreeLetterIsoCode = "LBY", - NumericIsoCode = 434, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Liechtenstein", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LI", - ThreeLetterIsoCode = "LIE", - NumericIsoCode = 438, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Lithuania", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LT", - ThreeLetterIsoCode = "LTU", - NumericIsoCode = 440, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Luxembourg", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LU", - ThreeLetterIsoCode = "LUX", - NumericIsoCode = 442, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Macau", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MO", - ThreeLetterIsoCode = "MAC", - NumericIsoCode = 446, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Macedonia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MK", - ThreeLetterIsoCode = "MKD", - NumericIsoCode = 807, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Madagascar", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MG", - ThreeLetterIsoCode = "MDG", - NumericIsoCode = 450, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Malawi", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MW", - ThreeLetterIsoCode = "MWI", - NumericIsoCode = 454, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Maldives", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MV", - ThreeLetterIsoCode = "MDV", - NumericIsoCode = 462, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mali", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ML", - ThreeLetterIsoCode = "MLI", - NumericIsoCode = 466, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Malta", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MT", - ThreeLetterIsoCode = "MLT", - NumericIsoCode = 470, - SubjectToVat = true, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Marshall Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MH", - ThreeLetterIsoCode = "MHL", - NumericIsoCode = 584, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Martinique", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MQ", - ThreeLetterIsoCode = "MTQ", - NumericIsoCode = 474, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mauritania", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MR", - ThreeLetterIsoCode = "MRT", - NumericIsoCode = 478, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mauritius", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MU", - ThreeLetterIsoCode = "MUS", - NumericIsoCode = 480, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mayotte", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "YT", - ThreeLetterIsoCode = "MYT", - NumericIsoCode = 175, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Micronesia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "FM", - ThreeLetterIsoCode = "FSM", - NumericIsoCode = 583, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Moldova", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MD", - ThreeLetterIsoCode = "MDA", - NumericIsoCode = 498, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Monaco", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MC", - ThreeLetterIsoCode = "MCO", - NumericIsoCode = 492, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mongolia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MN", - ThreeLetterIsoCode = "MNG", - NumericIsoCode = 496, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Montenegro", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ME", - ThreeLetterIsoCode = "MNE", - NumericIsoCode = 499, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Montserrat", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MS", - ThreeLetterIsoCode = "MSR", - NumericIsoCode = 500, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Morocco", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MA", - ThreeLetterIsoCode = "MAR", - NumericIsoCode = 504, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Mozambique", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MZ", - ThreeLetterIsoCode = "MOZ", - NumericIsoCode = 508, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Myanmar", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MM", - ThreeLetterIsoCode = "MMR", - NumericIsoCode = 104, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Namibia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NA", - ThreeLetterIsoCode = "NAM", - NumericIsoCode = 516, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Nauru", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NR", - ThreeLetterIsoCode = "NRU", - NumericIsoCode = 520, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Nepal", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NP", - ThreeLetterIsoCode = "NPL", - NumericIsoCode = 524, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Netherlands Antilles", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "AN", - ThreeLetterIsoCode = "ANT", - NumericIsoCode = 530, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "New Caledonia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NC", - ThreeLetterIsoCode = "NCL", - NumericIsoCode = 540, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Nicaragua", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NI", - ThreeLetterIsoCode = "NIC", - NumericIsoCode = 558, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Niger", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NE", - ThreeLetterIsoCode = "NER", - NumericIsoCode = 562, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Nigeria", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NG", - ThreeLetterIsoCode = "NGA", - NumericIsoCode = 566, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Niue", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NU", - ThreeLetterIsoCode = "NIU", - NumericIsoCode = 570, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Norfolk Island", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "NF", - ThreeLetterIsoCode = "NFK", - NumericIsoCode = 574, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Northern Mariana Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "MP", - ThreeLetterIsoCode = "MNP", - NumericIsoCode = 580, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Oman", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "OM", - ThreeLetterIsoCode = "OMN", - NumericIsoCode = 512, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Palau", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PW", - ThreeLetterIsoCode = "PLW", - NumericIsoCode = 585, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Panama", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PA", - ThreeLetterIsoCode = "PAN", - NumericIsoCode = 591, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Papua New Guinea", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PG", - ThreeLetterIsoCode = "PNG", - NumericIsoCode = 598, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Pitcairn", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PN", - ThreeLetterIsoCode = "PCN", - NumericIsoCode = 612, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Reunion", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "RE", - ThreeLetterIsoCode = "REU", - NumericIsoCode = 638, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Rwanda", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "RW", - ThreeLetterIsoCode = "RWA", - NumericIsoCode = 646, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Saint Kitts and Nevis", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "KN", - ThreeLetterIsoCode = "KNA", - NumericIsoCode = 659, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Saint Lucia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LC", - ThreeLetterIsoCode = "LCA", - NumericIsoCode = 662, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Saint Vincent and the Grenadines", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VC", - ThreeLetterIsoCode = "VCT", - NumericIsoCode = 670, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Samoa", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "WS", - ThreeLetterIsoCode = "WSM", - NumericIsoCode = 882, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "San Marino", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SM", - ThreeLetterIsoCode = "SMR", - NumericIsoCode = 674, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Sao Tome and Principe", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ST", - ThreeLetterIsoCode = "STP", - NumericIsoCode = 678, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Senegal", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SN", - ThreeLetterIsoCode = "SEN", - NumericIsoCode = 686, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Seychelles", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SC", - ThreeLetterIsoCode = "SYC", - NumericIsoCode = 690, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Sierra Leone", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SL", - ThreeLetterIsoCode = "SLE", - NumericIsoCode = 694, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Solomon Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SB", - ThreeLetterIsoCode = "SLB", - NumericIsoCode = 90, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Somalia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SO", - ThreeLetterIsoCode = "SOM", - NumericIsoCode = 706, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "South Georgia & South Sandwich Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "GS", - ThreeLetterIsoCode = "SGS", - NumericIsoCode = 239, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Sri Lanka", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "LK", - ThreeLetterIsoCode = "LKA", - NumericIsoCode = 144, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "St. Helena", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SH", - ThreeLetterIsoCode = "SHN", - NumericIsoCode = 654, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "St. Pierre and Miquelon", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "PM", - ThreeLetterIsoCode = "SPM", - NumericIsoCode = 666, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Sudan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SD", - ThreeLetterIsoCode = "SDN", - NumericIsoCode = 736, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Suriname", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SR", - ThreeLetterIsoCode = "SUR", - NumericIsoCode = 740, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Svalbard and Jan Mayen Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SJ", - ThreeLetterIsoCode = "SJM", - NumericIsoCode = 744, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Swaziland", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SZ", - ThreeLetterIsoCode = "SWZ", - NumericIsoCode = 748, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Syrian Arab Republic", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "SY", - ThreeLetterIsoCode = "SYR", - NumericIsoCode = 760, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Tajikistan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TJ", - ThreeLetterIsoCode = "TJK", - NumericIsoCode = 762, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Tanzania", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TZ", - ThreeLetterIsoCode = "TZA", - NumericIsoCode = 834, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Togo", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TG", - ThreeLetterIsoCode = "TGO", - NumericIsoCode = 768, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Tokelau", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TK", - ThreeLetterIsoCode = "TKL", - NumericIsoCode = 772, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Tonga", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TO", - ThreeLetterIsoCode = "TON", - NumericIsoCode = 776, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Trinidad and Tobago", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TT", - ThreeLetterIsoCode = "TTO", - NumericIsoCode = 780, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Tunisia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TN", - ThreeLetterIsoCode = "TUN", - NumericIsoCode = 788, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Turkmenistan", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TM", - ThreeLetterIsoCode = "TKM", - NumericIsoCode = 795, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Turks and Caicos Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TC", - ThreeLetterIsoCode = "TCA", - NumericIsoCode = 796, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Tuvalu", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "TV", - ThreeLetterIsoCode = "TUV", - NumericIsoCode = 798, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Uganda", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "UG", - ThreeLetterIsoCode = "UGA", - NumericIsoCode = 800, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Vanuatu", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VU", - ThreeLetterIsoCode = "VUT", - NumericIsoCode = 548, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Vatican City State (Holy See)", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VA", - ThreeLetterIsoCode = "VAT", - NumericIsoCode = 336, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Viet Nam", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VN", - ThreeLetterIsoCode = "VNM", - NumericIsoCode = 704, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Virgin Islands (British)", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VG", - ThreeLetterIsoCode = "VGB", - NumericIsoCode = 92, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Virgin Islands (U.S.)", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "VI", - ThreeLetterIsoCode = "VIR", - NumericIsoCode = 850, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Wallis and Futuna Islands", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "WF", - ThreeLetterIsoCode = "WLF", - NumericIsoCode = 876, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Western Sahara", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "EH", - ThreeLetterIsoCode = "ESH", - NumericIsoCode = 732, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Yemen", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "YE", - ThreeLetterIsoCode = "YEM", - NumericIsoCode = 887, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Zambia", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ZM", - ThreeLetterIsoCode = "ZMB", - NumericIsoCode = 894, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - new Country - { - Name = "Zimbabwe", - AllowsBilling = true, - AllowsShipping = true, - TwoLetterIsoCode = "ZW", - ThreeLetterIsoCode = "ZWE", - NumericIsoCode = 716, - SubjectToVat = false, - DisplayOrder = 100, - Published = true - }, - }; - this.Alter(entities); - return entities; - } - } + { + Name = "Argentina", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AR", + ThreeLetterIsoCode = "ARG", + NumericIsoCode = 32, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Armenia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AM", + ThreeLetterIsoCode = "ARM", + NumericIsoCode = 51, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Aruba", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AW", + ThreeLetterIsoCode = "ABW", + NumericIsoCode = 533, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Australia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AU", + ThreeLetterIsoCode = "AUS", + NumericIsoCode = 36, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Azerbaijan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AZ", + ThreeLetterIsoCode = "AZE", + NumericIsoCode = 31, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bahamas", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BS", + ThreeLetterIsoCode = "BHS", + NumericIsoCode = 44, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bangladesh", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BD", + ThreeLetterIsoCode = "BGD", + NumericIsoCode = 50, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Belarus", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BY", + ThreeLetterIsoCode = "BLR", + NumericIsoCode = 112, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Belgium", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BE", + ThreeLetterIsoCode = "BEL", + NumericIsoCode = 56, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Belize", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BZ", + ThreeLetterIsoCode = "BLZ", + NumericIsoCode = 84, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bermuda", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BM", + ThreeLetterIsoCode = "BMU", + NumericIsoCode = 60, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bolivia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BO", + ThreeLetterIsoCode = "BOL", + NumericIsoCode = 68, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bosnia and Herzegowina", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BA", + ThreeLetterIsoCode = "BIH", + NumericIsoCode = 70, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Brazil", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BR", + ThreeLetterIsoCode = "BRA", + NumericIsoCode = 76, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bulgaria", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BG", + ThreeLetterIsoCode = "BGR", + NumericIsoCode = 100, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cayman Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KY", + ThreeLetterIsoCode = "CYM", + NumericIsoCode = 136, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Chile", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CL", + ThreeLetterIsoCode = "CHL", + NumericIsoCode = 152, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "China", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CN", + ThreeLetterIsoCode = "CHN", + NumericIsoCode = 156, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Colombia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CO", + ThreeLetterIsoCode = "COL", + NumericIsoCode = 170, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Costa Rica", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CR", + ThreeLetterIsoCode = "CRI", + NumericIsoCode = 188, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Croatia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "HR", + ThreeLetterIsoCode = "HRV", + NumericIsoCode = 191, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cuba", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CU", + ThreeLetterIsoCode = "CUB", + NumericIsoCode = 192, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cyprus", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CY", + ThreeLetterIsoCode = "CYP", + NumericIsoCode = 196, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Czech Republic", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CZ", + ThreeLetterIsoCode = "CZE", + NumericIsoCode = 203, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Denmark", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "DK", + ThreeLetterIsoCode = "DNK", + NumericIsoCode = 208, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Dominican Republic", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "DO", + ThreeLetterIsoCode = "DOM", + NumericIsoCode = 214, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Ecuador", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "EC", + ThreeLetterIsoCode = "ECU", + NumericIsoCode = 218, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Egypt", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "EG", + ThreeLetterIsoCode = "EGY", + NumericIsoCode = 818, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Finland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "FI", + ThreeLetterIsoCode = "FIN", + NumericIsoCode = 246, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "France", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "FR", + ThreeLetterIsoCode = "FRA", + NumericIsoCode = 250, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Georgia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GE", + ThreeLetterIsoCode = "GEO", + NumericIsoCode = 268, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Gibraltar", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GI", + ThreeLetterIsoCode = "GIB", + NumericIsoCode = 292, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Greece", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GR", + ThreeLetterIsoCode = "GRC", + NumericIsoCode = 300, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Guatemala", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GT", + ThreeLetterIsoCode = "GTM", + NumericIsoCode = 320, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Hong Kong", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "HK", + ThreeLetterIsoCode = "HKG", + NumericIsoCode = 344, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Hungary", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "HU", + ThreeLetterIsoCode = "HUN", + NumericIsoCode = 348, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "India", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IN", + ThreeLetterIsoCode = "IND", + NumericIsoCode = 356, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Indonesia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ID", + ThreeLetterIsoCode = "IDN", + NumericIsoCode = 360, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Ireland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IE", + ThreeLetterIsoCode = "IRL", + NumericIsoCode = 372, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Israel", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IL", + ThreeLetterIsoCode = "ISR", + NumericIsoCode = 376, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Italy", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IT", + ThreeLetterIsoCode = "ITA", + NumericIsoCode = 380, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Jamaica", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "JM", + ThreeLetterIsoCode = "JAM", + NumericIsoCode = 388, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Japan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "JP", + ThreeLetterIsoCode = "JPN", + NumericIsoCode = 392, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Jordan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "JO", + ThreeLetterIsoCode = "JOR", + NumericIsoCode = 400, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Kazakhstan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KZ", + ThreeLetterIsoCode = "KAZ", + NumericIsoCode = 398, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Korea, Democratic People's Republic of", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KP", + ThreeLetterIsoCode = "PRK", + NumericIsoCode = 408, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Kuwait", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KW", + ThreeLetterIsoCode = "KWT", + NumericIsoCode = 414, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Malaysia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MY", + ThreeLetterIsoCode = "MYS", + NumericIsoCode = 458, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mexico", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MX", + ThreeLetterIsoCode = "MEX", + NumericIsoCode = 484, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Netherlands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NL", + ThreeLetterIsoCode = "NLD", + NumericIsoCode = 528, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "New Zealand", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NZ", + ThreeLetterIsoCode = "NZL", + NumericIsoCode = 554, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Norway", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NO", + ThreeLetterIsoCode = "NOR", + NumericIsoCode = 578, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Pakistan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PK", + ThreeLetterIsoCode = "PAK", + NumericIsoCode = 586, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Paraguay", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PY", + ThreeLetterIsoCode = "PRY", + NumericIsoCode = 600, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Peru", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PE", + ThreeLetterIsoCode = "PER", + NumericIsoCode = 604, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Philippines", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PH", + ThreeLetterIsoCode = "PHL", + NumericIsoCode = 608, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Poland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PL", + ThreeLetterIsoCode = "POL", + NumericIsoCode = 616, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Portugal", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PT", + ThreeLetterIsoCode = "PRT", + NumericIsoCode = 620, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Puerto Rico", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PR", + ThreeLetterIsoCode = "PRI", + NumericIsoCode = 630, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Qatar", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "QA", + ThreeLetterIsoCode = "QAT", + NumericIsoCode = 634, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Romania", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "RO", + ThreeLetterIsoCode = "ROM", + NumericIsoCode = 642, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Russia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "RU", + ThreeLetterIsoCode = "RUS", + NumericIsoCode = 643, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Saudi Arabia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SA", + ThreeLetterIsoCode = "SAU", + NumericIsoCode = 682, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Singapore", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SG", + ThreeLetterIsoCode = "SGP", + NumericIsoCode = 702, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Slovakia (Slovak Republic)", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SK", + ThreeLetterIsoCode = "SVK", + NumericIsoCode = 703, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Slovenia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SI", + ThreeLetterIsoCode = "SVN", + NumericIsoCode = 705, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "South Africa", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ZA", + ThreeLetterIsoCode = "ZAF", + NumericIsoCode = 710, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Spain", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ES", + ThreeLetterIsoCode = "ESP", + NumericIsoCode = 724, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Sweden", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SE", + ThreeLetterIsoCode = "SWE", + NumericIsoCode = 752, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Taiwan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TW", + ThreeLetterIsoCode = "TWN", + NumericIsoCode = 158, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Thailand", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TH", + ThreeLetterIsoCode = "THA", + NumericIsoCode = 764, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Turkey", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TR", + ThreeLetterIsoCode = "TUR", + NumericIsoCode = 792, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Ukraine", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "UA", + ThreeLetterIsoCode = "UKR", + NumericIsoCode = 804, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "United Arab Emirates", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AE", + ThreeLetterIsoCode = "ARE", + NumericIsoCode = 784, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "United Kingdom", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GB", + ThreeLetterIsoCode = "GBR", + NumericIsoCode = 826, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "United States minor outlying islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "UM", + ThreeLetterIsoCode = "UMI", + NumericIsoCode = 581, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Uruguay", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "UY", + ThreeLetterIsoCode = "URY", + NumericIsoCode = 858, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Uzbekistan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "UZ", + ThreeLetterIsoCode = "UZB", + NumericIsoCode = 860, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Venezuela", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VE", + ThreeLetterIsoCode = "VEN", + NumericIsoCode = 862, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Serbia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "RS", + ThreeLetterIsoCode = "SRB", + NumericIsoCode = 688, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Afghanistan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AF", + ThreeLetterIsoCode = "AFG", + NumericIsoCode = 4, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Albania", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AL", + ThreeLetterIsoCode = "ALB", + NumericIsoCode = 8, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Algeria", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "DZ", + ThreeLetterIsoCode = "DZA", + NumericIsoCode = 12, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "American Samoa", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AS", + ThreeLetterIsoCode = "ASM", + NumericIsoCode = 16, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Andorra", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AD", + ThreeLetterIsoCode = "AND", + NumericIsoCode = 20, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Angola", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AO", + ThreeLetterIsoCode = "AGO", + NumericIsoCode = 24, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Anguilla", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AI", + ThreeLetterIsoCode = "AIA", + NumericIsoCode = 660, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Antarctica", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AQ", + ThreeLetterIsoCode = "ATA", + NumericIsoCode = 10, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Antigua and Barbuda", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AG", + ThreeLetterIsoCode = "ATG", + NumericIsoCode = 28, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bahrain", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BH", + ThreeLetterIsoCode = "BHR", + NumericIsoCode = 48, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Barbados", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BB", + ThreeLetterIsoCode = "BRB", + NumericIsoCode = 52, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Benin", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BJ", + ThreeLetterIsoCode = "BEN", + NumericIsoCode = 204, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bhutan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BT", + ThreeLetterIsoCode = "BTN", + NumericIsoCode = 64, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Botswana", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BW", + ThreeLetterIsoCode = "BWA", + NumericIsoCode = 72, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Bouvet Island", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BV", + ThreeLetterIsoCode = "BVT", + NumericIsoCode = 74, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "British Indian Ocean Territory", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IO", + ThreeLetterIsoCode = "IOT", + NumericIsoCode = 86, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Brunei Darussalam", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BN", + ThreeLetterIsoCode = "BRN", + NumericIsoCode = 96, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Burkina Faso", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BF", + ThreeLetterIsoCode = "BFA", + NumericIsoCode = 854, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Burundi", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "BI", + ThreeLetterIsoCode = "BDI", + NumericIsoCode = 108, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cambodia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KH", + ThreeLetterIsoCode = "KHM", + NumericIsoCode = 116, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cameroon", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CM", + ThreeLetterIsoCode = "CMR", + NumericIsoCode = 120, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cape Verde", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CV", + ThreeLetterIsoCode = "CPV", + NumericIsoCode = 132, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Central African Republic", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CF", + ThreeLetterIsoCode = "CAF", + NumericIsoCode = 140, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Chad", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TD", + ThreeLetterIsoCode = "TCD", + NumericIsoCode = 148, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Christmas Island", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CX", + ThreeLetterIsoCode = "CXR", + NumericIsoCode = 162, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cocos (Keeling) Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CC", + ThreeLetterIsoCode = "CCK", + NumericIsoCode = 166, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Comoros", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KM", + ThreeLetterIsoCode = "COM", + NumericIsoCode = 174, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Congo", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CG", + ThreeLetterIsoCode = "COG", + NumericIsoCode = 178, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cook Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CK", + ThreeLetterIsoCode = "COK", + NumericIsoCode = 184, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Cote D'Ivoire", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "CI", + ThreeLetterIsoCode = "CIV", + NumericIsoCode = 384, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Djibouti", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "DJ", + ThreeLetterIsoCode = "DJI", + NumericIsoCode = 262, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Dominica", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "DM", + ThreeLetterIsoCode = "DMA", + NumericIsoCode = 212, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "El Salvador", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SV", + ThreeLetterIsoCode = "SLV", + NumericIsoCode = 222, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Equatorial Guinea", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GQ", + ThreeLetterIsoCode = "GNQ", + NumericIsoCode = 226, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Eritrea", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ER", + ThreeLetterIsoCode = "ERI", + NumericIsoCode = 232, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Estonia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "EE", + ThreeLetterIsoCode = "EST", + NumericIsoCode = 233, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Ethiopia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ET", + ThreeLetterIsoCode = "ETH", + NumericIsoCode = 231, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Falkland Islands (Malvinas)", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "FK", + ThreeLetterIsoCode = "FLK", + NumericIsoCode = 238, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Faroe Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "FO", + ThreeLetterIsoCode = "FRO", + NumericIsoCode = 234, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Fiji", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "FJ", + ThreeLetterIsoCode = "FJI", + NumericIsoCode = 242, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "French Guiana", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GF", + ThreeLetterIsoCode = "GUF", + NumericIsoCode = 254, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "French Polynesia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PF", + ThreeLetterIsoCode = "PYF", + NumericIsoCode = 258, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "French Southern Territories", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TF", + ThreeLetterIsoCode = "ATF", + NumericIsoCode = 260, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Gabon", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GA", + ThreeLetterIsoCode = "GAB", + NumericIsoCode = 266, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Gambia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GM", + ThreeLetterIsoCode = "GMB", + NumericIsoCode = 270, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Ghana", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GH", + ThreeLetterIsoCode = "GHA", + NumericIsoCode = 288, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Greenland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GL", + ThreeLetterIsoCode = "GRL", + NumericIsoCode = 304, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Grenada", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GD", + ThreeLetterIsoCode = "GRD", + NumericIsoCode = 308, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Guadeloupe", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GP", + ThreeLetterIsoCode = "GLP", + NumericIsoCode = 312, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Guam", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GU", + ThreeLetterIsoCode = "GUM", + NumericIsoCode = 316, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Guinea", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GN", + ThreeLetterIsoCode = "GIN", + NumericIsoCode = 324, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Guinea-bissau", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GW", + ThreeLetterIsoCode = "GNB", + NumericIsoCode = 624, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Guyana", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GY", + ThreeLetterIsoCode = "GUY", + NumericIsoCode = 328, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Haiti", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "HT", + ThreeLetterIsoCode = "HTI", + NumericIsoCode = 332, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Heard and Mc Donald Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "HM", + ThreeLetterIsoCode = "HMD", + NumericIsoCode = 334, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Honduras", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "HN", + ThreeLetterIsoCode = "HND", + NumericIsoCode = 340, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Iceland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IS", + ThreeLetterIsoCode = "ISL", + NumericIsoCode = 352, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Iran (Islamic Republic of)", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IR", + ThreeLetterIsoCode = "IRN", + NumericIsoCode = 364, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Iraq", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "IQ", + ThreeLetterIsoCode = "IRQ", + NumericIsoCode = 368, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Kenya", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KE", + ThreeLetterIsoCode = "KEN", + NumericIsoCode = 404, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Kiribati", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KI", + ThreeLetterIsoCode = "KIR", + NumericIsoCode = 296, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Korea", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KR", + ThreeLetterIsoCode = "KOR", + NumericIsoCode = 410, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Kyrgyzstan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KG", + ThreeLetterIsoCode = "KGZ", + NumericIsoCode = 417, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Lao People's Democratic Republic", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LA", + ThreeLetterIsoCode = "LAO", + NumericIsoCode = 418, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Latvia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LV", + ThreeLetterIsoCode = "LVA", + NumericIsoCode = 428, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Lebanon", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LB", + ThreeLetterIsoCode = "LBN", + NumericIsoCode = 422, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Lesotho", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LS", + ThreeLetterIsoCode = "LSO", + NumericIsoCode = 426, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Liberia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LR", + ThreeLetterIsoCode = "LBR", + NumericIsoCode = 430, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Libyan Arab Jamahiriya", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LY", + ThreeLetterIsoCode = "LBY", + NumericIsoCode = 434, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Liechtenstein", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LI", + ThreeLetterIsoCode = "LIE", + NumericIsoCode = 438, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Lithuania", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LT", + ThreeLetterIsoCode = "LTU", + NumericIsoCode = 440, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Luxembourg", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LU", + ThreeLetterIsoCode = "LUX", + NumericIsoCode = 442, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Macau", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MO", + ThreeLetterIsoCode = "MAC", + NumericIsoCode = 446, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Macedonia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MK", + ThreeLetterIsoCode = "MKD", + NumericIsoCode = 807, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Madagascar", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MG", + ThreeLetterIsoCode = "MDG", + NumericIsoCode = 450, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Malawi", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MW", + ThreeLetterIsoCode = "MWI", + NumericIsoCode = 454, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Maldives", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MV", + ThreeLetterIsoCode = "MDV", + NumericIsoCode = 462, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mali", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ML", + ThreeLetterIsoCode = "MLI", + NumericIsoCode = 466, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Malta", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MT", + ThreeLetterIsoCode = "MLT", + NumericIsoCode = 470, + SubjectToVat = true, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Marshall Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MH", + ThreeLetterIsoCode = "MHL", + NumericIsoCode = 584, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Martinique", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MQ", + ThreeLetterIsoCode = "MTQ", + NumericIsoCode = 474, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mauritania", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MR", + ThreeLetterIsoCode = "MRT", + NumericIsoCode = 478, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mauritius", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MU", + ThreeLetterIsoCode = "MUS", + NumericIsoCode = 480, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mayotte", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "YT", + ThreeLetterIsoCode = "MYT", + NumericIsoCode = 175, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Micronesia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "FM", + ThreeLetterIsoCode = "FSM", + NumericIsoCode = 583, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Moldova", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MD", + ThreeLetterIsoCode = "MDA", + NumericIsoCode = 498, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Monaco", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MC", + ThreeLetterIsoCode = "MCO", + NumericIsoCode = 492, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mongolia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MN", + ThreeLetterIsoCode = "MNG", + NumericIsoCode = 496, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Montenegro", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ME", + ThreeLetterIsoCode = "MNE", + NumericIsoCode = 499, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Montserrat", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MS", + ThreeLetterIsoCode = "MSR", + NumericIsoCode = 500, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Morocco", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MA", + ThreeLetterIsoCode = "MAR", + NumericIsoCode = 504, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Mozambique", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MZ", + ThreeLetterIsoCode = "MOZ", + NumericIsoCode = 508, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Myanmar", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MM", + ThreeLetterIsoCode = "MMR", + NumericIsoCode = 104, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Namibia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NA", + ThreeLetterIsoCode = "NAM", + NumericIsoCode = 516, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Nauru", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NR", + ThreeLetterIsoCode = "NRU", + NumericIsoCode = 520, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Nepal", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NP", + ThreeLetterIsoCode = "NPL", + NumericIsoCode = 524, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Netherlands Antilles", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "AN", + ThreeLetterIsoCode = "ANT", + NumericIsoCode = 530, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "New Caledonia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NC", + ThreeLetterIsoCode = "NCL", + NumericIsoCode = 540, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Nicaragua", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NI", + ThreeLetterIsoCode = "NIC", + NumericIsoCode = 558, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Niger", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NE", + ThreeLetterIsoCode = "NER", + NumericIsoCode = 562, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Nigeria", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NG", + ThreeLetterIsoCode = "NGA", + NumericIsoCode = 566, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Niue", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NU", + ThreeLetterIsoCode = "NIU", + NumericIsoCode = 570, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Norfolk Island", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "NF", + ThreeLetterIsoCode = "NFK", + NumericIsoCode = 574, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Northern Mariana Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "MP", + ThreeLetterIsoCode = "MNP", + NumericIsoCode = 580, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Oman", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "OM", + ThreeLetterIsoCode = "OMN", + NumericIsoCode = 512, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Palau", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PW", + ThreeLetterIsoCode = "PLW", + NumericIsoCode = 585, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Panama", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PA", + ThreeLetterIsoCode = "PAN", + NumericIsoCode = 591, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Papua New Guinea", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PG", + ThreeLetterIsoCode = "PNG", + NumericIsoCode = 598, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Pitcairn", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PN", + ThreeLetterIsoCode = "PCN", + NumericIsoCode = 612, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Reunion", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "RE", + ThreeLetterIsoCode = "REU", + NumericIsoCode = 638, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Rwanda", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "RW", + ThreeLetterIsoCode = "RWA", + NumericIsoCode = 646, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Saint Kitts and Nevis", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "KN", + ThreeLetterIsoCode = "KNA", + NumericIsoCode = 659, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Saint Lucia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LC", + ThreeLetterIsoCode = "LCA", + NumericIsoCode = 662, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Saint Vincent and the Grenadines", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VC", + ThreeLetterIsoCode = "VCT", + NumericIsoCode = 670, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Samoa", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "WS", + ThreeLetterIsoCode = "WSM", + NumericIsoCode = 882, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "San Marino", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SM", + ThreeLetterIsoCode = "SMR", + NumericIsoCode = 674, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Sao Tome and Principe", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ST", + ThreeLetterIsoCode = "STP", + NumericIsoCode = 678, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Senegal", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SN", + ThreeLetterIsoCode = "SEN", + NumericIsoCode = 686, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Seychelles", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SC", + ThreeLetterIsoCode = "SYC", + NumericIsoCode = 690, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Sierra Leone", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SL", + ThreeLetterIsoCode = "SLE", + NumericIsoCode = 694, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Solomon Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SB", + ThreeLetterIsoCode = "SLB", + NumericIsoCode = 90, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Somalia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SO", + ThreeLetterIsoCode = "SOM", + NumericIsoCode = 706, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "South Georgia & South Sandwich Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "GS", + ThreeLetterIsoCode = "SGS", + NumericIsoCode = 239, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Sri Lanka", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "LK", + ThreeLetterIsoCode = "LKA", + NumericIsoCode = 144, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "St. Helena", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SH", + ThreeLetterIsoCode = "SHN", + NumericIsoCode = 654, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "St. Pierre and Miquelon", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "PM", + ThreeLetterIsoCode = "SPM", + NumericIsoCode = 666, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Sudan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SD", + ThreeLetterIsoCode = "SDN", + NumericIsoCode = 736, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Suriname", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SR", + ThreeLetterIsoCode = "SUR", + NumericIsoCode = 740, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Svalbard and Jan Mayen Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SJ", + ThreeLetterIsoCode = "SJM", + NumericIsoCode = 744, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Swaziland", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SZ", + ThreeLetterIsoCode = "SWZ", + NumericIsoCode = 748, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Syrian Arab Republic", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "SY", + ThreeLetterIsoCode = "SYR", + NumericIsoCode = 760, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Tajikistan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TJ", + ThreeLetterIsoCode = "TJK", + NumericIsoCode = 762, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Tanzania", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TZ", + ThreeLetterIsoCode = "TZA", + NumericIsoCode = 834, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Togo", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TG", + ThreeLetterIsoCode = "TGO", + NumericIsoCode = 768, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Tokelau", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TK", + ThreeLetterIsoCode = "TKL", + NumericIsoCode = 772, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Tonga", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TO", + ThreeLetterIsoCode = "TON", + NumericIsoCode = 776, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Trinidad and Tobago", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TT", + ThreeLetterIsoCode = "TTO", + NumericIsoCode = 780, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Tunisia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TN", + ThreeLetterIsoCode = "TUN", + NumericIsoCode = 788, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Turkmenistan", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TM", + ThreeLetterIsoCode = "TKM", + NumericIsoCode = 795, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Turks and Caicos Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TC", + ThreeLetterIsoCode = "TCA", + NumericIsoCode = 796, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Tuvalu", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "TV", + ThreeLetterIsoCode = "TUV", + NumericIsoCode = 798, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Uganda", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "UG", + ThreeLetterIsoCode = "UGA", + NumericIsoCode = 800, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Vanuatu", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VU", + ThreeLetterIsoCode = "VUT", + NumericIsoCode = 548, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Vatican City State (Holy See)", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VA", + ThreeLetterIsoCode = "VAT", + NumericIsoCode = 336, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Viet Nam", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VN", + ThreeLetterIsoCode = "VNM", + NumericIsoCode = 704, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Virgin Islands (British)", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VG", + ThreeLetterIsoCode = "VGB", + NumericIsoCode = 92, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Virgin Islands (U.S.)", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "VI", + ThreeLetterIsoCode = "VIR", + NumericIsoCode = 850, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Wallis and Futuna Islands", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "WF", + ThreeLetterIsoCode = "WLF", + NumericIsoCode = 876, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Western Sahara", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "EH", + ThreeLetterIsoCode = "ESH", + NumericIsoCode = 732, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Yemen", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "YE", + ThreeLetterIsoCode = "YEM", + NumericIsoCode = 887, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Zambia", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ZM", + ThreeLetterIsoCode = "ZMB", + NumericIsoCode = 894, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + new Country + { + Name = "Zimbabwe", + AllowsBilling = true, + AllowsShipping = true, + TwoLetterIsoCode = "ZW", + ThreeLetterIsoCode = "ZWE", + NumericIsoCode = 716, + SubjectToVat = false, + DisplayOrder = 100, + Published = true + }, + }; + this.Alter(entities); + return entities; + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Manufacturers.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Manufacturers.cs index 33a51c59fd..51afc48fcd 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Manufacturers.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Manufacturers.cs @@ -1,7 +1,7 @@ -using System; +using System.Collections.Generic; using System.Linq; -using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Discounts; namespace SmartStore.Data.Setup { @@ -9,469 +9,334 @@ public abstract partial class InvariantSeedData { public IList Manufacturers() { - //pictures - var sampleImagesPath = this._sampleImagesPath; - - var manufacturerTemplateInGridAndLines = - this.ManufacturerTemplates().Where(pt => pt.ViewPath == "ManufacturerTemplate.ProductsInGridOrLines").FirstOrDefault(); - - //var categoryTemplateInGridAndLines = - // this.CategoryTemplates().Where(pt => pt.Name == "Products in Grid or Lines").FirstOrDefault(); - - // - - #region Jack Wolfskin + var imagesPath = _sampleImagesPath; + var gridOrLinesTemplate = ManufacturerTemplates().Where(x => x.ViewPath == "ManufacturerTemplate.ProductsInGridOrLines").FirstOrDefault(); + var discounts = _ctx.Set().Where(x => x.DiscountTypeId == (int)DiscountType.AssignedToManufacturers).ToList(); var manufacturerJackWolfskin = new Manufacturer { Name = "Jack-Wolfskin", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/Jack_Wolfskin.png"), Published = true, DisplayOrder = 1 }; - #endregion - - #region Mey & Edlich - - var manufacturerMeyAndEdlich = new Manufacturer - { - Name = "Mey-And-Edlich", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/MeyAndEdlich.jpg", GetSeName("Mey Edlich")), - Published = true, - DisplayOrder = 1 - }; - - #endregion - - #region EA Sports - var manufacturerEASports = new Manufacturer { Name = "EA Sports", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/EA_Sports.png"), Published = true, DisplayOrder = 1 }; - #endregion EA Sports - - #region Warner Home Video Games - var manufacturerWarnerHome = new Manufacturer { Name = "Warner Home Video Games", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/wb.png", GetSeName("Warner Home Video Games")), Published = true, DisplayOrder = 1 }; - #endregion Warner Home Video Games - - #region Breitling - var manufacturerBreitling = new Manufacturer { Name = "Breitling", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/breitling.png"), Published = true, DisplayOrder = 1 }; - - #endregion Breitling - - #region Tissot + if (discounts.Any()) + { + manufacturerBreitling.HasDiscountsApplied = true; + manufacturerBreitling.AppliedDiscounts.Add(discounts.First()); + } var manufacturerTissot = new Manufacturer { Name = "Tissot", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/Tissot.png"), Published = true, DisplayOrder = 1 }; - #endregion Tissot - - #region Seiko - var manufacturerSeiko = new Manufacturer { Name = "Seiko", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/seiko.png"), Published = true, DisplayOrder = 1 }; - - #endregion Seiko - - #region Titleist + if (discounts.Any()) + { + manufacturerSeiko.HasDiscountsApplied = true; + manufacturerSeiko.AppliedDiscounts.Add(discounts.First()); + } var manufacturerTitleist = new Manufacturer { Name = "Titleist", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/titleist.png"), Published = true, DisplayOrder = 1 }; - #endregion Titleist - - #region Puma - var manufacturerPuma = new Manufacturer { Name = "Puma", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/puma.jpg"), Published = true, DisplayOrder = 1 }; - #endregion Puma - - #region Nike - var manufacturerNike = new Manufacturer { Name = "Nike", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/nike.png"), Published = true, DisplayOrder = 1 }; - #endregion Nike - - #region Wilson - - var manufacturerWilson = new Manufacturer - { - Name = "Wilson", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/wilson.png"), - Published = true, - DisplayOrder = 1 - }; - - #endregion Wilson - - #region Adidas - var manufacturerAdidas = new Manufacturer { Name = "Adidas", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/adidas.png"), Published = true, DisplayOrder = 1 }; - #endregion Adidas - - #region Ray-ban - var manufacturerRayban = new Manufacturer { Name = "Ray-Ban", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/ray-ban.jpg"), Published = true, DisplayOrder = 1 }; - #endregion Ray-ban - - #region Oakley - var manufacturerOakley = new Manufacturer { Name = "Oakley", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/oakley.png"), Published = true, DisplayOrder = 1 }; - #endregion Oakley - - #region Apple - var manufacturerApple = new Manufacturer { Name = "Apple", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/apple.png"), Published = true, DisplayOrder = 1 }; - #endregion Apple - - #region Android - - var manufacturerAndroid = new Manufacturer - { - Name = "Android", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/android.png"), - Published = true, - DisplayOrder = 2 - }; - - #endregion Android - - #region LG - - var manufacturerLG = new Manufacturer - { - Name = "LG", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/lg.png"), - Published = true, - DisplayOrder = 3 - }; - - #endregion LG - - #region Dell - - var manufacturerDell = new Manufacturer - { - Name = "Dell", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/dell.png"), - Published = true, - DisplayOrder = 4 - }; - - #endregion Dell - - #region HP - - var manufacturerHP = new Manufacturer - { - Name = "HP", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/hp.png"), - Published = true, - DisplayOrder = 5 - }; - - #endregion HP - - #region Microsoft - var manufacturerMicrosoft = new Manufacturer { Name = "Microsoft", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/microsoft-icon.png", GetSeName("Microsoft")), Published = true, DisplayOrder = 6 }; - #endregion Microsoft - - #region Samsung - - var manufacturerSamsung = new Manufacturer - { - Name = "Samsung", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/samsung.png"), - Published = true, - DisplayOrder = 7 - }; - - #endregion Samsung - - #region Acer - - var manufacturerAcer = new Manufacturer - { - Name = "Acer", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/acer.jpg"), - Published = true, - DisplayOrder = 8 - }; - - #endregion Acer - - #region TrekStor - - var manufacturerTrekStor = new Manufacturer - { - Name = "TrekStor", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/trekstor.png"), - Published = true, - DisplayOrder = 9 - }; - - #endregion TrekStor - - #region Western Digital - - var manufacturerWesternDigital = new Manufacturer - { - Name = "Western Digital", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/westerndigital.png"), - Published = true, - DisplayOrder = 10 - }; - - #endregion Western Digital - - #region MSI - - var manufacturerMSI = new Manufacturer - { - Name = "MSI", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/msi.png"), - Published = true, - DisplayOrder = 11 - }; - - #endregion MSI - - #region Canon - - var manufacturerCanon = new Manufacturer - { - Name = "Canon", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/canon.png"), - Published = true, - DisplayOrder = 12 - }; - - #endregion Canon - - #region Casio - - var manufacturerCasio = new Manufacturer - { - Name = "Casio", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/casio.png"), - Published = true, - DisplayOrder = 13 - }; - - #endregion Casio - - #region Panasonic - - var manufacturerPanasonic = new Manufacturer - { - Name = "Panasonic", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/panasonic.png"), - Published = true, - DisplayOrder = 14 - }; - - #endregion Panasonic - - #region BlackBerry - - var manufacturerBlackBerry = new Manufacturer - { - Name = "BlackBerry", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/blackberry.png"), - Published = true, - DisplayOrder = 15 - }; - - #endregion BlackBerry - - #region HTC - - var manufacturerHTC = new Manufacturer - { - Name = "HTC", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, - MediaFile = CreatePicture("brand/htc.png"), - Published = true, - DisplayOrder = 16 - }; - - #endregion HTC - - #region Festina - var manufacturerFestina = new Manufacturer { Name = "Festina", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/festina.png"), Published = true, DisplayOrder = 17 }; - #endregion Festina - - #region Certina - var manufacturerCertina = new Manufacturer { Name = "Certina", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/certina.png"), Published = true, DisplayOrder = 18 }; - #endregion Certina - - #region Sony - var manufacturerSony = new Manufacturer { Name = "Sony", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/sony.png"), Published = true, DisplayOrder = 19 }; - #endregion Sony - - #region Ubisoft - var manufacturerUbisoft = new Manufacturer { Name = "Ubisoft", - ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + ManufacturerTemplateId = gridOrLinesTemplate.Id, MediaFile = CreatePicture("brand/ubisoft.png"), Published = true, DisplayOrder = 20 }; - #endregion Ubisoft + //var manufacturerMeyAndEdlich = new Manufacturer + //{ + // Name = "Mey-And-Edlich", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/MeyAndEdlich.jpg", GetSeName("Mey Edlich")), + // Published = true, + // DisplayOrder = 1 + //}; + + //var manufacturerWilson = new Manufacturer + //{ + // Name = "Wilson", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/wilson.png"), + // Published = true, + // DisplayOrder = 1 + //}; + //var manufacturerAndroid = new Manufacturer + //{ + // Name = "Android", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/android.png"), + // Published = true, + // DisplayOrder = 2 + //}; + + //var manufacturerLG = new Manufacturer + //{ + // Name = "LG", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/lg.png"), + // Published = true, + // DisplayOrder = 3 + //}; + + //var manufacturerDell = new Manufacturer + //{ + // Name = "Dell", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/dell.png"), + // Published = true, + // DisplayOrder = 4 + //}; + + //var manufacturerHP = new Manufacturer + //{ + // Name = "HP", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/hp.png"), + // Published = true, + // DisplayOrder = 5 + //}; + + //var manufacturerSamsung = new Manufacturer + //{ + // Name = "Samsung", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/samsung.png"), + // Published = true, + // DisplayOrder = 7 + //}; + + //var manufacturerAcer = new Manufacturer + //{ + // Name = "Acer", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/acer.jpg"), + // Published = true, + // DisplayOrder = 8 + //}; + + //var manufacturerTrekStor = new Manufacturer + //{ + // Name = "TrekStor", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/trekstor.png"), + // Published = true, + // DisplayOrder = 9 + //}; + + //var manufacturerWesternDigital = new Manufacturer + //{ + // Name = "Western Digital", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/westerndigital.png"), + // Published = true, + // DisplayOrder = 10 + //}; + + //var manufacturerMSI = new Manufacturer + //{ + // Name = "MSI", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/msi.png"), + // Published = true, + // DisplayOrder = 11 + //}; + + //var manufacturerCanon = new Manufacturer + //{ + // Name = "Canon", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/canon.png"), + // Published = true, + // DisplayOrder = 12 + //}; + + //var manufacturerCasio = new Manufacturer + //{ + // Name = "Casio", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/casio.png"), + // Published = true, + // DisplayOrder = 13 + //}; + + //var manufacturerPanasonic = new Manufacturer + //{ + // Name = "Panasonic", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/panasonic.png"), + // Published = true, + // DisplayOrder = 14 + //}; + + //var manufacturerBlackBerry = new Manufacturer + //{ + // Name = "BlackBerry", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/blackberry.png"), + // Published = true, + // DisplayOrder = 15 + //}; + + //var manufacturerHTC = new Manufacturer + //{ + // Name = "HTC", + // ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + // MediaFile = CreatePicture("brand/htc.png"), + // Published = true, + // DisplayOrder = 16 + //}; var entities = new List { - manufacturerEASports,manufacturerWarnerHome,manufacturerBreitling,manufacturerTissot,manufacturerSeiko, manufacturerTitleist, manufacturerApple, - manufacturerSamsung,manufacturerLG,manufacturerTrekStor, manufacturerWesternDigital,manufacturerDell, manufacturerMSI, - manufacturerCanon, manufacturerCasio, manufacturerPanasonic, manufacturerBlackBerry, manufacturerHTC, manufacturerFestina, manufacturerCertina, - manufacturerHP, manufacturerAcer, manufacturerSony, manufacturerUbisoft, manufacturerOakley, manufacturerRayban, manufacturerAdidas, manufacturerWilson, - manufacturerPuma,manufacturerNike, manufacturerMeyAndEdlich, manufacturerJackWolfskin, manufacturerMicrosoft + manufacturerEASports, manufacturerWarnerHome, manufacturerBreitling, manufacturerTissot, manufacturerSeiko, + manufacturerTitleist, manufacturerApple, manufacturerFestina, manufacturerCertina, + manufacturerSony, manufacturerUbisoft, manufacturerOakley, manufacturerRayban, manufacturerAdidas, + manufacturerPuma, manufacturerNike, manufacturerJackWolfskin, manufacturerMicrosoft, }; - this.Alter(entities); + Alter(entities); return entities; } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Products.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Products.cs index 483a9793ae..1886ae9f64 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Products.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Products.cs @@ -1,10 +1,12 @@ using System; -using System.Linq; using System.Collections.Generic; -using SmartStore.Core.Domain.Catalog; using System.IO; -using SmartStore.Core.Domain.Media; +using System.Linq; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Tax; namespace SmartStore.Data.Setup { @@ -106,7 +108,8 @@ public IList ProductTags() #endregion tag download - #region tag watches + #region Tag Watches + var productTagWatches = new ProductTag { Name = "watches" @@ -114,28 +117,112 @@ public IList ProductTags() _ctx.Set().Where(pt => pt.MetaTitle == "Certina DS Podium Big Size").FirstOrDefault().ProductTags.Add(productTagWatches); - #endregion tag download + #endregion Tag Watches + + #region Tag Furniture + + var productTagFurniture = new ProductTag + { + Name = "Furniture" + }; + + _ctx.Set().Where(x => x.ProductCategories.Any(x => x.Category.ParentCategoryId == 4)).Each(y => y.ProductTags.Add(productTagFurniture)); + + #endregion Tag Furniture var entities = new List { productTagGift, productTagBook, productTagCooking, productTagCars, productTagMotorbikes, - productTagMP3, productTagDownload + productTagMP3, productTagDownload, productTagFurniture }; this.Alter(entities); return entities; } - private List GetFashionProducts(Dictionary specAttributes) + private List GetFashionProducts( + Dictionary categories, + Dictionary manufacturers, + Dictionary specAttributes) { - var result = new List(); - var productTemplateSimple = _ctx.Set().First(x => x.ViewPath == "Product"); - var firstDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 0); - var fashionCategory = _ctx.Set().First(x => x.Alias == "Fashion"); var specialPriceEndDate = DateTime.UtcNow.AddMonths(1); + + var productTemplate = _ctx.Set().First(x => x.ViewPath == "Product"); + var firstDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 0); + var thirdDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 2); + var specOptionCotton = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 9); + var taxCategoryIdApparel = _ctx.Set().First(x => x.Name.Equals(TaxNameApparel)).Id; + + #region Category Shoes + + #region Jack Wolfskin COOGEE LOW + + var jackWolfskinCooGeeLow = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "COOGEE LOW M", + MetaTitle = "COOGEE LOW M", + ShortDescription = "MEN'S LEISURE SHOES", + FullDescription = "

You are always on the go: to the cinema, to the newly opened bar or to the next city festival. The stylish COOGEE XT LOW is THE shoe for your life in the city. Because it combines function with style.

", + Sku = "Wolfskin-4032541", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 69.90M, + OldPrice = 99.95M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 5, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(jackWolfskinCooGeeLow, "product/wolfskin_shoes_coogee_1.png", "wolfskin-shoes-coogee-1"); + + jackWolfskinCooGeeLow.ProductCategories.Add(new ProductCategory { Category = categories["Shoes"], DisplayOrder = 1 }); + jackWolfskinCooGeeLow.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Jack-Wolfskin"], DisplayOrder = 1, }); + + #endregion Jack Wolfskin COOGEE LOW + + #region Adidas SUPERSTAR SCHUH + + var adidasSuperstarSchuh = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "SUPERSTAR SHOE", + MetaTitle = "SUPERSTAR SHOE", + ShortDescription = "THE STREETWEAR CLASSIC WITH THE SHELL TOE.", + FullDescription = "

The Adidas Superstar was first released in 1969 and soon lived up to its name. Today he is considered a street style legend. In this version, the shoe comes with a comfortable full grain leather upper. The look is perfected by the classic rubber shell toe for added durability.

", + Sku = "Adidas-C77124", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 99.95M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 5, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(adidasSuperstarSchuh, "product/adidas_superstar_schuh_1.png", "adidas-superstar-schuh-1"); + + jackWolfskinCooGeeLow.ProductCategories.Add(new ProductCategory { Category = categories["Shoes"], DisplayOrder = 1 }); + jackWolfskinCooGeeLow.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1, }); + + #endregion Adidas SUPERSTAR SCHUH + + #region Converse All Star - // Converse All Star var converseAllStar = new Product { ProductType = ProductType.SimpleProduct, @@ -144,7 +231,7 @@ private List GetFashionProducts(Dictionary ShortDescription = "The classical sneaker!", FullDescription = "

Since 1912 and to this day unrivalled: the converse All Star sneaker. A shoe for every occasion.

", Sku = "Fashion-112355", - ProductTemplateId = productTemplateSimple.Id, + ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, ShowOnHomePage = true, @@ -156,225 +243,775 @@ private List GetFashionProducts(Dictionary NotifyAdminForQuantityBelow = 1, IsShipEnabled = true, DeliveryTime = firstDeliveryTime, - DisplayOrder = 1 + DisplayOrder = 1, + TaxCategoryId = taxCategoryIdApparel }; - converseAllStar.ProductCategories.Add(new ProductCategory + AddProductPicture(converseAllStar, "product/allstar_converse.jpg", "allstar-converse"); + AddProductPicture(converseAllStar, "product/allstar_hi_charcoal.jpg", "allstar-converse-charcoal"); + AddProductPicture(converseAllStar, "product/allstar_hi_maroon.jpg", "allstar-converse-maroon"); + AddProductPicture(converseAllStar, "product/allstar_hi_navy.jpg", "allstar-converse-navy"); + AddProductPicture(converseAllStar, "product/allstar_hi_purple.jpg", "allstar-converse-purple"); + AddProductPicture(converseAllStar, "product/allstar_hi_white.jpg", "allstar-converse-white"); + + converseAllStar.ProductCategories.Add(new ProductCategory { Category = categories["Shoes"], DisplayOrder = 1 }); + + converseAllStar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { - Category = fashionCategory, - DisplayOrder = 1 + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - var allStarImages = new string[] { "product/allstar_converse.jpg", "product/allstar_hi_charcoal.jpg", "product/allstar_hi_maroon.jpg", "product/allstar_hi_navy.jpg", - "product/allstar_hi_purple.jpg", "product/allstar_hi_white.jpg" }; + #endregion Converse All Star + + #endregion Category Shoes - for (var i = 0; i < allStarImages.Length; ++i) + #region Category Jackets + + #region Ladies Jacket + + var ladiesJacket = new Product { - converseAllStar.ProductPictures.Add(new ProductMediaFile - { - MediaFile = CreatePicture(allStarImages[i]), - DisplayOrder = i + 1 - }); + ProductType = ProductType.SimpleProduct, + Name = "Ladies Sports Jacket", + MetaTitle = "Ladies Sports Jacket", + FullDescription = "

Lightweight wind and water repellent fabric, lining of soft single jersey knit cuffs on arm and waistband. 2 side pockets with zipper, hood in slightly waisted cut.

    Material: 100% polyamideLining: 65% polyester, 35% cottonLining 2: 100% polyester.
", + Sku = "Fashion-JN1107", + ManufacturerPartNumber = "JN1107", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 55.00M, + OldPrice = 60.00M, + ProductCost = 20.00M, + SpecialPrice = 52.99M, + SpecialPriceStartDateTimeUtc = new DateTime(2017, 5, 1, 0, 0, 0), + SpecialPriceEndDateTimeUtc = specialPriceEndDate, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 2, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(ladiesJacket, "product/ladies_jacket_silver.jpg", "ladies-jacket-silver"); + AddProductPicture(ladiesJacket, "product/ladies_jacket_black.jpg", "ladies-jacket-black"); + AddProductPicture(ladiesJacket, "product/ladies_jacket_red.jpg", "ladies-jacket-red"); + AddProductPicture(ladiesJacket, "product/ladies_jacket_orange.jpg", "ladies-jacket-orange"); + AddProductPicture(ladiesJacket, "product/ladies_jacket_green.jpg", "ladies-jacket-green"); + AddProductPicture(ladiesJacket, "product/ladies_jacket_blue.jpg", "ladies-jacket-blue"); + AddProductPicture(ladiesJacket, "product/ladies_jacket_navy.jpg", "ladies-jacket-navy"); + + ladiesJacket.ProductCategories.Add(new ProductCategory { Category = categories["Jackets"], DisplayOrder = 1 }); + + ladiesJacket.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 11) + }); + + #endregion Ladies Jacket + + #region Jack Wolfskin KANUKA POINT + + var jackWolfskinKanukaPoint = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "KANUKA POINT JACKET M", + MetaTitle = "KANUKA POINT JACKET M", + ShortDescription = "SOFTSHELL JACKET MEN", + FullDescription = "

Sporty design for sporty tours: The KANUKA POINT likes to be on the move as much as you do. The softshell jacket is made of super-elastic and very breathable material that adapts to your every move on the road. With the KANUKA POINT, you can take every pass with ease, and you don't have to worry about your jacket even when you're scrambling to the top, because its material is very durable, even in windy conditions and light showers.

", + Sku = "jack-1305851", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 83.90M, + OldPrice = 119.95M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 5, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(jackWolfskinKanukaPoint, "product/jack_wolfskin_kanuka_point_1.png"); + + jackWolfskinKanukaPoint.ProductCategories.Add(new ProductCategory { Category = categories["Jackets"], DisplayOrder = 1 }); + jackWolfskinKanukaPoint.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Jack-Wolfskin"], DisplayOrder = 1, }); + + #endregion Jack Wolfskin KANUKA POINT + + #endregion Category Jackets + + #region Category Sunglasses + + #region RayBan TopBar + + var rayBanTopBar = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Ray-Ban Top Bar RB 3183", + IsEsd = false, + ShortDescription = "The Ray-Ban Original Wayfarer is the most famous style in the history of sunglasses. With the original design from 1952 the Wayfarer is popular with celebrities, musicians, artists and fashion experts.", + FullDescription = "

The Ray-Ban ® RB3183 sunglasses give me their aerodynamic shape a reminiscence of speed.

A rectangular shape and the classic Ray-Ban logo imprinted on the straps characterize this light Halbrand model.

", + Sku = "P-3004", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Ray-Ban Top Bar RB 3183", + Price = 139M, + IsGiftCard = false, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(rayBanTopBar, "product/RayBanTopBar_1.jpg", "rayban-top-bar-1"); + AddProductPicture(rayBanTopBar, "product/RayBanTopBar_2.jpg", "rayban-top-bar-2"); + AddProductPicture(rayBanTopBar, "product/RayBanTopBar_3.jpg", "rayban-top-bar-3"); + + rayBanTopBar.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); + rayBanTopBar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); + + #endregion RayBan TopBar + + #region ORIGINAL WAYFARER AT COLLECTION + + var originalWayfarer = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "ORIGINAL WAYFARER AT COLLECTION", + IsEsd = false, + ShortDescription = "The Ray-Ban Original Wayfarer is the most famous style in the history of sunglasses. With the original design from 1952 the Wayfarer is popular with celebrities, musicians, artists and fashion experts.", + FullDescription = "

Radar® EV Path™ PRIZM™ Road

A new milestone in the heritage of performance, Radar® EV takes breakthroughs of a revolutionary design even further with a taller lens that extends the upper field of view. From the comfort and protection of the O Matter® frame to the grip of its Unobtanium® components, this premium design builds on the legacy of Radar innovation and style.

Features

  • PRIZM™ is a revolutionary lens technology that fine-tunes vision for specific sports and environments. See what you’ve been missing. Click here to learn more about Prizm Lens Technology.
  • Path lenses enhance performance if traditional lenses touch your cheeks and help extend the upper field of view
  • Engineered for maximized airflow for optimal ventilation to keep you cool
  • Unobtanium® earsocks and nosepads keep glasses in place, increasing grip despite perspiration
  • Interchangeable Lenses let you change lenses in seconds to optimize vision in any sport environment
", + Sku = "P-3003", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "ORIGINAL WAYFARER AT COLLECTION", + Price = 149M, + IsGiftCard = false, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(originalWayfarer, "product/OriginalWayfarer_1.jpg", "wayfarer-blue-gray-classic-black-1"); + AddProductPicture(originalWayfarer, "product/OriginalWayfarer_2.jpg", "wayfarer-blue-gray-classic-black-2"); + AddProductPicture(originalWayfarer, "product/OriginalWayfarer_3.jpg", "wayfarer-gray-course-black"); + AddProductPicture(originalWayfarer, "product/OriginalWayfarer_4.jpg", "wayfarer-brown-course-havana"); + AddProductPicture(originalWayfarer, "product/OriginalWayfarer_5.jpg", "wayfarer-green-classic-havana-black"); + AddProductPicture(originalWayfarer, "product/OriginalWayfarer_6.jpg", "wayfarer-blue-gray-classic-black-3"); + + originalWayfarer.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); + originalWayfarer.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); + + #endregion ORIGINAL WAYFARER AT COLLECTION + + #region Radar EV Prizm Sports Sunglasses + + var radarSportsSunglasses = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Radar EV Prizm Sports Sunglasses", + IsEsd = false, + ShortDescription = "", + FullDescription = "

Radar® EV Path™ PRIZM™ Road

A new milestone in the heritage of performance, Radar® EV takes breakthroughs of a revolutionary design even further with a taller lens that extends the upper field of view. From the comfort and protection of the O Matter® frame to the grip of its Unobtanium® components, this premium design builds on the legacy of Radar innovation and style.

Features

  • PRIZM™ is a revolutionary lens technology that fine-tunes vision for specific sports and environments. See what you’ve been missing. Click here to learn more about Prizm Lens Technology.
  • Path lenses enhance performance if traditional lenses touch your cheeks and help extend the upper field of view
  • Engineered for maximized airflow for optimal ventilation to keep you cool
  • Unobtanium® earsocks and nosepads keep glasses in place, increasing grip despite perspiration
  • Interchangeable Lenses let you change lenses in seconds to optimize vision in any sport environment
", + Sku = "P-3001", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Radar EV Prizm Sports Sunglasses", + Price = 149M, + IsGiftCard = false, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(radarSportsSunglasses, "product/radar_ev_prizm.jpg"); + + radarSportsSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); + radarSportsSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); + + #endregion Radar EV Prizm Sports Sunglasses + + #region Custom Flak Sunglasses + + var customFlakSunglasses = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Custom Flak Sunglasses", + IsEsd = false, + ShortDescription = "", + FullDescription = "

Radar® EV Path™ PRIZM™ Road

A new milestone in the heritage of performance, Radar® EV takes breakthroughs of a revolutionary design even further with a taller lens that extends the upper field of view. From the comfort and protection of the O Matter® frame to the grip of its Unobtanium® components, this premium design builds on the legacy of Radar innovation and style.

Features

  • PRIZM™ is a revolutionary lens technology that fine-tunes vision for specific sports and environments. See what you’ve been missing. Click here to learn more about Prizm Lens Technology.
  • Path lenses enhance performance if traditional lenses touch your cheeks and help extend the upper field of view
  • Engineered for maximized airflow for optimal ventilation to keep you cool
  • Unobtanium® earsocks and nosepads keep glasses in place, increasing grip despite perspiration
  • Interchangeable Lenses let you change lenses in seconds to optimize vision in any sport environment
", + Sku = "P-3002", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Custom Flak Sunglasses", + Price = 179M, + IsGiftCard = false, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(customFlakSunglasses, "product/CustomFlakSunglasses.jpg", "customflak"); + AddProductPicture(customFlakSunglasses, "product/CustomFlakSunglasses_black_white.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_gray.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_clear.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_jadeiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_positiverediridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_rubyiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_sapphireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_violetiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_24kiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_matteblack_fireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_24kiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_clear.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_fireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_gray.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_jadeiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_positiverediridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_rubyiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_sapphireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_orangeflare_violetiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_24kiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_clear.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_fireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_gray.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_jadeiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_rubyiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_sapphireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_violetiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_polishedwhite_positiverediridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_24kiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_clear.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_fireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_gray.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_jadeiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_positiverediridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_rubyiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_sapphireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_redline_violetiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_24kiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_clear.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_fireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_gray.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_jadeiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_positiverediridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_rubyiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_sapphireiridium.jpg"); + AddProductPicture(customFlakSunglasses, "product/CustomFlak_skyblue_violetiridium.jpg"); + + customFlakSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); + customFlakSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); + + #endregion Custom Flak Sunglasses + + #endregion Category Sunglasses + + #region Shirt Meccanica + + var shirtMeccanica = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Sleeveless shirt Meccanica", + MetaTitle = "Sleeveless shirt Meccanica", + ShortDescription = "Woman shirt with trendy imprint", + FullDescription = "

Also in summer, the Ducati goes with fashion style! With the sleeveless shirt Meccanica, every woman can express her passion for Ducati with a comfortable and versatile piece of clothing. The shirt is available in black and vintage red. It carries on the front the traditional lettering in plastisol print, which makes it even clearer and more radiant, while on the back in the neck area is the famous logo with the typical \"wings\" of the fifties.

", + Sku = "Fashion-987693502", + ManufacturerPartNumber = "987693502", + Gtin = "987693502", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 38.00M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 4, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_black_1.jpg", "shirt-meccanica-black-1"); + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_black_2.jpg", "shirt-meccanica-black-2"); + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_black_3.jpg", "shirt-meccanica-black-3"); + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_red_1.jpg", "shirt-meccanica-red-1"); + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_red_2.jpg", "shirt-meccanica-red-2"); + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_red_3.jpg", "shirt-meccanica-red-3"); + AddProductPicture(shirtMeccanica, "product/shirt_meccanica_red_4.jpg", "shirt-meccanica-red-4"); + + shirtMeccanica.ProductCategories.Add(new ProductCategory { Category = categories["Fashion"], DisplayOrder = 1 }); + + shirtMeccanica.TierPrices.Add(new TierPrice + { + Quantity = 10, + Price = 36.00M + }); + + shirtMeccanica.TierPrices.Add(new TierPrice + { + Quantity = 50, + Price = 29.00M + }); + + shirtMeccanica.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + + #endregion Shirt Meccanica + + #region Clark Premium Blue Jeans + + var clarkPremiumBlueJeans = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Clark Premium Blue Jeans", + MetaTitle = "Clark Premium Blue Jeans", + ShortDescription = "Modern Jeans in Easy Comfort Fit", + FullDescription = "

Real five-pocket jeans by Joker with additional, set-up pocket. Thanks to easy comfort fit with normal rise and comfortable leg width suitable for any character.

  • Material: softer, lighter premium denim made of 100% cotton.
  • Waist (inch): 29-46
  • leg (inch): 30 to 38
", + Sku = "Fashion-65986524", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 109.90M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 5, + TaxCategoryId = taxCategoryIdApparel + }; + + AddProductPicture(clarkPremiumBlueJeans, "product/clark_premium_jeans.jpg", "clark-premium-jeans"); + + clarkPremiumBlueJeans.ProductCategories.Add(new ProductCategory { Category = categories["Trousers"], DisplayOrder = 1 }); + + clarkPremiumBlueJeans.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + + #endregion Clark Premium Blue Jeans + + return new List + { + jackWolfskinCooGeeLow, adidasSuperstarSchuh, converseAllStar, ladiesJacket, jackWolfskinKanukaPoint, + rayBanTopBar, originalWayfarer, radarSportsSunglasses, customFlakSunglasses, shirtMeccanica, + clarkPremiumBlueJeans + }; + } + + private List GetFurnitureProducts(Dictionary categories, Dictionary specAttributes) + { + var productTemplateSimple = _ctx.Set().First(x => x.ViewPath == "Product"); + var thirdDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 2); + var taxCategoryIdElectronics = _ctx.Set().First(x => x.Name.Equals(TaxNameElectronics)).Id; + var discounts = _ctx.Set().Where(x => x.DiscountTypeId == (int)DiscountType.AssignedToSkus && !x.RequiresCouponCode).ToList(); + + #region Category Sofas + + #region Le Corbusier LC2 Sofa + + var corbusierSofa = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Le Corbusier LC2 sofa, 3-seater (1929)", + MetaTitle = "Le Corbusier LC2 sofa, 3-seater (1929)", + ShortDescription = "Sofa 3-seater LC 2, Designer: Le Corbusier, tubular steel frame (chrome), cushions of polyurethane foam and dacron wadding, seat upholstery with down, cover: leather", + FullDescription = "

The 3-seater sofa LC2, together with the two-seater of the same name, is the perfect complement to the well-known Corbusier armchairs. " + + "Together they form a perfectly shaped seating group for lobbies, lofts or salons with high design standards. Even though the Corbusier sofas bear the abbreviation of the designer (LC), they were not designed by him. They merely lean closely on the LC2 and LC3 seating furniture designed by Le Corbusier. " + + "However, this circumstance does not harm the appearance and comfort. The frame of this sofa consists of an elaborately curved tubular steel frame in chrome. The leather cushions are filled with polyurethane foam and dacron wadding, and the seat of the sofa is additionally upholstered with down feathers for optimum seating comfort.

" + + "

Dimensions of the sofa: W x D x H: 180 x 70 x 67 cm, seat height: approx. 45 cm

", + Sku = "LC2 DS/23-1", + ProductTemplateId = productTemplateSimple.Id, + AllowCustomerReviews = true, + Published = true, + Price = 1999.00M, + HasTierPrices = true, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = thirdDeliveryTime, + TaxCategoryId = taxCategoryIdElectronics + }; + + AddProductPicture(corbusierSofa, "product/le_corbusier_lc2_sofa_1.jpg"); + + corbusierSofa.ProductCategories.Add(new ProductCategory { Category = categories["Sofas"], DisplayOrder = 1 }); + + corbusierSofa.TierPrices.Add(new TierPrice + { + Quantity = 2, + Price = 1799.10M + }); + corbusierSofa.TierPrices.Add(new TierPrice + { + Quantity = 4, + Price = 1699.15M + }); + + corbusierSofa.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + corbusierSofa.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 2, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) + }); + + if (discounts.Any()) + { + corbusierSofa.HasDiscountsApplied = true; + corbusierSofa.AppliedDiscounts.Add(discounts.First()); + + // Add the mapping manually. We do not want to wait for the schedule task to do this automatically. + corbusierSofa.ProductCategories.Add(new ProductCategory { Category = categories["Sale"], IsSystemMapping = true }); } - converseAllStar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + #endregion Le Corbusier LC2 Sofa + + #region Josef Hoffmann Sofa + + var josefHoffmannSofa = new Product + { + ProductType = ProductType.SimpleProduct, + Name = "Josef Hoffmann sofa 2-seater Cubus (1910)", + MetaTitle = "Josef Hoffmann sofa 2-seater Cubus (1910)", + ShortDescription = "Sofa Kubus, 2-seater, Designer: Josef Hoffmann, width 166 cm, depth 72 cm, height 77 cm, base frame: solid beech wood, upholstery: solid polyurethane foam (dimensionally stable), cover: leather", + FullDescription = "

The two-seater from Josef Hoffmann's Kubus series is a stylish eye-catcher in residential and business premises. Together with the Kubus armchair and the Kubus three-seater, it creates a stylish seating group for reception halls and large living rooms." + + "Josef Hoffmann's sofa is upholstered with a shape-retaining upholstery, covered with leather and, using a special sewing technique, shows numerous squares that form an overall picture. " + + "The purely geometric form of this Hoffman design was also a pioneer of Cubism, which reached its peak at the beginning of the 20th century.

" + + "

Dimensions: width 166 cm, depth 72 cm, height 77 cm, cbm: 1,50

", + Sku = "JH DS/82-1", + ProductTemplateId = productTemplateSimple.Id, + AllowCustomerReviews = true, + Published = true, + Price = 3099.00M, + HasTierPrices = true, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = thirdDeliveryTime, + TaxCategoryId = taxCategoryIdElectronics + }; + + AddProductPicture(josefHoffmannSofa, "product/josef_hoffmann_sofa_cubus_1.jpg"); + + josefHoffmannSofa.ProductCategories.Add(new ProductCategory { Category = categories["Sofas"], DisplayOrder = 1 }); + + josefHoffmannSofa.TierPrices.Add(new TierPrice + { + Quantity = 2, + Price = 2659.05M + }); + josefHoffmannSofa.TierPrices.Add(new TierPrice + { + Quantity = 4, + Price = 2519.10M + }); + josefHoffmannSofa.TierPrices.Add(new TierPrice + { + Quantity = 6, + Price = 2379.15M + }); + josefHoffmannSofa.TierPrices.Add(new TierPrice + { + Quantity = 10, + Price = 2239.20M + }); + + josefHoffmannSofa.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + josefHoffmannSofa.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, - DisplayOrder = 1, - SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + DisplayOrder = 2, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) }); - result.Add(converseAllStar); + #endregion Josef Hoffmann Sofa - // Shirt Meccanica - var shirtMeccanica = new Product + #region Mies van der Rohe Barcelona Sofa + + var miesBarcelonaSofa = new Product { ProductType = ProductType.SimpleProduct, - Name = "Sleeveless shirt Meccanica", - MetaTitle = "Sleeveless shirt Meccanica", - ShortDescription = "Woman shirt with trendy imprint", - FullDescription = "

Also in summer, the Ducati goes with fashion style! With the sleeveless shirt Meccanica, every woman can express her passion for Ducati with a comfortable and versatile piece of clothing. The shirt is available in black and vintage red. It carries on the front the traditional lettering in plastisol print, which makes it even clearer and more radiant, while on the back in the neck area is the famous logo with the typical \"wings\" of the fifties.

", - Sku = "Fashion-987693502", - ManufacturerPartNumber = "987693502", - Gtin = "987693502", + Name = "Mies van der Rohe Barcelona - Loveseat sofa (1929)", + MetaTitle = "Mies van der Rohe Barcelona - Loveseat sofa (1929)", + ShortDescription = "Armchair Barcelona long, Designer: Mies van der Rohe, L x D x H: 147 x 75 x 75 cm, chrome-plated frame of special spring steel, covering: core leather strips, upholstery with polyurethane foam core, cover: leather", + FullDescription = "

The Loveseat Sofa Barcelona is one of the most famous pieces of furniture of the Bauhaus era. It was designed by Mies van der Rohe and presented at the World Exhibition in Barcelona in 1929. Mies van der Rohe dedicated it to the Spanish royal couple. The Barcelona sofa in the long version is ideal as a seating furniture for exhibitions or business premises. " + + "Together with the slim design and a table by Mies van der Rohe, a stylish sitting area for living rooms is also created. " + + "The Loveseat Barcelona sofa has a frame made of special high quality spring steel, covered with leather strips. " + + "On top of this lies the upholstery with leather covering.individual squares create a symmetrical, stylish picture.

" + + "

Dimensions: length 147 cm, depth 75 cm, height 75 cm, cbm: 0.96

", + Sku = "LR 556", ProductTemplateId = productTemplateSimple.Id, AllowCustomerReviews = true, Published = true, - Price = 38.00M, + Price = 2799.00M, + HasTierPrices = true, ManageInventoryMethod = ManageInventoryMethod.ManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, IsShipEnabled = true, - DeliveryTime = firstDeliveryTime, - DisplayOrder = 4 + DeliveryTime = thirdDeliveryTime, + TaxCategoryId = taxCategoryIdElectronics }; - shirtMeccanica.ProductCategories.Add(new ProductCategory - { - Category = fashionCategory, - DisplayOrder = 1 - }); + AddProductPicture(miesBarcelonaSofa, "product/mies_van_der_rohe_sofa_barcelona_1.jpg"); - shirtMeccanica.TierPrices.Add(new TierPrice + miesBarcelonaSofa.ProductCategories.Add(new ProductCategory { Category = categories["Sofas"], DisplayOrder = 1 }); + + miesBarcelonaSofa.TierPrices.Add(new TierPrice { - Quantity = 10, - Price = 36.00M + Quantity = 4, + Price = 2659.05M }); - shirtMeccanica.TierPrices.Add(new TierPrice + miesBarcelonaSofa.TierPrices.Add(new TierPrice { - Quantity = 50, - Price = 29.00M + Quantity = 6, + Price = 2519.10M }); - - var shirtMeccanicaImages = new string[] { "product/shirt_meccanica_black_1.jpg", "product/shirt_meccanica_black_2.jpg", "product/shirt_meccanica_black_3.jpg", - "product/shirt_meccanica_red_1.jpg", "product/shirt_meccanica_red_2.jpg", "product/shirt_meccanica_red_3.jpg", "product/shirt_meccanica_red_4.jpg" }; - - for (var i = 0; i < shirtMeccanicaImages.Length; ++i) + miesBarcelonaSofa.TierPrices.Add(new TierPrice { - shirtMeccanica.ProductPictures.Add(new ProductMediaFile - { - MediaFile = CreatePicture(shirtMeccanicaImages[i]), - DisplayOrder = i + 1 - }); - } + Quantity = 10, + Price = 2379.15M + }); - shirtMeccanica.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + miesBarcelonaSofa.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + miesBarcelonaSofa.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 2, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) }); - result.Add(shirtMeccanica); + #endregion Mies van der Rohe Barcelona - // Ladies jacket - var ladiesJacket = new Product + #endregion Category Sofas + + #region Category Tables + + #region Le Corbusier LC 6 tTble + + var corbusierTable = new Product { ProductType = ProductType.SimpleProduct, - Name = "Ladies Sports Jacket", - MetaTitle = "Ladies Sports Jacket", - FullDescription = "

Lightweight wind and water repellent fabric, lining of soft single jersey knit cuffs on arm and waistband. 2 side pockets with zipper, hood in slightly waisted cut.

    Material: 100% polyamideLining: 65% polyester, 35% cottonLining 2: 100% polyester.
", - Sku = "Fashion-JN1107", - ManufacturerPartNumber = "JN1107", + Name = "Le Corbusier LC 6 dining table (1929)", + MetaTitle = "Le Corbusier LC 6 dining table (1929)", + ShortDescription = "Dining table LC 6, Designer: Le Corbusier, W x H x D: 225 x 69/74 (adjustable) x 85 cm, substructure: steel pipe, glass plate: Clear or sandblasted, 15 or 19 mm, height-adjustable.", + FullDescription = "

Four small plates carry a glass plate. The structure of the steel pipe is covered in clear structures. The LC6 is a true classic of Bauhaus art and is used in combination with the swivel chairs LC7 as a form-beautiful Le Corbusier dining area. In addition, the table is also increasingly found in offices or in halls. It is height-adjustable and can thus be perfectly adapted to the respective purpose.

Le Corbusier's beautifully shaped table is available with a clear or sandblasted glass plate. The substructure consists of oval steel tubes.

", + Sku = "Furniture-lc6", ProductTemplateId = productTemplateSimple.Id, AllowCustomerReviews = true, Published = true, - Price = 55.00M, - OldPrice = 60.00M, - ProductCost = 20.00M, - SpecialPrice = 52.99M, - SpecialPriceStartDateTimeUtc = new DateTime(2017, 5, 1, 0, 0, 0), - SpecialPriceEndDateTimeUtc = specialPriceEndDate, + Price = 749.00M, + HasTierPrices = true, ManageInventoryMethod = ManageInventoryMethod.ManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, IsShipEnabled = true, - DeliveryTime = firstDeliveryTime, - DisplayOrder = 2 + DeliveryTime = thirdDeliveryTime, + TaxCategoryId = taxCategoryIdElectronics }; - ladiesJacket.ProductCategories.Add(new ProductCategory - { - Category = fashionCategory, - DisplayOrder = 1 - }); + AddProductPicture(corbusierTable, "product/corbusier_lc6_table_1.jpg", "corbusier-lc6-table-1"); + AddProductPicture(corbusierTable, "product/corbusier_lc6_table_2.jpg", "corbusier-lc6-table-2"); + AddProductPicture(corbusierTable, "product/corbusier_lc6_table_3.jpg", "corbusier-lc6-table-3"); + AddProductPicture(corbusierTable, "product/corbusier_lc6_table_4.jpg", "corbusier-lc6-table-4"); - var ladiesJacketImages = new string[] { "product/ladies_jacket_silver.jpg", "product/ladies_jacket_black.jpg", "product/ladies_jacket_red.jpg", - "product/ladies_jacket_orange.jpg", "product/ladies_jacket_green.jpg", "product/ladies_jacket_blue.jpg", "product/ladies_jacket_navy.jpg", }; + corbusierTable.ProductCategories.Add(new ProductCategory { Category = categories["Tables"], DisplayOrder = 1 }); - for (var i = 0; i < ladiesJacketImages.Length; ++i) + corbusierTable.TierPrices.Add(new TierPrice { - ladiesJacket.ProductPictures.Add(new ProductMediaFile - { - MediaFile = CreatePicture(ladiesJacketImages[i]), - DisplayOrder = i + 1 - }); - } + Quantity = 2, + Price = 647.10M + }); + corbusierTable.TierPrices.Add(new TierPrice + { + Quantity = 4, + Price = 636.65M + }); - ladiesJacket.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + corbusierTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 11) + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + corbusierTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 2, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) }); - result.Add(ladiesJacket); + #endregion Le Corbusier LC 6 Table + + #region Isamu Noguchi Coffee Table - // Clark Premium Blue Jeans - var clarkJeans = new Product + var noguchiTable = new Product { ProductType = ProductType.SimpleProduct, - Name = "Clark Premium Blue Jeans", - MetaTitle = "Clark Premium Blue Jeans", - ShortDescription = "Modern Jeans in Easy Comfort Fit", - FullDescription = "

Real five-pocket jeans by Joker with additional, set-up pocket. Thanks to easy comfort fit with normal rise and comfortable leg width suitable for any character.

  • Material: softer, lighter premium denim made of 100% cotton.
  • Waist (inch): 29-46
  • leg (inch): 30 to 38
", - Sku = "Fashion-65986524", + Name = "Isamu Noguchi couch table, Coffee Table (1945)", + MetaTitle = "Isamu Noguchi couch table, Coffee Table (1945)", + ShortDescription = "Coffee table, Designer: Isamu Noguchi, W x H x D: 128 x 40 x 92.5 cm, base: wood, table top: crystal glass, 15 or 19 mm", + FullDescription = "

The coffee table of Isamu Noguchi once impressed even the president of the New York Museum of Modern Art. " + + "Because it was originally designed for him. The curved ash base is an elegant eye-catcher. " + + "It appears unobtrusive and is perfectly shown off to advantage through the transparent, three-sided glass plate. " + + "A Bauhaus - furniture, which today is used in many rooms with upscale furnishings as a side table. " + + "It fits perfectly in the lounge, living room and reception areas.

" + + "

Dimensions: width 128 cm, height 40 cm, depth 92,5 cm

", + Sku = "IN 200", ProductTemplateId = productTemplateSimple.Id, AllowCustomerReviews = true, Published = true, - Price = 109.90M, + Price = 499.00M, + HasTierPrices = true, ManageInventoryMethod = ManageInventoryMethod.ManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, IsShipEnabled = true, - DeliveryTime = firstDeliveryTime, - DisplayOrder = 5 + DeliveryTime = thirdDeliveryTime, + TaxCategoryId = taxCategoryIdElectronics }; - clarkJeans.ProductCategories.Add(new ProductCategory + AddProductPicture(noguchiTable, "product/isamu_noguchi_coffeetable_1.jpg"); + AddProductPicture(noguchiTable, "product/isamu_noguchi_coffeetable_2.jpg"); + + noguchiTable.ProductCategories.Add(new ProductCategory { Category = categories["Tables"], DisplayOrder = 1 }); + + noguchiTable.TierPrices.Add(new TierPrice { - Category = fashionCategory, - DisplayOrder = 1 + Quantity = 2, + Price = 449.10M }); - - clarkJeans.ProductPictures.Add(new ProductMediaFile + noguchiTable.TierPrices.Add(new TierPrice { - MediaFile = CreatePicture("product/clark_premium_jeans.jpg"), - DisplayOrder = 1 + Quantity = 4, + Price = 424.15M }); - clarkJeans.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + noguchiTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + noguchiTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 2, + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) }); - result.Add(clarkJeans); - - return result; - } + #endregion Isamu Noguchi Coffee Table - private List GetFurnitureProducts(Dictionary specAttributes) - { - var result = new List(); - var productTemplateSimple = _ctx.Set().First(x => x.ViewPath == "Product"); - var thirdDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 2); - var furnitureCategory = _ctx.Set().First(x => x.MetaTitle == "Furniture"); + #region Mies van der Rohe Barcelona Table - // Le Corbusier LC 6 table - var corbusierTable = new Product + var miesBarcelonaTable = new Product { ProductType = ProductType.SimpleProduct, - Name = "Le Corbusier LC 6 dining table (1929)", - MetaTitle = "Le Corbusier LC 6 dining table (1929)", - ShortDescription = "Dining table LC 6, designer: Le Corbusier, W x H x D: 225 x 69/74 (adjustable) x 85 cm, substructure: steel pipe, glass plate: Clear or sandblasted, 15 or 19 mm, height-adjustable.", - FullDescription = "

Four small plates carry a glass plate. The structure of the steel pipe is covered in clear structures. The LC6 is a true classic of Bauhaus art and is used in combination with the swivel chairs LC7 as a form-beautiful Le Corbusier dining area. In addition, the table is also increasingly found in offices or in halls. It is height-adjustable and can thus be perfectly adapted to the respective purpose.

Le Corbusier's beautifully shaped table is available with a clear or sandblasted glass plate. The substructure consists of oval steel tubes.

", - Sku = "Furniture-lc6", + Name = "Ludwig Mies van der Rohe table Barcelona (1930)", + MetaTitle = "Ludwig Mies van der Rohe table Barcelona (1930)", + ShortDescription = "Table Barcelona, Designer: Mies van der Rohe, width 90 cm, height 46 cm, depth 90 cm, base: chromed flat steel, table top: glass (12 mm)", + FullDescription = "

This table by Mies van der Rohe matches the famous Barcelona series of armchair and stool, which was designed for the King of Spain and presented at the World Fair in 1929. " + + "Although the coffee table was not made until some time later by Mies van der Rohe for the house 'Tugendhat', it forms an attractive sitting area for offices and living rooms with the furniture of the Barcelona series. " + + "The table by Mies van der Rohe consists of a flat steel frame and a 12 mm thick glass plate, under the transparent plate a chromed 'X' appears through the construction.

" + + "

dimensions: width 90 cm, height 46 cm, depth 90 cm, glass plate thickness: 12 mm

", + Sku = "LM T/98", ProductTemplateId = productTemplateSimple.Id, AllowCustomerReviews = true, Published = true, - Price = 749.00M, + Price = 629.00M, HasTierPrices = true, ManageInventoryMethod = ManageInventoryMethod.ManageStock, OrderMinimumQuantity = 1, @@ -382,46 +1019,38 @@ private List GetFurnitureProducts(Dictionary x.DisplayOrder == 1) }); - corbusierTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + miesBarcelonaTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -429,9 +1058,14 @@ private List GetFurnitureProducts(Dictionary x.DisplayOrder == 14) }); - result.Add(corbusierTable); + #endregion Mies van der Rohe Barcelona Table + + #endregion Category Tables + + #region Category Armchairs + + #region Ball Chair - // Ball Chair var ballChair = new Product { ProductType = ProductType.SimpleProduct, @@ -450,25 +1084,14 @@ private List GetFurnitureProducts(Dictionary GetFurnitureProducts(Dictionary x.DisplayOrder == 5) }); - result.Add(ballChair); + #endregion Ball Chair + + #region Charles Eames Lounge Chair - // Lounge chair var loungeChair = new Product { ProductType = ProductType.SimpleProduct, Name = "Charles Eames Lounge Chair (1956)", MetaTitle = "Charles Eames Lounge Chair (1956)", - ShortDescription = "Club lounge chair, designer: Charles Eames, width 80 cm, depth 80 cm, height 60 cm, seat shell: plywood, foot (rotatable): Aluminium casting, cushion (upholstered) with leather cover.", + ShortDescription = "Club lounge chair, Designer: Charles Eames, width 80 cm, depth 80 cm, height 60 cm, seat shell: plywood, foot (rotatable): Aluminium casting, cushion (upholstered) with leather cover.", FullDescription = "

That's how you sit in a baseball glove. In any case, this was one of the ideas Charles Eames had in mind when designing this club chair. The lounge chair should be a comfort armchair, in which one can sink luxuriously. Through the construction of three interconnected, movable seat shells and a comfortable upholstery Charles Eames succeeded in the implementation. In fact, the club armchair with a swiveling foot is a contrast to the Bauhaus characteristics that emphasized minimalism and functionality. Nevertheless, he became a classic of Bauhaus history and still provides in many living rooms and clubs for absolute comfort with style.

Dimensions: Width 80 cm, depth 60 cm, height total 80 cm (height backrest: 60 cm). CBM: 0.70.

Lounge chair with seat shell of laminated curved plywood with rosewood veneer, walnut nature or in black. Rotatable base made of aluminium cast black with polished edges or optionally fully chromed. Elaborate upholstery of pillows in leather.

All upholstery units are removable at the Eames Lounge chair (seat, armrest, backrest, headrest).

", Sku = "Furniture-lounge-chair", ProductTemplateId = productTemplateSimple.Id, @@ -512,7 +1136,6 @@ private List GetFurnitureProducts(Dictionary GetFurnitureProducts(Dictionary GetFurnitureProducts(Dictionary x.DisplayOrder == 4) }); - result.Add(loungeChair); + #endregion Charles Eames Lounge Chair + + #region Josef Hoffmann Cube Chair - // Cube chair var cubeChair = new Product { ProductType = ProductType.SimpleProduct, @@ -601,20 +1214,13 @@ private List GetFurnitureProducts(Dictionary GetFurnitureProducts(Dictionary x.DisplayOrder == 5) }); - result.Add(cubeChair); + if (discounts.Any()) + { + cubeChair.HasDiscountsApplied = true; + cubeChair.AppliedDiscounts.Add(discounts.First()); + + // Add the mapping manually. We do not want to wait for the schedule task to do this automatically. + cubeChair.ProductCategories.Add(new ProductCategory { Category = categories["Sale"], IsSystemMapping = true }); + } + + #endregion Josef Hoffmann Cube Chair + + #endregion Category Armchairs - return result; + return new List + { + miesBarcelonaTable, noguchiTable, corbusierTable, ballChair, loungeChair, cubeChair, + miesBarcelonaSofa, corbusierSofa, josefHoffmannSofa + }; } public IList Products() @@ -652,135 +1273,15 @@ public IList Products() var manufacturers = _ctx.Set().ToList().ToDictionarySafe(x => x.Name, x => x); var categories = _ctx.Set().ToList().ToDictionarySafe(x => x.Alias, x => x); var specAttributes = _ctx.Set().ToList().ToDictionarySafe(x => x.DisplayOrder, x => x); + var taxCategories = _ctx.Set().ToList().ToDictionarySafe(x => x.Name, x => x); + #region Category Sports - //#region category shoes - - //var categoryShoes = _ctx.Set().First(x => x.Alias == "Shoes"); - - //#region product Jack Wolfskin COOGEE LOW M - - //var productJackWolfskinCooGeeLowM = new Product - //{ - // ProductType = ProductType.SimpleProduct, - // Name = "COOGEE LOW M", - // MetaTitle = "COOGEE LOW M", - // ShortDescription = "MÄNNER FREIZEITSCHUHE", - // FullDescription = "

Du bist immer auf dem Sprung: zum Kino, zur neueröffneten Bar oder zum nächsten Stadtfest. Der stylische COOGEE LOW ist DER Schuh für dein Leben in der Stadt. Denn er verbindet Funktion mit Style. Die sportliche EVA - Zwischensohle ist nicht nur leicht und flexibel, sondern dämpft deine Ferse mit ihrer Erhöhung im hinteren Bereich auch besonders gut und ist sehr komfortabel.So sind deine Füße auch nach vielen Stunden noch fit – du wirst es merken, wenn nach einem langen Tag z.B.noch eine spontane Party ansteht. Die Passform haben wir nach dem Vorbild einer Socke gestaltet, sie ist locker und komfortabel.Dennoch bleibt der Schuh in seiner schmalen Form. Dank des atmungsaktiven Obermaterials bleibt das Fußklima angenehm, egal, wie warm es draussen ist.Gleichzeitig ist der Schuh sehr pflegeleicht.Worauf wartest du also noch ? Ab in die City!

", - // Sku = "Wolfskin-4032541", - // ProductTemplateId = productTemplate.Id, - // AllowCustomerReviews = true, - // Published = true, - // Price = 69.90M, - // OldPrice = 99.95M, - // ManageInventoryMethod = ManageInventoryMethod.ManageStock, - // OrderMinimumQuantity = 1, - // OrderMaximumQuantity = 10000, - // StockQuantity = 10000, - // NotifyAdminForQuantityBelow = 1, - // IsShipEnabled = true, - // DeliveryTime = firstDeliveryTime, - // DisplayOrder = 5 - //}; - - //AddProductPicture(productJackWolfskinCooGeeLowM, "product/wolfskin_shoes_coogee_1.jpg", "jack-wolfskin-shoes-coogee-low-m-1"); - - //productJackWolfskinCooGeeLowM.ProductCategories.Add(new ProductCategory { Category = categoryShoes, DisplayOrder = 1 }); - - //productJackWolfskinCooGeeLowM.ProductManufacturers.Add(new ProductManufacturer - //{ - // Manufacturer = _ctx.Set().Where(c => c.Name == "Jack-Wolfskin").Single(), - // DisplayOrder = 1, - //}); - - //#endregion product Jack Wolfskin COOGEE LOW M - - //#region product Adidas SUPERSTAR SCHUH - - //var productAdidasSuperstarSchuh = new Product - //{ - // ProductType = ProductType.SimpleProduct, - // Name = "SUPERSTAR SCHUH", - // MetaTitle = "SUPERSTAR SCHUH", - // ShortDescription = "DER STREETWEAR-KLASSIKER MIT DER SHELL TOE.", - // FullDescription = "

Der adidas Superstar wurde erstmals 1969 herausgebracht und machte seinem Namen schon bald alle Ehre. Heute gilt er als Streetstyle-Legende. In dieser Version kommt der Schuh mit einem bequemen Obermaterial aus Full-Grain-Leder. Perfekt wird der Look durch die klassische Shell Toe aus Gummi für mehr Strapazierfähigkeit.

", - // Sku = "Adidas-C77124", - // ProductTemplateId = productTemplate.Id, - // AllowCustomerReviews = true, - // Published = true, - // Price = 99.95M, - // ManageInventoryMethod = ManageInventoryMethod.ManageStock, - // OrderMinimumQuantity = 1, - // OrderMaximumQuantity = 10000, - // StockQuantity = 10000, - // NotifyAdminForQuantityBelow = 1, - // IsShipEnabled = true, - // DeliveryTime = firstDeliveryTime, - // DisplayOrder = 5 - //}; - - //AddProductPicture(productAdidasSuperstarSchuh, "product/adidas_superstar_schuh_1.jpg", "adidas-superstar-schuh-1"); - - //productJackWolfskinCooGeeLowM.ProductCategories.Add(new ProductCategory { Category = categoryShoes, DisplayOrder = 1 }); - - //productJackWolfskinCooGeeLowM.ProductManufacturers.Add(new ProductManufacturer - //{ - // Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - // DisplayOrder = 1, - //}); + #region Category Golf + #region Titleist SM6 Tour Chrome - //#endregion product Adidas SUPERSTAR SCHUH - //#endregion category shoes - - #region category jackets - - var categoryJackets = _ctx.Set().First(x => x.Alias == "Jackets"); - - #region product Jack Wolfskin KANUKA POINT - - var productJackWolfsKinKanukaPoint = new Product - { - ProductType = ProductType.SimpleProduct, - Name = "KANUKA POINT JACKET M", - MetaTitle = "KANUKA POINT JACKET M", - ShortDescription = "SOFTSHELLJACKE MÄNNER", - FullDescription = "

Sportliches Design für sportliche Touren: Die KANUKA POINT ist so gern in Bewegung wie du. Die Softshelljacke besteht aus superelastischem und sehr atmungsaktivem Material, das sich unterwegs jeder deiner Bewegungen anpasst. Deswegen nimmst du mit der KANUKA POINT jeden Pass mit Leichtigkeit.Und musst dir auch bei Kraxeleien zum Gipfel keine Gedanken um deine Jacke machen, denn ihr Material hält einiges aus.Auch bei Wind und leichten Schauern bleibst du gelassen.

", - Sku = "jack-1305851", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - Price = 83.90M, - OldPrice = 119.95M, - ManageInventoryMethod = ManageInventoryMethod.ManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - IsShipEnabled = true, - DeliveryTime = firstDeliveryTime, - DisplayOrder = 5 - }; - - AddProductPicture(productJackWolfsKinKanukaPoint, "product/jack_wolfskin_kanuka_point_1.png"); - - productJackWolfsKinKanukaPoint.ProductCategories.Add(new ProductCategory { Category = categoryJackets, DisplayOrder = 1 }); - - productJackWolfsKinKanukaPoint.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Jack-Wolfskin").Single(), - DisplayOrder = 1, - }); - - #endregion product Jack Wolfskin KANUKA POINT - #endregion category jackets - - - #region category golf - - #region product Titleist SM6 Tour Chrome - - var productTitleistSM6TourChrome = new Product + var titleistSM6TourChrome = new Product { ProductType = ProductType.SimpleProduct, Name = "Titleist SM6 Tour Chrome", @@ -802,20 +1303,20 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productTitleistSM6TourChrome, "product/titleist_sm6_tour_chrome.jpg"); + AddProductPicture(titleistSM6TourChrome, "product/titleist_sm6_tour_chrome.jpg"); - productTitleistSM6TourChrome.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + titleistSM6TourChrome.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + titleistSM6TourChrome.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); - productTitleistSM6TourChrome.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); + #endregion Titleist SM6 Tour Chrome - #endregion product Titleist SM6 Tour Chrome + #region Titleist Pro V1x - #region product Titleist Pro V1x - - var productTitleistProV1x = new Product + var titleistProV1x = new Product { ProductType = ProductType.SimpleProduct, Name = "Titleist Pro V1x", @@ -836,20 +1337,20 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productTitleistProV1x, "product/titleist-pro-v1x.jpg"); + AddProductPicture(titleistProV1x, "product/titleist-pro-v1x.jpg"); - productTitleistProV1x.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + titleistProV1x.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + titleistProV1x.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); - productTitleistProV1x.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); + #endregion Titleist Pro V1x - #endregion product Titleist Pro V1x + #region Supreme Golfball - #region product Supreme Golfball - - var productSupremeGolfball = new Product + var supremeGolfball = new Product { ProductType = ProductType.SimpleProduct, Name = "Supreme Golfball", @@ -870,21 +1371,21 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productSupremeGolfball, "product/supremeGolfball_1.jpg"); - AddProductPicture(productSupremeGolfball, "product/supremeGolfball_2.jpg"); + AddProductPicture(supremeGolfball, "product/supremeGolfball_1.jpg"); + AddProductPicture(supremeGolfball, "product/supremeGolfball_2.jpg"); - productSupremeGolfball.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + supremeGolfball.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + supremeGolfball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); - productSupremeGolfball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); + #endregion Supreme Golfball - #endregion product Supreme Golfball + #region GBB Epic Sub Zero Driver - #region product GBB Epic Sub Zero Driver - - var productGBBEpicSubZeroDriver = new Product + var gbbEpicSubZeroDriver = new Product { ProductType = ProductType.SimpleProduct, Name = "GBB Epic Sub Zero Driver", @@ -905,24 +1406,24 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productGBBEpicSubZeroDriver, "product/gbb-epic-sub-zero-driver.jpg"); + AddProductPicture(gbbEpicSubZeroDriver, "product/gbb-epic-sub-zero-driver.jpg"); - productGBBEpicSubZeroDriver.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + gbbEpicSubZeroDriver.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); + gbbEpicSubZeroDriver.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); - productGBBEpicSubZeroDriver.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); + #endregion GBB Epic Sub Zero Driver - #endregion product GBB Epic Sub Zero Driver + #endregion Category Golf - #endregion category golf + #region Category Soccer - #region category Soccer + #region Nike Strike Football - #region product Nike Strike Football - - var productNikeStrikeFootball = new Product + var nikeStrikeFootball = new Product { ProductType = ProductType.SimpleProduct, Name = "Nike Strike Football", @@ -945,16 +1446,16 @@ public IList Products() AllowBackInStockSubscriptions = false, IsShipEnabled = true, DeliveryTimeId = thirdDeliveryTime.Id, - HasTierPrices = true + HasTierPrices = true, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productNikeStrikeFootball, "product/nike-strike-football.jpg"); + AddProductPicture(nikeStrikeFootball, "product/nike-strike-football.jpg"); - productNikeStrikeFootball.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + nikeStrikeFootball.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + nikeStrikeFootball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); - productNikeStrikeFootball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); - - productNikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + nikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -962,7 +1463,7 @@ public IList Products() // Manufacturer -> Nike SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 20) }); - productNikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + nikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -971,15 +1472,15 @@ public IList Products() SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 12) }); - productNikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 26.90M }); - productNikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 12, Price = 24.90M }); - productNikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 24, Price = 22.90M }); + nikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 26.90M }); + nikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 12, Price = 24.90M }); + nikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 24, Price = 22.90M }); - #endregion product Nike Strike Football + #endregion Nike Strike Football - #region product Evopower 5.3 Trainer HS Ball + #region Evopower 5.3 Trainer HS Ball - var productNikeEvoPowerBall = new Product + var nikeEvoPowerBall = new Product { ProductType = ProductType.SimpleProduct, Name = "Evopower 5.3 Trainer HS Ball", @@ -1000,16 +1501,16 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productNikeEvoPowerBall, "product/nike-vopower-53-trainer-hs-ball.jpg"); + AddProductPicture(nikeEvoPowerBall, "product/nike-vopower-53-trainer-hs-ball.jpg"); - productNikeEvoPowerBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + nikeEvoPowerBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + nikeEvoPowerBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); - productNikeEvoPowerBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); - - productNikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + nikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1017,7 +1518,7 @@ public IList Products() // Manufacturer -> Nike SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 20) }); - productNikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + nikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1028,9 +1529,9 @@ public IList Products() #endregion Evopower 5.3 Trainer HS Ball - #region product Torfabrik official game ball + #region Torfabrik official game ball - var productTorfabrikOfficialGameBall = new Product + var torfabrikOfficialGameBall = new Product { ProductType = ProductType.SimpleProduct, Name = "Torfabrik official game ball", @@ -1051,20 +1552,20 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productTorfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_white.png", "official-game-ball-white"); - AddProductPicture(productTorfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_red.png", "official-game-ball-red"); - AddProductPicture(productTorfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_yellow.png", "official-game-ball-yellow"); - AddProductPicture(productTorfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_blue.png", "official-game-ball-blue"); - AddProductPicture(productTorfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_green.png", "official-game-ball-green"); + AddProductPicture(torfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_white.png", "official-game-ball-white"); + AddProductPicture(torfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_red.png", "official-game-ball-red"); + AddProductPicture(torfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_yellow.png", "official-game-ball-yellow"); + AddProductPicture(torfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_blue.png", "official-game-ball-blue"); + AddProductPicture(torfabrikOfficialGameBall, "product/torfabrik-offizieller-spielball_green.png", "official-game-ball-green"); - productTorfabrikOfficialGameBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + torfabrikOfficialGameBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + torfabrikOfficialGameBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); - productTorfabrikOfficialGameBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); - - productTorfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + torfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1072,7 +1573,7 @@ public IList Products() // Manufacturer -> Adidas SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 19) }); - productTorfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + torfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1083,9 +1584,9 @@ public IList Products() #endregion Torfabrik official game ball - #region product Adidas TANGO SALA BALL + #region Adidas TANGO SALA BALL - var productAdidasTangoSalaBall = new Product + var adidasTangoSalaBall = new Product { ProductType = ProductType.SimpleProduct, Name = "Adidas TANGO SALA BALL", @@ -1106,22 +1607,22 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-white.png"); - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-yellow.jpg"); - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-red.jpg"); - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-green.jpg"); - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-gray.jpg"); - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-brown.jpg"); - AddProductPicture(productAdidasTangoSalaBall, "product/adidas-tango-pasadena-ball-blue.jpg"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-white.png"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-yellow.jpg"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-red.jpg"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-green.jpg"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-gray.jpg"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-brown.jpg"); + AddProductPicture(adidasTangoSalaBall, "product/adidas-tango-pasadena-ball-blue.jpg"); - productAdidasTangoSalaBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + adidasTangoSalaBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); + adidasTangoSalaBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); - productAdidasTangoSalaBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); - - productAdidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + adidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1129,7 +1630,7 @@ public IList Products() // Manufacturer -> Adidas SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 19) }); - productAdidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + adidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1140,13 +1641,13 @@ public IList Products() #endregion Adidas TANGO SALA BALL - #endregion category Soccer + #endregion Category Soccer - #region category Basketball + #region Category Basketball #region Wilson Evolution High School Game Basketball - var productEvolutionHighSchoolGameBasketball = new Product + var evolutionBasketball = new Product { ProductType = ProductType.SimpleProduct, Name = "Evolution High School Game Basketball", @@ -1169,24 +1670,24 @@ public IList Products() AllowBackInStockSubscriptions = false, IsShipEnabled = true, DeliveryTimeId = thirdDeliveryTime.Id, - HasTierPrices = true + HasTierPrices = true, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productEvolutionHighSchoolGameBasketball, "product/evolution-high-school-game-basketball.jpg"); + AddProductPicture(evolutionBasketball, "product/evolution-high-school-game-basketball.jpg"); - productEvolutionHighSchoolGameBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); + evolutionBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); + evolutionBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); - productEvolutionHighSchoolGameBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); - - productEvolutionHighSchoolGameBasketball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 24.90M }); - productEvolutionHighSchoolGameBasketball.TierPrices.Add(new TierPrice { Quantity = 12, Price = 22.90M }); - productEvolutionHighSchoolGameBasketball.TierPrices.Add(new TierPrice { Quantity = 24, Price = 20.90M }); + evolutionBasketball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 24.90M }); + evolutionBasketball.TierPrices.Add(new TierPrice { Quantity = 12, Price = 22.90M }); + evolutionBasketball.TierPrices.Add(new TierPrice { Quantity = 24, Price = 20.90M }); #endregion Wilson Evolution High School Game Basketball #region All Court Basketball - var productAllCourtBasketball = new Product + var allCourtBasketball = new Product { ProductType = ProductType.SimpleProduct, Name = "All-Court Basketball", @@ -1207,218 +1708,157 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameApparel].Id }; - AddProductPicture(productAllCourtBasketball, "product/all-court-basketball.png"); + AddProductPicture(allCourtBasketball, "product/all-court-basketball.png"); - productAllCourtBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); - - productAllCourtBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); + allCourtBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); + allCourtBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); #endregion All Court Basketball - #endregion category Basketball + #endregion Category Basketball + + #endregion Category Sports - #region category sunglasses + #region Category Gift Cards - #region product Top bar + #region GiftCard10 - var productRayBanTopBar = new Product + var giftCard10 = new Product { ProductType = ProductType.SimpleProduct, - Name = "Ray-Ban Top Bar RB 3183", - IsEsd = false, - ShortDescription = "The Ray-Ban Original Wayfarer is the most famous style in the history of sunglasses. With the original design from 1952 the Wayfarer is popular with celebrities, musicians, artists and fashion experts.", - FullDescription = "

The Ray-Ban ® RB3183 sunglasses give me their aerodynamic shape a reminiscence of speed.

A rectangular shape and the classic Ray-Ban logo imprinted on the straps characterize this light Halbrand model.

", - Sku = "P-3004", + Name = "$10 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$10 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-1000", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, - MetaTitle = "Ray-Ban Top Bar RB 3183", - Price = 139M, - IsGiftCard = false, - ManageInventoryMethod = ManageInventoryMethod.ManageStock, + MetaTitle = "$10 Virtual Gift Card", + Price = 10M, + IsGiftCard = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DisplayOrder = 1, + TaxCategoryId = taxCategories[TaxNameTaxFree].Id }; - AddProductPicture(productRayBanTopBar, "product/RayBanTopBar_1.jpg", "rayban-top-bar-1"); - AddProductPicture(productRayBanTopBar, "product/RayBanTopBar_2.jpg", "rayban-top-bar-2"); - AddProductPicture(productRayBanTopBar, "product/RayBanTopBar_3.jpg", "rayban-top-bar-3"); - - productRayBanTopBar.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - - productRayBanTopBar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); + AddProductPicture(giftCard10, "product/gift_card_10.png"); + giftCard10.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - #endregion product Top bar + #endregion GiftCard10 - #region product ORIGINAL WAYFARER AT COLLECTION + #region GiftCard25 - var productOriginalWayfarer = new Product + var giftCard25 = new Product { ProductType = ProductType.SimpleProduct, - Name = "ORIGINAL WAYFARER AT COLLECTION", - IsEsd = false, - ShortDescription = "The Ray-Ban Original Wayfarer is the most famous style in the history of sunglasses. With the original design from 1952 the Wayfarer is popular with celebrities, musicians, artists and fashion experts.", - FullDescription = "

Radar® EV Path™ PRIZM™ Road

A new milestone in the heritage of performance, Radar® EV takes breakthroughs of a revolutionary design even further with a taller lens that extends the upper field of view. From the comfort and protection of the O Matter® frame to the grip of its Unobtanium® components, this premium design builds on the legacy of Radar innovation and style.

Features

  • PRIZM™ is a revolutionary lens technology that fine-tunes vision for specific sports and environments. See what you’ve been missing. Click here to learn more about Prizm Lens Technology.
  • Path lenses enhance performance if traditional lenses touch your cheeks and help extend the upper field of view
  • Engineered for maximized airflow for optimal ventilation to keep you cool
  • Unobtanium® earsocks and nosepads keep glasses in place, increasing grip despite perspiration
  • Interchangeable Lenses let you change lenses in seconds to optimize vision in any sport environment
", - Sku = "P-3003", + Name = "$25 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$25 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-1001", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, - MetaTitle = "ORIGINAL WAYFARER AT COLLECTION", - Price = 149M, - IsGiftCard = false, - ManageInventoryMethod = ManageInventoryMethod.ManageStock, + MetaTitle = "$25 Virtual Gift Card", + Price = 25M, + IsGiftCard = true, + GiftCardType = GiftCardType.Virtual, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DisplayOrder = 2, + TaxCategoryId = taxCategories[TaxNameTaxFree].Id }; - AddProductPicture(productOriginalWayfarer, "product/OriginalWayfarer_1.jpg", "wayfarer-blue-gray-classic-black-1"); - AddProductPicture(productOriginalWayfarer, "product/OriginalWayfarer_2.jpg", "wayfarer-blue-gray-classic-black-2"); - AddProductPicture(productOriginalWayfarer, "product/OriginalWayfarer_3.jpg", "wayfarer-gray-course-black"); - AddProductPicture(productOriginalWayfarer, "product/OriginalWayfarer_4.jpg", "wayfarer-brown-course-havana"); - AddProductPicture(productOriginalWayfarer, "product/OriginalWayfarer_5.jpg", "wayfarer-green-classic-havana-black"); - AddProductPicture(productOriginalWayfarer, "product/OriginalWayfarer_6.jpg", "wayfarer-blue-gray-classic-black-3"); - - productOriginalWayfarer.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); + AddProductPicture(giftCard25, "product/gift_card_25.png"); + giftCard25.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - productOriginalWayfarer.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); + #endregion GiftCard25 - #endregion product ORIGINAL WAYFARER AT COLLECTION + #region GiftCard50 - #region product Radar EV Prizm Sports Sunglasses - - var productRadarEVPrizmSportsSunglasses = new Product + var giftCard50 = new Product { ProductType = ProductType.SimpleProduct, - Name = "Radar EV Prizm Sports Sunglasses", - IsEsd = false, - ShortDescription = "", - FullDescription = "

Radar® EV Path™ PRIZM™ Road

A new milestone in the heritage of performance, Radar® EV takes breakthroughs of a revolutionary design even further with a taller lens that extends the upper field of view. From the comfort and protection of the O Matter® frame to the grip of its Unobtanium® components, this premium design builds on the legacy of Radar innovation and style.

Features

  • PRIZM™ is a revolutionary lens technology that fine-tunes vision for specific sports and environments. See what you’ve been missing. Click here to learn more about Prizm Lens Technology.
  • Path lenses enhance performance if traditional lenses touch your cheeks and help extend the upper field of view
  • Engineered for maximized airflow for optimal ventilation to keep you cool
  • Unobtanium® earsocks and nosepads keep glasses in place, increasing grip despite perspiration
  • Interchangeable Lenses let you change lenses in seconds to optimize vision in any sport environment
", - Sku = "P-3001", + Name = "$50 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$50 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-1002", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, - MetaTitle = "Radar EV Prizm Sports Sunglasses", - Price = 149M, - IsGiftCard = false, - ManageInventoryMethod = ManageInventoryMethod.ManageStock, + MetaTitle = "$50 Virtual Gift Card", + Price = 50M, + IsGiftCard = true, + GiftCardType = GiftCardType.Virtual, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DisplayOrder = 3, + TaxCategoryId = taxCategories[TaxNameTaxFree].Id }; - AddProductPicture(productRadarEVPrizmSportsSunglasses, "product/radar_ev_prizm.jpg"); + AddProductPicture(giftCard50, "product/gift_card_50.png"); + giftCard50.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - productRadarEVPrizmSportsSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); + #endregion GiftCard50 - productRadarEVPrizmSportsSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); + #region GiftCard100 - #endregion product Radar EV Prizm Sports Sunglasses - - #region product Custom Flak Sunglasses - - var productCustomFlakSunglasses = new Product + var giftCard100 = new Product { ProductType = ProductType.SimpleProduct, - Name = "Custom Flak Sunglasses", - IsEsd = false, - ShortDescription = "", - FullDescription = "

Radar® EV Path™ PRIZM™ Road

A new milestone in the heritage of performance, Radar® EV takes breakthroughs of a revolutionary design even further with a taller lens that extends the upper field of view. From the comfort and protection of the O Matter® frame to the grip of its Unobtanium® components, this premium design builds on the legacy of Radar innovation and style.

Features

  • PRIZM™ is a revolutionary lens technology that fine-tunes vision for specific sports and environments. See what you’ve been missing. Click here to learn more about Prizm Lens Technology.
  • Path lenses enhance performance if traditional lenses touch your cheeks and help extend the upper field of view
  • Engineered for maximized airflow for optimal ventilation to keep you cool
  • Unobtanium® earsocks and nosepads keep glasses in place, increasing grip despite perspiration
  • Interchangeable Lenses let you change lenses in seconds to optimize vision in any sport environment
", - Sku = "P-3002", + Name = "$100 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$100 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-10033", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, - MetaTitle = "Custom Flak Sunglasses", - Price = 179M, - IsGiftCard = false, - ManageInventoryMethod = ManageInventoryMethod.ManageStock, + MetaTitle = "$100 Virtual Gift Card", + Price = 100M, + IsGiftCard = true, + GiftCardType = GiftCardType.Virtual, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id - }; - - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlakSunglasses.jpg", "customflak"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlakSunglasses_black_white.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_gray.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_clear.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_jadeiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_positiverediridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_rubyiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_sapphireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_violetiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_24kiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_matteblack_fireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_24kiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_clear.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_fireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_gray.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_jadeiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_positiverediridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_rubyiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_sapphireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_orangeflare_violetiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_24kiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_clear.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_fireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_gray.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_jadeiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_rubyiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_sapphireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_violetiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_polishedwhite_positiverediridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_24kiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_clear.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_fireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_gray.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_jadeiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_positiverediridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_rubyiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_sapphireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_redline_violetiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_24kiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_clear.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_fireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_gray.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_jadeiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_positiverediridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_rubyiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_sapphireiridium.jpg"); - AddProductPicture(productCustomFlakSunglasses, "product/CustomFlak_skyblue_violetiridium.jpg"); - - productCustomFlakSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - - productCustomFlakSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); - - #endregion product Custom Flak Sunglasses - - - #endregion category sunglasses - - #region category apple - - #region product iphone plus - - var productIphoneplus = new Product + DisplayOrder = 4, + TaxCategoryId = taxCategories[TaxNameTaxFree].Id + }; + + AddProductPicture(giftCard100, "product/gift_card_100.png"); + giftCard100.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); + + #endregion GiftCard100 + + #endregion Category Gift Cards + + #region Category Apple + + #region IPhone Plus + + var iPhonePlus = new Product { ProductType = ProductType.SimpleProduct, Name = "iPhone Plus", @@ -1441,20 +1881,22 @@ public IList Products() AllowBackInStockSubscriptions = false, IsShipEnabled = true, IsFreeShipping = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productIphoneplus, "product/iphone-plus_all_colors.jpg"); - AddProductPicture(productIphoneplus, "product/iphoneplus_1.jpg", "iphone-plus-default"); - AddProductPicture(productIphoneplus, "product/iphone-plus_red.jpg"); - AddProductPicture(productIphoneplus, "product/iphone-plus_silver.jpg"); - AddProductPicture(productIphoneplus, "product/iphone-plus_black.jpg"); - AddProductPicture(productIphoneplus, "product/iphone-plus_rose.jpg"); - AddProductPicture(productIphoneplus, "product/iphone-plus_gold.jpg"); + AddProductPicture(iPhonePlus, "product/iphone-plus_all_colors.jpg"); + AddProductPicture(iPhonePlus, "product/iphoneplus_1.jpg", "iphone-plus-default"); + AddProductPicture(iPhonePlus, "product/iphone-plus_red.jpg"); + AddProductPicture(iPhonePlus, "product/iphone-plus_silver.jpg"); + AddProductPicture(iPhonePlus, "product/iphone-plus_black.jpg"); + AddProductPicture(iPhonePlus, "product/iphone-plus_rose.jpg"); + AddProductPicture(iPhonePlus, "product/iphone-plus_gold.jpg"); - productIphoneplus.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + iPhonePlus.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + iPhonePlus.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPhonePlus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1463,7 +1905,7 @@ public IList Products() SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPhonePlus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1471,7 +1913,7 @@ public IList Products() // storage capacity -> 64gb SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPhonePlus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1479,7 +1921,7 @@ public IList Products() // storage capacity -> 128gb SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPhonePlus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1488,14 +1930,13 @@ public IList Products() SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - #endregion product iphone plus + #endregion IPhone Plus - #region product Watch Series 2 + #region Watch Series 2 - var productWatchSeries2 = new Product + var watchSeries2 = new Product { ProductType = ProductType.SimpleProduct, - Visibility = ProductVisibility.Hidden, Name = "Watch Series 2", IsEsd = false, ShortDescription = "Live a better day. Built-in GPS. Water resistance to 50 meters.1 A lightning-fast dual‑core processor. And a display that’s two times brighter than before. Full of features that help you stay active, motivated, and connected, Apple Watch Series 2 is the perfect partner for a healthy life.", @@ -1515,17 +1956,17 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productWatchSeries2, "product/watchseries2_1.jpg"); - AddProductPicture(productWatchSeries2, "product/watchseries2_2.jpg"); + AddProductPicture(watchSeries2, "product/watchseries2_1.jpg"); + AddProductPicture(watchSeries2, "product/watchseries2_2.jpg"); - productWatchSeries2.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + watchSeries2.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + watchSeries2.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - productWatchSeries2.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - - productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + watchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1534,7 +1975,7 @@ public IList Products() SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); - productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + watchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1543,7 +1984,7 @@ public IList Products() SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + watchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1552,11 +1993,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - #endregion product Watch Series 2 + #endregion Watch Series 2 - #region product Airpods + #region Airpods - var productAirpods = new Product + var airPods = new Product { ProductType = ProductType.SimpleProduct, Name = "AirPods", @@ -1577,25 +2018,25 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productAirpods, "product/airpods_white.jpg", "airpods-white"); - AddProductPicture(productAirpods, "product/airpods_turquoise.jpg", "airpods-turquoise"); - AddProductPicture(productAirpods, "product/airpods_lightblue.jpg", "airpods-lightblue"); - AddProductPicture(productAirpods, "product/airpods_rose.jpg", "airpods-rose"); - AddProductPicture(productAirpods, "product/airpods_gold.jpg", "airpods-gold"); - AddProductPicture(productAirpods, "product/airpods_mint.jpg", "airpods-mint"); + AddProductPicture(airPods, "product/airpods_white.jpg", "airpods-white"); + AddProductPicture(airPods, "product/airpods_turquoise.jpg", "airpods-turquoise"); + AddProductPicture(airPods, "product/airpods_lightblue.jpg", "airpods-lightblue"); + AddProductPicture(airPods, "product/airpods_rose.jpg", "airpods-rose"); + AddProductPicture(airPods, "product/airpods_gold.jpg", "airpods-gold"); + AddProductPicture(airPods, "product/airpods_mint.jpg", "airpods-mint"); - productAirpods.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + airPods.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + airPods.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 7 }); - productAirpods.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 7 }); + #endregion Airpods - #endregion product Airpods + #region Ultimate Apple Pro Hipster Bundle - #region product Ultimate Apple Pro Hipster Bundle - - var productAppleProHipsterBundle = new Product + var appleProHipsterBundle = new Product { ProductType = ProductType.BundledProduct, Name = "Ultimate Apple Pro Hipster Bundle", @@ -1620,24 +2061,24 @@ public IList Products() DeliveryTimeId = thirdDeliveryTime.Id, BundleTitleText = "Bundle includes", BundlePerItemPricing = true, - BundlePerItemShoppingCart = true + BundlePerItemShoppingCart = true, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productAppleProHipsterBundle, "product/ultimate-apple-pro-hipster-bundle.jpg"); - AddProductPicture(productAppleProHipsterBundle, "product/airpods_white.jpg", "bundle-airpods-white"); - AddProductPicture(productAppleProHipsterBundle, "product/watchseries2_2.jpg", "bundle-watchseries"); - AddProductPicture(productAppleProHipsterBundle, "product/iphoneplus_2.jpg", "bundle-iphoneplus"); - AddProductPicture(productAppleProHipsterBundle, "category/apple.png", "bundle-apple"); - - productAppleProHipsterBundle.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + AddProductPicture(appleProHipsterBundle, "product/ultimate-apple-pro-hipster-bundle.jpg"); + AddProductPicture(appleProHipsterBundle, "product/airpods_white.jpg", "bundle-airpods-white"); + AddProductPicture(appleProHipsterBundle, "product/watchseries2_2.jpg", "bundle-watchseries"); + AddProductPicture(appleProHipsterBundle, "product/iphoneplus_2.jpg", "bundle-iphoneplus"); + AddProductPicture(appleProHipsterBundle, "category/apple.png", "bundle-apple"); - productAppleProHipsterBundle.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); + appleProHipsterBundle.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + appleProHipsterBundle.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - #endregion product Ultimate Apple Pro Hipster Bundle + #endregion Ultimate Apple Pro Hipster Bundle - #region product 9,7 iPad + #region 9,7 IPad - var product97ipad = new Product + var iPad = new Product { ProductType = ProductType.SimpleProduct, Name = "9,7' iPad", @@ -1651,7 +2092,6 @@ public IList Products() ShowOnHomePage = true, MetaTitle = "9,7' iPad", Price = 319.00M, - OldPrice = 349.00M, SpecialPrice = 299.00M, SpecialPriceStartDateTimeUtc = new DateTime(2017, 5, 1, 0, 0, 0), SpecialPriceEndDateTimeUtc = specialPriceEndDate, @@ -1663,26 +2103,26 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(product97ipad, "product/ipad_1.jpg"); - AddProductPicture(product97ipad, "product/ipad_2.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-yellow.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-turquoise.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-lightblue.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-purple.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-mint.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-rose.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-spacegray.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-gold.jpg"); - AddProductPicture(product97ipad, "product/97-ipad-silver.jpg"); - - product97ipad.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + AddProductPicture(iPad, "product/ipad_1.jpg"); + AddProductPicture(iPad, "product/ipad_2.jpg"); + AddProductPicture(iPad, "product/97-ipad-yellow.jpg"); + AddProductPicture(iPad, "product/97-ipad-turquoise.jpg"); + AddProductPicture(iPad, "product/97-ipad-lightblue.jpg"); + AddProductPicture(iPad, "product/97-ipad-purple.jpg"); + AddProductPicture(iPad, "product/97-ipad-mint.jpg"); + AddProductPicture(iPad, "product/97-ipad-rose.jpg"); + AddProductPicture(iPad, "product/97-ipad-spacegray.jpg"); + AddProductPicture(iPad, "product/97-ipad-gold.jpg"); + AddProductPicture(iPad, "product/97-ipad-silver.jpg"); - product97ipad.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); + iPad.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + iPad.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1691,7 +2131,7 @@ public IList Products() SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1699,7 +2139,7 @@ public IList Products() // storage capacity -> 64gb SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1707,7 +2147,7 @@ public IList Products() // storage capacity -> 128gb SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + iPad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1716,142 +2156,15 @@ public IList Products() SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - #endregion product 9,7 iPad - - #endregion category apple - - #region category Gift Cards - - #region product10GiftCard - - var product10GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - Name = "$10 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$10 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-1000", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$10 Virtual Gift Card", - Price = 10M, - IsGiftCard = true, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 1 - }; - - AddProductPicture(product10GiftCard, "product/gift_card_10.png"); - product10GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - - #endregion product10GiftCard - - #region product25GiftCard - - var product25GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - Name = "$25 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$25 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-1001", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$25 Virtual Gift Card", - Price = 25M, - IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 2 - }; - - AddProductPicture(product25GiftCard, "product/gift_card_25.png"); - product25GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - - #endregion product25GiftCard - - #region product50GiftCard - - var product50GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - Name = "$50 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$50 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-1002", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$50 Virtual Gift Card", - Price = 50M, - IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 3 - }; - - AddProductPicture(product50GiftCard, "product/gift_card_50.png"); - product50GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - - #endregion product50GiftCard - - #region product100GiftCard - - var product100GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - Name = "$100 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$100 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-10033", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$100 Virtual Gift Card", - Price = 100M, - IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 4, - }; - - AddProductPicture(product100GiftCard, "product/gift_card_100.png"); - product100GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); + #endregion 9,7 IPad - #endregion product100GiftCard + #endregion Category Apple - #endregion category Gift Cards + #region Category Books - #region category books + #region Überman - #region productBooksUberMan - - var productBooksUberMan = new Product + var booksUberMan = new Product { ProductType = ProductType.SimpleProduct, Name = "Überman: The novel", @@ -1869,27 +2182,28 @@ public IList Products() StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - IsShipEnabled = true + IsShipEnabled = true, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksUberMan, "product/0000932_uberman-der-roman.jpeg"); - productBooksUberMan.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); + AddProductPicture(booksUberMan, "product/0000932_uberman-der-roman.jpeg"); + booksUberMan.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 3, SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 3, SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1897,11 +2211,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksUberMan + #endregion Überman - #region productBooksGefangeneDesHimmels + #region Gefangene des Himmels - var productBooksGefangeneDesHimmels = new Product + var booksGefangeneDesHimmels = new Product { ProductType = ProductType.SimpleProduct, Name = "The Prisoner of Heaven: A Novel", @@ -1920,13 +2234,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksGefangeneDesHimmels, "product/0000935_der-gefangene-des-himmels-roman_300.jpeg"); - productBooksGefangeneDesHimmels.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); + AddProductPicture(booksGefangeneDesHimmels, "product/0000935_der-gefangene-des-himmels-roman_300.jpeg"); + booksGefangeneDesHimmels.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1935,7 +2250,7 @@ public IList Products() SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1943,7 +2258,7 @@ public IList Products() // Category -> bound SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1952,11 +2267,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksGefangeneDesHimmels + #endregion Gefangene des Himmels - #region productBooksBestGrillingRecipes + #region Best grilling recipes - var productBooksBestGrillingRecipes = new Product + var booksBestGrillingRecipes = new Product { ProductType = ProductType.SimpleProduct, Name = "Best Grilling Recipes", @@ -1975,13 +2290,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksBestGrillingRecipes, "product/bestgrillingrecipes.jpg"); - productBooksBestGrillingRecipes.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); + AddProductPicture(booksBestGrillingRecipes, "product/bestgrillingrecipes.jpg"); + booksBestGrillingRecipes.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1989,7 +2305,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -1997,7 +2313,7 @@ public IList Products() // Category -> cook & bake SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2006,11 +2322,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - #endregion productBooksBestGrillingRecipes + #endregion Best grilling recipes - #region productBooksCookingForTwo + #region Cooking for two - var productBooksCookingForTwo = new Product + var booksCookingForTwo = new Product { ProductType = ProductType.SimpleProduct, Name = "Cooking for Two", @@ -2029,13 +2345,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = secondDeliveryTime.Id + DeliveryTimeId = secondDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksCookingForTwo, "product/cookingfortwo.jpg"); - productBooksCookingForTwo.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); + AddProductPicture(booksCookingForTwo, "product/cookingfortwo.jpg"); + booksCookingForTwo.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2043,7 +2360,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2051,7 +2368,7 @@ public IList Products() // Category -> cook & bake SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2060,16 +2377,16 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - #endregion productBooksCookingForTwo + #endregion Cooking for two - #region productBooksAutosDerSuperlative + #region Autos der Superlative - var productBooksAutosDerSuperlative = new Product + var booksAutosDerSuperlative = new Product { ProductType = ProductType.SimpleProduct, - Name = "Car of superlatives: the strongest, the first, the most beautiful, the fastest", + Name = "Cars of superlatives: The strongest, the first, the most beautiful, the fastest", ShortDescription = "Hardcover", - FullDescription = "

For some, the car is only a useful means of transportation. For everyone else, there are 'cars - The Ultimate Guide' of art-connoisseur Michael Doerflinger. With authentic images, all important data and a lot of information can be presented to the fastest, most innovative, the strongest, the most unusual and the most successful examples of automotive history. A comprehensive manual for the specific reference and extensive browsing.

", + FullDescription = "

For some, the car is only a useful means of transportation. For everyone else, there are 'cars - The Ultimate Guide' of art-connoisseur Michael Doerflinger. With authentic images, all important data and a lot of information can be presented to the fastest, most innovative, the strongest, the most unusual and the most successful examples of automotive history. A comprehensive manual for the specific reference and extensive browsing.

", Sku = "P-1007", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, @@ -2083,13 +2400,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksAutosDerSuperlative, "product/0000944_autos-der-superlative-die-starksten-die-ersten-die-schonsten-die-schnellsten.jpeg"); - productBooksAutosDerSuperlative.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); + AddProductPicture(booksAutosDerSuperlative, "product/0000944_autos-der-superlative-die-starksten-die-ersten-die-schonsten-die-schnellsten.jpeg"); + booksAutosDerSuperlative.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2097,7 +2415,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2105,7 +2423,7 @@ public IList Products() // Category -> cars SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2114,11 +2432,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksAutosDerSuperlative + #endregion Autos der Superlative - #region productBooksBildatlasMotorraeder + #region Bildatlas Motorraeder - var productBooksBildatlasMotorraeder = new Product + var booksBildatlasMotorraeder = new Product { ProductType = ProductType.SimpleProduct, Name = "Picture Atlas Motorcycles: With more than 350 brilliant images", @@ -2137,13 +2455,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksBildatlasMotorraeder, "product/0000942_bildatlas-motorrader-mit-mehr-als-350-brillanten-abbildungen.jpeg"); - productBooksBildatlasMotorraeder.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); + AddProductPicture(booksBildatlasMotorraeder, "product/0000942_bildatlas-motorrader-mit-mehr-als-350-brillanten-abbildungen.jpeg"); + booksBildatlasMotorraeder.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2151,7 +2470,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2159,7 +2478,7 @@ public IList Products() // Category -> non-fiction SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2168,11 +2487,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksBildatlasMotorraeder + #endregion Bildatlas Motorraeder - #region productBooksAutoBuch + #region Auto Buch - var productBooksAutoBuch = new Product + var booksAutoBuch = new Product { ProductType = ProductType.SimpleProduct, Name = "The Car Book. The great history with over 1200 models", @@ -2191,13 +2510,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksAutoBuch, "product/0000947_das-auto-buch-die-grose-chronik-mit-uber-1200-modellen_300.jpeg"); - productBooksAutoBuch.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); + AddProductPicture(booksAutoBuch, "product/0000947_das-auto-buch-die-grose-chronik-mit-uber-1200-modellen_300.jpeg"); + booksAutoBuch.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2205,7 +2525,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2213,7 +2533,7 @@ public IList Products() // Category -> non-fiction SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2222,11 +2542,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksAutoBuch + #endregion Auto Buch - #region productBooksFastCars + #region Fast cars - var productBooksFastCars = new Product + var booksFastCars = new Product { ProductType = ProductType.SimpleProduct, Name = "Fast Cars, Image Calendar 2013", @@ -2245,13 +2565,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksFastCars, "product/0000946_fast-cars-bildkalender-2013_300.jpeg"); - productBooksFastCars.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); + AddProductPicture(booksFastCars, "product/0000946_fast-cars-bildkalender-2013_300.jpeg"); + booksFastCars.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2259,7 +2580,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2267,7 +2588,7 @@ public IList Products() // Category -> cars SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2276,11 +2597,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksFastCars + #endregion Fast cars - #region productBooksMotorradAbenteuer + #region Motorrad Abenteuer - var productBooksMotorradAbenteuer = new Product + var booksMotorradAbenteuer = new Product { ProductType = ProductType.SimpleProduct, Name = "Motorcycle Adventures: Riding for travel enduros", @@ -2299,13 +2620,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = secondDeliveryTime.Id + DeliveryTimeId = secondDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameBooks].Id }; - AddProductPicture(productBooksMotorradAbenteuer, "product/0000943_motorrad-abenteuer-fahrtechnik-fur-reise-enduros.jpeg"); - productBooksMotorradAbenteuer.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); + AddProductPicture(booksMotorradAbenteuer, "product/0000943_motorrad-abenteuer-fahrtechnik-fur-reise-enduros.jpeg"); + booksMotorradAbenteuer.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2313,7 +2635,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2321,7 +2643,7 @@ public IList Products() // Category -> cars SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2330,15 +2652,15 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion productBooksMotorradAbenteuer + #endregion Motorrad Abenteuer - #endregion category books + #endregion Category Books - #region Instant Download Music / Digital Products + #region Category Digital Products & Instant Downloads - #region product Books Stone of the Wise + #region Stone of the Wise - var productBooksStoneOfTheWise = new Product + var booksStoneOfTheWise = new Product { ProductType = ProductType.SimpleProduct, Name = "Ebook 'Stone of the Wise' in 'Lorem ipsum'", @@ -2358,13 +2680,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsDownload = true, - HasSampleDownload = true + HasSampleDownload = true, + TaxCategoryId = taxCategories[TaxNameDigitalGoods].Id }; - AddProductPicture(productBooksStoneOfTheWise, "product/stone_of_wisdom.jpg"); - productBooksStoneOfTheWise.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); + AddProductPicture(booksStoneOfTheWise, "product/stone_of_wisdom.jpg"); + booksStoneOfTheWise.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); - productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2372,7 +2695,7 @@ public IList Products() // Edition -> bound SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2380,7 +2703,7 @@ public IList Products() // Category -> cars SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); - productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + booksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2389,12 +2712,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - #endregion product Books Stone of the Wise + #endregion Stone of the Wise + #region Antonio Vivaldi: Spring - #region Antonio Vivaldi: then spring - - var productInstantDownloadVivaldi = new Product + var instantDownloadVivaldi = new Product { ProductType = ProductType.SimpleProduct, Name = "Antonio Vivaldi: spring", @@ -2414,13 +2736,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsDownload = true, - HasSampleDownload = true + HasSampleDownload = true, + TaxCategoryId = taxCategories[TaxNameDigitalGoods].Id }; - AddProductPicture(productInstantDownloadVivaldi, "product/vivaldi.jpg"); - productInstantDownloadVivaldi.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); + AddProductPicture(instantDownloadVivaldi, "product/vivaldi.jpg"); + instantDownloadVivaldi.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); - productInstantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + instantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2428,7 +2751,7 @@ public IList Products() // mp3 quality > 320 kbit/S SpecificationAttributeOption = specAttributes[18].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productInstantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + instantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2437,11 +2760,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[19].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); - #endregion Antonio Vivildi: then spring + #endregion Antonio Vivaldi: Spring #region Beethoven für Elise - var productInstantDownloadBeethoven = new Product + var instantDownloadBeethoven = new Product { ProductType = ProductType.SimpleProduct, Name = "Ludwig van Beethoven: For Elise", @@ -2462,13 +2785,14 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsDownload = true, - HasSampleDownload = true + HasSampleDownload = true, + TaxCategoryId = taxCategories[TaxNameDigitalGoods].Id }; - AddProductPicture(productInstantDownloadBeethoven, "product/Beethoven.jpg"); - productInstantDownloadBeethoven.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); + AddProductPicture(instantDownloadBeethoven, "product/Beethoven.jpg"); + instantDownloadBeethoven.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); - productInstantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + instantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2476,7 +2800,7 @@ public IList Products() // mp3 quality > 320 kbit/S SpecificationAttributeOption = specAttributes[18].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productInstantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + instantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2487,13 +2811,13 @@ public IList Products() #endregion Beethoven für Elise - #endregion Instant Download Music + #endregion Category Digital Products & Instant Downloads - #region watches + #region Category Watches - #region productTRANSOCEANCHRONOGRAPH + #region Transocean Chronograph - var productTRANSOCEANCHRONOGRAPH = new Product + var transoceanChronograph = new Product { ProductType = ProductType.SimpleProduct, Name = "TRANSOCEAN CHRONOGRAPH", @@ -2506,7 +2830,6 @@ public IList Products() MetaTitle = "TRANSOCEAN CHRONOGRAPH", ShowOnHomePage = true, Price = 24110.00M, - OldPrice = 26230.00M, ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, @@ -2514,15 +2837,15 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameJewelry].Id }; - AddProductPicture(productTRANSOCEANCHRONOGRAPH, "product/transocean-chronograph.jpg"); - productTRANSOCEANCHRONOGRAPH.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + AddProductPicture(transoceanChronograph, "product/transocean-chronograph.jpg"); + transoceanChronograph.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + transoceanChronograph.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Breitling"], DisplayOrder = 1 }); - productTRANSOCEANCHRONOGRAPH.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Breitling"], DisplayOrder = 1 }); - - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2530,7 +2853,7 @@ public IList Products() // offer > promotion SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2538,7 +2861,7 @@ public IList Products() // manufacturer > Breitling SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 18) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2546,7 +2869,7 @@ public IList Products() // housing > steel SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2554,7 +2877,7 @@ public IList Products() // material -> leather SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2562,7 +2885,7 @@ public IList Products() // Gender -> gentlemen SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2570,7 +2893,7 @@ public IList Products() // movement -> mechanical, self winding SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2578,7 +2901,7 @@ public IList Products() // diameter -> 44mm SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + transoceanChronograph.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2587,11 +2910,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - #endregion productTRANSOCEANCHRONOGRAPH + #endregion Transocean Chronograph - #region productTissotT-TouchExpertSolar + #region TissotT Touch Expert Solar - var productTissotTTouchExpertSolar = new Product + var tissotTTouchExpertSolar = new Product { ProductType = ProductType.SimpleProduct, Name = "Tissot T-Touch Expert Solar", @@ -2612,17 +2935,17 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameJewelry].Id }; - AddProductPicture(productTissotTTouchExpertSolar, "product/tissot-t-touch-expert-solar.jpg"); - AddProductPicture(productTissotTTouchExpertSolar, "product/tissot-t-touch-expert-solar-t091_2.jpg"); + AddProductPicture(tissotTTouchExpertSolar, "product/tissot-t-touch-expert-solar.jpg"); + AddProductPicture(tissotTTouchExpertSolar, "product/tissot-t-touch-expert-solar-t091_2.jpg"); - productTissotTTouchExpertSolar.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + tissotTTouchExpertSolar.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + tissotTTouchExpertSolar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Tissot"], DisplayOrder = 1 }); - productTissotTTouchExpertSolar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Tissot"], DisplayOrder = 1 }); - - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2630,7 +2953,7 @@ public IList Products() // offer > best price SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2638,7 +2961,7 @@ public IList Products() // manufacturer > Tissot SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 17) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2646,7 +2969,7 @@ public IList Products() // housing > steel SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2654,7 +2977,7 @@ public IList Products() // material -> silicone SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2662,7 +2985,7 @@ public IList Products() // Gender -> gentlemen SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2670,7 +2993,7 @@ public IList Products() // movement -> Automatic, self-winding SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2678,7 +3001,7 @@ public IList Products() // diameter -> 44mm SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + tissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2687,11 +3010,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - #endregion productTissotT-TouchExpertSolar + #endregion TissotT Touch Expert Solar - #region productSeikoSRPA49K1 + #region Seiko SRPA49K1 - var productSeikoSRPA49K1 = new Product + var seikoSRPA49K1 = new Product { ProductType = ProductType.SimpleProduct, Name = "Seiko Mechanical Automatic SRPA49K1", @@ -2704,7 +3027,6 @@ public IList Products() MetaTitle = "Seiko Mechanical Automatic SRPA49K1", ShowOnHomePage = true, Price = 269.00M, - OldPrice = 329.00M, ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, @@ -2712,15 +3034,15 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameJewelry].Id }; - AddProductPicture(productSeikoSRPA49K1, "product/SeikoSRPA49K1.jpg"); - productSeikoSRPA49K1.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + AddProductPicture(seikoSRPA49K1, "product/SeikoSRPA49K1.jpg"); + seikoSRPA49K1.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + seikoSRPA49K1.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Seiko"], DisplayOrder = 1 }); - productSeikoSRPA49K1.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Seiko"], DisplayOrder = 1 }); - - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2728,7 +3050,7 @@ public IList Products() // housing > steel SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2736,7 +3058,7 @@ public IList Products() // material -> stainless steel SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2744,7 +3066,7 @@ public IList Products() // manufacturer > Seiko SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 16) }); - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2752,7 +3074,7 @@ public IList Products() // Gender -> gentlemen SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2760,7 +3082,7 @@ public IList Products() // movement -> quarz SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2768,7 +3090,7 @@ public IList Products() // closure -> folding clasp SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + seikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2777,12 +3099,11 @@ public IList Products() SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - #endregion productSeikoSRPA49K1 - + #endregion Seiko SRPA49K1 - #region productWatchesCertinaDSPodiumBigSize + #region Certina DS Podium Big Size - var productWatchesCertinaDSPodiumBigSize = new Product + var certinaDSPodiumBigSize = new Product { ProductType = ProductType.SimpleProduct, Name = "Certina DS Podium Big Size", @@ -2802,15 +3123,15 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = thirdDeliveryTime.Id + DeliveryTimeId = thirdDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameJewelry].Id }; - AddProductPicture(productWatchesCertinaDSPodiumBigSize, "product/certina_ds_podium_big.png"); - productWatchesCertinaDSPodiumBigSize.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - - productWatchesCertinaDSPodiumBigSize.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Certina"], DisplayOrder = 1 }); + AddProductPicture(certinaDSPodiumBigSize, "product/certina_ds_podium_big.png"); + certinaDSPodiumBigSize.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); + certinaDSPodiumBigSize.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Certina"], DisplayOrder = 1 }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2818,7 +3139,7 @@ public IList Products() // housing > steel SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2826,7 +3147,7 @@ public IList Products() // material -> leather SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2834,7 +3155,7 @@ public IList Products() // manufacturer > Certina SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2842,7 +3163,7 @@ public IList Products() // Gender -> gentlemen SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2850,7 +3171,7 @@ public IList Products() // movement -> quarz SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2858,7 +3179,7 @@ public IList Products() // closure -> folding clasp SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + certinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, @@ -2867,24 +3188,15 @@ public IList Products() SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); - #endregion productWatchesCertinaDSPodiumBigSize + #endregion Certina DS Podium Big Size - #endregion watches + #endregion Category Watches - #region gaming + #region Category Gaming - var manuSony = _ctx.Set().First(c => c.Name == "Sony"); - var manuMicrosoft = _ctx.Set().First(c => c.Name == "Microsoft"); - var manuEASports = _ctx.Set().First(c => c.Name == "EA Sports"); - var manuUbisoft = _ctx.Set().First(c => c.Name == "Ubisoft"); - var categoryGaming = _ctx.Set().First(c => c.Alias == "Gaming"); - var categoryGamingAccessories = _ctx.Set().First(c => c.Alias == "Gaming Accessories"); - var categoryGamingGames = _ctx.Set().First(c => c.Alias == "Games"); - var manuWarnerHomme = _ctx.Set().First(c => c.Name == "Warner Home Video Games"); + #region PS3 - #region bundlePs3AssassinCreed - - var productPs3 = new Product + var playstation3 = new Product { ProductType = ProductType.SimpleProduct, Sku = "Sony-PS399000", @@ -2905,16 +3217,21 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productPs3, "product/ps4_w_controller.jpg", "ps4-w-controller"); - AddProductPicture(productPs3, "product/ps4_wo_controller.jpg", "ps4-wo-single"); + AddProductPicture(playstation3, "product/ps4_w_controller.jpg", "ps4-w-controller"); + AddProductPicture(playstation3, "product/ps4_wo_controller.jpg", "ps4-wo-single"); + + playstation3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + playstation3.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 4 }); + + #endregion PS3 - productPs3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productPs3.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 4 }); + #region Dualshock 4 Controller - var productDualshock4Controller = new Product + var dualshock4Controller = new Product { ProductType = ProductType.SimpleProduct, Sku = "Sony-PS399004", @@ -2934,15 +3251,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productDualshock4Controller, "product/dualshock4.jpg"); - productDualshock4Controller.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productDualshock4Controller.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 1 }); + AddProductPicture(dualshock4Controller, "product/dualshock4.jpg"); + dualshock4Controller.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + dualshock4Controller.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 1 }); + + #endregion Dualshock 4 Controller + #region Minecraft - var productMinecraft = new Product + var gamingMinecraft = new Product { ProductType = ProductType.SimpleProduct, //Sku = "Ubi-acreed3", @@ -2964,15 +3285,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productMinecraft, "product/minecraft.jpg"); - productMinecraft.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productMinecraft.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 4 }); + AddProductPicture(gamingMinecraft, "product/minecraft.jpg"); + gamingMinecraft.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + gamingMinecraft.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 4 }); + + #endregion Minecraft + #region Bundle PS3, Controller & Minecraft - var productBundlePs3AssassinCreed = new Product + var bundleMinecraftPs3 = new Product { ProductType = ProductType.BundledProduct, Sku = "Sony-PS399105", @@ -2996,23 +3321,22 @@ public IList Products() ShowOnHomePage = true, BundleTitleText = "Bundle includes", BundlePerItemPricing = true, - BundlePerItemShoppingCart = true + BundlePerItemShoppingCart = true, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productBundlePs3AssassinCreed, "product/ps4_bundle_minecraft.jpg"); - productBundlePs3AssassinCreed.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productBundlePs3AssassinCreed.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 1 }); + AddProductPicture(bundleMinecraftPs3, "product/ps4_bundle_minecraft.jpg"); + bundleMinecraftPs3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + bundleMinecraftPs3.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 1 }); - #endregion bundlePs3AssassinCreed + #endregion Bundle PS3, Controller & Minecraft - #region bundlePs4 + #region PS4 - var productPs4 = new Product + var playstation4 = new Product { ProductType = ProductType.SimpleProduct, Sku = "Sony-PS410034", - //Sku = "PS410037", - Name = "PlayStation 4", ShortDescription = "The best place to play. Working with some of the most creative minds in the industry, PlayStation®4 delivers breathtaking and unique gaming experiences.", FullDescription = "

The power to perform.

PlayStation®4 was designed from the ground up to ensure that game creators can unleash their imaginations to develop the very best games and deliver new play experiences never before possible. With ultra-fast customized processors and 8GB of high-performance unified system memory, PS4™ is the home to games with rich, high-fidelity graphics and deeply immersive experiences that shatter expectations.

  • Processor

    • Processor Technology : Low power x86-64 AMD 'Jaguar', 8 cores
  • Software

    • Processor : Single-chip custom processor
  • Display

    • Display Technology : HDMI
      Digital Output (optical)
  • General

    • Ethernet ports x speed : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T); IEEE 802.11 b/g/n; Bluetooth® 2.1 (EDR)
    • Hard disk : Built-in
  • General Specifications

    • Video : BD 6xCAV
      DVD 8xCAV
  • Graphics

    • Graphics Processor : 1.84 TFLOPS, AMD Radeon™ Graphics Core Next engine
  • Interface

    • I/O Port : Super-Speed USB (USB 3.0), AUX
  • Memory

    • Internal Memory : GDDR5 8GB

", @@ -3028,17 +3352,21 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productPs4, "product/sony_ps4.png", "sony-ps4"); - AddProductPicture(productPs4, "product/sony_dualshock4_wirelesscontroller.png"); + AddProductPicture(playstation4, "product/sony_ps4.png", "sony-ps4"); + AddProductPicture(playstation4, "product/sony_dualshock4_wirelesscontroller.png"); - productPs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productPs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 5 }); + playstation4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + playstation4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 5 }); + #endregion PS4 - var productPs4Camera = new Product + #region PS4 Camera + + var playstation4Camera = new Product { ProductType = ProductType.SimpleProduct, Sku = "Sony-PS410040", @@ -3057,15 +3385,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productPs4Camera, "product/sony_ps4_camera.png"); - productPs4Camera.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productPs4Camera.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 3 }); + AddProductPicture(playstation4Camera, "product/sony_ps4_camera.png"); + playstation4Camera.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + playstation4Camera.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 3 }); + #endregion PS4 Camera - var productBundlePs4 = new Product + #region Bundle PS4, Controller & Camera + + var bundlePs4 = new Product { ProductType = ProductType.BundledProduct, Sku = "Sony-PS410099", @@ -3087,18 +3419,19 @@ public IList Products() AllowBackInStockSubscriptions = false, IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id, - BundleTitleText = "Bundle includes" + BundleTitleText = "Bundle includes", + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productBundlePs4, "product/sony_ps4_bundle.png"); - productBundlePs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productBundlePs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 2 }); + AddProductPicture(bundlePs4, "product/sony_ps4_bundle.png"); + bundlePs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + bundlePs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 2 }); - #endregion bundlePs4 + #endregion Bundle PS4, Controller & Camera - #region groupAccessories + #region Accessories - var productGroupAccessories = new Product + var groupAccessories = new Product { ProductType = ProductType.GroupedProduct, Sku = "Sony-GroupAccessories", @@ -3117,18 +3450,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - ShowOnHomePage = true + ShowOnHomePage = true, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productGroupAccessories, "category/gaming_accessories.png"); - productGroupAccessories.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productGroupAccessories.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 3 }); + AddProductPicture(groupAccessories, "category/gaming_accessories.png"); + groupAccessories.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + groupAccessories.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 3 }); - #endregion groupAccessories + #endregion Accessories - #region Ps3PlusOneGame + #region PS3 & Prince of Persia - var productPrinceOfPersia = new Product + var gamingPrinceOfPersia = new Product { ProductType = ProductType.SimpleProduct, Sku = "Ubi-princepersia", @@ -3147,18 +3481,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productPrinceOfPersia, "product/princeofpersia.jpg"); - productPrinceOfPersia.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ubisoft"], DisplayOrder = 1 }); - productPrinceOfPersia.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); + AddProductPicture(gamingPrinceOfPersia, "product/princeofpersia.jpg"); + gamingPrinceOfPersia.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ubisoft"], DisplayOrder = 1 }); + gamingPrinceOfPersia.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); - #endregion Ps3PlusOneGame + #endregion PS3 & Prince of Persia #region Horizon Zero Down - var productHorizonZeroDown = new Product + var gamingHorizonZeroDown = new Product { ProductType = ProductType.SimpleProduct, //Sku = "Ubi-princepersia", @@ -3179,18 +3514,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productHorizonZeroDown, "product/horizon.jpg"); - productHorizonZeroDown.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); - productHorizonZeroDown.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); + AddProductPicture(gamingHorizonZeroDown, "product/horizon.jpg"); + gamingHorizonZeroDown.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + gamingHorizonZeroDown.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Horizon Zero Down - #region Fifa 17 + #region FIFA 17 - var productFifa17 = new Product + var gamingFifa17 = new Product { ProductType = ProductType.SimpleProduct, //Sku = "Ubi-princepersia", @@ -3211,18 +3547,19 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productFifa17, "product/fifa17.jpg"); - productFifa17.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["EA Sports"], DisplayOrder = 1 }); - productFifa17.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); + AddProductPicture(gamingFifa17, "product/fifa17.jpg"); + gamingFifa17.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["EA Sports"], DisplayOrder = 1 }); + gamingFifa17.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); - #endregion Fifa 17 + #endregion FIFA 17 #region Lego Worlds - var productLegoWorlds = new Product + var gamingLegoWorlds = new Product { ProductType = ProductType.SimpleProduct, Sku = "Gaming-Lego-001", @@ -3242,66 +3579,65 @@ public IList Products() NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id }; - AddProductPicture(productLegoWorlds, "product/legoworlds.jpg"); - productLegoWorlds.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Warner Home Video Games"], DisplayOrder = 1 }); - productLegoWorlds.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 3 }); + AddProductPicture(gamingLegoWorlds, "product/legoworlds.jpg"); + gamingLegoWorlds.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Warner Home Video Games"], DisplayOrder = 1 }); + gamingLegoWorlds.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 3 }); #endregion Lego Worlds - //#region XBox One S - - //var productXBoxOneS = new Product - //{ - // ProductType = ProductType.SimpleProduct, - // Sku = "Microsoft-xbox1s", - // Name = "Xbox One S 500 GB Konsole", - // ShortDescription = "Genieße über 100 Spiele, die es nur für die Konsole gibt, sowie eine ständig größer werdende Bibliothek an Xbox 360-Spielen auf der Xbox One S im neuen Design – der einzigen Konsole mit 4K Ultra HD Blu-ray, 4K-Videostreaming und HDR. Streame deine Lieblingsfilme und -sendungen in atemberaubendem 4K Ultra HD. Spiele Blockbuster wie Gears of War 4 und Battlefield 1 mit Freunden auf Xbox Live, dem schnellsten und zuverlässigsten Gaming-Netzwerk.", - // FullDescription = "
  • Die ultimativen Spiele und 4K-Entertainment-System.
  • 40 % kompaktere Konsole
    Lasse dich nicht von der Größe täuschen. Mit integriertem Netzteil und bis zu 2 TB Speicherplatz ist die Xbox One S die fortschrittlichste Xbox überhaupt.
  • Der beste Controller - jetzt noch besser
    Der neue Xbox Wireless Controller bietet ein schlankes, optimiertes Design, texturierte Grip - Fläche und Bluetooth zum Spielen auf Windows 10 Geräten. Genieße individuelle Tastenbelegung und verbesserte drahtlose Reichweite und stecke jeden kompatiblen Kopfhörer mit der 3, 5 mm Stereo - Headset - Buchse ein.
", - // ProductTemplateId = productTemplate.Id, - // AllowCustomerReviews = true, - // Published = true, - // MetaTitle = "Xbox One S", - // Price = 279.99M, - // OldPrice = 279.99M, - // ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - // OrderMinimumQuantity = 1, - // OrderMaximumQuantity = 10000, - // StockQuantity = 10000, - // NotifyAdminForQuantityBelow = 1, - // AllowBackInStockSubscriptions = false, - // IsShipEnabled = true, - // DeliveryTimeId = firstDeliveryTime.Id - //}; - - //AddProductPicture(productXBoxOneS, "product/xbox_one_s_1.png", "microsoft-xbox-one-s-1"); - //productXBoxOneS.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuMicrosoft, DisplayOrder = 1 }); - //productXBoxOneS.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 1 }); - - //#endregion - - #endregion gaming + #region XBox One S + + var xBoxOneS = new Product + { + ProductType = ProductType.SimpleProduct, + Sku = "Microsoft-xbox1s", + Name = "Xbox One S 500 GB Konsole", + ShortDescription = "Genieße über 100 Spiele, die es nur für die Konsole gibt, sowie eine ständig größer werdende Bibliothek an Xbox 360-Spielen auf der Xbox One S im neuen Design – der einzigen Konsole mit 4K Ultra HD Blu-ray, 4K-Videostreaming und HDR. Streame deine Lieblingsfilme und -sendungen in atemberaubendem 4K Ultra HD. Spiele Blockbuster wie Gears of War 4 und Battlefield 1 mit Freunden auf Xbox Live, dem schnellsten und zuverlässigsten Gaming-Netzwerk.", + FullDescription = "
  • Die ultimativen Spiele und 4K-Entertainment-System.
  • 40 % kompaktere Konsole
    Lasse dich nicht von der Größe täuschen. Mit integriertem Netzteil und bis zu 2 TB Speicherplatz ist die Xbox One S die fortschrittlichste Xbox überhaupt.
  • Der beste Controller - jetzt noch besser
    Der neue Xbox Wireless Controller bietet ein schlankes, optimiertes Design, texturierte Grip - Fläche und Bluetooth zum Spielen auf Windows 10 Geräten. Genieße individuelle Tastenbelegung und verbesserte drahtlose Reichweite und stecke jeden kompatiblen Kopfhörer mit der 3, 5 mm Stereo - Headset - Buchse ein.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Xbox One S", + Price = 279.99M, + OldPrice = 279.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = firstDeliveryTime.Id, + TaxCategoryId = taxCategories[TaxNameElectronics].Id + }; + + AddProductPicture(xBoxOneS, "product/xbox_one_s_1.png", "microsoft-xbox-one-s-1"); + xBoxOneS.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 1 }); + xBoxOneS.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Microsoft"], DisplayOrder = 1 }); + + #endregion XBox One S + + #endregion Category Gaming var entities = new List { - productTRANSOCEANCHRONOGRAPH, productTissotTTouchExpertSolar, productSeikoSRPA49K1, - productTitleistSM6TourChrome, productTitleistProV1x, productGBBEpicSubZeroDriver, - productSupremeGolfball, productBooksStoneOfTheWise, productNikeStrikeFootball, productNikeEvoPowerBall, - productTorfabrikOfficialGameBall, productAdidasTangoSalaBall, productAllCourtBasketball, productEvolutionHighSchoolGameBasketball, productRayBanTopBar, - productOriginalWayfarer, productCustomFlakSunglasses, productRadarEVPrizmSportsSunglasses, productAppleProHipsterBundle, product97ipad, productAirpods, - productIphoneplus,productWatchSeries2,product10GiftCard, product25GiftCard, product50GiftCard,product100GiftCard, productBooksUberMan, productBooksGefangeneDesHimmels, - productBooksBestGrillingRecipes, productBooksCookingForTwo, productBooksAutosDerSuperlative, productBooksBildatlasMotorraeder, - productBooksAutoBuch, productBooksFastCars, productBooksMotorradAbenteuer, - productInstantDownloadVivaldi, productInstantDownloadBeethoven, productWatchesCertinaDSPodiumBigSize, - productPs3, productMinecraft, productBundlePs3AssassinCreed, - productPs4, productDualshock4Controller, productPs4Camera, productBundlePs4, - productGroupAccessories, productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17, productJackWolfsKinKanukaPoint + transoceanChronograph, tissotTTouchExpertSolar, seikoSRPA49K1, certinaDSPodiumBigSize, titleistSM6TourChrome, + titleistProV1x, gbbEpicSubZeroDriver, supremeGolfball, nikeStrikeFootball, nikeEvoPowerBall, + torfabrikOfficialGameBall, adidasTangoSalaBall, allCourtBasketball, evolutionBasketball, appleProHipsterBundle, + iPad, airPods, iPhonePlus, watchSeries2, giftCard10, giftCard25, giftCard50, giftCard100, + booksUberMan, booksGefangeneDesHimmels, booksBestGrillingRecipes, booksCookingForTwo, booksStoneOfTheWise, + booksAutosDerSuperlative, booksBildatlasMotorraeder, booksAutoBuch, booksFastCars, booksMotorradAbenteuer, + instantDownloadVivaldi, instantDownloadBeethoven, playstation3, bundleMinecraftPs3, playstation4, + dualshock4Controller, playstation4Camera, bundlePs4, gamingMinecraft, groupAccessories, gamingPrinceOfPersia, + gamingLegoWorlds, gamingHorizonZeroDown, gamingFifa17, xBoxOneS }; - entities.AddRange(GetFashionProducts(specAttributes)); - entities.AddRange(GetFurnitureProducts(specAttributes)); + entities.AddRange(GetFashionProducts(categories, manufacturers, specAttributes)); + entities.AddRange(GetFurnitureProducts(categories, specAttributes)); this.Alter(entities); return entities; @@ -3309,15 +3645,16 @@ public IList Products() public IList ProductBundleItems() { - var utcNow = DateTime.UtcNow; + var products = _ctx.Set().ToList().ToDictionarySafe(x => x.Sku, x => x); + + #region Bundles Apple - #region apple bundles - var bundleAppleProHipster = _ctx.Set().First(x => x.Sku == "P-2005-Bundle"); + var BundleAppleProHipster = _ctx.Set().First(x => x.Sku == "P-2005-Bundle"); - var bundleItemIproductIphoneplus = new ProductBundleItem() + var bundleItemIphonePlus = new ProductBundleItem() { - BundleProduct = bundleAppleProHipster, - Product = _ctx.Set().First(x => x.Sku == "P-2001"), + BundleProduct = BundleAppleProHipster, + Product = products["P-2001"], Quantity = 1, Discount = 40.0M, Visible = true, @@ -3325,10 +3662,10 @@ public IList ProductBundleItems() DisplayOrder = 1 }; - var bundleItemProductWatchSeries2 = new ProductBundleItem() + var bundleItemWatchSeries2 = new ProductBundleItem() { - BundleProduct = bundleAppleProHipster, - Product = _ctx.Set().First(x => x.Sku == "P-2002"), + BundleProduct = BundleAppleProHipster, + Product = products["P-2002"], Quantity = 2, Discount = 30.0M, Visible = true, @@ -3336,10 +3673,10 @@ public IList ProductBundleItems() DisplayOrder = 2 }; - var bundleItemproductAirpods = new ProductBundleItem() + var bundleItemAirPods = new ProductBundleItem() { - BundleProduct = bundleAppleProHipster, - Product = _ctx.Set().First(x => x.Sku == "P-2003"), + BundleProduct = BundleAppleProHipster, + Product = products["P-2003"], Quantity = 1, Discount = 30.0M, Visible = true, @@ -3347,10 +3684,10 @@ public IList ProductBundleItems() DisplayOrder = 3 }; - var bundleItemproductIpad = new ProductBundleItem() + var bundleItemIpad = new ProductBundleItem() { - BundleProduct = bundleAppleProHipster, - Product = _ctx.Set().First(x => x.Sku == "P-2004"), + BundleProduct = BundleAppleProHipster, + Product = products["P-2004"], Quantity = 1, Discount = 30.0M, Visible = true, @@ -3358,17 +3695,16 @@ public IList ProductBundleItems() DisplayOrder = 3 }; - #endregion apple bundles + #endregion Bundles Apple - #region gaming + #region Bundles Gaming - //var bundlePs3AssassinCreed = _ctx.Set().First(x => x.Sku == "Sony-PS399105"); - var bundlePs4Minecraft = _ctx.Set().First(x => x.Sku == "Sony-PS399105"); + var bundlePs4Minecraft = products["Sony-PS399105"]; var bundleItemPs4Minecraft1 = new ProductBundleItem() { BundleProduct = bundlePs4Minecraft, - Product = _ctx.Set().First(x => x.Sku == "Sony-PS399000"), + Product = products["Sony-PS399000"], Quantity = 1, Discount = 20.0M, Visible = true, @@ -3379,7 +3715,7 @@ public IList ProductBundleItems() var bundleItemPs4Minecraft2 = new ProductBundleItem() { BundleProduct = bundlePs4Minecraft, - Product = _ctx.Set().First(x => x.Sku == "Sony-PS399004"), + Product = products["Sony-PS399004"], Quantity = 2, Discount = 30.0M, Visible = true, @@ -3390,7 +3726,7 @@ public IList ProductBundleItems() var bundleItemPs4Minecraft3 = new ProductBundleItem() { BundleProduct = bundlePs4Minecraft, - Product = _ctx.Set().First(x => x.Sku == "PD-Minecraft4ps4"), + Product = products["PD-Minecraft4ps4"], Quantity = 1, Discount = 20.0M, Visible = true, @@ -3399,12 +3735,12 @@ public IList ProductBundleItems() }; - var bundlePs4 = _ctx.Set().First(x => x.Sku == "Sony-PS410099"); + var bundlePs4 = products["Sony-PS410099"]; var bundleItemPs41 = new ProductBundleItem { BundleProduct = bundlePs4, - Product = _ctx.Set().First(x => x.Sku == "Sony-PS410034"), + Product = products["Sony-PS410034"], Quantity = 1, Visible = true, Published = true, @@ -3414,8 +3750,7 @@ public IList ProductBundleItems() var bundleItemPs42 = new ProductBundleItem { BundleProduct = bundlePs4, - //Product = _ctx.Set().First(x => x.Sku == "Sony-PS410037"), - Product = _ctx.Set().First(x => x.Sku == "Sony-PS399004"), + Product = products["Sony-PS399004"], Quantity = 1, Visible = true, Published = true, @@ -3425,19 +3760,19 @@ public IList ProductBundleItems() var bundleItemPs43 = new ProductBundleItem { BundleProduct = bundlePs4, - Product = _ctx.Set().First(x => x.Sku == "Sony-PS410040"), + Product = products["Sony-PS410040"], Quantity = 1, Visible = true, Published = true, DisplayOrder = 3 }; - #endregion gaming + #endregion Bundles Gaming var entities = new List { - bundleItemPs4Minecraft1, bundleItemPs4Minecraft2, bundleItemPs4Minecraft3, - bundleItemPs41, bundleItemPs42, bundleItemPs43,bundleItemIproductIphoneplus, bundleItemProductWatchSeries2,bundleItemproductAirpods,bundleItemproductIpad + bundleItemPs4Minecraft1, bundleItemPs4Minecraft2, bundleItemPs4Minecraft3, bundleItemPs41, bundleItemPs42, + bundleItemPs43, bundleItemIphonePlus, bundleItemWatchSeries2, bundleItemAirPods, bundleItemIpad }; this.Alter(entities); @@ -3535,13 +3870,7 @@ public void AssignGroupedProducts(IList savedProducts) savedProducts .Where(x => gamingAccessoriesSkus.Contains(x.Sku)) .ToList() - .Each(x => - { - x.ParentGroupedProductId = productGamingAccessoriesId; - - //_ctx.Set().Attach(x); - //_ctx.Entry(x).State = System.Data.Entity.EntityState.Modified; - }); + .Each(x => { x.ParentGroupedProductId = productGamingAccessoriesId; }); _ctx.SaveChanges(); } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.SpecificationAttributes.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.SpecificationAttributes.cs index e767d2068a..e8deecb501 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.SpecificationAttributes.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.SpecificationAttributes.cs @@ -1,1090 +1,1089 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; namespace SmartStore.Data.Setup { public abstract partial class InvariantSeedData { - public IList SpecificationAttributes() - { - #region sa1 CPU-Manufacturer - - var sa1 = new SpecificationAttribute - { - Name = "CPU-Manufacturer", - DisplayOrder = 1, - }; - sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "AMD", - DisplayOrder = 1, - }); - sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Intel", - DisplayOrder = 2, - }); - sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "ARM", - DisplayOrder = 3, - }); - sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Samsung", - DisplayOrder = 4, - }); - sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Apple", - DisplayOrder = 5, - }); - - #endregion sa1 CPU-Manufacturer - - #region sa2 color - - var sa2 = new SpecificationAttribute - { - Name = "Color", - DisplayOrder = 2, - }; - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "white", - DisplayOrder = 1, - }); - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "black", - DisplayOrder = 2, - }); - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "beige", - DisplayOrder = 3, - }); - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "red", - DisplayOrder = 4, - }); - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "blue", - DisplayOrder = 5, - }); - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "green", - DisplayOrder = 6, - }); - sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "yellow", - DisplayOrder = 7, - }); - - #endregion sa2 color - - #region sa3 harddisk capacity - - var sa3 = new SpecificationAttribute - { - Name = "Harddisk capacity", - DisplayOrder = 3, - }; - sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "250 GB", - DisplayOrder = 1, - }); - sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "500 GB", - DisplayOrder = 2, - }); - sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "750 GB", - DisplayOrder = 3, - }); - sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "1000 GB", - DisplayOrder = 4, - }); - sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "1500 GB", - DisplayOrder = 5, - }); - - #endregion sa3 harddisk capacity - - #region sa4 ram - - var sa4 = new SpecificationAttribute - { - Name = "RAM", - DisplayOrder = 4, - }; - sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "4 GB", - DisplayOrder = 1, - }); - sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "8 GB", - DisplayOrder = 2, - }); - sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "13 GB", - DisplayOrder = 3, - }); - sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "2 GB", - DisplayOrder = 4, - }); - sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "1500 GB", - DisplayOrder = 5, - }); - - #endregion sa4 ram - - #region sa5 Operating System - - var sa5 = new SpecificationAttribute - { - Name = "Operating System", - DisplayOrder = 5, - }; - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Windows 7 32 Bit", - DisplayOrder = 1, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Windows 7 64 Bit", - DisplayOrder = 2, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Windows 8 32 Bit", - DisplayOrder = 3, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Windows 8 64 Bit", - DisplayOrder = 4, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Linux", - DisplayOrder = 5, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Mac OS", - DisplayOrder = 6, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Android 2", - DisplayOrder = 7, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Android 4", - DisplayOrder = 8, - }); - sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "iOS", - DisplayOrder = 9, - }); - - #endregion sa5 Operating System - - #region sa6 ports - - var sa6 = new SpecificationAttribute - { - Name = "Ports", - DisplayOrder = 6, - }; - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "USB 2.0", - DisplayOrder = 1, - }); - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "USB 3.0", - DisplayOrder = 2, - }); - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Firewire", - DisplayOrder = 3, - }); - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "HDMI", - DisplayOrder = 4, - }); - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "DVI", - DisplayOrder = 5, - }); - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "VGA", - DisplayOrder = 6, - }); - sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Ethernet/RJ45", - DisplayOrder = 7, - }); - - #endregion sa6 ports - - #region sa7 Gender - - var sa7 = new SpecificationAttribute - { - Name = "Gender", - DisplayOrder = 7, - }; - sa7.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "gentlemen", - DisplayOrder = 1, - }); - sa7.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "ladies", - DisplayOrder = 2, - }); - sa7.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "unisex", - DisplayOrder = 3, - }); - - #endregion sa7 Gender - - #region sa8 material - - var sa8 = new SpecificationAttribute - { - Name = "Material", - DisplayOrder = 8, - }; - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "stainless steel", - DisplayOrder = 1, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "titanium", - DisplayOrder = 2, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "plastic", - DisplayOrder = 3, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "aluminium", - DisplayOrder = 4, - }); - - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "leather", - DisplayOrder = 5, - }); - - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "nylon", - DisplayOrder = 6, - }); - - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "silicone", - DisplayOrder = 7, - }); - - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "ceramic", - DisplayOrder = 8, - }); - - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "cotton", - DisplayOrder = 9, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "100% organic cotton", - DisplayOrder = 10, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "polyamide", - DisplayOrder = 11, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "rubber", - DisplayOrder = 12, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "wood", - DisplayOrder = 13, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "glass", - DisplayOrder = 14, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "spandex", - DisplayOrder = 15, - }); - sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption - { - Name = "polyester", - DisplayOrder = 16, - }); - - #endregion sa8 material - - #region sa9 movement - - var sa9 = new SpecificationAttribute - { - Name = "Movement", - DisplayOrder = 9, - }; - sa9.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "mechanical, self winding", - DisplayOrder = 1, - }); - sa9.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "mechanical", - DisplayOrder = 2, - }); - sa9.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "quarz, battery operated", - DisplayOrder = 3, - }); - - #endregion sa9 movement - - #region sa10 clasp - - var sa10 = new SpecificationAttribute - { - Name = "Clasp", - DisplayOrder = 10, - }; - sa10.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "glidelock", - DisplayOrder = 1, - }); - sa10.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "folding clasp", - DisplayOrder = 2, - }); - sa10.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "thorn close", - DisplayOrder = 3, - }); - - #endregion sa10 clasp - - #region sa11 window material - - var sa11 = new SpecificationAttribute - { - Name = "Window material", - DisplayOrder = 11, - }; - sa11.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "mineral", - DisplayOrder = 1, - }); - sa11.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "sapphire", - DisplayOrder = 2, - }); - - #endregion sa11 window material - - #region sa12 language - - var sa12 = new SpecificationAttribute - { - Name = "Language", - DisplayOrder = 12, - }; - sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "german", - DisplayOrder = 1, - }); - sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "english", - DisplayOrder = 2, - }); - sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "french", - DisplayOrder = 3, - }); - sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "italian", - DisplayOrder = 4, - }); - - #endregion sa12 language - - #region sa13 edition - - var sa13 = new SpecificationAttribute - { - Name = "Edition", - DisplayOrder = 13, - }; - sa13.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "bound", - DisplayOrder = 1, - }); - sa13.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "paperback", - DisplayOrder = 2, - }); - - #endregion sa13 edition - - #region sa14 category - - var sa14 = new SpecificationAttribute - { - Name = "Category", - DisplayOrder = 14, - }; - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "adventure", - DisplayOrder = 1, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "fantasy & science fiction", - DisplayOrder = 2, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "history", - DisplayOrder = 3, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "internet & computer", - DisplayOrder = 4, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "thriller", - DisplayOrder = 5, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "cars", - DisplayOrder = 6, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "novel", - DisplayOrder = 7, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "cook and bake", - DisplayOrder = 8, - }); - sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "non-fiction", - DisplayOrder = 9, - }); - - #endregion sa14 category - - #region sa15 Computer-type - - var sa15 = new SpecificationAttribute - { - Name = "Computer-type", - DisplayOrder = 15, - }; - sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "desktop", - DisplayOrder = 1, - }); - sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "all-in-one", - DisplayOrder = 2, - }); - sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "laptop", - DisplayOrder = 3, - }); - sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "tablet", - DisplayOrder = 4, - }); - - #endregion sa15 Computer-type - - #region sa16 type of mass-storage - - var sa16 = new SpecificationAttribute - { - Name = "Type of mass-storage", - DisplayOrder = 16, - }; - sa16.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "HDD", - DisplayOrder = 1, - }); - sa16.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "SSD", - DisplayOrder = 2, - }); - sa16.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Flash", - DisplayOrder = 3, - }); - - #endregion sa16 type of mass-storage - - #region sa17 Size (ext. HDD) - - var sa17 = new SpecificationAttribute - { - Name = "Size (ext. HDD)", - DisplayOrder = 17, - }; - sa17.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "3.5", - DisplayOrder = 1, - }); - sa17.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "2.5", - DisplayOrder = 2, - }); - - #endregion sa17 Size (ext. HDD) - - #region sa18 MP3 quality - - var sa18 = new SpecificationAttribute - { - Name = "MP3 quality", - DisplayOrder = 18, - }; - sa18.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "128 kbit/s", - DisplayOrder = 1, - }); - sa18.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "256 kbit/s", - DisplayOrder = 2, - }); - sa18.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "320 kbit/s", - DisplayOrder = 3, - }); - - #endregion sa18 MP3 quality - - #region sa19 music genre - - var sa19 = new SpecificationAttribute - { - Name = "Music genre", - DisplayOrder = 19, - }; - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "blues", - DisplayOrder = 1, - }); - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "jazz", - DisplayOrder = 2, - }); - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "disco", - DisplayOrder = 3, - }); - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "pop", - DisplayOrder = 4, - }); - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "funk", - DisplayOrder = 5, - }); - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "classical", - DisplayOrder = 6, - }); - sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "R&B", - DisplayOrder = 7, - }); - - #endregion sa19 music genre - - #region sa20 manufacturer - - var sa20 = new SpecificationAttribute - { - Name = "Manufacturer", - DisplayOrder = 20, - }; - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Apple", - DisplayOrder = 1, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Samsung", - DisplayOrder = 2, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "HTC", - DisplayOrder = 3, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "LG", - DisplayOrder = 4, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Motorola", - DisplayOrder = 5, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Nokia", - DisplayOrder = 6, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Sony", - DisplayOrder = 7, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Blackberry", - DisplayOrder = 8, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Microsoft", - DisplayOrder = 9, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "HP", - DisplayOrder = 10, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Canon", - DisplayOrder = 11, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Casio", - DisplayOrder = 12, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Panasonic", - DisplayOrder = 13, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Certina", - DisplayOrder = 14, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Festina", - DisplayOrder = 15, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Seiko", - DisplayOrder = 16, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Tissot", - DisplayOrder = 17, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Breitling", - DisplayOrder = 18, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Adidas", - DisplayOrder = 19, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Nike", - DisplayOrder = 20, - }); - sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Puma", - DisplayOrder = 21, - }); - - #endregion sa20 manufacturer - - #region sa21 Watches for whom - - var sa21 = new SpecificationAttribute - { - Name = "For whom", - DisplayOrder = 21, - }; - sa21.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "For him", - DisplayOrder = 1, - }); - sa21.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "For her", - DisplayOrder = 2, - }); - - #endregion sa11 Watches for whom - - #region sa22 Offer - - var sa22 = new SpecificationAttribute - { - Name = "Offer", - DisplayOrder = 22, - }; - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Clearance", - DisplayOrder = 1, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Permanent low price", - DisplayOrder = 2, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Promotion", - DisplayOrder = 3, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Reduced price", - DisplayOrder = 4, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Special Buy", - DisplayOrder = 5, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Offer of the day", - DisplayOrder = 6, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Weekly offer", - DisplayOrder = 7, - }); - - sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Best Price", - DisplayOrder = 8, - }); - - #endregion sa22 Offer - - #region sa23 Size - - var sa23 = new SpecificationAttribute - { - Name = "Size", - DisplayOrder = 23, - }; - - sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "XS", - DisplayOrder = 1, - }); - - sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "S", - DisplayOrder = 2, - }); - - sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "M", - DisplayOrder = 3, - }); - - sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "L", - DisplayOrder = 4, - }); - - sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "XL", - DisplayOrder = 5, - }); - - sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "XXL", - DisplayOrder = 6, - }); - - - #endregion sa23 Size - - #region sa24 diameter - - var sa24 = new SpecificationAttribute - { - Name = "Diameter", - DisplayOrder = 24, - }; - - sa24.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "38mm", - DisplayOrder = 1, - }); - - sa24.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "40mm", - DisplayOrder = 2, - }); - - sa24.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "44mm", - DisplayOrder = 3, - }); - - #endregion sa24 diameter - - #region sa25 closure - - var sa25 = new SpecificationAttribute - { - Name = "Closure", - DisplayOrder = 25, - }; - - sa25.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "snap closure", - DisplayOrder = 1, - }); - - sa25.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "folding clasp", - DisplayOrder = 2, - }); - - sa25.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "thorn close", - DisplayOrder = 3, - }); - - #endregion sa25 closure - - #region sa26 facial shape - - var sa26 = new SpecificationAttribute - { - Name = "Facial shape", - DisplayOrder = 26, - }; - - sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "oval", - DisplayOrder = 1, - }); - - sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "round", - DisplayOrder = 2, - }); - - sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "heart shaped", - DisplayOrder = 3, - }); - - sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "angular", - DisplayOrder = 4, - }); - - #endregion sa26 facial shape - - #region sa27 storage capacity - - var sa27 = new SpecificationAttribute - { - Name = "Storage capacity", - DisplayOrder = 27, - }; - - sa27.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "32 GB", - DisplayOrder = 1, - }); - - sa27.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "64 GB", - DisplayOrder = 2, - }); - - sa27.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "128 GB", - DisplayOrder = 3, - }); - - #endregion sa27 facial shape - - #region sa28 Dial window material type - - var sa28 = new SpecificationAttribute - { - Name = "Dial window material type", - DisplayOrder = 28, - }; - - sa28.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Mineral", - DisplayOrder = 1, - }); - - sa28.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() - { - Name = "Sapphire", - DisplayOrder = 2, - }); - - #endregion sa28 Dial window material type - - var entities = new List - { - sa1,sa2,sa3,sa4,sa5,sa6,sa7,sa8,sa9,sa10,sa11,sa12,sa13,sa14,sa15,sa16,sa17,sa18,sa19,sa20,sa21,sa22,sa23,sa24,sa25,sa26,sa27,sa28 - }; - - this.Alter(entities); - return entities; - } - } + public IList SpecificationAttributes() + { + #region sa1 CPU-Manufacturer + + var sa1 = new SpecificationAttribute + { + Name = "CPU-Manufacturer", + DisplayOrder = 1, + }; + sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "AMD", + DisplayOrder = 1, + }); + sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Intel", + DisplayOrder = 2, + }); + sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "ARM", + DisplayOrder = 3, + }); + sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Samsung", + DisplayOrder = 4, + }); + sa1.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Apple", + DisplayOrder = 5, + }); + + #endregion sa1 CPU-Manufacturer + + #region sa2 color + + var sa2 = new SpecificationAttribute + { + Name = "Color", + DisplayOrder = 2, + }; + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "white", + DisplayOrder = 1, + }); + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "black", + DisplayOrder = 2, + }); + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "beige", + DisplayOrder = 3, + }); + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "red", + DisplayOrder = 4, + }); + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "blue", + DisplayOrder = 5, + }); + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "green", + DisplayOrder = 6, + }); + sa2.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "yellow", + DisplayOrder = 7, + }); + + #endregion sa2 color + + #region sa3 harddisk capacity + + var sa3 = new SpecificationAttribute + { + Name = "Harddisk capacity", + DisplayOrder = 3, + }; + sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "250 GB", + DisplayOrder = 1, + }); + sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "500 GB", + DisplayOrder = 2, + }); + sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "750 GB", + DisplayOrder = 3, + }); + sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "1000 GB", + DisplayOrder = 4, + }); + sa3.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "1500 GB", + DisplayOrder = 5, + }); + + #endregion sa3 harddisk capacity + + #region sa4 ram + + var sa4 = new SpecificationAttribute + { + Name = "RAM", + DisplayOrder = 4, + }; + sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "4 GB", + DisplayOrder = 1, + }); + sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "8 GB", + DisplayOrder = 2, + }); + sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "13 GB", + DisplayOrder = 3, + }); + sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "2 GB", + DisplayOrder = 4, + }); + sa4.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "1500 GB", + DisplayOrder = 5, + }); + + #endregion sa4 ram + + #region sa5 Operating System + + var sa5 = new SpecificationAttribute + { + Name = "Operating System", + DisplayOrder = 5, + }; + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Windows 7 32 Bit", + DisplayOrder = 1, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Windows 7 64 Bit", + DisplayOrder = 2, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Windows 8 32 Bit", + DisplayOrder = 3, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Windows 8 64 Bit", + DisplayOrder = 4, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Linux", + DisplayOrder = 5, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Mac OS", + DisplayOrder = 6, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Android 2", + DisplayOrder = 7, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Android 4", + DisplayOrder = 8, + }); + sa5.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "iOS", + DisplayOrder = 9, + }); + + #endregion sa5 Operating System + + #region sa6 ports + + var sa6 = new SpecificationAttribute + { + Name = "Ports", + DisplayOrder = 6, + }; + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "USB 2.0", + DisplayOrder = 1, + }); + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "USB 3.0", + DisplayOrder = 2, + }); + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Firewire", + DisplayOrder = 3, + }); + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "HDMI", + DisplayOrder = 4, + }); + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "DVI", + DisplayOrder = 5, + }); + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "VGA", + DisplayOrder = 6, + }); + sa6.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Ethernet/RJ45", + DisplayOrder = 7, + }); + + #endregion sa6 ports + + #region sa7 Gender + + var sa7 = new SpecificationAttribute + { + Name = "Gender", + DisplayOrder = 7, + }; + sa7.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "gentlemen", + DisplayOrder = 1, + }); + sa7.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "ladies", + DisplayOrder = 2, + }); + sa7.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "unisex", + DisplayOrder = 3, + }); + + #endregion sa7 Gender + + #region sa8 material + + var sa8 = new SpecificationAttribute + { + Name = "Material", + DisplayOrder = 8, + }; + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "stainless steel", + DisplayOrder = 1, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "titanium", + DisplayOrder = 2, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "plastic", + DisplayOrder = 3, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "aluminium", + DisplayOrder = 4, + }); + + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "leather", + DisplayOrder = 5, + }); + + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "nylon", + DisplayOrder = 6, + }); + + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "silicone", + DisplayOrder = 7, + }); + + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "ceramic", + DisplayOrder = 8, + }); + + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "cotton", + DisplayOrder = 9, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "100% organic cotton", + DisplayOrder = 10, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "polyamide", + DisplayOrder = 11, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "rubber", + DisplayOrder = 12, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "wood", + DisplayOrder = 13, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "glass", + DisplayOrder = 14, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "spandex", + DisplayOrder = 15, + }); + sa8.SpecificationAttributeOptions.Add(new SpecificationAttributeOption + { + Name = "polyester", + DisplayOrder = 16, + }); + + #endregion sa8 material + + #region sa9 movement + + var sa9 = new SpecificationAttribute + { + Name = "Movement", + DisplayOrder = 9, + }; + sa9.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "mechanical, self winding", + DisplayOrder = 1, + }); + sa9.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "mechanical", + DisplayOrder = 2, + }); + sa9.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "quarz, battery operated", + DisplayOrder = 3, + }); + + #endregion sa9 movement + + #region sa10 clasp + + var sa10 = new SpecificationAttribute + { + Name = "Clasp", + DisplayOrder = 10, + }; + sa10.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "glidelock", + DisplayOrder = 1, + }); + sa10.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "folding clasp", + DisplayOrder = 2, + }); + sa10.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "thorn close", + DisplayOrder = 3, + }); + + #endregion sa10 clasp + + #region sa11 window material + + var sa11 = new SpecificationAttribute + { + Name = "Window material", + DisplayOrder = 11, + }; + sa11.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "mineral", + DisplayOrder = 1, + }); + sa11.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "sapphire", + DisplayOrder = 2, + }); + + #endregion sa11 window material + + #region sa12 language + + var sa12 = new SpecificationAttribute + { + Name = "Language", + DisplayOrder = 12, + }; + sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "german", + DisplayOrder = 1, + }); + sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "english", + DisplayOrder = 2, + }); + sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "french", + DisplayOrder = 3, + }); + sa12.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "italian", + DisplayOrder = 4, + }); + + #endregion sa12 language + + #region sa13 edition + + var sa13 = new SpecificationAttribute + { + Name = "Edition", + DisplayOrder = 13, + }; + sa13.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "bound", + DisplayOrder = 1, + }); + sa13.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "paperback", + DisplayOrder = 2, + }); + + #endregion sa13 edition + + #region sa14 category + + var sa14 = new SpecificationAttribute + { + Name = "Category", + DisplayOrder = 14, + }; + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "adventure", + DisplayOrder = 1, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "fantasy & science fiction", + DisplayOrder = 2, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "history", + DisplayOrder = 3, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "internet & computer", + DisplayOrder = 4, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "thriller", + DisplayOrder = 5, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "cars", + DisplayOrder = 6, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "novel", + DisplayOrder = 7, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "cook and bake", + DisplayOrder = 8, + }); + sa14.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "non-fiction", + DisplayOrder = 9, + }); + + #endregion sa14 category + + #region sa15 Computer-type + + var sa15 = new SpecificationAttribute + { + Name = "Computer-type", + DisplayOrder = 15, + }; + sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "desktop", + DisplayOrder = 1, + }); + sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "all-in-one", + DisplayOrder = 2, + }); + sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "laptop", + DisplayOrder = 3, + }); + sa15.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "tablet", + DisplayOrder = 4, + }); + + #endregion sa15 Computer-type + + #region sa16 type of mass-storage + + var sa16 = new SpecificationAttribute + { + Name = "Type of mass-storage", + DisplayOrder = 16, + }; + sa16.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "HDD", + DisplayOrder = 1, + }); + sa16.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "SSD", + DisplayOrder = 2, + }); + sa16.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Flash", + DisplayOrder = 3, + }); + + #endregion sa16 type of mass-storage + + #region sa17 Size (ext. HDD) + + var sa17 = new SpecificationAttribute + { + Name = "Size (ext. HDD)", + DisplayOrder = 17, + }; + sa17.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "3.5", + DisplayOrder = 1, + }); + sa17.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "2.5", + DisplayOrder = 2, + }); + + #endregion sa17 Size (ext. HDD) + + #region sa18 MP3 quality + + var sa18 = new SpecificationAttribute + { + Name = "MP3 quality", + DisplayOrder = 18, + }; + sa18.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "128 kbit/s", + DisplayOrder = 1, + }); + sa18.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "256 kbit/s", + DisplayOrder = 2, + }); + sa18.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "320 kbit/s", + DisplayOrder = 3, + }); + + #endregion sa18 MP3 quality + + #region sa19 music genre + + var sa19 = new SpecificationAttribute + { + Name = "Music genre", + DisplayOrder = 19, + }; + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "blues", + DisplayOrder = 1, + }); + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "jazz", + DisplayOrder = 2, + }); + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "disco", + DisplayOrder = 3, + }); + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "pop", + DisplayOrder = 4, + }); + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "funk", + DisplayOrder = 5, + }); + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "classical", + DisplayOrder = 6, + }); + sa19.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "R&B", + DisplayOrder = 7, + }); + + #endregion sa19 music genre + + #region sa20 manufacturer + + var sa20 = new SpecificationAttribute + { + Name = "Manufacturer", + DisplayOrder = 20, + }; + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Apple", + DisplayOrder = 1, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Samsung", + DisplayOrder = 2, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "HTC", + DisplayOrder = 3, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "LG", + DisplayOrder = 4, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Motorola", + DisplayOrder = 5, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Nokia", + DisplayOrder = 6, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Sony", + DisplayOrder = 7, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Blackberry", + DisplayOrder = 8, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Microsoft", + DisplayOrder = 9, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "HP", + DisplayOrder = 10, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Canon", + DisplayOrder = 11, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Casio", + DisplayOrder = 12, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Panasonic", + DisplayOrder = 13, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Certina", + DisplayOrder = 14, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Festina", + DisplayOrder = 15, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Seiko", + DisplayOrder = 16, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Tissot", + DisplayOrder = 17, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Breitling", + DisplayOrder = 18, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Adidas", + DisplayOrder = 19, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Nike", + DisplayOrder = 20, + }); + sa20.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Puma", + DisplayOrder = 21, + }); + + #endregion sa20 manufacturer + + #region sa21 Watches for whom + + var sa21 = new SpecificationAttribute + { + Name = "For whom", + DisplayOrder = 21, + }; + sa21.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "For him", + DisplayOrder = 1, + }); + sa21.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "For her", + DisplayOrder = 2, + }); + + #endregion sa11 Watches for whom + + #region sa22 Offer + + var sa22 = new SpecificationAttribute + { + Name = "Offer", + DisplayOrder = 22, + }; + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Clearance", + DisplayOrder = 1, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Permanent low price", + DisplayOrder = 2, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Promotion", + DisplayOrder = 3, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Reduced price", + DisplayOrder = 4, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Special Buy", + DisplayOrder = 5, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Offer of the day", + DisplayOrder = 6, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Weekly offer", + DisplayOrder = 7, + }); + + sa22.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Best Price", + DisplayOrder = 8, + }); + + #endregion sa22 Offer + + #region sa23 Size + + var sa23 = new SpecificationAttribute + { + Name = "Size", + DisplayOrder = 23, + }; + + sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "XS", + DisplayOrder = 1, + }); + + sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "S", + DisplayOrder = 2, + }); + + sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "M", + DisplayOrder = 3, + }); + + sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "L", + DisplayOrder = 4, + }); + + sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "XL", + DisplayOrder = 5, + }); + + sa23.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "XXL", + DisplayOrder = 6, + }); + + + #endregion sa23 Size + + #region sa24 diameter + + var sa24 = new SpecificationAttribute + { + Name = "Diameter", + DisplayOrder = 24, + }; + + sa24.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "38mm", + DisplayOrder = 1, + }); + + sa24.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "40mm", + DisplayOrder = 2, + }); + + sa24.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "44mm", + DisplayOrder = 3, + }); + + #endregion sa24 diameter + + #region sa25 closure + + var sa25 = new SpecificationAttribute + { + Name = "Closure", + DisplayOrder = 25, + }; + + sa25.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "snap closure", + DisplayOrder = 1, + }); + + sa25.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "folding clasp", + DisplayOrder = 2, + }); + + sa25.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "thorn close", + DisplayOrder = 3, + }); + + #endregion sa25 closure + + #region sa26 facial shape + + var sa26 = new SpecificationAttribute + { + Name = "Facial shape", + DisplayOrder = 26, + }; + + sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "oval", + DisplayOrder = 1, + }); + + sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "round", + DisplayOrder = 2, + }); + + sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "heart shaped", + DisplayOrder = 3, + }); + + sa26.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "angular", + DisplayOrder = 4, + }); + + #endregion sa26 facial shape + + #region sa27 storage capacity + + var sa27 = new SpecificationAttribute + { + Name = "Storage capacity", + DisplayOrder = 27, + }; + + sa27.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "32 GB", + DisplayOrder = 1, + }); + + sa27.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "64 GB", + DisplayOrder = 2, + }); + + sa27.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "128 GB", + DisplayOrder = 3, + }); + + #endregion sa27 facial shape + + #region sa28 Dial window material type + + var sa28 = new SpecificationAttribute + { + Name = "Dial window material type", + DisplayOrder = 28, + }; + + sa28.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Mineral", + DisplayOrder = 1, + }); + + sa28.SpecificationAttributeOptions.Add(new SpecificationAttributeOption() + { + Name = "Sapphire", + DisplayOrder = 2, + }); + + #endregion sa28 Dial window material type + + var entities = new List + { + sa1,sa2,sa3,sa4,sa5,sa6,sa7,sa8,sa9,sa10,sa11,sa12,sa13,sa14,sa15,sa16,sa17,sa18,sa19,sa20,sa21,sa22,sa23,sa24,sa25,sa26,sa27,sa28 + }; + + this.Alter(entities); + return entities; + } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Variants.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Variants.cs index fe94686864..840dd8d046 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Variants.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.Variants.cs @@ -1,9 +1,8 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; -using System.Text; namespace SmartStore.Data.Setup { @@ -258,11 +257,11 @@ public IList ProductVariantAttributes() new { Name = "Dark red", Color = "#5e0000" } }; + var products = _ctx.Set().ToList().ToDictionarySafe(x => x.Sku, x => x); #region Oakley custom flak - var productCustomFlak = _ctx.Set().First(x => x.Sku == "P-3002"); - + var productCustomFlak = products["P-3002"]; var attributeLensType = new ProductVariantAttribute() { Product = productCustomFlak, @@ -470,10 +469,9 @@ public IList ProductVariantAttributes() #endregion Oakley custom flak + #region Wayfarer - #region wayfarer - - var productWayfarer = _ctx.Set().First(x => x.Sku == "P-3003"); + var productWayfarer = products["P-3003"]; var wayfarerFramePictures = _ctx.Set().Where(x => x.Name.StartsWith("wayfarer-")).ToList(); var attributeWayfarerLenscolor = new ProductVariantAttribute() @@ -575,15 +573,13 @@ public IList ProductVariantAttributes() MediaFileId = wayfarerFramePicture.Id }); - entities.Add(attributeWayfarerFramecolor); #endregion wayfarer #region 9,7 iPad - var product97iPad = _ctx.Set().First(x => x.Sku == "P-2004"); - + var product97iPad = products["P-2004"]; var attribute97iPadMemoryCapacity = new ProductVariantAttribute() { Product = product97iPad, @@ -616,7 +612,6 @@ public IList ProductVariantAttributes() entities.Add(attribute97iPadMemoryCapacity); - var attribute97iPadColor = new ProductVariantAttribute() { Product = product97iPad, @@ -717,15 +712,13 @@ public IList ProductVariantAttributes() Color = "#dfddb6" }); - entities.Add(attribute97iPadColor); #endregion 9,7 iPad #region iPhone 7 plus - var productIphone7Plus = _ctx.Set().First(x => x.Sku == "P-2001"); - + var productIphone7Plus = products["P-2001"]; var attributeIphone7PlusMemoryCapacity = new ProductVariantAttribute() { Product = productIphone7Plus, @@ -823,97 +816,9 @@ public IList ProductVariantAttributes() #endregion iPhone 7 plus - #region iPhone - - // var productIphone = _ctx.Set().First(x => x.Sku == "Apple-1001"); - - //var attributeIphoneMemoryCapacity = new ProductVariantAttribute() - //{ - // Product = productIphone, - // ProductAttribute = attrMemoryCapacity, - // IsRequired = true, - // DisplayOrder = 1, - // AttributeControlType = AttributeControlType.DropdownList - //}; - - //attributeIphoneMemoryCapacity.ProductVariantAttributeValues.Add(new ProductVariantAttributeValue() - //{ - // Name = "16 GB", - // Alias = "16gb", - // IsPreSelected = true, - // DisplayOrder = 1, - // Quantity = 1, - // ValueType = ProductVariantAttributeValueType.Simple - //}); - - //attributeIphoneMemoryCapacity.ProductVariantAttributeValues.Add(new ProductVariantAttributeValue() - //{ - // Name = "64 GB", - // Alias = "64gb", - // DisplayOrder = 2, - // Quantity = 1, - // ValueType = ProductVariantAttributeValueType.Simple, - // PriceAdjustment = 100.0M - //}); - - //attributeIphoneMemoryCapacity.ProductVariantAttributeValues.Add(new ProductVariantAttributeValue() - //{ - // Name = "128 GB", - // Alias = "128gb", - // DisplayOrder = 3, - // Quantity = 1, - // ValueType = ProductVariantAttributeValueType.Simple, - // PriceAdjustment = 200.0M - //}); - - //entities.Add(attributeIphoneMemoryCapacity); - - - //var attributeIphoneColor = new ProductVariantAttribute() - //{ - // Product = productIphone, - // ProductAttribute = attrIphoneColor, - // IsRequired = true, - // DisplayOrder = 2, - // AttributeControlType = AttributeControlType.DropdownList - //}; - - //attributeIphoneColor.ProductVariantAttributeValues.Add(new ProductVariantAttributeValue() - //{ - // Name = "Silver", - // Alias = "silver", - // IsPreSelected = true, - // DisplayOrder = 1, - // Quantity = 1, - // ValueType = ProductVariantAttributeValueType.Simple - //}); - - //attributeIphoneColor.ProductVariantAttributeValues.Add(new ProductVariantAttributeValue() - //{ - // Name = "Gold", - // Alias = "gold", - // DisplayOrder = 2, - // Quantity = 1, - // ValueType = ProductVariantAttributeValueType.Simple - //}); - - //attributeIphoneColor.ProductVariantAttributeValues.Add(new ProductVariantAttributeValue() - //{ - // Name = "Space gray", - // Alias = "spacegray", - // DisplayOrder = 3, - // Quantity = 1, - // ValueType = ProductVariantAttributeValueType.Simple - //}); - - //entities.Add(attributeIphoneColor); - - #endregion iPhone - - #region attributeDualshock3ControllerColor - - var productPs3 = _ctx.Set().First(x => x.Sku == "Sony-PS399000"); + #region Dualshock3ControllerColor + var productPs3 = products["Sony-PS399000"]; var attributeDualshock3ControllerColor = new ProductVariantAttribute() { Product = productPs3, @@ -945,12 +850,11 @@ public IList ProductVariantAttributes() entities.Add(attributeDualshock3ControllerColor); - #endregion attributeDualshock3ControllerColor + #endregion Dualshock3ControllerColor - #region attribute Apple Airpod - - var productAirpod = _ctx.Set().First(x => x.Sku == "P-2003"); + #region Apple Airpod + var productAirpod = products["P-2003"]; var attributeAirpod = new ProductVariantAttribute() { Product = productAirpod, @@ -1035,12 +939,11 @@ public IList ProductVariantAttributes() entities.Add(attributeAirpod); - #endregion attribute Apple Airpod - - #region attribute Evopower 5.3 Trainer HS Ball + #endregion Apple Airpod - var productEvopower = _ctx.Set().First(x => x.Sku == "P-5003"); + #region Evopower 5.3 Trainer HS Ball + var productEvopower = products["P-5003"]; var attributeEvopower = new ProductVariantAttribute() { Product = productEvopower, @@ -1084,14 +987,13 @@ public IList ProductVariantAttributes() //LinkedProductId = _ctx.Set().First(x => x.Sku == "Ubi-princepersia").Id }); - entities.Add(attributeEvopower); - #endregion attribute Evopower 5.3 Trainer HS Ball + #endregion Evopower 5.3 Trainer HS Ball - #region attributePs3OneGameFree + #region Ps3OneGameFree - // var productPs3OneGameFree = _ctx.Set().First(x => x.Sku == "Sony-PS310111"); + //var productPs3OneGameFree = _ctx.Set().First(x => x.Sku == "Sony-PS310111"); //var attributePs3OneGameFree = new ProductVariantAttribute() //{ @@ -1144,11 +1046,11 @@ public IList ProductVariantAttributes() //entities.Add(attributePs3OneGameFree); - #endregion attributePs3OneGameFree + #endregion Ps3OneGameFree #region Fashion - Converse All Star - var productAllStar = _ctx.Set().First(x => x.Sku == "Fashion-112355"); + var productAllStar = products["Fashion-112355"]; var allStarColors = new string[] { "Charcoal", "Maroon", "Navy", "Purple", "White" }; var allStarPictures = _ctx.Set().Where(x => x.Name.StartsWith("allstar-")).ToList(); @@ -1211,7 +1113,7 @@ public IList ProductVariantAttributes() #region Fashion - Shirt Meccanica - var productShirtMeccanica = _ctx.Set().First(x => x.Sku == "Fashion-987693502"); + var productShirtMeccanica = products["Fashion-987693502"]; var shirtMeccanicaSizes = new string[] { "XS", "S", "M", "L", "XL" }; var shirtMeccanicaColors = new[] { @@ -1268,7 +1170,7 @@ public IList ProductVariantAttributes() #region Fashion - Ladies Jacket - var productLadiesJacket = _ctx.Set().First(x => x.Sku == "Fashion-JN1107"); + var productLadiesJacket = products["Fashion-JN1107"]; var ladiesJacketSizes = new string[] { "XS", "S", "M", "L", "XL" }; var ladiesJacketColors = new[] { @@ -1330,7 +1232,7 @@ public IList ProductVariantAttributes() #region Fashion - Clark Jeans - var productClarkJeans = _ctx.Set().First(x => x.Sku == "Fashion-65986524"); + var productClarkJeans = products["Fashion-65986524"]; var clarkJeansWidth = new string[] { "31", "32", "33", "34", "35", "36", "38", "40", "42", "44", "46" }; var clarkJeansLength = new string[] { "30", "32", "34" }; @@ -1382,8 +1284,7 @@ public IList ProductVariantAttributes() #region Furniture - Le Corbusier LC 6 table - var productCorbusierTable = _ctx.Set().First(x => x.Sku == "Furniture-lc6"); - + var productCorbusierTable = products["Furniture-lc6"]; var attrCorbusierTablePlate = new ProductVariantAttribute { Product = productCorbusierTable, @@ -1438,7 +1339,7 @@ public IList ProductVariantAttributes() #region Soccer Adidas TANGO SALA BALL - var productAdidasTANGOSALABALL = _ctx.Set().First(x => x.Sku == "P-5001"); + var productAdidasTANGOSALABALL = products["P-5001"]; var productAdidasTANGOSALABALLSizes = new string[] { "3", "4", "5" }; var productAdidasTANGOSALABALLColors = new[] { @@ -1500,7 +1401,7 @@ public IList ProductVariantAttributes() #region Torfabrik official game ball - var productTorfabrikBall = _ctx.Set().First(x => x.Sku == "P-5002"); + var productTorfabrikBall = products["P-5002"]; var productTorfabrikBallSizes = new string[] { "3", "4", "5" }; var productTorfabrikBallColors = new[] { @@ -1560,8 +1461,7 @@ public IList ProductVariantAttributes() #region Furniture - Ball chair - var productBallChair = _ctx.Set().First(x => x.Sku == "Furniture-ball-chair"); - + var productBallChair = products["Furniture-ball-chair"]; var attrBallChairMaterial = new ProductVariantAttribute { Product = productBallChair, @@ -1648,8 +1548,7 @@ public IList ProductVariantAttributes() #region Furniture - Lounge chair - var productLoungeChair = _ctx.Set().First(x => x.Sku == "Furniture-lounge-chair"); - + var productLoungeChair = products["Furniture-lounge-chair"]; var attrLoungeChairMaterial = new ProductVariantAttribute { Product = productLoungeChair, @@ -1752,8 +1651,7 @@ public IList ProductVariantAttributes() #region Furniture - Cube chair - var productCubeChair = _ctx.Set().First(x => x.Sku == "Furniture-cube-chair"); - + var productCubeChair = products["Furniture-cube-chair"]; var attrCubeChairMaterial = new ProductVariantAttribute { Product = productCubeChair, @@ -1830,15 +1728,12 @@ public IList ProductVariantAttributeCombinat var attrWayfarerLenscolor = _ctx.Set().First(x => x.Alias == "wayfarerlenscolor"); var attrWayfarerFramecolor = _ctx.Set().First(x => x.Alias == "wayfarerframecolor"); - #region ORIGINAL WAYFARER AT COLLECTION var productWayfarer = _ctx.Set().First(x => x.Sku == "P-3003"); var wayfarerPictureIds = productWayfarer.ProductPictures.Select(pp => pp.MediaFileId).ToList(); var picturesWayfarer = _ctx.Set().Where(x => wayfarerPictureIds.Contains(x.Id)).ToList(); - //var attributeColorIphone7Plus = _ctx.Set().First(x => x.ProductId == productIphone7Plus.Id && x.ProductAttributeId == attrColor.Id); - var wayfarerLenscolor = _ctx.Set().First(x => x.ProductId == productWayfarer.Id && x.ProductAttributeId == attrWayfarerLenscolor.Id); var wayfarerLenscolorValues = _ctx.Set().Where(x => x.ProductVariantAttributeId == wayfarerLenscolor.Id).ToList(); @@ -1846,6 +1741,7 @@ public IList ProductVariantAttributeCombinat var wayfarerFramecolorValues = _ctx.Set().Where(x => x.ProductVariantAttributeId == wayfarerFramecolor.Id).ToList(); #region blue-gray-classic-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1865,6 +1761,7 @@ public IList ProductVariantAttributeCombinat #endregion blue-gray-classic-black #region gray-course-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1884,6 +1781,7 @@ public IList ProductVariantAttributeCombinat #endregion gray-course-black #region brown-course-havana + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1903,6 +1801,7 @@ public IList ProductVariantAttributeCombinat #endregion brown-course-havana #region green-classic-havana-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1921,9 +1820,8 @@ public IList ProductVariantAttributeCombinat #endregion green-classic-havana-black - // not available products not available products not available products not available products not available products - #region blue-gray-classic-havana-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1943,6 +1841,7 @@ public IList ProductVariantAttributeCombinat #endregion green-classic-havana-black #region blue-gray-classic-havana + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1961,8 +1860,8 @@ public IList ProductVariantAttributeCombinat #endregion green-classic-rayban-black - // gray-course #region gray-course-havana-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -1982,6 +1881,7 @@ public IList ProductVariantAttributeCombinat #endregion gray-course-havana-black #region gray-course-havana + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -2001,6 +1901,7 @@ public IList ProductVariantAttributeCombinat #endregion gray-course-rayban-black #region green-classic-rayban-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -2020,6 +1921,7 @@ public IList ProductVariantAttributeCombinat #endregion green-classic-rayban-black #region green-classic-havana + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -2038,8 +1940,8 @@ public IList ProductVariantAttributeCombinat #endregion gray-course-rayban-black - // brown-course #region brown-course-havana-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -2059,6 +1961,7 @@ public IList ProductVariantAttributeCombinat #endregion brown-course-havana-black #region brown-course-rayban-black + entities.Add(new ProductVariantAttributeCombination() { Product = productWayfarer, @@ -2129,9 +2032,6 @@ public IList ProductVariantAttributeCombinat } } - - - #endregion Custom Flak #region ps3 diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index 940be29c7b..8f1ac6d219 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Text; using SmartStore.Core; using SmartStore.Core.Configuration; using SmartStore.Core.Domain; @@ -31,1435 +30,1576 @@ using SmartStore.Core.Domain.Themes; using SmartStore.Core.Domain.Topics; using SmartStore.Core.IO; +using SmartStore.Rules; +using SmartStore.Rules.Domain; using SmartStore.Utilities; namespace SmartStore.Data.Setup { public abstract partial class InvariantSeedData - { - private SmartObjectContext _ctx; - private string _sampleImagesPath; - - protected InvariantSeedData() - { - } - - public void Initialize(SmartObjectContext context) - { - _ctx = context; - _sampleImagesPath = CommonHelper.MapPath("~/App_Data/Samples/"); - } - - #region Mandatory data creators - - public IList Pictures() - { - var entities = new List - { - CreatePicture("company-logo.png"), - CreatePicture("product/allstar_charcoal.jpg"), - CreatePicture("product/allstar_maroon.jpg"), - CreatePicture("product/allstar_navy.jpg"), - CreatePicture("product/allstar_purple.jpg"), - CreatePicture("product/allstar_white.jpg"), - CreatePicture("product/wayfarer_havana.png"), - CreatePicture("product/wayfarer_havana_black.png"), - CreatePicture("product/wayfarer_rayban-black.png") - }; - - this.Alter(entities); - return entities; - } - - public IList Stores() - { - var imgCompanyLogo = _ctx.Set().Where(x => x.Name == "company-logo.png").FirstOrDefault(); - - var currency = _ctx.Set().FirstOrDefault(x => x.CurrencyCode == "EUR"); - if (currency == null) - currency = _ctx.Set().First(); - - var entities = new List() - { - new Store() - { - Name = "Your store name", - Url = "http://www.yourStore.com/", - Hosts = "yourstore.com,www.yourstore.com", - SslEnabled = false, - DisplayOrder = 1, - LogoMediaFileId = imgCompanyLogo.Id, - PrimaryStoreCurrencyId = currency.Id, - PrimaryExchangeRateCurrencyId = currency.Id - } - }; - this.Alter(entities); - return entities; - } - - public IList MeasureDimensions() - { - var entities = new List() - { - new MeasureDimension() - { - Name = "millimetre", - SystemKeyword = "mm", - Ratio = 25.4M, - DisplayOrder = 1, - }, - new MeasureDimension() - { - Name = "centimetre", - SystemKeyword = "cm", - Ratio = 0.254M, - DisplayOrder = 2, - }, - new MeasureDimension() - { - Name = "meter", - SystemKeyword = "m", - Ratio = 0.0254M, - DisplayOrder = 3, - }, - new MeasureDimension() - { - Name = "in", - SystemKeyword = "inch", - Ratio = 1M, - DisplayOrder = 4, - }, - new MeasureDimension() - { - Name = "feet", - SystemKeyword = "ft", - Ratio = 0.08333333M, - DisplayOrder = 5, - } - }; - - this.Alter(entities); - return entities; - } - - public IList MeasureWeights() - { - var entities = new List() - { - new MeasureWeight() - { - Name = "ounce", // Ounce, Unze + { + private SmartObjectContext _ctx; + private string _sampleImagesPath; + + protected InvariantSeedData() + { + } + + public void Initialize(SmartObjectContext context) + { + _ctx = context; + _sampleImagesPath = CommonHelper.MapPath("~/App_Data/Samples/"); + } + + #region Mandatory data creators + + public IList Pictures() + { + var entities = new List + { + CreatePicture("company-logo.png"), + CreatePicture("product/allstar_charcoal.jpg"), + CreatePicture("product/allstar_maroon.jpg"), + CreatePicture("product/allstar_navy.jpg"), + CreatePicture("product/allstar_purple.jpg"), + CreatePicture("product/allstar_white.jpg"), + CreatePicture("product/wayfarer_havana.png"), + CreatePicture("product/wayfarer_havana_black.png"), + CreatePicture("product/wayfarer_rayban-black.png") + }; + + this.Alter(entities); + return entities; + } + + public IList Stores() + { + var imgCompanyLogo = _ctx.Set().Where(x => x.Name == "company-logo.png").FirstOrDefault(); + + var currency = _ctx.Set().FirstOrDefault(x => x.CurrencyCode == "EUR"); + if (currency == null) + currency = _ctx.Set().First(); + + var entities = new List() + { + new Store() + { + Name = "Your store name", + Url = "http://www.yourStore.com/", + Hosts = "yourstore.com,www.yourstore.com", + SslEnabled = false, + DisplayOrder = 1, + LogoMediaFileId = imgCompanyLogo.Id, + PrimaryStoreCurrencyId = currency.Id, + PrimaryExchangeRateCurrencyId = currency.Id + } + }; + this.Alter(entities); + return entities; + } + + public IList MeasureDimensions() + { + var entities = new List() + { + new MeasureDimension() + { + Name = "millimetre", + SystemKeyword = "mm", + Ratio = 25.4M, + DisplayOrder = 1, + }, + new MeasureDimension() + { + Name = "centimetre", + SystemKeyword = "cm", + Ratio = 0.254M, + DisplayOrder = 2, + }, + new MeasureDimension() + { + Name = "meter", + SystemKeyword = "m", + Ratio = 0.0254M, + DisplayOrder = 3, + }, + new MeasureDimension() + { + Name = "in", + SystemKeyword = "inch", + Ratio = 1M, + DisplayOrder = 4, + }, + new MeasureDimension() + { + Name = "feet", + SystemKeyword = "ft", + Ratio = 0.08333333M, + DisplayOrder = 5, + } + }; + + this.Alter(entities); + return entities; + } + + public IList MeasureWeights() + { + var entities = new List() + { + new MeasureWeight() + { + Name = "ounce", // Ounce, Unze SystemKeyword = "oz", - Ratio = 16M, - DisplayOrder = 5, - }, - new MeasureWeight() - { - Name = "lb", // Pound + Ratio = 16M, + DisplayOrder = 5, + }, + new MeasureWeight() + { + Name = "lb", // Pound SystemKeyword = "lb", - Ratio = 1M, - DisplayOrder = 6, - }, - - new MeasureWeight() - { - Name = "kg", - SystemKeyword = "kg", - Ratio = 0.45359237M, - DisplayOrder = 1, - }, - new MeasureWeight() - { - Name = "gram", - SystemKeyword = "g", - Ratio = 453.59237M, - DisplayOrder = 2, - }, - new MeasureWeight() - { - Name = "liter", - SystemKeyword = "l", - Ratio = 0.45359237M, - DisplayOrder = 3, - }, - new MeasureWeight() - { - Name = "milliliter", - SystemKeyword = "ml", - Ratio = 0.45359237M, - DisplayOrder = 4, - } - }; - - this.Alter(entities); - return entities; - } - - protected virtual string TaxNameBooks - { - get => "Books"; - } - - protected virtual string TaxNameDigitalGoods - { - get => "Downloadable Products"; - } - - protected virtual string TaxNameJewelry - { - get => "Jewelry"; - } - - protected virtual string TaxNameApparel - { - get => "Apparel & Shoes"; - } - - protected virtual string TaxNameFood - { - get => "Food"; - } - - protected virtual string TaxNameElectronics - { - get => "Electronics & Software"; - } - - protected virtual string TaxNameTaxFree - { - get => "Tax free"; - } - - public virtual decimal[] FixedTaxRates - { - get => new decimal[] { 0, 0, 0, 0, 0 }; - } - - public IList TaxCategories() - { - var entities = new List - { - new TaxCategory - { - Name = this.TaxNameBooks, - DisplayOrder = 1, - }, - new TaxCategory - { - Name = this.TaxNameElectronics, - DisplayOrder = 5, - }, - new TaxCategory - { - Name = this.TaxNameDigitalGoods, - DisplayOrder = 10, - }, - new TaxCategory - { - Name = this.TaxNameJewelry, - DisplayOrder = 15, - }, - new TaxCategory - { - Name = this.TaxNameApparel, - DisplayOrder = 20, - }, - }; - - this.Alter(entities); - return entities; - } - - public IList Currencies() - { - var entities = new List() - { - CreateCurrency("en-US", published: true, rate: 1M, order: 0), - CreateCurrency("en-GB", published: true, rate: 0.61M, order: 5), - CreateCurrency("en-AU", published: true, rate: 0.94M, order: 10), - CreateCurrency("en-CA", published: true, rate: 0.98M, order: 15), - CreateCurrency("de-DE", rate: 0.79M, order: 20/*, formatting: string.Format("0.00 {0}", "\u20ac")*/), - CreateCurrency("de-CH", rate: 0.93M, order: 25, formatting: "CHF #,##0.00"), - CreateCurrency("zh-CN", rate: 6.48M, order: 30), - CreateCurrency("zh-HK", rate: 7.75M, order: 35), - CreateCurrency("ja-JP", rate: 80.07M, order: 40), - CreateCurrency("ru-RU", rate: 27.7M, order: 45), - CreateCurrency("tr-TR", rate: 1.78M, order: 50), - CreateCurrency("sv-SE", rate: 6.19M, order: 55) - }; - - this.Alter(entities); - return entities; - } - - public IList ShippingMethods() - { - var entities = new List() - { - new ShippingMethod - { - Name = "In-Store Pickup", - Description ="Pick up your items at the store", - DisplayOrder = 0 - }, - new ShippingMethod - { - Name = "By Ground", - Description ="Compared to other shipping methods, like by flight or over seas, ground shipping is carried out closer to the earth", - DisplayOrder = 1 - }, - }; - this.Alter(entities); - return entities; - } - - public IList CustomerRoles() - { - var crAdministrators = new CustomerRole - { - Name = "Administrators", - Active = true, - IsSystemRole = true, - SystemName = SystemCustomerRoleNames.Administrators, - }; - var crForumModerators = new CustomerRole - { - Name = "Forum Moderators", - Active = true, - IsSystemRole = true, - SystemName = SystemCustomerRoleNames.ForumModerators, - }; - var crRegistered = new CustomerRole - { - Name = "Registered", - Active = true, - IsSystemRole = true, - SystemName = SystemCustomerRoleNames.Registered, - }; - var crGuests = new CustomerRole - { - Name = "Guests", - Active = true, - IsSystemRole = true, - SystemName = SystemCustomerRoleNames.Guests, - }; - var entities = new List - { - crAdministrators, - crForumModerators, - crRegistered, - crGuests - }; - this.Alter(entities); - return entities; - } - - public Address AdminAddress() - { + Ratio = 1M, + DisplayOrder = 6, + }, + + new MeasureWeight() + { + Name = "kg", + SystemKeyword = "kg", + Ratio = 0.45359237M, + DisplayOrder = 1, + }, + new MeasureWeight() + { + Name = "gram", + SystemKeyword = "g", + Ratio = 453.59237M, + DisplayOrder = 2, + }, + new MeasureWeight() + { + Name = "liter", + SystemKeyword = "l", + Ratio = 0.45359237M, + DisplayOrder = 3, + }, + new MeasureWeight() + { + Name = "milliliter", + SystemKeyword = "ml", + Ratio = 0.45359237M, + DisplayOrder = 4, + } + }; + + this.Alter(entities); + return entities; + } + + protected virtual string TaxNameBooks => "Books"; + + protected virtual string TaxNameDigitalGoods => "Downloadable Products"; + + protected virtual string TaxNameJewelry => "Jewelry"; + + protected virtual string TaxNameApparel => "Apparel & Shoes"; + + protected virtual string TaxNameFood => "Food"; + + protected virtual string TaxNameElectronics => "Electronics & Software"; + + protected virtual string TaxNameTaxFree => "Tax free"; + + public virtual decimal[] FixedTaxRates => new decimal[] { 0, 0, 0, 0, 0 }; + + public IList TaxCategories() + { + var entities = new List + { + new TaxCategory + { + Name = this.TaxNameTaxFree, + DisplayOrder = 0, + }, + new TaxCategory + { + Name = this.TaxNameBooks, + DisplayOrder = 1, + }, + new TaxCategory + { + Name = this.TaxNameElectronics, + DisplayOrder = 5, + }, + new TaxCategory + { + Name = this.TaxNameDigitalGoods, + DisplayOrder = 10, + }, + new TaxCategory + { + Name = this.TaxNameJewelry, + DisplayOrder = 15, + }, + new TaxCategory + { + Name = this.TaxNameApparel, + DisplayOrder = 20, + } + }; + + this.Alter(entities); + return entities; + } + + public IList Currencies() + { + var entities = new List() + { + CreateCurrency("en-US", published: true, rate: 1M, order: 0), + CreateCurrency("en-GB", published: true, rate: 0.61M, order: 5), + CreateCurrency("en-AU", published: true, rate: 0.94M, order: 10), + CreateCurrency("en-CA", published: true, rate: 0.98M, order: 15), + CreateCurrency("de-DE", rate: 0.79M, order: 20/*, formatting: string.Format("0.00 {0}", "\u20ac")*/), + CreateCurrency("de-CH", rate: 0.93M, order: 25, formatting: "CHF #,##0.00"), + CreateCurrency("zh-CN", rate: 6.48M, order: 30), + CreateCurrency("zh-HK", rate: 7.75M, order: 35), + CreateCurrency("ja-JP", rate: 80.07M, order: 40), + CreateCurrency("ru-RU", rate: 27.7M, order: 45), + CreateCurrency("tr-TR", rate: 1.78M, order: 50), + CreateCurrency("sv-SE", rate: 6.19M, order: 55) + }; + + this.Alter(entities); + return entities; + } + + public IList ShippingMethods(bool includeSamples) + { + var entities = new List + { + new ShippingMethod + { + Name = "In-Store Pickup", + Description ="Pick up your items at the store", + DisplayOrder = 0 + }, + new ShippingMethod + { + Name = "By Ground", + Description ="Compared to other shipping methods, like by flight or over seas, ground shipping is carried out closer to the earth", + DisplayOrder = 1 + }, + }; + + if (includeSamples) + { + entities.Add(new ShippingMethod + { + Name = "Free shipping", + DisplayOrder = 2, + IgnoreCharges = true + }); + } + + Alter(entities); + return entities; + } + + public IList CustomerRoles(bool includeSamples) + { + var entities = new List + { + new CustomerRole + { + Name = "Administrators", + Active = true, + IsSystemRole = true, + SystemName = SystemCustomerRoleNames.Administrators, + }, + new CustomerRole + { + Name = "Forum Moderators", + Active = true, + IsSystemRole = true, + SystemName = SystemCustomerRoleNames.ForumModerators, + }, + new CustomerRole + { + Name = "Registered", + Active = true, + IsSystemRole = true, + SystemName = SystemCustomerRoleNames.Registered, + }, + new CustomerRole + { + Name = "Guests", + Active = true, + IsSystemRole = true, + SystemName = SystemCustomerRoleNames.Guests, + } + }; + + if (includeSamples) + { + entities.Add(new CustomerRole + { + Name = "Inactive new customers", + Active = true, + IsSystemRole = false, + // SystemName is not required. It's only used here to assign a rule set later. + SystemName = "InactiveNewCustomers" + }); + } + + Alter(entities); + return entities; + } + + public Address AdminAddress() + { var cCountry = _ctx.Set() .Where(x => x.ThreeLetterIsoCode == "USA") .FirstOrDefault(); - var entity = new Address() - { - FirstName = "John", - LastName = "Smith", - PhoneNumber = "12345678", - Email = "admin@myshop.com", - FaxNumber = "", - Company = "John Smith LLC", - Address1 = "1234 Main Road", - Address2 = "", - City = "New York", - StateProvince = cCountry.StateProvinces.FirstOrDefault(), - Country = cCountry, - ZipPostalCode = "12212", - CreatedOnUtc = DateTime.UtcNow, - }; - this.Alter(entity); - return entity; - } - - public Customer SearchEngineUser() - { - var entity = new Customer - { - Email = "builtin@search-engine-record.com", - CustomerGuid = Guid.NewGuid(), - PasswordFormat = PasswordFormat.Clear, - AdminComment = "Built-in system guest record used for requests from search engines.", - Active = true, - IsSystemAccount = true, - SystemName = SystemCustomerNames.SearchEngine, - CreatedOnUtc = DateTime.UtcNow, - LastActivityDateUtc = DateTime.UtcNow, - }; - - this.Alter(entity); - return entity; - } - - public Customer BackgroundTaskUser() - { - var entity = new Customer - { - Email = "builtin@background-task-record.com", - CustomerGuid = Guid.NewGuid(), - PasswordFormat = PasswordFormat.Clear, - AdminComment = "Built-in system record used for background tasks.", - Active = true, - IsSystemAccount = true, - SystemName = SystemCustomerNames.BackgroundTask, - CreatedOnUtc = DateTime.UtcNow, - LastActivityDateUtc = DateTime.UtcNow, - }; - - this.Alter(entity); - return entity; - } - - public Customer PdfConverterUser() - { - var entity = new Customer - { - Email = "builtin@pdf-converter-record.com", - CustomerGuid = Guid.NewGuid(), - PasswordFormat = PasswordFormat.Clear, - AdminComment = "Built-in system record used for the PDF converter.", - Active = true, - IsSystemAccount = true, - SystemName = SystemCustomerNames.PdfConverter, - CreatedOnUtc = DateTime.UtcNow, - LastActivityDateUtc = DateTime.UtcNow, - }; - - this.Alter(entity); - return entity; - } - - public IList EmailAccounts() - { - var entities = new List() - { - new EmailAccount - { - Email = "test@mail.com", - DisplayName = "Store name", - Host = "smtp.mail.com", - Port = 25, - Username = "123", - Password = "123", - EnableSsl = false, - UseDefaultCredentials = false - } - }; - this.Alter(entities); - return entities; - } - - public IList Topics() - { - var entities = new List() - { - new Topic - { - SystemName = "AboutUs", - IncludeInSitemap = false, - IsPasswordProtected = false, - Title = "About Us", - Body = "

Put your "About Us" information here. You can edit this in the admin site.

" - }, - new Topic - { - SystemName = "CheckoutAsGuestOrRegister", - IncludeInSitemap = false, - IsPasswordProtected = false, + var entity = new Address() + { + FirstName = "John", + LastName = "Smith", + PhoneNumber = "12345678", + Email = "admin@myshop.com", + FaxNumber = "", + Company = "John Smith LLC", + Address1 = "1234 Main Road", + Address2 = "", + City = "New York", + StateProvince = cCountry.StateProvinces.FirstOrDefault(), + Country = cCountry, + ZipPostalCode = "12212", + CreatedOnUtc = DateTime.UtcNow, + }; + this.Alter(entity); + return entity; + } + + public Customer SearchEngineUser() + { + var entity = new Customer + { + Email = "builtin@search-engine-record.com", + CustomerGuid = Guid.NewGuid(), + PasswordFormat = PasswordFormat.Clear, + AdminComment = "Built-in system guest record used for requests from search engines.", + Active = true, + IsSystemAccount = true, + SystemName = SystemCustomerNames.SearchEngine, + CreatedOnUtc = DateTime.UtcNow, + LastActivityDateUtc = DateTime.UtcNow, + }; + + this.Alter(entity); + return entity; + } + + public Customer BackgroundTaskUser() + { + var entity = new Customer + { + Email = "builtin@background-task-record.com", + CustomerGuid = Guid.NewGuid(), + PasswordFormat = PasswordFormat.Clear, + AdminComment = "Built-in system record used for background tasks.", + Active = true, + IsSystemAccount = true, + SystemName = SystemCustomerNames.BackgroundTask, + CreatedOnUtc = DateTime.UtcNow, + LastActivityDateUtc = DateTime.UtcNow, + }; + + this.Alter(entity); + return entity; + } + + public Customer PdfConverterUser() + { + var entity = new Customer + { + Email = "builtin@pdf-converter-record.com", + CustomerGuid = Guid.NewGuid(), + PasswordFormat = PasswordFormat.Clear, + AdminComment = "Built-in system record used for the PDF converter.", + Active = true, + IsSystemAccount = true, + SystemName = SystemCustomerNames.PdfConverter, + CreatedOnUtc = DateTime.UtcNow, + LastActivityDateUtc = DateTime.UtcNow, + }; + + this.Alter(entity); + return entity; + } + + public IList EmailAccounts() + { + var entities = new List() + { + new EmailAccount + { + Email = "test@mail.com", + DisplayName = "Store name", + Host = "smtp.mail.com", + Port = 25, + Username = "123", + Password = "123", + EnableSsl = false, + UseDefaultCredentials = false + } + }; + this.Alter(entities); + return entities; + } + + public IList Topics() + { + var entities = new List() + { + new Topic + { + SystemName = "AboutUs", + IncludeInSitemap = false, + IsPasswordProtected = false, + Title = "About Us", + Body = "

Put your "About Us" information here. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "CheckoutAsGuestOrRegister", + IncludeInSitemap = false, + IsPasswordProtected = false, RenderAsWidget = true, WidgetWrapContent = false, - Title = "", - Body = "

Register and save time!
Register with us for future convenience:

  • Fast and easy check out
  • Easy access to your order history and status
" - }, - new Topic - { - SystemName = "ConditionsOfUse", - IncludeInSitemap = false, - IsPasswordProtected = false, - Title = "Conditions of use", - Body = "

Put your conditions of use information here. You can edit this in the admin site.

" - }, - new Topic - { - SystemName = "ContactUs", - IncludeInSitemap = false, - IsPasswordProtected = false, + Title = "", + Body = "

Register and save time!
Register with us for future convenience:

  • Fast and easy check out
  • Easy access to your order history and status
" + }, + new Topic + { + SystemName = "ConditionsOfUse", + IncludeInSitemap = false, + IsPasswordProtected = false, + Title = "Conditions of use", + Body = "

Put your conditions of use information here. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "ContactUs", + IncludeInSitemap = false, + IsPasswordProtected = false, RenderAsWidget = true, WidgetWrapContent = false, Title = "Contact us", - Body = "

Put your contact information here. You can edit this in the admin site.

" - }, - new Topic - { - SystemName = "ForumWelcomeMessage", - IncludeInSitemap = false, - IsPasswordProtected = false, + Body = "

Put your contact information here. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "ForumWelcomeMessage", + IncludeInSitemap = false, + IsPasswordProtected = false, RenderAsWidget = true, WidgetWrapContent = false, Title = "Forums", - Body = "

Put your welcome message here. You can edit this in the admin site.

" - }, - new Topic - { - SystemName = "HomePageText", - IncludeInSitemap = false, - IsPasswordProtected = false, + Body = "

Put your welcome message here. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "HomePageText", + IncludeInSitemap = false, + IsPasswordProtected = false, RenderAsWidget = true, WidgetWrapContent = false, Title = "Welcome to our store", - Body = "

Online shopping is the process consumers go through to purchase products or services over the Internet. You can edit this in the admin site.

" - }, - new Topic - { - SystemName = "LoginRegistrationInfo", - IncludeInSitemap = false, - IsPasswordProtected = false, + Body = "

Online shopping is the process consumers go through to purchase products or services over the Internet. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "LoginRegistrationInfo", + IncludeInSitemap = false, + IsPasswordProtected = false, RenderAsWidget = true, WidgetWrapContent = false, Title = "About login / registration", - Body = "

Not registered yet?

Create your own account now and experience our diversity. With an account you can place orders faster and will always have a perfect overview of your current and previous orders.

" - }, - new Topic - { - SystemName = "PrivacyInfo", - IncludeInSitemap = false, - IsPasswordProtected = false, - ShortTitle = "Privacy", - Title = "Privacy policy", - Body = "

" - }, - new Topic - { - SystemName = "ShippingInfo", - IncludeInSitemap = false, - IsPasswordProtected = false, - Title = "Shipping & Returns", - Body = "

Put your shipping & returns information here. You can edit this in the admin site.

" - }, - - new Topic - { - SystemName = "Imprint", - IncludeInSitemap = false, - IsPasswordProtected = false, - Title = "Imprint", - Body = @"

Put your imprint information here. YOu can edit this in the admin site.

" - }, - new Topic - { - SystemName = "Disclaimer", - IncludeInSitemap = false, - IsPasswordProtected = false, - Title = "Disclaimer", - Body = "

Put your disclaimer information here. You can edit this in the admin site.

" - }, - new Topic - { - SystemName = "PaymentInfo", - IncludeInSitemap = false, - IsPasswordProtected = false, - Title = "Payment info", - Body = "

Put your payment information here. You can edit this in the admin site.

" - }, - }; - this.Alter(entities); - return entities; - } - - public IList Settings() - { + Body = "

Not registered yet?

Create your own account now and experience our diversity. With an account you can place orders faster and will always have a perfect overview of your current and previous orders.

" + }, + new Topic + { + SystemName = "PrivacyInfo", + IncludeInSitemap = false, + IsPasswordProtected = false, + ShortTitle = "Privacy", + Title = "Privacy policy", + Body = "

" + }, + new Topic + { + SystemName = "ShippingInfo", + IncludeInSitemap = false, + IsPasswordProtected = false, + Title = "Shipping & Returns", + Body = "

Put your shipping & returns information here. You can edit this in the admin site.

" + }, + + new Topic + { + SystemName = "Imprint", + IncludeInSitemap = false, + IsPasswordProtected = false, + Title = "Imprint", + Body = "

Put your imprint information here. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "Disclaimer", + IncludeInSitemap = false, + IsPasswordProtected = false, + Title = "Disclaimer", + Body = "

Put your disclaimer information here. You can edit this in the admin site.

" + }, + new Topic + { + SystemName = "PaymentInfo", + IncludeInSitemap = false, + IsPasswordProtected = false, + Title = "Payment info", + Body = "

Put your payment information here. You can edit this in the admin site.

" + }, + }; + this.Alter(entities); + return entities; + } + + public IList Settings() + { var defaultDimensionId = _ctx.Set().FirstOrDefault(x => x.SystemKeyword == "inch")?.Id ?? 0; var defaultWeightId = _ctx.Set().FirstOrDefault(x => x.SystemKeyword == "lb")?.Id ?? 0; var defaultLanguageId = _ctx.Set().FirstOrDefault()?.Id ?? 0; var defaultEmailAccountId = _ctx.Set().FirstOrDefault()?.Id ?? 0; - var entities = new List - { - new PdfSettings - { - }, - new CommonSettings - { - }, - new SeoSettings - { - }, - new SocialSettings - { - }, - new AdminAreaSettings - { - }, - new CatalogSettings - { - }, - new LocalizationSettings - { - DefaultAdminLanguageId = defaultLanguageId - }, - new CustomerSettings - { - }, - new AddressSettings - { - }, - new MediaSettings - { - }, - new StoreInformationSettings - { - }, - new RewardPointsSettings - { - }, - new CurrencySettings - { - }, - new MeasureSettings - { - BaseDimensionId = defaultDimensionId, - BaseWeightId = defaultWeightId, - }, - new ShoppingCartSettings - { - }, - new OrderSettings - { - }, - new SecuritySettings - { - }, - new ShippingSettings - { - }, - new PaymentSettings - { - ActivePaymentMethodSystemNames = new List - { - "Payments.CashOnDelivery", - "Payments.Manual", - "Payments.PayInStore", - "Payments.Prepayment" - } - }, - new TaxSettings - { - }, - new BlogSettings - { - }, - new NewsSettings - { - }, - new ForumSettings - { - }, - new EmailAccountSettings - { - DefaultEmailAccountId = defaultEmailAccountId - }, - new ThemeSettings - { - } - }; - - this.Alter(entities); - return entities; - } - - public IList ProductTemplates() - { - var entities = new List - { - new ProductTemplate - { - Name = "Default Product Template", - ViewPath = "Product", - DisplayOrder = 10 - } - }; - this.Alter(entities); - return entities; - } - - public IList CategoryTemplates() - { - var entities = new List - { - new CategoryTemplate - { - Name = "Products in Grid or Lines", - ViewPath = "CategoryTemplate.ProductsInGridOrLines", - DisplayOrder = 1 - }, - }; - this.Alter(entities); - return entities; - } - - public IList ManufacturerTemplates() - { - var entities = new List - { - new ManufacturerTemplate - { - Name = "Products in Grid or Lines", - ViewPath = "ManufacturerTemplate.ProductsInGridOrLines", - DisplayOrder = 1 - }, - }; - this.Alter(entities); - return entities; - } - - public IList ScheduleTasks() - { - var entities = new List - { - new ScheduleTask - { - Name = "Send emails", - CronExpression = "* * * * *", // every Minute + var entities = new List + { + new PdfSettings + { + }, + new CommonSettings + { + }, + new SeoSettings + { + }, + new SocialSettings + { + }, + new AdminAreaSettings + { + }, + new CatalogSettings + { + }, + new LocalizationSettings + { + DefaultAdminLanguageId = defaultLanguageId + }, + new CustomerSettings + { + }, + new AddressSettings + { + }, + new MediaSettings + { + }, + new StoreInformationSettings + { + }, + new RewardPointsSettings + { + }, + new CurrencySettings + { + }, + new MeasureSettings + { + BaseDimensionId = defaultDimensionId, + BaseWeightId = defaultWeightId, + }, + new ShoppingCartSettings + { + }, + new OrderSettings + { + }, + new SecuritySettings + { + }, + new ShippingSettings + { + }, + new PaymentSettings + { + ActivePaymentMethodSystemNames = new List + { + "Payments.CashOnDelivery", + "Payments.Manual", + "Payments.PayInStore", + "Payments.Prepayment" + } + }, + new TaxSettings + { + }, + new BlogSettings + { + }, + new NewsSettings + { + }, + new ForumSettings + { + }, + new EmailAccountSettings + { + DefaultEmailAccountId = defaultEmailAccountId + }, + new ThemeSettings + { + }, + new HomePageSettings + { + } + }; + + this.Alter(entities); + return entities; + } + + public IList ProductTemplates() + { + var entities = new List + { + new ProductTemplate + { + Name = "Default Product Template", + ViewPath = "Product", + DisplayOrder = 10 + } + }; + this.Alter(entities); + return entities; + } + + public IList CategoryTemplates() + { + var entities = new List + { + new CategoryTemplate + { + Name = "Products in Grid or Lines", + ViewPath = "CategoryTemplate.ProductsInGridOrLines", + DisplayOrder = 1 + }, + }; + this.Alter(entities); + return entities; + } + + public IList ManufacturerTemplates() + { + var entities = new List + { + new ManufacturerTemplate + { + Name = "Products in Grid or Lines", + ViewPath = "ManufacturerTemplate.ProductsInGridOrLines", + DisplayOrder = 1 + }, + }; + this.Alter(entities); + return entities; + } + + public IList ScheduleTasks() + { + var entities = new List + { + new ScheduleTask + { + Name = "Send emails", + CronExpression = "* * * * *", // every Minute Type = "SmartStore.Services.Messages.QueuedMessagesSendTask, SmartStore.Services", - Enabled = true, - StopOnError = false, + Enabled = true, + StopOnError = false, Priority = TaskPriority.High - }, - new ScheduleTask - { - Name = "Delete guests", - CronExpression = "*/10 * * * *", // Every 10 minutes + }, + new ScheduleTask + { + Name = "Delete guests", + CronExpression = "*/10 * * * *", // Every 10 minutes Type = "SmartStore.Services.Customers.DeleteGuestsTask, SmartStore.Services", - Enabled = true, - StopOnError = false, - }, - new ScheduleTask - { - Name = "Delete logs", - CronExpression = "0 1 * * *", // At 01:00 + Enabled = true, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Delete logs", + CronExpression = "0 1 * * *", // At 01:00 Type = "SmartStore.Services.Logging.DeleteLogsTask, SmartStore.Services", - Enabled = true, - StopOnError = false, - }, - new ScheduleTask - { - Name = "Clear cache", - CronExpression = "0 */12 * * *", // Every 12 hours + Enabled = true, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Clear cache", + CronExpression = "0 */12 * * *", // Every 12 hours Type = "SmartStore.Services.Caching.ClearCacheTask, SmartStore.Services", - Enabled = false, - StopOnError = false, - }, - new ScheduleTask - { - Name = "Update currency exchange rates", - CronExpression = "0 */6 * * *", // Every 6 hours + Enabled = false, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Update currency exchange rates", + CronExpression = "0 */6 * * *", // Every 6 hours Type = "SmartStore.Services.Directory.UpdateExchangeRateTask, SmartStore.Services", - Enabled = false, - StopOnError = false, + Enabled = false, + StopOnError = false, Priority = TaskPriority.High }, - new ScheduleTask - { - Name = "Clear transient uploads", - CronExpression = "30 1,13 * * *", // At 01:30 and 13:30 + new ScheduleTask + { + Name = "Clear transient uploads", + CronExpression = "30 1,13 * * *", // At 01:30 and 13:30 Type = "SmartStore.Services.Media.TransientMediaClearTask, SmartStore.Services", - Enabled = true, - StopOnError = false, - }, - new ScheduleTask - { - Name = "Clear email queue", - CronExpression = "0 2 * * *", // At 02:00 + Enabled = true, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Clear email queue", + CronExpression = "0 2 * * *", // At 02:00 Type = "SmartStore.Services.Messages.QueuedMessagesClearTask, SmartStore.Services", - Enabled = true, - StopOnError = false, - }, - new ScheduleTask - { - Name = "Cleanup temporary files", - CronExpression = "30 3 * * *", // At 03:30 + Enabled = true, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Cleanup temporary files", + CronExpression = "30 3 * * *", // At 03:30 Type = "SmartStore.Services.Common.TempFileCleanupTask, SmartStore.Services", - Enabled = true, - StopOnError = false - }, + Enabled = true, + StopOnError = false + }, + new ScheduleTask + { + Name = "Rebuild XML Sitemap", + CronExpression = "45 3 * * *", + Type = "SmartStore.Services.Seo.RebuildXmlSitemapTask, SmartStore.Services", + Enabled = true, + StopOnError = false + }, + new ScheduleTask + { + Name = "Update assignments of customers to customer roles", + CronExpression = "15 2 * * *", // At 02:15 + Type = "SmartStore.Services.Customers.TargetGroupEvaluatorTask, SmartStore.Services", + Enabled = true, + StopOnError = false + }, new ScheduleTask { - Name = "Rebuild XML Sitemap", - CronExpression = "45 3 * * *", - Type = "SmartStore.Services.Seo.RebuildXmlSitemapTask, SmartStore.Services", - Enabled = true, - StopOnError = false + Name = "Update assignments of products to categories", + CronExpression = "20 2 * * *", // At 02:20 + Type = "SmartStore.Services.Catalog.ProductRuleEvaluatorTask, SmartStore.Services", + Enabled = true, + StopOnError = false + } + }; + this.Alter(entities); + return entities; + } + + #endregion + + #region Sample data creators + + public IList ForumGroups() + { + var forumGroupGeneral = new ForumGroup + { + Name = "General", + Description = "", + DisplayOrder = 1 + }; + + var entities = new List + { + forumGroupGeneral + }; + + this.Alter(entities); + return entities; + } + + public IList Forums() + { + var group = _ctx.Set().FirstOrDefault(c => c.DisplayOrder == 1); + + var newProductsForum = new Forum + { + ForumGroup = group, + Name = "New Products", + Description = "Discuss new products and industry trends", + NumTopics = 0, + NumPosts = 0, + LastPostCustomerId = 0, + LastPostTime = null, + DisplayOrder = 1 + }; + + var packagingShippingForum = new Forum + { + ForumGroup = group, + Name = "Packaging & Shipping", + Description = "Discuss packaging & shipping", + NumTopics = 0, + NumPosts = 0, + LastPostTime = null, + DisplayOrder = 20 + }; + + + var entities = new List + { + newProductsForum, packagingShippingForum + }; + + this.Alter(entities); + return entities; + } + + public IList Discounts() + { + var ruleSets = _ctx.Set().Expand(x => x.Rules); + + var couponCodeDiscount = new Discount + { + Name = "Sample discount with coupon code", + DiscountType = DiscountType.AssignedToSkus, + DiscountLimitation = DiscountLimitationType.Unlimited, + UsePercentage = false, + DiscountAmount = 10, + RequiresCouponCode = true, + CouponCode = "123" + }; + + var orderTotalDiscount = new Discount + { + Name = "20% order total discount", + DiscountType = DiscountType.AssignedToOrderTotal, + DiscountLimitation = DiscountLimitationType.Unlimited, + UsePercentage = true, + DiscountPercentage = 20, + StartDateUtc = new DateTime(2020, 2, 1), + EndDateUtc = new DateTime(2020, 2, 10) + }; + orderTotalDiscount.RuleSets.Add(ruleSets.FirstOrDefault(x => x.Rules.Any(y => y.RuleType == "CartOrderCount"))); + + var weekendDiscount = new Discount + { + Name = "5% on weekend orders", + DiscountType = DiscountType.AssignedToOrderSubTotal, + DiscountLimitation = DiscountLimitationType.Unlimited, + UsePercentage = true, + DiscountPercentage = 5, + StartDateUtc = new DateTime(2020, 3, 1), + EndDateUtc = new DateTime(2020, 3, 10) + }; + weekendDiscount.RuleSets.Add(ruleSets.FirstOrDefault(x => x.Rules.Any(y => y.RuleType == "Weekday"))); + + var manufacturersDiscount = new Discount + { + Name = "10% for certain manufacturers", + DiscountType = DiscountType.AssignedToManufacturers, + DiscountLimitation = DiscountLimitationType.Unlimited, + UsePercentage = true, + DiscountPercentage = 10, + StartDateUtc = new DateTime(2020, 4, 5), + EndDateUtc = new DateTime(2020, 4, 10) + }; + + var categoriesDiscount = new Discount + { + Name = "20% for certain categories", + DiscountType = DiscountType.AssignedToCategories, + DiscountLimitation = DiscountLimitationType.Unlimited, + UsePercentage = true, + DiscountPercentage = 20, + StartDateUtc = new DateTime(2020, 6, 1), + EndDateUtc = new DateTime(2020, 6, 30) + }; + + var productsDiscount = new Discount + { + Name = "25% on certain products", + DiscountType = DiscountType.AssignedToSkus, + DiscountLimitation = DiscountLimitationType.Unlimited, + UsePercentage = true, + DiscountPercentage = 25, + StartDateUtc = new DateTime(2020, 5, 10), + EndDateUtc = new DateTime(2020, 5, 15) + }; + + + var entities = new List + { + couponCodeDiscount, orderTotalDiscount, weekendDiscount, + manufacturersDiscount, categoriesDiscount, productsDiscount + }; + + Alter(entities); + return entities; + } + + public IList DeliveryTimes() + { + var entities = new List() + { + new DeliveryTime + { + Name = "available and ready to ship", + DisplayOrder = 0, + ColorHexValue = "#008000", + MinDays = 1, + MaxDays = 3 + }, + new DeliveryTime + { + Name = "2-5 woking days", + DisplayOrder = 1, + ColorHexValue = "#FFFF00", + MinDays = 2, + MaxDays = 5 + }, + new DeliveryTime + { + Name = "7 working days", + DisplayOrder = 2, + ColorHexValue = "#FF9900", + MinDays = 7, + MaxDays = 14 + }, + }; + this.Alter(entities); + return entities; + } + + public IList QuantityUnits() + { + var count = 0; + var entities = new List(); + + var quPluralEn = new Dictionary + { + { "Piece", "Pieces" }, + { "Box", "Boxes" }, + { "Parcel", "Parcels" }, + { "Palette", "Pallets" }, + { "Unit", "Units" }, + { "Sack", "Sacks" }, + { "Bag", "Bags" }, + { "Can", "Cans" }, + { "Packet", "Packets" }, + { "Bar", "Bars" }, + { "Bottle", "Bottles" }, + { "Glass", "Glasses" }, + { "Bunch", "Bunches" }, + { "Roll", "Rolls" }, + { "Cup", "Cups" }, + { "Bundle", "Bundles" }, + { "Barrel", "Barrels" }, + { "Set", "Sets" }, + { "Bucket", "Buckets" } + }; + + foreach (var qu in quPluralEn) + { + entities.Add(new QuantityUnit + { + Name = qu.Key, + NamePlural = qu.Value, + Description = qu.Key, + IsDefault = qu.Key == "Piece", + DisplayOrder = count++ + }); + } + + this.Alter(entities); + return entities; + } + + public IList PollAnswers() + { + var pollAnswer1 = new PollAnswer() + { + Name = "Excellent", + DisplayOrder = 1, + }; + var pollAnswer2 = new PollAnswer() + { + Name = "Good", + DisplayOrder = 2, + }; + var pollAnswer3 = new PollAnswer() + { + Name = "Poor", + DisplayOrder = 3, + }; + var pollAnswer4 = new PollAnswer() + { + Name = "Very bad", + DisplayOrder = 4, + }; + var pollAnswer5 = new PollAnswer() + { + Name = "Daily", + DisplayOrder = 5, + }; + var pollAnswer6 = new PollAnswer() + { + Name = "Once a week", + DisplayOrder = 6, + }; + var pollAnswer7 = new PollAnswer() + { + Name = "Every two weeks", + DisplayOrder = 7, + }; + var pollAnswer8 = new PollAnswer() + { + Name = "Once a month", + DisplayOrder = 8, + }; + + var entities = new List + { + pollAnswer1, pollAnswer2, pollAnswer3, pollAnswer4, pollAnswer5, pollAnswer6, pollAnswer7, pollAnswer8 + }; + + this.Alter(entities); + return entities; + } + + public IList Polls() + { + var defaultLanguage = _ctx.Set().FirstOrDefault(); + var poll1 = new Poll + { + Language = defaultLanguage, + Name = "How do you like the shop?", + SystemKeyword = "Blog", + Published = true, + DisplayOrder = 1, + }; + + poll1.PollAnswers.Add(new PollAnswer + { + Name = "Excellent", + DisplayOrder = 1, + }); + + poll1.PollAnswers.Add(new PollAnswer + { + Name = "Good", + DisplayOrder = 2, + }); + + poll1.PollAnswers.Add(new PollAnswer + { + Name = "Poor", + DisplayOrder = 3, + }); + + poll1.PollAnswers.Add(new PollAnswer + { + Name = "Very bad", + DisplayOrder = 4, + }); + + + var poll2 = new Poll + { + Language = defaultLanguage, + Name = "How often do you buy online?", + SystemKeyword = "Blog", + Published = true, + DisplayOrder = 2, + }; + + poll2.PollAnswers.Add(new PollAnswer + { + Name = "Daily", + DisplayOrder = 1, + }); + + poll2.PollAnswers.Add(new PollAnswer + { + Name = "Once a week", + DisplayOrder = 2, + }); + + poll2.PollAnswers.Add(new PollAnswer + { + Name = "Every two weeks", + DisplayOrder = 3, + }); + + poll2.PollAnswers.Add(new PollAnswer + { + Name = "Once a month", + DisplayOrder = 4, + }); + + + var entities = new List + { + poll1, poll2 + }; + + this.Alter(entities); + return entities; + } + + public IList Campaigns() + { + var entities = new List + { + new Campaign + { + Name = "Reminder of inactive new customers", + Subject = "New, exciting products are waiting for you to be discovered.", + Body = "

Efficiently unleash client-centric technologies and go forward information. Conveniently benchmark client-focused resources vis-a-vis interdependent paradigms. Synergistically disseminate interdependent supply chains via equity invested internal or 'organic' sources. Objectively exploit seamless growth strategies without orthogonal methodologies. Intrinsicly disseminate bricks-and-clicks web-readiness and e-business e-services.

Objectively develop performance based e-business and interdependent sources. Objectively evolve flexible markets via leveraged interfaces. Professionally deliver focused 'outside the box' thinking rather than global sources. Energistically redefine leveraged supply chains through customized relationships. Dramatically actualize resource sucking content rather than cross-platform e-business.

Seamlessly synthesize vertical mindshare without flexible sources. Distinctively productize timely infrastructures rather than cross-media niches. Dynamically evisculate pandemic convergence and scalable mindshare. Seamlessly embrace fully tested relationships whereas go forward initiatives. Globally actualize user-centric channels.

", + CreatedOnUtc = DateTime.UtcNow, + SubjectToAcl = true } }; - this.Alter(entities); - return entities; - } - - #endregion - - #region Sample data creators - - public IList ForumGroups() - { - var forumGroupGeneral = new ForumGroup - { - Name = "General", - Description = "", - DisplayOrder = 1 - }; - - var entities = new List - { - forumGroupGeneral - }; - - this.Alter(entities); - return entities; - } - - public IList Forums() - { - var group = _ctx.Set().FirstOrDefault(c => c.DisplayOrder == 1); - var newProductsForum = new Forum - { - ForumGroup = group, - Name = "New Products", - Description = "Discuss new products and industry trends", - NumTopics = 0, - NumPosts = 0, - LastPostCustomerId = 0, - LastPostTime = null, - DisplayOrder = 1 - }; - - var packagingShippingForum = new Forum - { - ForumGroup = group, - Name = "Packaging & Shipping", - Description = "Discuss packaging & shipping", - NumTopics = 0, - NumPosts = 0, - LastPostTime = null, - DisplayOrder = 20 - }; - - - var entities = new List - { - newProductsForum, packagingShippingForum - }; - - this.Alter(entities); - return entities; - } - - public IList Discounts() - { - var sampleDiscountWithCouponCode = new Discount() - { - Name = "Sample discount with coupon code", - DiscountType = DiscountType.AssignedToSkus, - DiscountLimitation = DiscountLimitationType.Unlimited, - UsePercentage = false, - DiscountAmount = 10, - RequiresCouponCode = true, - CouponCode = "123" - }; - var sampleDiscounTwentyPercentTotal = new Discount() - { - Name = "'20% order total' discount", - DiscountType = DiscountType.AssignedToOrderTotal, - DiscountLimitation = DiscountLimitationType.Unlimited, - UsePercentage = true, - DiscountPercentage = 20, - StartDateUtc = new DateTime(2013, 1, 1), - EndDateUtc = new DateTime(2020, 1, 1), - RequiresCouponCode = true, - CouponCode = "456" - }; - - var entities = new List - { - sampleDiscountWithCouponCode, sampleDiscounTwentyPercentTotal - }; - - this.Alter(entities); - return entities; - } - - public IList DeliveryTimes() - { - var entities = new List() - { - new DeliveryTime - { - Name = "available and ready to ship", - DisplayOrder = 0, - ColorHexValue = "#008000" - }, - new DeliveryTime - { - Name = "2-5 woking days", - DisplayOrder = 1, - ColorHexValue = "#FFFF00" - }, - new DeliveryTime - { - Name = "7 working days", - DisplayOrder = 2, - ColorHexValue = "#FF9900" - }, - }; - this.Alter(entities); - return entities; - } + Alter(entities); + return entities; + } - public IList QuantityUnits() + public IList RuleSets() { - var count = 0; - var entities = new List(); + // Cart: weekends. + var weekends = new RuleSetEntity + { + Scope = RuleScope.Cart, + Name = "Weekends", + IsActive = true + }; + weekends.Rules.Add(new RuleEntity + { + RuleType = "Weekday", + Operator = RuleOperator.In, + Value = $"{(int)DayOfWeek.Sunday},{(int)DayOfWeek.Saturday}" + }); - var quPluralEn = new Dictionary + // Cart: major customers. + var majorCustomers = new RuleSetEntity { - { "Piece", "Pieces" }, - { "Box", "Boxes" }, - { "Parcel", "Parcels" }, - { "Palette", "Pallets" }, - { "Unit", "Units" }, - { "Sack", "Sacks" }, - { "Bag", "Bags" }, - { "Can", "Cans" }, - { "Packet", "Packets" }, - { "Bar", "Bars" }, - { "Bottle", "Bottles" }, - { "Glass", "Glasses" }, - { "Bunch", "Bunches" }, - { "Roll", "Rolls" }, - { "Cup", "Cups" }, - { "Bundle", "Bundles" }, - { "Barrel", "Barrels" }, - { "Set", "Sets" }, - { "Bucket", "Buckets" } + Scope = RuleScope.Cart, + Name = "Major customers", + Description = "3 or more orders and current order value at least 200,- Euro.", + IsActive = true }; + majorCustomers.Rules.Add(new RuleEntity + { + RuleType = "CartSubtotal", + Operator = RuleOperator.GreaterThanOrEqualTo, + Value = "200" + }); + majorCustomers.Rules.Add(new RuleEntity + { + RuleType = "CartOrderCount", + Operator = RuleOperator.GreaterThanOrEqualTo, + Value = "3" + }); - foreach (var qu in quPluralEn) + // Product: sale. + var saleProducts = new RuleSetEntity { - entities.Add(new QuantityUnit - { - Name = qu.Key, - NamePlural = qu.Value, - Description = qu.Key, - IsDefault = qu.Key == "Piece", - DisplayOrder = count++ - }); + Scope = RuleScope.Product, + Name = "Sale", + Description = "Products with applied discounts.", + IsActive = true + }; + saleProducts.Rules.Add(new RuleEntity + { + RuleType = "Discount", + Operator = RuleOperator.IsEqualTo, + Value = "true" + }); + + // Customer: inactive new customers. + var inactiveNewCustomers = new RuleSetEntity + { + Scope = RuleScope.Customer, + Name = "Inactive new customers", + Description = "One completed order placed at least 90 days ago.", + IsActive = true + }; + inactiveNewCustomers.Rules.Add(new RuleEntity + { + RuleType = "CompletedOrderCount", + Operator = RuleOperator.IsEqualTo, + Value = "1" + }); + inactiveNewCustomers.Rules.Add(new RuleEntity + { + RuleType = "LastOrderDateDays", + Operator = RuleOperator.GreaterThanOrEqualTo, + Value = "90" + }); + + + // Offer free shipping method for major customers. + var freeShipping = _ctx.Set().FirstOrDefault(x => x.DisplayOrder == 2); + if (freeShipping != null) + { + freeShipping.RuleSets.Add(majorCustomers); } - - this.Alter(entities); + + // Assign rule conditions for inactive new customers to the related customer role. + // We later bind the reminder campaign to it. + var inactiveNewCustomersRole = _ctx.Set().FirstOrDefault(x => x.SystemName == "InactiveNewCustomers"); + if (inactiveNewCustomersRole != null) + { + inactiveNewCustomersRole.RuleSets.Add(inactiveNewCustomers); + } + + + var entities = new List + { + weekends, majorCustomers, saleProducts, inactiveNewCustomers + }; + + Alter(entities); return entities; } - public IList BlogPosts() - { - var defaultLanguage = _ctx.Set().FirstOrDefault(); - var blogPostDiscountCoupon = new BlogPost() - { - AllowComments = true, - Language = defaultLanguage, - Title = "Online Discount Coupons", - Body = "

Online discount coupons enable access to great offers from some of the world’s best sites for Internet shopping. The online coupons are designed to allow compulsive online shoppers to access massive discounts on a variety of products. The regular shopper accesses the coupons in bulk and avails of great festive offers and freebies thrown in from time to time. The coupon code option is most commonly used when using a shopping cart. The coupon code is entered on the order page just before checking out. Every online shopping resource has a discount coupon submission option to confirm the coupon code. The dedicated web sites allow the shopper to check whether or not a discount is still applicable. If it is, the sites also enable the shopper to calculate the total cost after deducting the coupon amount like in the case of grocery coupons. Online discount coupons are very convenient to use. They offer great deals and professionally negotiated rates if bought from special online coupon outlets. With a little research and at times, insider knowledge the online discount coupons are a real steal. They are designed to promote products by offering ‘real value for money’ packages. The coupons are legitimate and help with budgeting, in the case of a compulsive shopper. They are available for special trade show promotions, nightlife, sporting events and dinner shows and just about anything that could be associated with the promotion of a product. The coupons enable the online shopper to optimize net access more effectively. Getting a ‘big deal’ is not more utopian amidst rising prices. The online coupons offer internet access to the best and cheapest products displayed online. Big discounts are only a code away! By Gaynor Borade (buzzle.com)

", - Tags = "e-commerce, money", - CreatedOnUtc = DateTime.UtcNow, - }; - var blogPostCustomerService = new BlogPost() - { - AllowComments = true, - Language = defaultLanguage, - Title = "Customer Service - Client Service", - Body = "

Managing online business requires different skills and abilities than managing a business in the ‘real world.’ Customers can easily detect the size and determine the prestige of a business when they have the ability to walk in and take a look around. Not only do ‘real-world’ furnishings and location tell the customer what level of professionalism to expect, but "real world" personal encounters allow first impressions to be determined by how the business approaches its customer service. When a customer walks into a retail business just about anywhere in the world, that customer expects prompt and personal service, especially with regards to questions that they may have about products they wish to purchase.

Customer service or the client service is the service provided to the customer for his satisfaction during and after the purchase. It is necessary to every business organization to understand the customer needs for value added service. So customer data collection is essential. For this, a good customer service is important. The easiest way to lose a client is because of the poor customer service. The importance of customer service changes by product, industry and customer. Client service is an important part of every business organization. Each organization is different in its attitude towards customer service. Customer service requires a superior quality service through a careful design and execution of a series of activities which include people, technology and processes. Good customer service starts with the design and communication between the company and the staff.

In some ways, the lack of a physical business location allows the online business some leeway that their ‘real world’ counterparts do not enjoy. Location is not important, furnishings are not an issue, and most of the visual first impression is made through the professional design of the business website.

However, one thing still remains true. Customers will make their first impressions on the customer service they encounter. Unfortunately, in online business there is no opportunity for front- line staff to make a good impression. Every interaction the customer has with the website will be their primary means of making their first impression towards the business and its client service. Good customer service in any online business is a direct result of good website design and planning.

By Jayashree Pakhare (buzzle.com)

", - Tags = "e-commerce, Smartstore.NET, asp.net, sample tag, money", - CreatedOnUtc = DateTime.UtcNow.AddSeconds(1), - }; - - var entities = new List - { - blogPostDiscountCoupon, blogPostCustomerService - }; - - this.Alter(entities); - return entities; - } - - public IList NewsItems() - { - var defaultLanguage = _ctx.Set().FirstOrDefault(); - var news1 = new NewsItem() - { - AllowComments = true, - Language = defaultLanguage, - Title = "Smartstore.NET 3.2 now with the new CMS Page Builder", - Short = "Create fascinating content with the new Page Builder", - Full = "

Create fascinating content from products, product groups, images, videos and texts.
Transitions, animations, gradients, hover effects or overlays are easily applied in the WYSIWYG editor.

More information about Smartstore 3.2 and Page Builder can be found at www.smartstore.com

", - Published = true, - MetaTitle = "Smartstore.NET 3.2", - CreatedOnUtc = DateTime.Now - }; - var news2 = new NewsItem() - { - AllowComments = true, - Language = defaultLanguage, - Title = "Smartstore.NET new release!", - Short = "Smartstore.NET includes everything you need to begin your e-commerce online store.", - Full = "

Smartstore.NET includes everything you need to begin your e-commerce online store.
We have thought of everything and it's all included!

From downloads to documentation, www.smartstore.com offers a comprehensive base of information, resources, and support to the Smartstore.NET community.

", - Published = true, - MetaTitle = "Smartstore.NET new release!", - CreatedOnUtc = DateTime.Now - }; + public void FinalizeSamples() + { + // Bind the reminder campaign to the rule conditions for inactive new customers. + var reminderCampaign = _ctx.Set().FirstOrDefault(x => x.SubjectToAcl); + if (reminderCampaign != null) + { + var inactiveNewCustomersRole = _ctx.Set().FirstOrDefault(x => x.SystemName == "InactiveNewCustomers"); + if (inactiveNewCustomersRole != null) + { + _ctx.Set().Add(new AclRecord + { + EntityId = reminderCampaign.Id, + EntityName = nameof(Campaign), + CustomerRoleId = inactiveNewCustomersRole.Id, + IsIdle = false + }); + } + } + } + + #region Alterations + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(Address entity) + { + } + + protected virtual void Alter(Customer entity) + { + } - var entities = new List - { - news1, news2 - }; - - this.Alter(entities); - return entities; - } - - public IList PollAnswers() - { - var pollAnswer1 = new PollAnswer() - { - Name = "Excellent", - DisplayOrder = 1, - }; - var pollAnswer2 = new PollAnswer() - { - Name = "Good", - DisplayOrder = 2, - }; - var pollAnswer3 = new PollAnswer() - { - Name = "Poor", - DisplayOrder = 3, - }; - var pollAnswer4 = new PollAnswer() - { - Name = "Very bad", - DisplayOrder = 4, - }; - var pollAnswer5 = new PollAnswer() - { - Name = "Daily", - DisplayOrder = 5, - }; - var pollAnswer6 = new PollAnswer() - { - Name = "Once a week", - DisplayOrder = 6, - }; - var pollAnswer7 = new PollAnswer() - { - Name = "Every two weeks", - DisplayOrder = 7, - }; - var pollAnswer8 = new PollAnswer() - { - Name = "Once a month", - DisplayOrder = 8, - }; - - var entities = new List - { - pollAnswer1, pollAnswer2, pollAnswer3, pollAnswer4, pollAnswer5, pollAnswer6, pollAnswer7, pollAnswer8 - }; - - this.Alter(entities); - return entities; - } - - public IList Polls() - { - var defaultLanguage = _ctx.Set().FirstOrDefault(); - var poll1 = new Poll - { - Language = defaultLanguage, - Name = "How do you like the shop?", - SystemKeyword = "Blog", - Published = true, - DisplayOrder = 1, - }; - - poll1.PollAnswers.Add(new PollAnswer - { - Name = "Excellent", - DisplayOrder = 1, - }); - - poll1.PollAnswers.Add(new PollAnswer - { - Name = "Good", - DisplayOrder = 2, - }); - - poll1.PollAnswers.Add(new PollAnswer - { - Name = "Poor", - DisplayOrder = 3, - }); - - poll1.PollAnswers.Add(new PollAnswer - { - Name = "Very bad", - DisplayOrder = 4, - }); - - - var poll2 = new Poll - { - Language = defaultLanguage, - Name = "How often do you buy online?", - SystemKeyword = "Blog", - Published = true, - DisplayOrder = 2, - }; - - poll2.PollAnswers.Add(new PollAnswer - { - Name = "Daily", - DisplayOrder = 1, - }); - - poll2.PollAnswers.Add(new PollAnswer - { - Name = "Once a week", - DisplayOrder = 2, - }); - - poll2.PollAnswers.Add(new PollAnswer - { - Name = "Every two weeks", - DisplayOrder = 3, - }); - - poll2.PollAnswers.Add(new PollAnswer - { - Name = "Once a month", - DisplayOrder = 4, - }); - - - var entities = new List - { - poll1, poll2 - }; - - this.Alter(entities); - return entities; - } - - #region Alterations - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(Address entity) - { - } - - protected virtual void Alter(Customer entity) - { - } - - protected virtual void Alter(IList entities) - { - } + protected virtual void Alter(IList entities) + { + } protected virtual void Alter(IList entities) { } - protected virtual void Alter(IList entities) - { - } + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList settings) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } + + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList entities) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList entities) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList entities) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList entities) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList settings) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList settings) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList settings) - { - } + protected virtual void Alter(IList entities) + { + } - protected virtual void Alter(IList settings) - { - } + protected virtual void Alter(UrlRecord entity) + { + } - protected virtual void Alter(IList settings) - { - } + #endregion Alterations - protected virtual void Alter(IList settings) - { - } + #endregion Sample data creators - protected virtual void Alter(IList settings) - { - } + #region Helpers - protected virtual void Alter(IList settings) - { - } + protected SmartObjectContext DbContext => _ctx; - protected virtual void Alter(IList entities) - { - } + protected string SampleImagesPath => _sampleImagesPath; - protected virtual void Alter(IList entities) - { - } + public virtual UrlRecord CreateUrlRecordFor(T entity) where T : BaseEntity, ISlugSupported, new() + { + var name = ""; - protected virtual void Alter(IList entities) - { - } + switch (entity) + { + case Category x: + name = x.Name; + break; + case Manufacturer x: + name = x.Name; + break; + case Product x: + name = x.Name; + break; + case BlogPost x: + name = x.Title; + break; + case NewsItem x: + name = x.Title; + break; + case Topic x: + name = SeoHelper.GetSeName(x.SystemName, true, false, true).Truncate(400); + break; + } - protected virtual void Alter(IList entities) - { - } + if (name.HasValue()) + { + var result = new UrlRecord + { + EntityId = entity.Id, + EntityName = entity.GetUnproxiedType().Name, + LanguageId = 0, + Slug = name, + IsActive = true + }; + + this.Alter(result); + return result; + } - protected virtual void Alter(IList entities) - { - } + return null; + } - protected virtual void Alter(IList entities) - { - } + protected MediaFile CreatePicture(string fileName, string seoFilename = null) + { + try + { + var ext = Path.GetExtension(fileName); + var path = Path.Combine(_sampleImagesPath, fileName).Replace('/', '\\'); + var mimeType = MimeTypes.MapNameToMimeType(ext); + var buffer = File.ReadAllBytes(path); + var now = DateTime.UtcNow; - protected virtual void Alter(IList entities) - { - } + var name = seoFilename.HasValue() + ? seoFilename.Truncate(100) + ext + : Path.GetFileName(fileName).ToLower().Replace('_', '-'); - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(IList entities) - { - } - - protected virtual void Alter(UrlRecord entity) - { - } - - #endregion Alterations - - #endregion Sample data creators - - #region Helpers - - protected SmartObjectContext DbContext - { - get - { - return _ctx; - } - } - - protected string SampleImagesPath - { - get - { - return _sampleImagesPath; - } - } - - public virtual UrlRecord CreateUrlRecordFor(T entity) where T : BaseEntity, ISlugSupported, new() - { - var name = ""; - var languageId = 0; - - switch (entity) - { - case Category x: - name = x.Name; - break; - case Manufacturer x: - name = x.Name; - break; - case Product x: - name = x.Name; - break; - case BlogPost x: - name = x.Title; - languageId = x.LanguageId; - break; - case NewsItem x: - name = x.Title; - languageId = x.LanguageId; - break; - case Topic x: - name = SeoHelper.GetSeName(x.SystemName, true, false, true).Truncate(400); - break; - } - - if (name.HasValue()) - { - var result = new UrlRecord - { - EntityId = entity.Id, - EntityName = entity.GetUnproxiedType().Name, - LanguageId = languageId, - Slug = name, - IsActive = true - }; - - this.Alter(result); - return result; - } - - return null; - } - - protected MediaFile CreatePicture(string fileName, string seoFilename = null) - { - try - { - var ext = Path.GetExtension(fileName); - var path = Path.Combine(_sampleImagesPath, fileName).Replace('/', '\\'); - var mimeType = MimeTypes.MapNameToMimeType(ext); - var buffer = File.ReadAllBytes(path); - var now = DateTime.UtcNow; - - var name = seoFilename.HasValue() - ? seoFilename.Truncate(100) + ext - : Path.GetFileName(fileName).ToLower().Replace('_', '-'); - - var file = new MediaFile - { - Name = name, + var file = new MediaFile + { + Name = name, MediaType = "image", MimeType = mimeType, Extension = ext.EmptyNull().TrimStart('.'), - CreatedOnUtc = now, - UpdatedOnUtc = now, - Size = buffer.Length, - MediaStorage = new MediaStorage { Data = buffer }, - Version = 1 // so that FolderId is set later during track detection - }; - - return file; - } - catch (Exception ex) - { - //throw ex; - System.Diagnostics.Debug.WriteLine(ex.Message); - return null; - } - } + CreatedOnUtc = now, + UpdatedOnUtc = now, + Size = buffer.Length, + MediaStorage = new MediaStorage { Data = buffer }, + Version = 1 // so that FolderId is set later during track detection + }; + + return file; + } + catch (Exception ex) + { + //throw ex; + System.Diagnostics.Debug.WriteLine(ex.Message); + return null; + } + } protected void AddProductPicture( Product product, @@ -1467,12 +1607,12 @@ protected void AddProductPicture( string seName = null, int displayOrder = 1) { - if (seName == null) - { - seName = GetSeName(Path.GetFileNameWithoutExtension(imageName)); - } + if (seName == null) + { + seName = GetSeName(Path.GetFileNameWithoutExtension(imageName)); + } - product.ProductPictures.Add(new ProductMediaFile + product.ProductPictures.Add(new ProductMediaFile { MediaFile = CreatePicture(imageName, seName), DisplayOrder = displayOrder @@ -1480,68 +1620,68 @@ protected void AddProductPicture( } protected string GetSeName(string name) - { - return SeoHelper.GetSeName(name, true, false); - } - - protected Currency CreateCurrency(string locale, decimal rate = 1M, string formatting = "", bool published = false, int order = 1) - { - RegionInfo info = null; - Currency currency = null; - try - { - info = new RegionInfo(locale); - if (info != null) - { - currency = new Currency - { - DisplayLocale = locale, - Name = info.CurrencyNativeName, - CurrencyCode = info.ISOCurrencySymbol, - Rate = rate, - CustomFormatting = formatting, - Published = published, - DisplayOrder = order - }; - } - } - catch - { - return null; - } - - return currency; - } - - protected string FormatAttributeXml(int attributeId, int valueId, bool withRootTag = true) - { - var xml = $"{valueId}"; - - if (withRootTag) - { - return string.Concat("", xml, ""); - } - - return xml; - } - protected string FormatAttributeXml(int attributeId1, int valueId1, int attributeId2, int valueId2) - { - return string.Concat( - "", - FormatAttributeXml(attributeId1, valueId1, false), - FormatAttributeXml(attributeId2, valueId2, false), - ""); - } - protected string FormatAttributeXml(int attributeId1, int valueId1, int attributeId2, int valueId2, int attributeId3, int valueId3) - { - return string.Concat( - "", - FormatAttributeXml(attributeId1, valueId1, false), - FormatAttributeXml(attributeId2, valueId2, false), - FormatAttributeXml(attributeId3, valueId3, false), - ""); - } - - #endregion - } + { + return SeoHelper.GetSeName(name, true, false); + } + + protected Currency CreateCurrency(string locale, decimal rate = 1M, string formatting = "", bool published = false, int order = 1) + { + RegionInfo info = null; + Currency currency = null; + try + { + info = new RegionInfo(locale); + if (info != null) + { + currency = new Currency + { + DisplayLocale = locale, + Name = info.CurrencyNativeName, + CurrencyCode = info.ISOCurrencySymbol, + Rate = rate, + CustomFormatting = formatting, + Published = published, + DisplayOrder = order + }; + } + } + catch + { + return null; + } + + return currency; + } + + protected string FormatAttributeXml(int attributeId, int valueId, bool withRootTag = true) + { + var xml = $"{valueId}"; + + if (withRootTag) + { + return string.Concat("", xml, ""); + } + + return xml; + } + protected string FormatAttributeXml(int attributeId1, int valueId1, int attributeId2, int valueId2) + { + return string.Concat( + "", + FormatAttributeXml(attributeId1, valueId1, false), + FormatAttributeXml(attributeId2, valueId2, false), + ""); + } + protected string FormatAttributeXml(int attributeId1, int valueId1, int attributeId2, int valueId2, int attributeId3, int valueId3) + { + return string.Concat( + "", + FormatAttributeXml(attributeId1, valueId1, false), + FormatAttributeXml(attributeId2, valueId2, false), + FormatAttributeXml(attributeId3, valueId3, false), + ""); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataConfiguration.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataConfiguration.cs index 6bed1ce189..df091e38f3 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataConfiguration.cs @@ -1,28 +1,25 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using SmartStore.Core.Domain.Localization; namespace SmartStore.Data.Setup { - public class SeedDataConfiguration - { - public SeedDataConfiguration() - { - this.SeedSampleData = true; - this.StoreMediaInDB = true; - this.ProgressMessageCallback = (x) => { }; // Noop - } + public class SeedDataConfiguration + { + public SeedDataConfiguration() + { + this.SeedSampleData = true; + this.StoreMediaInDB = true; + this.ProgressMessageCallback = (x) => { }; // Noop + } - public string DefaultUserName { get; set; } - public string DefaultUserPassword { get; set; } - public Language Language { get; set; } - public InvariantSeedData Data { get; set; } - public bool SeedSampleData { get; set; } - public bool StoreMediaInDB { get; set; } - public Action ProgressMessageCallback { get; set; } - } + public string DefaultUserName { get; set; } + public string DefaultUserPassword { get; set; } + public Language Language { get; set; } + public InvariantSeedData Data { get; set; } + public bool SeedSampleData { get; set; } + public bool StoreMediaInDB { get; set; } + public Action ProgressMessageCallback { get; set; } + } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataException.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataException.cs index 6267ab633b..ab70d99eb4 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataException.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/SeedDataException.cs @@ -1,14 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace SmartStore.Data.Setup { - + public class SeedDataException : Exception { - public SeedDataException(string stage, Exception inner) + public SeedDataException(string stage, Exception inner) : base("An error occurred during installation. Stage: " + stage, inner) { this.Stage = stage; diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/SeedEntityExtensions.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/SeedEntityExtensions.cs index 5526370b43..f1ae049a4a 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/SeedEntityExtensions.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/SeedEntityExtensions.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; using SmartStore.Core; using SmartStore.Core.Configuration; @@ -73,13 +70,13 @@ public SeedEntityAlterer(IList list, Func keyPredicate) TKey key = fn.Invoke(entity); _entityMap.Add(key, entity); } - + } public SeedEntityAlterer Alter(TKey key, Action action) { T entity = default(T); - if (_entityMap.TryGetValue(key, out entity)) + if (_entityMap.TryGetValue(key, out entity)) { action(entity); } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedDbMigrationEvent.cs b/src/Libraries/SmartStore.Data/Setup/SeedDbMigrationEvent.cs index 8220b1a438..720dddb90d 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedDbMigrationEvent.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedDbMigrationEvent.cs @@ -1,5 +1,4 @@ -using System; -using System.Data.Entity; +using System.Data.Entity; namespace SmartStore.Data.Setup { diff --git a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs index d18414d6e9..e58a59a192 100644 --- a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs +++ b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs @@ -1,22 +1,20 @@ -using System; -using System.Linq; -using System.Data.Entity; +using System.Data.Entity; using System.Data.Entity.Core.Common; +using System.Data.Entity.Infrastructure; +using System.Data.Entity.Infrastructure.DependencyResolution; +using System.Web.Hosting; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using SmartStore.Data.Caching; -using System.Web.Hosting; -using System.Data.Entity.Infrastructure; -using System.Data.Entity.Infrastructure.DependencyResolution; using SmartStore.Utilities; namespace SmartStore.Data { - public class SmartDbConfiguration : DbConfiguration - { - public SmartDbConfiguration() - { - // DB model caching + public class SmartDbConfiguration : DbConfiguration + { + public SmartDbConfiguration() + { + // DB model caching if (HostingEnvironment.IsHosted) { var cacheLocation = CommonHelper.MapPath("~/App_Data/EfCache"); @@ -25,41 +23,41 @@ public SmartDbConfiguration() AddDependencyResolver(new SingletonDependencyResolver(modelStore)); } - IEfDataProvider provider = null; - try - { - provider = (new EfDataProviderFactory(DataSettings.Current).LoadDataProvider()) as IEfDataProvider; - } - catch - { - /* SmartStore is not installed yet! */ - } - - if (provider != null) - { - base.SetDefaultConnectionFactory(provider.GetConnectionFactory()); + IEfDataProvider provider = null; + try + { + provider = (new EfDataProviderFactory(DataSettings.Current).LoadDataProvider()) as IEfDataProvider; + } + catch + { + /* SmartStore is not installed yet! */ + } - if (HostingEnvironment.IsHosted && DataSettings.DatabaseIsInstalled()) - { - Loaded += (sender, args) => - { - // prepare EntityFramework 2nd level cache - IDbCache cache = null; - try - { - cache = EngineContext.Current.Resolve(); - } - catch - { - cache = new NullDbCache(); - } + if (provider != null) + { + base.SetDefaultConnectionFactory(provider.GetConnectionFactory()); + + if (HostingEnvironment.IsHosted && DataSettings.DatabaseIsInstalled()) + { + Loaded += (sender, args) => + { + // prepare EntityFramework 2nd level cache + IDbCache cache = null; + try + { + cache = EngineContext.Current.Resolve(); + } + catch + { + cache = new NullDbCache(); + } - var cacheInterceptor = new CacheTransactionInterceptor(cache); - AddInterceptor(cacheInterceptor); - args.ReplaceService((s, o) => new CachingProviderServices(s, cacheInterceptor)); - }; - } - } - } - } + var cacheInterceptor = new CacheTransactionInterceptor(cache); + AddInterceptor(cacheInterceptor); + args.ReplaceService((s, o) => new CachingProviderServices(s, cacheInterceptor)); + }; + } + } + } + } } diff --git a/src/Libraries/SmartStore.Data/SmartObjectContext.cs b/src/Libraries/SmartStore.Data/SmartObjectContext.cs index 3cf49c66cd..d74769d6ed 100644 --- a/src/Libraries/SmartStore.Data/SmartObjectContext.cs +++ b/src/Libraries/SmartStore.Data/SmartObjectContext.cs @@ -13,27 +13,27 @@ namespace SmartStore.Data { public class SmartObjectContext : ObjectContextBase { - static SmartObjectContext() - { - if (!DataSettings.DatabaseIsInstalled()) - return; + static SmartObjectContext() + { + if (!DataSettings.DatabaseIsInstalled()) + return; - var initializer = new MigrateDatabaseInitializer - { - TablesToCheck = new[] { "Customer", "Discount", "Order", "Product", "ShoppingCartItem", "QueuedEmailAttachment", "ExportProfile" } - }; - Database.SetInitializer(initializer); - } + var initializer = new MigrateDatabaseInitializer + { + TablesToCheck = new[] { "Customer", "Discount", "Order", "Product", "ShoppingCartItem", "QueuedEmailAttachment", "ExportProfile" } + }; + Database.SetInitializer(initializer); + } - /// - /// For tooling support, e.g. EF Migrations - /// - public SmartObjectContext() - : base() - { - } + /// + /// For tooling support, e.g. EF Migrations + /// + public SmartObjectContext() + : base() + { + } - public SmartObjectContext(string nameOrConnectionString) + public SmartObjectContext(string nameOrConnectionString) : base(nameOrConnectionString) { } @@ -49,14 +49,14 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) // && type.BaseType != null // && type.BaseType.IsGenericType // && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>)); - + var typesToRegister = from t in Assembly.GetExecutingAssembly().GetTypes() - where t.Namespace.HasValue() && - t.BaseType != null && - t.BaseType.IsGenericType - let genericType = t.BaseType.GetGenericTypeDefinition() - where genericType == typeof(EntityTypeConfiguration<>) || genericType == typeof(ComplexTypeConfiguration<>) - select t; + where t.Namespace.HasValue() && + t.BaseType != null && + t.BaseType.IsGenericType + let genericType = t.BaseType.GetGenericTypeDefinition() + where genericType == typeof(EntityTypeConfiguration<>) || genericType == typeof(ComplexTypeConfiguration<>) + select t; foreach (var type in typesToRegister) { diff --git a/src/Libraries/SmartStore.Data/SmartObjectContextSaveHook.cs b/src/Libraries/SmartStore.Data/SmartObjectContextSaveHook.cs new file mode 100644 index 0000000000..6c024dad8b --- /dev/null +++ b/src/Libraries/SmartStore.Data/SmartObjectContextSaveHook.cs @@ -0,0 +1,9 @@ +using SmartStore.Data; + +namespace SmartStore.Core.Data.Hooks +{ + public abstract class DbSaveHook : DbSaveHook + where TEntity : class + { + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 2d38d24f91..fb594ed6a4 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Data SmartStore.Data - v4.6.1 + v4.7.2 512 @@ -34,6 +34,7 @@ 4 false latest + false pdbonly @@ -66,21 +67,24 @@ latest - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll True - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll True - ..\..\packages\EntityFramework.SqlServerCompact.6.2.0\lib\net45\EntityFramework.SqlServerCompact.dll + ..\..\packages\EntityFramework.SqlServerCompact.6.4.4\lib\net45\EntityFramework.SqlServerCompact.dll True + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + True ..\..\packages\Microsoft.SqlServer.Scripting.11.0.2100.61\lib\Microsoft.SqlServer.ConnectionInfo.dll @@ -93,20 +97,24 @@ True ..\..\packages\Microsoft.SqlServer.Scripting.11.0.2100.61\lib\Microsoft.SqlServer.Smo.dll - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + True ..\..\packages\Microsoft.SqlServer.Compact.4.0.8876.1\lib\net40\System.Data.SqlServerCe.dll - - - ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + @@ -469,6 +477,74 @@ 202005111006305_CampaignSubjectToAcl.cs + + + 202005201826512_MediaManager3.cs + + + + 202006250801086_V400Resources.cs + + + + 202007160058551_MediaFileIndexReorg.cs + + + + 202007241131557_V401Resources.cs + + + + 202007291847004_NewPropertiesAndIndexes.cs + + + + 202007301117363_AddCustomerRoleOrderAmount.cs + + + + 202008181949580_RenamedCustomerRoleOrderTotal.cs + + + + 202009021705132_Merge9.cs + + + + 202009041122208_ShopIcons.cs + + + + 202009090817220_DeliveryTimeMinMaxDays.cs + + + + 202010011005387_TopicCookieType.cs + + + + 202010030939136_AttributeChoiceBehaviour.cs + + + + 202010121359446_RemoveBlogAndNewsLanguage.cs + + + + 202011091154314_V410Resources.cs + + + + 202012051645539_AddBlogAndNewsLanguage.cs + + + + 202101251149352_V411Resources.cs + + + + 202112171231491_V420Resources.cs + @@ -523,6 +599,7 @@ + @@ -602,8 +679,10 @@ + + @@ -852,6 +931,57 @@ 202005111006305_CampaignSubjectToAcl.cs + + 202005201826512_MediaManager3.cs + + + 202006250801086_V400Resources.cs + + + 202007160058551_MediaFileIndexReorg.cs + + + 202007241131557_V401Resources.cs + + + 202007291847004_NewPropertiesAndIndexes.cs + + + 202007301117363_AddCustomerRoleOrderAmount.cs + + + 202008181949580_RenamedCustomerRoleOrderTotal.cs + + + 202009021705132_Merge9.cs + + + 202009041122208_ShopIcons.cs + + + 202009090817220_DeliveryTimeMinMaxDays.cs + + + 202010011005387_TopicCookieType.cs + + + 202010030939136_AttributeChoiceBehaviour.cs + + + 202010121359446_RemoveBlogAndNewsLanguage.cs + + + 202011091154314_V410Resources.cs + + + 202012051645539_AddBlogAndNewsLanguage.cs + + + 202101251149352_V411Resources.cs + + + 202112171231491_V420Resources.cs + diff --git a/src/Libraries/SmartStore.Data/SqlCeDataProvider.cs b/src/Libraries/SmartStore.Data/SqlCeDataProvider.cs index ba2681d732..676365febc 100644 --- a/src/Libraries/SmartStore.Data/SqlCeDataProvider.cs +++ b/src/Libraries/SmartStore.Data/SqlCeDataProvider.cs @@ -4,26 +4,20 @@ namespace SmartStore.Data { - public class SqlCeDataProvider : IEfDataProvider + public class SqlCeDataProvider : IEfDataProvider { public virtual IDbConnectionFactory GetConnectionFactory() { - return new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); - } - - public bool StoredProceduresSupported - { - get { return false; } + return new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); } + public bool StoredProceduresSupported => false; + public DbParameter GetParameter() { return new SqlParameter(); } - public string ProviderInvariantName - { - get { return "System.Data.SqlServerCe.4.0"; } - } + public string ProviderInvariantName => "System.Data.SqlServerCe.4.0"; } } diff --git a/src/Libraries/SmartStore.Data/SqlServerDataProvider.cs b/src/Libraries/SmartStore.Data/SqlServerDataProvider.cs index 1662d45c9e..b331488ed7 100644 --- a/src/Libraries/SmartStore.Data/SqlServerDataProvider.cs +++ b/src/Libraries/SmartStore.Data/SqlServerDataProvider.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; +using System.Data.Common; using System.Data.Entity.Infrastructure; using System.Data.SqlClient; namespace SmartStore.Data { - public class SqlServerDataProvider : IEfDataProvider + public class SqlServerDataProvider : IEfDataProvider { /// /// Get connection factory @@ -20,10 +18,7 @@ public virtual IDbConnectionFactory GetConnectionFactory() /// /// A value indicating whether this data provider supports stored procedures /// - public bool StoredProceduresSupported - { - get { return true; } - } + public bool StoredProceduresSupported => true; /// /// Gets a support database parameter object (used by stored procedures) @@ -34,9 +29,6 @@ public DbParameter GetParameter() return new SqlParameter(); } - public string ProviderInvariantName - { - get { return "System.Data.SqlClient"; } - } - } + public string ProviderInvariantName => "System.Data.SqlClient"; + } } diff --git a/src/Libraries/SmartStore.Data/Utilities/BlogPostConverter.cs b/src/Libraries/SmartStore.Data/Utilities/BlogPostConverter.cs new file mode 100644 index 0000000000..235e020564 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Utilities/BlogPostConverter.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Blogs; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; +using SmartStore.Utilities; + +namespace SmartStore.Data.Utilities +{ + public sealed class BlogPostConverter + { + private readonly SmartObjectContext _ctx; + + public BlogPostConverter(IDbContext context) + { + _ctx = context as SmartObjectContext; + if (_ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + } + + /// + /// Loads all blog post from disk (~/App_Data/Samples/blog/) + /// + /// Language + /// The virtual root path of blogs to load, e.g. "~/Plugins/MyPlugins/BlogPosts". Default is "~/App_Data/Samples/blog". + /// List of deserialized blog posts xml + public IEnumerable LoadAll(Language language, string virtualRootPath = null) + { + Guard.NotNull(language, nameof(language)); + + var dir = ResolveBlogDirectory(language, virtualRootPath); + var files = dir.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly); + + foreach (var file in files) + { + yield return DeserializeBlog(file.FullName); + } + } + + /// + /// Imports all blog xml files to BlogPost table + /// + /// The virtual root path of blogs to import, e.g. "~/Plugins/MyPlugins/BlogPost". Default is "~/App_Data/Samples/blog". + /// List of new imported blog posts + public IList ImportAll(Language language, string virtualRootPath = null) + { + var blogsImported = new List(); + var table = _ctx.Set(); + var sourceBlogs = LoadAll(language); + var dbBlogMap = table.ToList().ToMultimap(x => x.Title, x => x, StringComparer.OrdinalIgnoreCase); + + foreach (var source in sourceBlogs) + { + if (dbBlogMap.ContainsKey(source.Title)) + { + foreach (var target in dbBlogMap[source.Title]) + { + if (source.Title.HasValue()) target.Title = source.Title; + if (source.MetaTitle.HasValue()) target.MetaTitle = source.MetaTitle; + if (source.MetaDescription.HasValue()) target.MetaDescription = source.MetaDescription; + if (source.Intro.HasValue()) target.Intro = source.Intro; + if (source.Body.HasValue()) target.Body = source.Body; + if (source.Tags.HasValue()) target.Tags = source.Tags; + if (source.CreatedOnUtc != null) target.CreatedOnUtc = source.CreatedOnUtc; + if (source.MediaFile != null) target.MediaFile = source.MediaFile; + if (source.PreviewMediaFile != null) target.PreviewMediaFile = source.PreviewMediaFile; + if (source.SectionBg.HasValue()) target.SectionBg = source.SectionBg; + target.DisplayTagsInPreview = source.DisplayTagsInPreview; + target.PreviewDisplayType = source.PreviewDisplayType; + target.AllowComments = source.AllowComments; + } + } + else + { + var blog = new BlogPost + { + Title = source.Title, + MetaTitle = source.MetaTitle, + MetaDescription = source.MetaDescription, + Intro = source.Intro, + Body = source.Body, + Tags = source.Tags, + DisplayTagsInPreview = source.DisplayTagsInPreview, + CreatedOnUtc = source.CreatedOnUtc, + PreviewDisplayType = source.PreviewDisplayType, + MediaFile = source.MediaFile, + PreviewMediaFile = source.PreviewMediaFile, + AllowComments = source.AllowComments, + SectionBg = source.SectionBg, + IsPublished = true + }; + + blogsImported.Add(blog); + table.Add(blog); + } + } + + _ctx.SaveChanges(); + return blogsImported; + } + + private DirectoryInfo ResolveBlogDirectory(Language language, string virtualRootPath = null) + { + var testPaths = new[] + { + language.LanguageCulture, + language.GetTwoLetterISOLanguageName(), + "en" + }; + + var rootPath = CommonHelper.MapPath(virtualRootPath.NullEmpty() ?? "~/App_Data/Samples/blog/"); + foreach (var path in testPaths.Select(x => Path.Combine(rootPath, x))) + { + if (Directory.Exists(path)) + { + return new DirectoryInfo(path); + } + } + + throw new DirectoryNotFoundException($"Could not obtain an blog post path for language {language.LanguageCulture}. Fallback to 'en' failed, because directory does not exist."); + } + + private BlogPost DeserializeBlog(string fullPath) + { + return DeserializeDocument(XDocument.Load(fullPath)); + } + + private BlogPost DeserializeDocument(XDocument doc) + { + var result = new BlogPost(); + var nodes = doc.Root.Nodes().OfType(); + + foreach (var node in nodes) + { + var value = node.Value.Trim(); + + switch (node.Name.LocalName) + { + case "Title": + result.Title = value; + break; + case "MetaTitle": + result.MetaTitle = value; + break; + case "MetaDescription": + result.MetaDescription = value; + break; + case "Intro": + result.Intro = value; + break; + case "Body": + result.Body = value; + break; + case "Tags": + result.Tags = value; + break; + case "DisplayTags": + result.DisplayTagsInPreview = value.ToBool(); + break; + case "CreatedOn": + result.CreatedOnUtc = value.ToDateTime(new DateTime()).Value; + break; + case "DisplayType": + result.PreviewDisplayType = (PreviewDisplayType)int.Parse(value); + break; + case "Image": + var seName = GetSeName(Path.GetFileNameWithoutExtension(value)); + result.MediaFile = CreateImage(value, seName); + break; + case "ImagePreview": + seName = GetSeName(Path.GetFileNameWithoutExtension(value)); + result.PreviewMediaFile = CreateImage(value, seName); + break; + case "Comments": + result.AllowComments = value.ToBool(); + break; + case "SectionBg": + result.SectionBg = value; + break; + } + } + + return result; + } + + private MediaFile CreateImage(string fileName, string seoFilename = null) + { + try + { + var ext = Path.GetExtension(fileName); + var mimeType = MimeTypes.MapNameToMimeType(ext); + var path = Path.Combine(CommonHelper.MapPath("~/App_Data/Samples/blog/"), fileName).Replace('/', '\\'); + var buffer = File.ReadAllBytes(path); + var now = DateTime.UtcNow; + + var name = seoFilename.HasValue() + ? seoFilename.Truncate(100) + ext + : Path.GetFileName(fileName).ToLower().Replace('_', '-'); + + var file = new MediaFile + { + Name = name, + MediaType = "image", + MimeType = mimeType, + Extension = ext.EmptyNull().TrimStart('.'), + CreatedOnUtc = now, + UpdatedOnUtc = now, + Size = buffer.Length, + MediaStorage = new MediaStorage { Data = buffer }, + Version = 1 // so that FolderId is set later during track detection + }; + + return file; + } + catch (Exception ex) + { + // Throw ex; + System.Diagnostics.Debug.WriteLine(ex.Message); + return null; + } + } + + private string GetSeName(string name) + { + return SeoHelper.GetSeName(name, true, false); + } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs index 3477725126..796f12c1fa 100644 --- a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs +++ b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Xml.Linq; using System.Xml.Serialization; @@ -26,16 +27,18 @@ using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Infrastructure; using SmartStore.Core.IO; +using SmartStore.Core.Logging; using SmartStore.Core.Security; using SmartStore.Rules; using SmartStore.Rules.Domain; using SmartStore.Utilities; +using SmartStore.Utilities.ObjectPools; using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data.Utilities { public static class DataMigrator - { + { #region Download.ProductId /// @@ -49,23 +52,23 @@ public static int SetDownloadProductId(IDbContext context) if (ctx == null) throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - const string entityName = "Product"; + const string entityName = "Product"; #pragma warning disable 612, 618 // Get all products with a download var productQuery = from p in ctx.Set().AsNoTracking() - where (p.DownloadId != 0) - orderby p.Id - select new { p.Id, p.DownloadId }; + where (p.DownloadId != 0) + orderby p.Id + select new { p.Id, p.DownloadId }; #pragma warning restore 612, 618 var downloads = context.Set().Select(x => x).ToDictionary(x => x.Id); - + int pageIndex = -1; while (true) { var products = PagedList.Create(productQuery, ++pageIndex, 1000); - + foreach (var p in products) { try @@ -100,463 +103,478 @@ orderby p.Id /// Product to fix /// true when value was fixed public static bool FixProductMainPictureId(IDbContext context, Product product, IEnumerable entities = null) - { - Guard.NotNull(product, nameof(product)); - - // INFO: this method must be able to handle pre-save state also. - - var ctx = context as SmartObjectContext; - if (ctx == null) - throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - - entities = entities ?? product.ProductPictures; - if (entities == null) - return false; - - var transientEntities = entities.Where(x => x.Id == 0); - - var sortedEntities = entities - // Remove transient entities - .Except(transientEntities) - .OrderBy(x => x.DisplayOrder) - .ThenBy(x => x.Id) - .Select(x => ctx.Entry(x)) - // Remove deleted and detached entities - .Where(x => x.State != EfState.Deleted && x.State != EfState.Detached) - .Select(x => x.Entity) - // Added/transient entities must be appended - .Concat(transientEntities.OrderBy(x => x.DisplayOrder)); - - var newMainPictureId = sortedEntities.FirstOrDefault()?.MediaFileId; - - if (newMainPictureId != product.MainPictureId) - { - product.MainPictureId = newMainPictureId; - return true; - } - - return false; - } - - /// - /// Traverses all products and fixes 'MainPictureId' property values if it is out of sync. - /// - /// Database context (must be ) - /// Minimum modified or created date of products to process. Pass null to fix all products. - /// The total count of fixed and updated product entities - public static int FixProductMainPictureIds(IDbContext context, DateTime? ifModifiedSinceUtc = null) - { - return FixProductMainPictureIds(context, false, ifModifiedSinceUtc); - } - - /// - /// Called from migration seeder and only processes product entities without MainPictureId value. - /// - /// The total count of fixed and updated product entities - internal static int FixProductMainPictureIds(IDbContext context, bool initial, DateTime? ifModifiedSinceUtc = null) - { - var ctx = context as SmartObjectContext; - if (ctx == null) - throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - - var query = from p in ctx.Set().AsNoTracking() - where (!initial || p.MainPictureId == null) && (ifModifiedSinceUtc == null || p.UpdatedOnUtc >= ifModifiedSinceUtc.Value) - orderby p.Id - select new { p.Id, p.MainPictureId }; - - // Key = ProductId, Value = MainPictureId - var toUpdate = new Dictionary(); - - // 1st pass - int pageIndex = -1; - while (true) - { - var products = PagedList.Create(query, ++pageIndex, 1000); - var map = GetPoductPictureMap(ctx, products.Select(x => x.Id).ToArray()); - - foreach (var p in products) - { - int? fixedPictureId = null; - if (map.ContainsKey(p.Id)) - { - // Product has still a pic. - fixedPictureId = map[p.Id]; - } - - // Update only if fixed PictureId differs from current - if (fixedPictureId != p.MainPictureId) - { - toUpdate.Add(p.Id, fixedPictureId); - } - } - - if (!products.HasNextPage) - break; - } - - // 2nd pass - foreach (var chunk in toUpdate.Slice(1000)) - { - using (var tx = ctx.Database.BeginTransaction()) - { - foreach (var kvp in chunk) - { - context.ExecuteSqlCommand("Update [Product] Set [MainPictureId] = {0} WHERE [Id] = {1}", false, null, kvp.Value, kvp.Key); - } - - context.SaveChanges(); - tx.Commit(); - } - } - - return toUpdate.Count; - } - - private static IDictionary GetPoductPictureMap(SmartObjectContext context, IEnumerable productIds) - { - var map = new Dictionary(); - - var query = from pp in context.Set().AsNoTracking() - where productIds.Contains(pp.ProductId) - group pp by pp.ProductId into g - select new - { - ProductId = g.Key, - PictureIds = g.OrderBy(x => x.DisplayOrder).ThenBy(x => x.Id) - .Take(1) - .Select(x => x.MediaFileId) - }; - - map = query.ToList().ToDictionary(x => x.ProductId, x => x.PictureIds.First()); - - return map; - } - - #endregion - - #region MoveFsMedia (V3.1) - - /// - /// Reorganizes media files in subfolders for V3.1 - /// - /// - /// - public static int MoveFsMedia(IDbContext context) - { - var ctx = context as SmartObjectContext; - if (ctx == null) - throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - - int dirMaxLength = 4; - - // Check whether FS storage provider is active... - var setting = context.Set().FirstOrDefault(x => x.Name == "Media.Storage.Provider"); - if (setting == null || !setting.Value.IsCaseInsensitiveEqual("MediaStorage.SmartStoreFileSystem")) - { - // DB provider is active: no need to move anything. - return 0; - } - - // What a huge, fucking hack! > IMediaFileSystem is defined in an - // assembly which we don't reference from here. But it also implements - // IFileSystem, which we can cast to. - var fsType = Type.GetType("SmartStore.Services.Media.IMediaFileSystem, SmartStore.Services"); - var fs = EngineContext.Current.Resolve(fsType) as IFileSystem; - - // Pattern for file matching. E.g. matches 0000234-0.png - var rg = new Regex(@"^([0-9]{7})-0[.](.{3,4})$", RegexOptions.Compiled | RegexOptions.Singleline); - var subfolders = new Dictionary(); - int i = 0; - - // Get root files - var files = fs.ListFiles(""); - foreach (var chunk in files.Slice(500)) - { - foreach (var file in chunk) - { - var match = rg.Match(file.Name); - if (match.Success) - { - var name = match.Groups[1].Value; - var ext = match.Groups[2].Value; - // The new file name without trailing -0 - var newName = string.Concat(name, ".", ext); - // The subfolder name, e.g. 0024, when file name is 0024893.png - var dirName = name.Substring(0, dirMaxLength); - - if (!subfolders.TryGetValue(dirName, out string subfolder)) - { - // Create subfolder "Storage/0000" - subfolder = fs.Combine("Storage", dirName); - fs.CreateFolder(subfolder); - subfolders[dirName] = subfolder; - } - - // Build destination path - var destinationPath = fs.Combine(subfolder, newName); - - // Move the file now! - fs.RenameFile(file.Path, destinationPath); - i++; - } - } - } - - return i; - } - - #endregion - - #region Address Formats - - public static int ImportAddressFormats(IDbContext context) - { - var ctx = context as SmartObjectContext; - if (ctx == null) - throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - - var filePath = CommonHelper.MapPath("~/App_Data/AddressFormats.xml"); - - if (!File.Exists(filePath)) - { - return 0; - } - - var countries = ctx.Set() - .Where(x => x.AddressFormat == null) - .ToList() - .ToDictionarySafe(x => x.TwoLetterIsoCode, StringComparer.OrdinalIgnoreCase); - - var doc = XDocument.Load(filePath); - - foreach (var node in doc.Root.Nodes().OfType()) - { - var code = node.Attribute("code")?.Value?.Trim(); - var format = node.Value.Trim(); - - if (code.HasValue() && countries.TryGetValue(code, out var country)) - { - country.AddressFormat = format; - } - } - - return ctx.SaveChanges(); - } - - #endregion - - #region MoveCustomerFields (V3.2) - - /// - /// Moves several customer fields saved as generic attributes to customer entity (Title, FirstName, LastName, BirthDate, Company, CustomerNumber) - /// - /// Database context (must be ) - /// The total count of fixed and updated customer entities - public static int MoveCustomerFields(IDbContext context) - { - var ctx = context as SmartObjectContext; - if (ctx == null) - throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - - // We delete attrs only if the WHOLE migration succeeded - var attrIdsToDelete = new List(1000); - var gaTable = context.Set(); - var candidates = new[] { "Title", "FirstName", "LastName", "Company", "CustomerNumber", "DateOfBirth" }; - - var query = gaTable - .AsNoTracking() - .Where(x => x.KeyGroup == "Customer" && candidates.Contains(x.Key)) - .OrderBy(x => x.Id); - - int numUpdated = 0; - - using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) - { - for (var pageIndex = 0; pageIndex < 9999999; ++pageIndex) - { - var attrs = new PagedList(query, pageIndex, 250); - - var customerIds = attrs.Select(a => a.EntityId).Distinct().ToArray(); - var customers = context.Set() - .Where(x => customerIds.Contains(x.Id)) - .ToDictionary(x => x.Id); - - // Move attrs one by one to customer - foreach (var attr in attrs) - { - var customer = customers.Get(attr.EntityId); - if (customer == null) - continue; - - switch (attr.Key) - { - case "Title": - customer.Title = attr.Value?.Truncate(100); - break; - case "FirstName": - customer.FirstName = attr.Value?.Truncate(225); - break; - case "LastName": - customer.LastName = attr.Value?.Truncate(225); - break; - case "Company": - customer.Company = attr.Value?.Truncate(255); - break; - case "CustomerNumber": - customer.CustomerNumber = attr.Value?.Truncate(100); - break; - case "DateOfBirth": - customer.BirthDate = attr.Value?.Convert(); - break; - } - - // Update FullName - var parts = new[] { customer.Title, customer.FirstName, customer.LastName }; - customer.FullName = string.Join(" ", parts.Where(x => x.HasValue())).NullEmpty(); - - attrIdsToDelete.Add(attr.Id); - } - - // Save batch - numUpdated += scope.Commit(); - - // Breathe - context.DetachAll(); - - if (!attrs.HasNextPage) - break; - } - - // Everything worked out, now delete all orpahned attributes - if (attrIdsToDelete.Count > 0) - { - try - { - // Don't rollback migration when this fails - var stubs = attrIdsToDelete.Select(x => new GenericAttribute { Id = x }).ToList(); - foreach (var chunk in stubs.Slice(500)) - { - chunk.Each(x => gaTable.Attach(x)); - gaTable.RemoveRange(chunk); - scope.Commit(); - } - } - catch (Exception ex) - { - var msg = ex.Message; - } - } - } - - return numUpdated; - } + { + Guard.NotNull(product, nameof(product)); + + // INFO: this method must be able to handle pre-save state also. + + var ctx = context as SmartObjectContext; + if (ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + entities = entities ?? product.ProductPictures; + if (entities == null) + return false; + var transientEntities = entities.Where(x => x.Id == 0); + + var sortedEntities = entities + // Remove transient entities + .Except(transientEntities) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Id) + .Select(x => ctx.Entry(x)) + // Remove deleted and detached entities + .Where(x => x.State != EfState.Deleted && x.State != EfState.Detached) + .Select(x => x.Entity) + // Added/transient entities must be appended + .Concat(transientEntities.OrderBy(x => x.DisplayOrder)); + + var newMainPictureId = sortedEntities.FirstOrDefault()?.MediaFileId; + + if (newMainPictureId != product.MainPictureId) + { + product.MainPictureId = newMainPictureId; + return true; + } + + return false; + } /// - /// Moves several customer fields saved as generic attributes to customer entity - /// (Gender, ZipPostalCode, VatNumberStatusId, TimeZoneId, TaxDisplayTypeId, CountryId, CurrencyId, LanguageId, LastForumVisit, LastUserAgent) + /// Traverses all products and fixes 'MainPictureId' property values if it is out of sync. /// /// Database context (must be ) - /// The total count of fixed and updated customer entities - public static int MoveFurtherCustomerFields(IDbContext context) + /// Minimum modified or created date of products to process. Pass null to fix all products. + /// The total count of fixed and updated product entities + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FixProductMainPictureIds(IDbContext context, DateTime? ifModifiedSinceUtc = null) + { + return FixProductMainPictureIds(context, false, ifModifiedSinceUtc); + } + + /// + /// Called from migration seeder and only processes product entities without MainPictureId value. + /// + /// The total count of fixed and updated product entities + internal static int FixProductMainPictureIds(IDbContext context, bool initial, DateTime? ifModifiedSinceUtc = null) { var ctx = context as SmartObjectContext; if (ctx == null) throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - // We delete attrs only if the WHOLE migration succeeded - var attrIdsToDelete = new List(1000); - var gaTable = context.Set(); - var candidates = new[] { "Gender", "VatNumberStatusId", "TimeZoneId", "TaxDisplayTypeId", "LastForumVisit", "LastUserAgent", "LastUserDeviceType" }; - - var query = gaTable - .AsNoTracking() - .Where(x => x.KeyGroup == "Customer" && candidates.Contains(x.Key)) - .OrderBy(x => x.Id); + var query = from p in ctx.Set().AsNoTracking() + where (!initial || p.MainPictureId == null) && (ifModifiedSinceUtc == null || p.UpdatedOnUtc >= ifModifiedSinceUtc.Value) + orderby p.Id + select new { p.Id, p.MainPictureId }; - int numUpdated = 0; + // Key = ProductId, Value = MainPictureId + var toUpdate = new Dictionary(); - using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + // 1st pass + int pageIndex = -1; + while (true) { - for (var pageIndex = 0; pageIndex < 9999999; ++pageIndex) + var products = PagedList.Create(query, ++pageIndex, 1000); + var map = GetPoductPictureMap(ctx, products.Select(x => x.Id).ToArray()); + + foreach (var p in products) { - var attrs = new PagedList(query, pageIndex, 250); + int? fixedPictureId = null; + if (map.ContainsKey(p.Id)) + { + // Product has still a pic. + fixedPictureId = map[p.Id]; + } - var customerIds = attrs.Select(a => a.EntityId).Distinct().ToArray(); - var customers = context.Set() - .Where(x => customerIds.Contains(x.Id)) - .ToDictionary(x => x.Id); + // Update only if fixed PictureId differs from current + if (fixedPictureId != p.MainPictureId) + { + toUpdate.Add(p.Id, fixedPictureId); + } + } - // Move attrs one by one to customer - foreach (var attr in attrs) + if (!products.HasNextPage) + break; + } + + // 2nd pass + foreach (var chunk in toUpdate.Slice(1000)) + { + using (var tx = ctx.Database.BeginTransaction()) + { + foreach (var kvp in chunk) { - var customer = customers.Get(attr.EntityId); - if (customer == null) - continue; + context.ExecuteSqlCommand("Update [Product] Set [MainPictureId] = {0} WHERE [Id] = {1}", false, null, kvp.Value, kvp.Key); + } - switch (attr.Key) + context.SaveChanges(); + tx.Commit(); + } + } + + return toUpdate.Count; + } + + private static IDictionary GetPoductPictureMap(SmartObjectContext context, IEnumerable productIds) + { + var map = new Dictionary(); + + var query = from pp in context.Set().AsNoTracking() + where productIds.Contains(pp.ProductId) + group pp by pp.ProductId into g + select new { - case "Gender": - customer.Gender = attr.Value?.Truncate(100); - break; - case "VatNumberStatusId": - customer.VatNumberStatusId = attr.Value.Convert(); - break; - case "TimeZoneId": - customer.TimeZoneId = attr.Value?.Truncate(255); - break; - case "TaxDisplayTypeId": - customer.TaxDisplayTypeId = attr.Value.Convert(); - break; - case "LastForumVisit": - customer.LastForumVisit = attr.Value.Convert(); - break; - case "LastUserAgent": - customer.LastUserAgent = attr.Value.Convert(); - break; - case "LastUserDeviceType": - // TODO: split - customer.LastUserDeviceType = attr.Value.Convert(); - break; + ProductId = g.Key, + PictureIds = g.OrderBy(x => x.DisplayOrder).ThenBy(x => x.Id) + .Take(1) + .Select(x => x.MediaFileId) + }; + + map = query.ToList().ToDictionary(x => x.ProductId, x => x.PictureIds.First()); + + return map; + } + + #endregion + + #region MoveFsMedia (V3.1) + + /// + /// Reorganizes media files in subfolders for V3.1 + /// + /// + /// + public static int MoveFsMedia(IDbContext context) + { + var ctx = context as SmartObjectContext; + if (ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + int dirMaxLength = 4; + + // Check whether FS storage provider is active... + var setting = context.Set().FirstOrDefault(x => x.Name == "Media.Storage.Provider"); + if (setting == null || !setting.Value.IsCaseInsensitiveEqual("MediaStorage.SmartStoreFileSystem")) + { + // DB provider is active: no need to move anything. + return 0; + } + + // What a huge, fucking hack! > IMediaFileSystem is defined in an + // assembly which we don't reference from here. But it also implements + // IFileSystem, which we can cast to. + var fsType = Type.GetType("SmartStore.Services.Media.IMediaFileSystem, SmartStore.Services"); + var fs = EngineContext.Current.Resolve(fsType) as IFileSystem; + + // Pattern for file matching. E.g. matches 0000234-0.png + var rg = new Regex(@"^([0-9]{7})-0[.](.{3,4})$", RegexOptions.Compiled | RegexOptions.Singleline); + var subfolders = new Dictionary(); + int i = 0; + + // Get root files + var files = fs.ListFiles(""); + foreach (var chunk in files.Slice(500)) + { + foreach (var file in chunk) + { + var match = rg.Match(file.Name); + if (match.Success) + { + var name = match.Groups[1].Value; + var ext = match.Groups[2].Value; + // The new file name without trailing -0 + var newName = string.Concat(name, ".", ext); + // The subfolder name, e.g. 0024, when file name is 0024893.png + var dirName = name.Substring(0, dirMaxLength); + + if (!subfolders.TryGetValue(dirName, out string subfolder)) + { + // Create subfolder "Storage/0000" + subfolder = fs.Combine("Storage", dirName); + fs.CreateFolder(subfolder); + subfolders[dirName] = subfolder; } - attrIdsToDelete.Add(attr.Id); + // Build destination path + var destinationPath = fs.Combine(subfolder, newName); + + // Move the file now! + fs.RenameFile(file.Path, destinationPath); + i++; } + } + } + + return i; + } + + #endregion + + #region Address Formats + + public static int ImportAddressFormats(IDbContext context) + { + var ctx = context as SmartObjectContext; + if (ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + var filePath = CommonHelper.MapPath("~/App_Data/AddressFormats.xml"); + + if (!File.Exists(filePath)) + { + return 0; + } + + var countries = ctx.Set() + .Where(x => x.AddressFormat == null) + .ToList() + .ToDictionarySafe(x => x.TwoLetterIsoCode, StringComparer.OrdinalIgnoreCase); + + var doc = XDocument.Load(filePath); + + foreach (var node in doc.Root.Nodes().OfType()) + { + var code = node.Attribute("code")?.Value?.Trim(); + var format = node.Value.Trim(); + + if (code.HasValue() && countries.TryGetValue(code, out var country)) + { + country.AddressFormat = format; + } + } + + return ctx.SaveChanges(); + } + + #endregion + + #region MoveCustomerFields (V3.2) + + class CustomerStub + { + public int Id { get; set; } + public IEnumerable Attributes { get; set; } + } + + class AttributeStub + { + public int Id { get; set; } + public string Key { get; set; } + public string Value { get; set; } + } + + /// + /// Moves several customer fields saved as generic attributes to customer entity (Title, FirstName, LastName, BirthDate, Company, CustomerNumber) + /// + /// Database context (must be ) + /// The total count of fixed and updated customer entities + public static int MoveCustomerFields( + IDbContext context, + Action, string, string> updater, + params string[] candidates) + { + Guard.NotNull(updater, nameof(updater)); + + if (!(context is SmartObjectContext ctx)) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - // Save batch - numUpdated += scope.Commit(); + if (candidates.Length == 0) + return 0; - // Breathe - context.DetachAll(); + const int pageSize = 1000; + var gaTable = context.Set().AsNoTracking(); + //var candidates = new[] { "Title", "FirstName", "LastName", "Company", "CustomerNumber", "DateOfBirth" }; - if (!attrs.HasNextPage) + var query = ( + from a in gaTable + where a.KeyGroup == "Customer" && candidates.Contains(a.Key) + group a by a.EntityId into grps + where grps.Any() + select new CustomerStub + { + Id = grps.Key, + Attributes = grps.Select(a2 => new AttributeStub { Id = a2.Id, Key = a2.Key, Value = a2.Value }) + }) + .OrderBy(x => x.Id); + + int numAffectedRows = 0; + int numUpdated = 0; + + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + for (var pageIndex = 0; pageIndex < 9999999; pageIndex++) + { + var data = query + .Skip(pageIndex * pageSize) + .Take(pageSize) + .ToList(); + + foreach (var chunk in data.Slice(100)) + { + numAffectedRows += GenerateAndExecuteSql(chunk); + } + + numUpdated += data.Count; + + if (data.Count == 0) break; } + } + + return numUpdated; + + int GenerateAndExecuteSql(IEnumerable customers) + { + var sb = PooledStringBuilder.Rent(); - // Everything worked out, now delete all orpahned attributes - if (attrIdsToDelete.Count > 0) + foreach (var c in customers) { - try + var attrs = c.Attributes.ToArray(); + + if (attrs.Length == 0) + continue; + + var columns = new Dictionary(); + + /// Generate UPDATE Customer statement + foreach (var attr in attrs) + { + updater(columns, attr.Key, attr.Value); + } + + if (columns.Count == 0) + continue; + + sb.Append("UPDATE [Customer] SET"); + + int i = 0; + foreach (var kvp in columns) { - // Don't rollback migration when this fails - var stubs = attrIdsToDelete.Select(x => new GenericAttribute { Id = x }).ToList(); - foreach (var chunk in stubs.Slice(500)) + sb.Append(" "); + if (i > 0) sb.Append(", "); + + sb.Append($"[{kvp.Key}] = "); + + if (kvp.Value is int n) + { + sb.Append($"{n.ToString(CultureInfo.InvariantCulture)}"); + } + else if (kvp.Value is DateTime d) + { + sb.Append($"'{d.ToIso8601String()}'"); + } + else if (kvp.Value == null) + { + sb.Append("NULL"); + } + else { - chunk.Each(x => gaTable.Attach(x)); - gaTable.RemoveRange(chunk); - scope.Commit(); + sb.Append($"N'{kvp.Value}'"); } + + i++; } - catch (Exception ex) + + sb.Append(" WHERE Id = {0}".FormatInvariant(c.Id)); + sb.Append("\n"); + + // Generate all GenericAttribute DELETE statements + for (i = 0; i < attrs.Length; i++) { - var msg = ex.Message; + var a = attrs[i]; + sb.AppendLine($"DELETE FROM [GenericAttribute] WHERE [Id] = {a.Id}"); } } + + var sql = sb.ToStringAndReturn(); + if (sql.HasValue()) + { + return ctx.Execute(sql); + } + + return 0; } + } - return numUpdated; + public static int DeleteGuestCustomerGenericAttributes(IDbContext context, TimeSpan olderThan) + { + if (!(context is SmartObjectContext ctx)) + { + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + } + + int numTotalDeleted = 0; + var maxDate = DateTime.UtcNow - olderThan; + + var sql = @" +DELETE TOP(50000) [g] + FROM [dbo].[GenericAttribute] AS [g] + LEFT OUTER JOIN [dbo].[Customer] AS [c] ON c.Id = g.EntityId + LEFT OUTER JOIN [dbo].[Order] AS [o] ON c.Id = o.CustomerId + LEFT OUTER JOIN [dbo].[CustomerContent] AS [cc] ON c.Id = cc.CustomerId + LEFT OUTER JOIN [dbo].[Forums_PrivateMessage] AS [pm] ON c.Id = pm.ToCustomerId + LEFT OUTER JOIN [dbo].[Forums_Post] AS [fp] ON c.Id = fp.CustomerId + LEFT OUTER JOIN [dbo].[Forums_Topic] AS [ft] ON c.Id = ft.CustomerId + WHERE g.KeyGroup = 'Customer' AND c.Username IS Null AND c.Email IS NULL AND c.IsSystemAccount = 0 AND c.LastActivityDateUtc < @p0 + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[ShoppingCartItem] AS [sci] WHERE c.Id = sci.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Order] AS [o1] WHERE c.Id = o1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[CustomerContent] AS [cc1] WHERE c.Id = cc1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Forums_PrivateMessage] AS [pm1] WHERE c.Id = pm1.ToCustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Forums_Post] AS [fp1] WHERE c.Id = fp1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Forums_Topic] AS [ft1] WHERE c.Id = ft1.CustomerId )) +"; + + while (true) + { + var numDeleted = ctx.Execute(sql, maxDate); + if (numDeleted == 0) + break; + + numTotalDeleted += numDeleted; + } + + return numTotalDeleted; } + public static int DeleteGuestCustomers(IDbContext context, TimeSpan olderThan) + { + if (!(context is SmartObjectContext ctx)) + { + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + } + + int numTotalDeleted = 0; + var maxDate = DateTime.UtcNow - olderThan; + + var sql = @" +DELETE TOP(20000) [c] + FROM [dbo].[Customer] AS [c] + LEFT OUTER JOIN [dbo].[Order] AS [o] ON c.Id = o.CustomerId + LEFT OUTER JOIN [dbo].[CustomerContent] AS [cc] ON c.Id = cc.CustomerId + LEFT OUTER JOIN [dbo].[Forums_PrivateMessage] AS [pm] ON c.Id = pm.ToCustomerId + LEFT OUTER JOIN [dbo].[Forums_Post] AS [fp] ON c.Id = fp.CustomerId + LEFT OUTER JOIN [dbo].[Forums_Topic] AS [ft] ON c.Id = ft.CustomerId + WHERE c.Username IS Null AND c.Email IS NULL AND c.IsSystemAccount = 0 AND c.LastActivityDateUtc < @p0 + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[ShoppingCartItem] AS [sci] WHERE c.Id = sci.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Order] AS [o1] WHERE c.Id = o1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[CustomerContent] AS [cc1] WHERE c.Id = cc1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Forums_PrivateMessage] AS [pm1] WHERE c.Id = pm1.ToCustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Forums_Post] AS [fp1] WHERE c.Id = fp1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Forums_Topic] AS [ft1] WHERE c.Id = ft1.CustomerId )) +"; + + while (true) + { + var numDeleted = ctx.Execute(sql, maxDate); + if (numDeleted == 0) + break; + + numTotalDeleted += numDeleted; + } + + return numTotalDeleted; + } #endregion @@ -763,6 +781,15 @@ public static void CreateSystemMenus(IDbContext context) Model = "topic:paymentinfo", DisplayOrder = ++order }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerService.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("CookieManager"), + Title = "Cookie Manager", + DisplayOrder = ++order, + CssClass = "cookie-manager" + }); scope.Commit(); order = 0; @@ -944,11 +971,20 @@ public static void AddGranularPermissions(IDbContext context) } var mappingSet = ctx.Set(); - var permissionSet = ctx.Set(); - var allPermissions = permissionSet.ToList(); var oldPermissions = GetOldPermissions(); - var allRoles = ctx.Set().ToList(); - var adminRole = allRoles.FirstOrDefault(x => x.SystemName.IsCaseInsensitiveEqual("Administrators")); + + var allRoleIds = ctx.Set() + .AsNoTracking() + .Where(x => !string.IsNullOrEmpty(x.SystemName)) + .Select(x => new { x.Id, x.SystemName }) + .ToList() + .ToDictionarySafe(x => x.SystemName, x => x.Id, StringComparer.OrdinalIgnoreCase); + + allRoleIds.TryGetValue(SystemCustomerRoleNames.Administrators, out var adminRoleId); + allRoleIds.TryGetValue(SystemCustomerRoleNames.ForumModerators, out var forumModRoleId); + allRoleIds.TryGetValue(SystemCustomerRoleNames.Registered, out var registeredRoleId); + allRoleIds.TryGetValue(SystemCustomerRoleNames.Guests, out var guestRoleId); + allRoleIds.Clear(); // Mapping has no entity and no navigation property -> use SQL. var oldMappings = context.SqlQuery("select * from [dbo].[PermissionRecord_Role_Mapping]") @@ -956,6 +992,11 @@ public static void AddGranularPermissions(IDbContext context) .ToMultimap(x => x.PermissionRecord_Id, x => x.CustomerRole_Id); var permissionToRoles = new Multimap(StringComparer.OrdinalIgnoreCase); + var allPermissions = ctx.Set() + .AsNoTracking() + .Select(x => new { x.Id, x.SystemName }) + .ToList(); + foreach (var permission in allPermissions) { var roleIds = oldMappings.ContainsKey(permission.Id) @@ -964,26 +1005,13 @@ public static void AddGranularPermissions(IDbContext context) permissionToRoles.AddRange(permission.SystemName, roleIds); } + allPermissions.Clear(); using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) { - // Name and category property cannot be null, exist in database, but not in the domain model -> use SQL. - var insertSql = "Insert Into PermissionRecord (SystemName, Name, Category) Values({0}, {1}, {2})"; var permissionSystemNames = PermissionHelper.GetPermissions(typeof(Permissions)); - // Delete existing granular permissions to ensure correct order in tree. - permissionSet.RemoveRange(permissionSet.Where(x => permissionSystemNames.Contains(x.SystemName)).ToList()); - scope.Commit(); - - // Insert new granular permissions. - foreach (var permissionName in permissionSystemNames) - { - ctx.Database.ExecuteSqlCommand(insertSql, permissionName, string.Empty, string.Empty); - } - - var newPermissions = permissionSet.Where(x => permissionSystemNames.Contains(x.SystemName)) - .ToList() - .ToDictionarySafe(x => x.SystemName, x => x); + var newPermissions = InsertPermissions(scope, permissionSystemNames); // Migrate mappings of standard permissions (whether the new permission is granted). foreach (var kvp in oldPermissions) @@ -997,20 +1025,19 @@ public static void AddGranularPermissions(IDbContext context) scope.Commit(); // Add mappings for new permissions. - AllowForRole(adminRole, + AllowForRole(adminRoleId, newPermissions[Permissions.Cart.Read], newPermissions[Permissions.System.Rule.Self]); // Add mappings originally added by old migrations. - // We had to remove these migration statementent again because the table does not yet exist at this time. - AllowForRole(adminRole, + // We had to remove these migration statements again because the table does not yet exist at this time. + AllowForRole(adminRoleId, newPermissions[Permissions.Configuration.Export.Self], newPermissions[Permissions.Configuration.Import.Self], newPermissions[Permissions.System.UrlRecord.Self], newPermissions[Permissions.Cms.Menu.Self]); scope.Commit(); - newPermissions.Clear(); // Migrate known plugin permissions. var pluginPermissionNames = new Dictionary @@ -1028,6 +1055,7 @@ public static void AddGranularPermissions(IDbContext context) { "ManageDlm", "SmartStore.Dlm.Security.DlmPermissions, SmartStore.Dlm" } }; + // Get new plugin permission names. var allPluginPermissionNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var kvp in pluginPermissionNames) { @@ -1050,32 +1078,69 @@ public static void AddGranularPermissions(IDbContext context) } } - // Delete existing granular permissions to ensure correct order in tree. - permissionSet.RemoveRange(permissionSet.Where(x => allPluginPermissionNames.Contains(x.SystemName)).ToList()); - scope.Commit(); + newPermissions.Clear(); + newPermissions = InsertPermissions(scope, allPluginPermissionNames); - // Insert new granular permissions. - foreach (var permissionName in allPluginPermissionNames) + // Add PermissionRoleMapping for old plugin permissions. + PermissionRecord pr; + if (newPermissions.TryGetValue("debitoor", out pr)) { - ctx.Database.ExecuteSqlCommand(insertSql, permissionName, string.Empty, string.Empty); + Allow("ManageDebitoor", pr); + } + if (newPermissions.TryGetValue("bizimporter", out pr)) + { + Allow("AccessImportBiz", pr); + } + if (newPermissions.TryGetValue("shopconnector", out pr)) + { + Allow("ManageShopConnector", pr); + } + if (newPermissions.TryGetValue("newsimporter", out pr)) + { + Allow("ManageNewsImporter", pr); + } + if (newPermissions.TryGetValue("webapi", out pr)) + { + Allow("ManageWebApi", pr); + } + if (newPermissions.TryGetValue("megasearch", out pr)) + { + Allow("ManageMegaSearch", pr); + } + if (newPermissions.TryGetValue("erpconnector", out pr)) + { + Allow("ManageErpConnector", pr); + } + if (newPermissions.TryGetValue("powerbi", out pr)) + { + Allow("ManagePowerBi", pr); + } + if (newPermissions.TryGetValue("wallet", out pr)) + { + Allow("ManageWallet", pr); + } + if (newPermissions.TryGetValue("pagebuilder", out pr)) + { + Allow("ManageStories", pr); + } + if (newPermissions.TryGetValue("dlm", out pr)) + { + Allow("ManageDlm", pr); } - newPermissions = permissionSet.Where(x => allPluginPermissionNames.Contains(x.SystemName)) - .ToList() - .ToDictionarySafe(x => x.SystemName, x => x); - - PermissionRecord pr; - if (newPermissions.TryGetValue("debitoor", out pr)) Allow("ManageDebitoor", pr); - if (newPermissions.TryGetValue("bizimporter", out pr)) Allow("AccessImportBiz", pr); - if (newPermissions.TryGetValue("shopconnector", out pr)) Allow("ManageShopConnector", pr); - if (newPermissions.TryGetValue("newsimporter", out pr)) Allow("ManageNewsImporter", pr); - if (newPermissions.TryGetValue("webapi", out pr)) Allow("ManageWebApi", pr); - if (newPermissions.TryGetValue("megasearch", out pr)) Allow("ManageMegaSearch", pr); - if (newPermissions.TryGetValue("erpconnector", out pr)) Allow("ManageErpConnector", pr); - if (newPermissions.TryGetValue("powerbi", out pr)) Allow("ManagePowerBi", pr); - if (newPermissions.TryGetValue("wallet", out pr)) Allow("ManageWallet", pr); - if (newPermissions.TryGetValue("pagebuilder", out pr)) Allow("ManageStories", pr); - if (newPermissions.TryGetValue("dlm", out pr)) Allow("ManageDlm", pr); + // Add PermissionRoleMapping for default plugin permissions. + if (newPermissions.TryGetValue("pagebuilder.displaystory", out pr)) + { + AllowForRole(forumModRoleId, pr); + AllowForRole(guestRoleId, pr); + AllowForRole(registeredRoleId, pr); + } + if (newPermissions.TryGetValue("contentslider.displayslider", out pr)) + { + AllowForRole(forumModRoleId, pr); + AllowForRole(guestRoleId, pr); + AllowForRole(registeredRoleId, pr); + } scope.Commit(); @@ -1123,17 +1188,17 @@ void Allow(string oldSystemName, params PermissionRecord[] permissions) } } - void AllowForRole(CustomerRole role, params PermissionRecord[] permissions) + void AllowForRole(int roleId, params PermissionRecord[] permissions) { foreach (var permission in permissions) { - if (!mappingSet.Any(x => x.PermissionRecordId == permission.Id && x.CustomerRoleId == role.Id)) + if (!mappingSet.Any(x => x.PermissionRecordId == permission.Id && x.CustomerRoleId == roleId)) { mappingSet.Add(new PermissionRoleMapping { Allow = true, PermissionRecordId = permission.Id, - CustomerRoleId = role.Id + CustomerRoleId = roleId }); } } @@ -1193,6 +1258,44 @@ Multimap GetOldPermissions() return map; } + + Dictionary InsertPermissions(DbContextScope scope, IEnumerable systemNames) + { + var newPermissions = new Dictionary(); + var newPermissionSql = "Select * From [dbo].[PermissionRecord] Where [Name] = '' And [Category] = ''"; + + // Delete existing granular permissions to ensure correct order in tree. + // Maybe this step is not required anymore but doesn't hurt. + var existingPermissions = ctx.SqlQuery(newPermissionSql); + foreach (var permission in existingPermissions) + { + if (systemNames.Contains(permission.SystemName)) + { + ctx.Database.ExecuteSqlCommand("Delete From [dbo].[PermissionRecord] Where [Id] = {0}", permission.Id); + } + } + + // Insert new granular permissions. + // Name and category property cannot be null, exist in database, but not in the domain model -> use SQL. + foreach (var name in systemNames) + { + ctx.Database.ExecuteSqlCommand("Insert Into PermissionRecord (SystemName, Name, Category) Values({0}, {1}, {2})", + name, string.Empty, string.Empty); + } + + // Now load new permissions. + var permissions = ctx.SqlQuery(newPermissionSql); + foreach (var permission in permissions) + { + if (systemNames.Contains(permission.SystemName)) + { + // It's a new granular permission. + newPermissions[permission.SystemName] = permission; + } + } + + return newPermissions; + } } public class OldPermissionRoleMapping @@ -1339,19 +1442,43 @@ void AddRuleSetsForDiscountRequirements() case "discountrequirement.purchasedallproducts": case "discountrequirement.purchasedoneproduct": var productIds = new HashSet(); + var failedRequirements = new List(); + foreach (var requirement in requirements[name].Where(x => x.RestrictedProductIds.HasValue())) { - productIds.AddRange(requirement.RestrictedProductIds.ToIntArray()); + try + { + productIds.AddRange(requirement.RestrictedProductIds.ToIntArray()); + } + catch + { + failedRequirements.Add(requirement); + } } - if (productIds.Any()) + if (!failedRequirements.Any()) { - ruleSet.Rules.Add(new RuleEntity + if (productIds.Any()) { - RuleType = name == "discountrequirement.hasallproducts" || name == "discountrequirement.hasoneproduct" ? "ProductInCart" : "CartPurchasedProduct", - Operator = name == "discountrequirement.hasallproducts" || name == "discountrequirement.purchasedallproducts" ? RuleOperator.Contains : RuleOperator.In, - Value = string.Join(",", productIds) - }); + ruleSet.Rules.Add(new RuleEntity + { + RuleType = name == "discountrequirement.hasallproducts" || name == "discountrequirement.hasoneproduct" ? "ProductInCart" : "CartPurchasedProduct", + Operator = name == "discountrequirement.hasallproducts" || name == "discountrequirement.purchasedallproducts" ? RuleOperator.Contains : RuleOperator.In, + Value = string.Join(",", productIds) + }); + } + } + else + { + var sb = PooledStringBuilder.Rent(); + sb.AppendLine($"Cannnot create a rule for '{name}'."); + sb.AppendLine($"Discount name: {discount.Name.NaIfEmpty()}"); + sb.AppendLine($"Discount ID: {discount.Id}"); + sb.AppendLine(); + sb.AppendLine("Unsupported expressions for restricted product IDs:"); + failedRequirements.Each(x => sb.AppendLine(x.RestrictedProductIds)); + + LogWarning($"Cannot migrate requirements for discount '{discount.Name.NaIfEmpty()}'.", sb.ToStringAndReturn()); } break; case "discountrequirement.store": @@ -1400,7 +1527,7 @@ void AddRuleSetsForDiscountRequirements() } break; default: - $"Cannot add rule set for unknown discount requirement type ({name}).".Dump(); + LogWarning($"Cannot add rule set for unknown discount requirement type ({name}).", null); break; } } @@ -1588,7 +1715,7 @@ void AddRuleSetsForPaymentFilters() } } - /*static*/ T GetExtraData(OldDiscountRequirement req, string name) + T GetExtraData(OldDiscountRequirement req, string name) { try { @@ -1609,7 +1736,7 @@ void AddRuleSetsForPaymentFilters() return default(T); } - /*static*/ PluginFilterConfigModel GetFilterData(Dictionary syncMappings, int entityId, string rootNodeName) + PluginFilterConfigModel GetFilterData(Dictionary syncMappings, int entityId, string rootNodeName) { try { @@ -1630,7 +1757,7 @@ void AddRuleSetsForPaymentFilters() return null; } - /*static*/ void AddRulesForCommonFilters(SmartObjectContext _ctx, RuleSetEntity ruleSet, PluginFilterConfigModel model, string pluginSystemName) + void AddRulesForCommonFilters(SmartObjectContext _ctx, RuleSetEntity ruleSet, PluginFilterConfigModel model, string pluginSystemName) { // Store id (if plugin is limited to stores). var settingName = $"PluginSetting.{pluginSystemName}.LimitedToStores"; @@ -1702,6 +1829,20 @@ void AddRuleSetsForPaymentFilters() } } } + + /*static*/ + static void LogWarning(string message, string fullDescription) + { + try + { + var logger = EngineContext.Current.Resolve().GetLogger(typeof(DataMigrator)); + if (logger != null) + { + logger.Warn(fullDescription.HasValue() ? new SmartException(fullDescription) : null, message); + } + } + catch { } + } } public class OldDiscountRequirement diff --git a/src/Libraries/SmartStore.Data/Utilities/FastPager.cs b/src/Libraries/SmartStore.Data/Utilities/FastPager.cs index bd21fcf250..568157d7c4 100644 --- a/src/Libraries/SmartStore.Data/Utilities/FastPager.cs +++ b/src/Libraries/SmartStore.Data/Utilities/FastPager.cs @@ -22,6 +22,7 @@ namespace SmartStore.Data.Utilities private readonly int _pageSize; private int? _maxId; + private int? _currentPage; public FastPager(IQueryable query, int pageSize = 1000) { @@ -35,16 +36,21 @@ public FastPager(IQueryable query, int pageSize = 1000) public void Reset() { _maxId = null; + _currentPage = null; } + public int? MaxId => _maxId; + + public int? CurrentPage => _currentPage; + public bool ReadNextPage(out IList page) { return ReadNextPage(x => x, x => x.Id, out page); } public bool ReadNextPage( - Expression> shaper, - Func idSelector, + Expression> shaper, + Func idSelector, out IList page) { Guard.NotNull(shaper, nameof(shaper)); @@ -54,6 +60,7 @@ public bool ReadNextPage( if (_maxId == null) { _maxId = int.MaxValue; + _currentPage = 0; } if (_maxId.Value <= 1) { @@ -74,6 +81,7 @@ public bool ReadNextPage( return false; } + _currentPage++; _maxId = idSelector(page.Last()); return true; } @@ -92,6 +100,7 @@ public async Task> ReadNextPageAsync( if (_maxId == null) { _maxId = int.MaxValue; + _currentPage = 0; } if (_maxId.Value <= 1) { @@ -112,6 +121,7 @@ public async Task> ReadNextPageAsync( return null; } + _currentPage++; _maxId = idSelector(page.Last()); return page; } diff --git a/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs b/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs index 310aa4a220..1ad0d218b5 100644 --- a/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs +++ b/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs @@ -1,219 +1,219 @@ using System; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Messages; using SmartStore.Utilities; -using System.Xml; -using System.Xml.Linq; namespace SmartStore.Data.Utilities { - public sealed class MessageTemplateConverter - { - private readonly SmartObjectContext _ctx; - private readonly EmailAccount _defaultEmailAccount; - - public MessageTemplateConverter(IDbContext context) - { - _ctx = context as SmartObjectContext; - if (_ctx == null) - throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); - - _defaultEmailAccount = _ctx.Set().FirstOrDefault(x => x.Email != null); - } - - /// - /// Loads a single message template from file (~/App_Data/EmailTemplates/) - /// and deserializes its xml content. - /// - /// Name of template without extension, e.g. 'GiftCard.Notification' - /// Language - /// The virtual root path of templates to load, e.g. "~/Plugins/MyPlugins/EmailTemplates". Default is "~/App_Data/EmailTemplates". - /// Deserialized template xml - public MessageTemplate Load(string templateName, Language language, string virtualRootPath = null) - { - Guard.NotEmpty(templateName, nameof(templateName)); - Guard.NotNull(language, nameof(language)); - - var dir = ResolveTemplateDirectory(language, virtualRootPath); - var fullPath = Path.Combine(dir.FullName, templateName + ".xml"); - - if (!File.Exists(fullPath)) - { - throw new FileNotFoundException($"File '{fullPath}' does not exist."); - } - - return DeserializeTemplate(fullPath); - } - - /// - /// Loads all message templates from disk (~/App_Data/EmailTemplates/) - /// - /// Language - /// The virtual root path of templates to load, e.g. "~/Plugins/MyPlugins/EmailTemplates". Default is "~/App_Data/EmailTemplates". - /// List of deserialized template xml - public IEnumerable LoadAll(Language language, string virtualRootPath = null) - { - Guard.NotNull(language, nameof(language)); - - var dir = ResolveTemplateDirectory(language, virtualRootPath); - var files = dir.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly); - - foreach (var file in files) - { - var template = DeserializeTemplate(file.FullName); - template.Name = Path.GetFileNameWithoutExtension(file.Name); - yield return template; - } - } - - public MessageTemplate Deserialize(string xml, string templateName) - { - Guard.NotEmpty(xml, nameof(xml)); - Guard.NotEmpty(templateName, nameof(templateName)); - - var template = DeserializeDocument(XDocument.Parse(xml)); - template.Name = templateName; - return template; - } - - public XmlDocument Save(MessageTemplate template, Language language) - { - Guard.NotNull(template, nameof(template)); - Guard.NotNull(language, nameof(language)); - - var doc = new XmlDocument(); - doc.LoadXml(""); - - var root = doc.DocumentElement; - root.AppendChild(doc.CreateElement("To")).InnerText = template.To; - if (template.ReplyTo.HasValue()) - root.AppendChild(doc.CreateElement("ReplyTo")).InnerText = template.ReplyTo; - root.AppendChild(doc.CreateElement("Subject")).InnerText = template.Subject; - root.AppendChild(doc.CreateElement("ModelTypes")).InnerText = template.ModelTypes; - root.AppendChild(doc.CreateElement("Body")).AppendChild(doc.CreateCDataSection(template.Body)); - - var path = Path.Combine(CommonHelper.MapPath("~/App_Data/EmailTemplates"), language.GetTwoLetterISOLanguageName()); - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - - // File path - path = Path.Combine(path, template.Name + ".xml"); - - var xml = Prettifier.PrettifyXML(doc.OuterXml); - File.WriteAllText(path, xml); - - return doc; - } - - /// - /// Imports all template xml files to MessageTemplate table - /// - /// The virtual root path of templates to import, e.g. "~/Plugins/MyPlugins/EmailTemplates". Default is "~/App_Data/EmailTemplates". - public void ImportAll(Language language, string virtualRootPath = null) - { - var table = _ctx.Set(); - - var sourceTemplates = LoadAll(language, virtualRootPath); - var dbTemplatesMap = table - .ToList() - .ToMultimap(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase); - - foreach (var source in sourceTemplates) - { - if (dbTemplatesMap.ContainsKey(source.Name)) - { - foreach (var target in dbTemplatesMap[source.Name]) - { - if (source.To.HasValue()) target.To = source.To; - if (source.ReplyTo.HasValue()) target.ReplyTo = source.ReplyTo; - if (source.Subject.HasValue()) target.Subject = source.Subject; - if (source.ModelTypes.HasValue()) target.ModelTypes = source.ModelTypes; - if (source.Body.HasValue()) target.Body = source.Body; - } - } - else - { - var template = new MessageTemplate - { - Name = source.Name, - To = source.To, - ReplyTo = source.ReplyTo, - Subject = source.Subject, - ModelTypes = source.ModelTypes, - Body = source.Body, - IsActive = true, - EmailAccountId = (_defaultEmailAccount?.Id).GetValueOrDefault(), - }; - - table.Add(template); - } - } - - _ctx.SaveChanges(); - } - - private DirectoryInfo ResolveTemplateDirectory(Language language, string virtualRootPath = null) - { - var rootPath = CommonHelper.MapPath(virtualRootPath.NullEmpty() ?? "~/App_Data/EmailTemplates/"); - var testPaths = new[] - { - language.LanguageCulture, - language.GetTwoLetterISOLanguageName(), - "en" - }; - - foreach (var path in testPaths.Select(x => Path.Combine(rootPath, x))) - { - if (Directory.Exists(path)) - { - return new DirectoryInfo(path); - } - } - - throw new DirectoryNotFoundException($"Could not obtain an email templates path for language {language.LanguageCulture}. Fallback to 'en' failed, because directory does not exist."); - } - - private MessageTemplate DeserializeTemplate(string fullPath) - { - return DeserializeDocument(XDocument.Load(fullPath)); - } - - private MessageTemplate DeserializeDocument(XDocument doc) - { - var root = doc.Root; - var result = new MessageTemplate(); - - foreach (var node in root.Nodes().OfType()) - { - var value = node.Value.Trim(); - - switch (node.Name.LocalName) - { - case "To": - result.To = value; - break; - case "ReplyTo": - result.ReplyTo = value; - break; - case "Subject": - result.Subject = value; - break; - case "ModelTypes": - result.ModelTypes = value; - break; - case "Body": - result.Body = value; - break; - } - } - - return result; - } - } + public sealed class MessageTemplateConverter + { + private readonly SmartObjectContext _ctx; + private readonly EmailAccount _defaultEmailAccount; + + public MessageTemplateConverter(IDbContext context) + { + _ctx = context as SmartObjectContext; + if (_ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + _defaultEmailAccount = _ctx.Set().FirstOrDefault(x => x.Email != null); + } + + /// + /// Loads a single message template from file (~/App_Data/EmailTemplates/) + /// and deserializes its xml content. + /// + /// Name of template without extension, e.g. 'GiftCard.Notification' + /// Language + /// The virtual root path of templates to load, e.g. "~/Plugins/MyPlugins/EmailTemplates". Default is "~/App_Data/EmailTemplates". + /// Deserialized template xml + public MessageTemplate Load(string templateName, Language language, string virtualRootPath = null) + { + Guard.NotEmpty(templateName, nameof(templateName)); + Guard.NotNull(language, nameof(language)); + + var dir = ResolveTemplateDirectory(language, virtualRootPath); + var fullPath = Path.Combine(dir.FullName, templateName + ".xml"); + + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"File '{fullPath}' does not exist."); + } + + return DeserializeTemplate(fullPath); + } + + /// + /// Loads all message templates from disk (~/App_Data/EmailTemplates/) + /// + /// Language + /// The virtual root path of templates to load, e.g. "~/Plugins/MyPlugins/EmailTemplates". Default is "~/App_Data/EmailTemplates". + /// List of deserialized template xml + public IEnumerable LoadAll(Language language, string virtualRootPath = null) + { + Guard.NotNull(language, nameof(language)); + + var dir = ResolveTemplateDirectory(language, virtualRootPath); + var files = dir.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly); + + foreach (var file in files) + { + var template = DeserializeTemplate(file.FullName); + template.Name = Path.GetFileNameWithoutExtension(file.Name); + yield return template; + } + } + + public MessageTemplate Deserialize(string xml, string templateName) + { + Guard.NotEmpty(xml, nameof(xml)); + Guard.NotEmpty(templateName, nameof(templateName)); + + var template = DeserializeDocument(XDocument.Parse(xml)); + template.Name = templateName; + return template; + } + + public XmlDocument Save(MessageTemplate template, Language language) + { + Guard.NotNull(template, nameof(template)); + Guard.NotNull(language, nameof(language)); + + var doc = new XmlDocument(); + doc.LoadXml(""); + + var root = doc.DocumentElement; + root.AppendChild(doc.CreateElement("To")).InnerText = template.To; + if (template.ReplyTo.HasValue()) + root.AppendChild(doc.CreateElement("ReplyTo")).InnerText = template.ReplyTo; + root.AppendChild(doc.CreateElement("Subject")).InnerText = template.Subject; + root.AppendChild(doc.CreateElement("ModelTypes")).InnerText = template.ModelTypes; + root.AppendChild(doc.CreateElement("Body")).AppendChild(doc.CreateCDataSection(template.Body)); + + var path = Path.Combine(CommonHelper.MapPath("~/App_Data/EmailTemplates"), language.GetTwoLetterISOLanguageName()); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + // File path + path = Path.Combine(path, template.Name + ".xml"); + + var xml = Prettifier.PrettifyXML(doc.OuterXml); + File.WriteAllText(path, xml); + + return doc; + } + + /// + /// Imports all template xml files to MessageTemplate table + /// + /// The virtual root path of templates to import, e.g. "~/Plugins/MyPlugins/EmailTemplates". Default is "~/App_Data/EmailTemplates". + public void ImportAll(Language language, string virtualRootPath = null) + { + var table = _ctx.Set(); + + var sourceTemplates = LoadAll(language, virtualRootPath); + var dbTemplatesMap = table + .ToList() + .ToMultimap(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase); + + foreach (var source in sourceTemplates) + { + if (dbTemplatesMap.ContainsKey(source.Name)) + { + foreach (var target in dbTemplatesMap[source.Name]) + { + if (source.To.HasValue()) target.To = source.To; + if (source.ReplyTo.HasValue()) target.ReplyTo = source.ReplyTo; + if (source.Subject.HasValue()) target.Subject = source.Subject; + if (source.ModelTypes.HasValue()) target.ModelTypes = source.ModelTypes; + if (source.Body.HasValue()) target.Body = source.Body; + } + } + else + { + var template = new MessageTemplate + { + Name = source.Name, + To = source.To, + ReplyTo = source.ReplyTo, + Subject = source.Subject, + ModelTypes = source.ModelTypes, + Body = source.Body, + IsActive = true, + EmailAccountId = (_defaultEmailAccount?.Id).GetValueOrDefault(), + }; + + table.Add(template); + } + } + + _ctx.SaveChanges(); + } + + private DirectoryInfo ResolveTemplateDirectory(Language language, string virtualRootPath = null) + { + var rootPath = CommonHelper.MapPath(virtualRootPath.NullEmpty() ?? "~/App_Data/EmailTemplates/"); + var testPaths = new[] + { + language.LanguageCulture, + language.GetTwoLetterISOLanguageName(), + "en" + }; + + foreach (var path in testPaths.Select(x => Path.Combine(rootPath, x))) + { + if (Directory.Exists(path)) + { + return new DirectoryInfo(path); + } + } + + throw new DirectoryNotFoundException($"Could not obtain an email templates path for language {language.LanguageCulture}. Fallback to 'en' failed, because directory does not exist."); + } + + private MessageTemplate DeserializeTemplate(string fullPath) + { + return DeserializeDocument(XDocument.Load(fullPath)); + } + + private MessageTemplate DeserializeDocument(XDocument doc) + { + var root = doc.Root; + var result = new MessageTemplate(); + + foreach (var node in root.Nodes().OfType()) + { + var value = node.Value.Trim(); + + switch (node.Name.LocalName) + { + case "To": + result.To = value; + break; + case "ReplyTo": + result.ReplyTo = value; + break; + case "Subject": + result.Subject = value; + break; + case "ModelTypes": + result.ModelTypes = value; + break; + case "Body": + result.Body = value; + break; + } + } + + return result; + } + } } diff --git a/src/Libraries/SmartStore.Data/Utilities/NewsItemConverter.cs b/src/Libraries/SmartStore.Data/Utilities/NewsItemConverter.cs new file mode 100644 index 0000000000..b9552f88a0 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Utilities/NewsItemConverter.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.News; +using SmartStore.Core.IO; +using SmartStore.Utilities; + +namespace SmartStore.Data.Utilities +{ + public sealed class NewsItemConverter + { + private readonly SmartObjectContext _ctx; + + public NewsItemConverter(IDbContext context) + { + _ctx = context as SmartObjectContext; + if (_ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + } + + /// + /// Loads all news item from disk (~/App_Data/Samples/news/) + /// + /// Language + /// The virtual root path of news to load, e.g. "~/Plugins/MyPlugins/NewsItem". Default is "~/App_Data/Samples/news". + /// List of deserialized news items xml + public IEnumerable LoadAll(Language language, string virtualRootPath = null) + { + Guard.NotNull(language, nameof(language)); + + var dir = ResolveNewsDirectory(language, virtualRootPath); + var files = dir.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly); + + foreach (var file in files) + { + yield return DeserializeNews(file.FullName); + } + } + + /// + /// Imports all news xml files to NewsItem table + /// + /// The virtual root path of blogs to import, e.g. "~/Plugins/MyPlugins/NewsItem". Default is "~/App_Data/Samples/news". + /// List of new imported news items + public IList ImportAll(Language language, string virtualRootPath = null) + { + var newsImported = new List(); + var table = _ctx.Set(); + var sourceBlogs = LoadAll(language); + var dbNewsMap = table.ToList().ToMultimap(x => x.Title, x => x, StringComparer.OrdinalIgnoreCase); + + foreach (var source in sourceBlogs) + { + if (dbNewsMap.ContainsKey(source.Title)) + { + foreach (var target in dbNewsMap[source.Title]) + { + if (source.Title.HasValue()) target.Title = source.Title; + if (source.MetaTitle.HasValue()) target.MetaTitle = source.MetaTitle; + if (source.MetaDescription.HasValue()) target.MetaDescription = source.MetaDescription; + if (source.Short.HasValue()) target.Short = source.Short; + if (source.Full.HasValue()) target.Full = source.Full; + if (source.CreatedOnUtc != null) target.CreatedOnUtc = source.CreatedOnUtc; + if (source.MediaFile != null) target.MediaFile = source.MediaFile; + if (source.PreviewMediaFile != null) target.PreviewMediaFile = source.PreviewMediaFile; + target.AllowComments = source.AllowComments; + } + } + else + { + var news = new NewsItem + { + Title = source.Title, + MetaTitle = source.MetaTitle, + MetaDescription = source.MetaDescription, + Short = source.Short, + Full = source.Full, + CreatedOnUtc = source.CreatedOnUtc, + MediaFile = source.MediaFile, + PreviewMediaFile = source.PreviewMediaFile, + AllowComments = source.AllowComments, + Published = true + }; + + newsImported.Add(news); + table.Add(news); + } + } + + _ctx.SaveChanges(); + return newsImported; + } + + private DirectoryInfo ResolveNewsDirectory(Language language, string virtualRootPath = null) + { + var testPaths = new[] + { + language.LanguageCulture, + language.GetTwoLetterISOLanguageName(), + "en" + }; + + var rootPath = CommonHelper.MapPath(virtualRootPath.NullEmpty() ?? "~/App_Data/Samples/news/"); + foreach (var path in testPaths.Select(x => Path.Combine(rootPath, x))) + { + if (Directory.Exists(path)) + { + return new DirectoryInfo(path); + } + } + + throw new DirectoryNotFoundException($"Could not obtain a news item path for language {language.LanguageCulture}. Fallback to 'en' failed, because directory does not exist."); + } + + private NewsItem DeserializeNews(string fullPath) + { + return DeserializeDocument(XDocument.Load(fullPath)); + } + + private NewsItem DeserializeDocument(XDocument doc) + { + var result = new NewsItem(); + var nodes = doc.Root.Nodes().OfType(); + + foreach (var node in nodes) + { + var value = node.Value.Trim(); + + switch (node.Name.LocalName) + { + case "Title": + result.Title = value; + break; + case "MetaTitle": + result.MetaTitle = value; + break; + case "MetaDescription": + result.MetaDescription = value; + break; + case "Short": + result.Short = value; + break; + case "Full": + result.Full = value; + break; + case "CreatedOn": + result.CreatedOnUtc = value.ToDateTime(new DateTime()).Value; + break; + case "Image": + var seName = GetSeName(Path.GetFileNameWithoutExtension(value)); + result.MediaFile = CreateImage(value, seName); + break; + case "ImagePreview": + seName = GetSeName(Path.GetFileNameWithoutExtension(value)); + var previewImage = CreateImage(value, seName); + result.PreviewMediaFile = previewImage.Name == result.MediaFile.Name + ? result.MediaFile + : previewImage; + break; + case "Comments": + result.AllowComments = value.ToBool(); + break; + } + } + + return result; + } + + private MediaFile CreateImage(string fileName, string seoFilename = null) + { + try + { + var ext = Path.GetExtension(fileName); + var mimeType = MimeTypes.MapNameToMimeType(ext); + var path = Path.Combine(CommonHelper.MapPath("~/App_Data/Samples/news/"), fileName).Replace('/', '\\'); + var buffer = File.ReadAllBytes(path); + var now = DateTime.UtcNow; + + var name = seoFilename.HasValue() + ? seoFilename.Truncate(100) + ext + : Path.GetFileName(fileName).ToLower().Replace('_', '-'); + + var file = new MediaFile + { + Name = name, + MediaType = "image", + MimeType = mimeType, + Extension = ext.EmptyNull().TrimStart('.'), + CreatedOnUtc = now, + UpdatedOnUtc = now, + Size = buffer.Length, + MediaStorage = new MediaStorage { Data = buffer }, + Version = 1 // so that FolderId is set later during track detection + }; + + return file; + } + catch (Exception ex) + { + // Throw ex; + System.Diagnostics.Debug.WriteLine(ex.Message); + return null; + } + } + + private string GetSeName(string name) + { + return SeoHelper.GetSeName(name, true, false); + } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Utilities/SqlBlobStream.cs b/src/Libraries/SmartStore.Data/Utilities/SqlBlobStream.cs index bbe2c3aa49..9e346c08ab 100644 --- a/src/Libraries/SmartStore.Data/Utilities/SqlBlobStream.cs +++ b/src/Libraries/SmartStore.Data/Utilities/SqlBlobStream.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Data; using System.Data.Common; using System.Data.Entity.Infrastructure; +using System.IO; namespace SmartStore.Data.Utilities { @@ -17,8 +16,8 @@ public class SqlBlobStream : Stream private long _dataIndex = 0; public SqlBlobStream( - IDbConnectionFactory connectionFactory, - string connectionString, + IDbConnectionFactory connectionFactory, + string connectionString, string tableName, string blobColumnName, string pkColumnName, @@ -26,8 +25,8 @@ public SqlBlobStream( { Guard.NotNull(connectionFactory, nameof(connectionFactory)); Guard.NotEmpty(connectionString, nameof(connectionString)); - Guard.NotEmpty(connectionString, nameof(tableName)); - Guard.NotEmpty(connectionString, nameof(blobColumnName)); + Guard.NotEmpty(tableName, nameof(tableName)); + Guard.NotEmpty(blobColumnName, nameof(blobColumnName)); Guard.NotEmpty(pkColumnName, nameof(pkColumnName)); Guard.NotNull(pkColumnValue, nameof(pkColumnValue)); @@ -99,7 +98,7 @@ public override long Seek(long offset, SeekOrigin origin) CloseReader(); return Seek(offset, origin); } - + if (offset > _dataIndex) { EnsureOpen(); @@ -116,14 +115,14 @@ public override long Seek(long offset, SeekOrigin origin) public override bool CanSeek => true; public override bool CanRead => true; public override bool CanWrite => false; - public override long Position - { - get => _dataIndex; - set => Seek(value, SeekOrigin.Begin); + public override long Position + { + get => _dataIndex; + set => Seek(value, SeekOrigin.Begin); } - public override long Length + public override long Length { - get + get { EnsureOpen(); @@ -139,22 +138,33 @@ public override long Length public override int Read(byte[] buffer, int offset, int count) { EnsureOpen(); - + var read = _reader.GetBytes(0, _dataIndex + offset, buffer, 0, count); _dataIndex += read; return (int)read; } - public override void Flush() => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override void Flush() + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } protected override void Dispose(bool disposing) { if (disposing) { - CloseReader(); + CloseReader(); } } diff --git a/src/Libraries/SmartStore.Data/app.config b/src/Libraries/SmartStore.Data/app.config index 63141836b9..a625161735 100644 --- a/src/Libraries/SmartStore.Data/app.config +++ b/src/Libraries/SmartStore.Data/app.config @@ -4,20 +4,28 @@ - + - - - - + + + + + + + + + + + + - + diff --git a/src/Libraries/SmartStore.Data/packages.config b/src/Libraries/SmartStore.Data/packages.config index a063941a5e..bc852168b3 100644 --- a/src/Libraries/SmartStore.Data/packages.config +++ b/src/Libraries/SmartStore.Data/packages.config @@ -1,10 +1,12 @@  - - - + + + + - - + + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs b/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs index 68475199cf..25abf782ae 100644 --- a/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs +++ b/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs @@ -7,46 +7,20 @@ namespace SmartStore.Services.Affiliates { - /// - /// Affiliate service - /// public partial class AffiliateService : IAffiliateService { - #region Fields - private readonly IRepository _affiliateRepository; - private readonly IEventPublisher _eventPublisher; - - #endregion - - #region Ctor - /// - /// Ctor - /// - /// Affiliate repository - /// Event published - public AffiliateService(IRepository affiliateRepository, - IEventPublisher eventPublisher) + public AffiliateService(IRepository affiliateRepository) { _affiliateRepository = affiliateRepository; - _eventPublisher = eventPublisher; } - #endregion - - #region Methods - - /// - /// Gets an affiliate by affiliate identifier - /// - /// Affiliate identifier - /// Affiliate public virtual Affiliate GetAffiliateById(int affiliateId) { if (affiliateId == 0) return null; - + return _affiliateRepository.GetById(affiliateId); } @@ -102,8 +76,5 @@ public virtual void UpdateAffiliate(Affiliate affiliate) _affiliateRepository.Update(affiliate); } - - #endregion - } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Affiliates/IAffiliateService.cs b/src/Libraries/SmartStore.Services/Affiliates/IAffiliateService.cs index c81ca3cd53..ab8dfe25a2 100644 --- a/src/Libraries/SmartStore.Services/Affiliates/IAffiliateService.cs +++ b/src/Libraries/SmartStore.Services/Affiliates/IAffiliateService.cs @@ -39,6 +39,6 @@ public partial interface IAffiliateService /// /// Affiliate void UpdateAffiliate(Affiliate affiliate); - + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizationResult.cs b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizationResult.cs index 1458b333e5..f1a4d248cf 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizationResult.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizationResult.cs @@ -17,10 +17,7 @@ public void AddError(string error) this.Errors.Add(error); } - public bool Success - { - get { return this.Errors.Count == 0; } - } + public bool Success => this.Errors.Count == 0; public OpenAuthenticationStatus Status { get; private set; } diff --git a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs index 35e853bbfc..da90c9f326 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs @@ -10,7 +10,7 @@ public partial class AuthorizeState public IList Errors { get; set; } private readonly string _returnUrl; - + public AuthorizeState(string returnUrl, OpenAuthenticationStatus openAuthenticationStatus) { Errors = new List(); @@ -29,11 +29,8 @@ public AuthorizeState(string returnUrl, AuthorizationResult authorizationResult) } public OpenAuthenticationStatus AuthenticationStatus { get; private set; } - - public bool Success - { - get { return (this.Errors.Count == 0); } - } + + public bool Success => (this.Errors.Count == 0); public void AddError(string error) { diff --git a/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs b/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs index 7eab4cd87a..c46a48d693 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs @@ -26,25 +26,25 @@ public partial class ExternalAuthorizer : IExternalAuthorizer private readonly CustomerSettings _customerSettings; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; private readonly IShoppingCartService _shoppingCartService; - private readonly IMessageFactory _messageFactory; - private readonly LocalizationSettings _localizationSettings; + private readonly IMessageFactory _messageFactory; + private readonly LocalizationSettings _localizationSettings; #endregion #region Ctor public ExternalAuthorizer( - IAuthenticationService authenticationService, + IAuthenticationService authenticationService, IOpenAuthenticationService openAuthenticationService, IGenericAttributeService genericAttributeService, ICustomerRegistrationService customerRegistrationService, - ICustomerActivityService customerActivityService, - ILocalizationService localizationService, - IWorkContext workContext, - CustomerSettings customerSettings, + ICustomerActivityService customerActivityService, + ILocalizationService localizationService, + IWorkContext workContext, + CustomerSettings customerSettings, ExternalAuthenticationSettings externalAuthenticationSettings, IShoppingCartService shoppingCartService, - IMessageFactory messageFactory, - LocalizationSettings localizationSettings) + IMessageFactory messageFactory, + LocalizationSettings localizationSettings) { _authenticationService = authenticationService; _openAuthenticationService = openAuthenticationService; @@ -130,13 +130,13 @@ public virtual AuthorizationResult Authorize(OpenAuthenticationParameters parame var registrationResult = _customerRegistrationService.RegisterCustomer(registrationRequest); if (registrationResult.Success) { - // store other parameters (form fields) - if (details.FirstName.HasValue()) - currentCustomer.FirstName = details.FirstName; + // store other parameters (form fields) + if (details.FirstName.HasValue()) + currentCustomer.FirstName = details.FirstName; if (details.LastName.HasValue()) - currentCustomer.LastName = details.LastName; + currentCustomer.LastName = details.LastName; - userFound = currentCustomer; + userFound = currentCustomer; _openAuthenticationService.AssociateExternalAccountWithUser(currentCustomer, parameters); ExternalAuthorizerHelper.RemoveParameters(); @@ -145,7 +145,7 @@ public virtual AuthorizationResult Authorize(OpenAuthenticationParameters parame //authenticate if (isApproved) _authenticationService.SignIn(userFound ?? userLoggedIn, false); - + //notifications if (_customerSettings.NotifyNewCustomerRegistration) _messageFactory.SendCustomerRegisteredNotificationMessage(currentCustomer, _localizationSettings.DefaultAdminLanguageId); @@ -153,24 +153,24 @@ public virtual AuthorizationResult Authorize(OpenAuthenticationParameters parame switch (_customerSettings.UserRegistrationType) { case UserRegistrationType.EmailValidation: - { - // email validation message - _genericAttributeService.SaveAttribute(currentCustomer, SystemCustomerAttributeNames.AccountActivationToken, Guid.NewGuid().ToString()); - _messageFactory.SendCustomerEmailValidationMessage(currentCustomer, _workContext.WorkingLanguage.Id); + { + // email validation message + _genericAttributeService.SaveAttribute(currentCustomer, SystemCustomerAttributeNames.AccountActivationToken, Guid.NewGuid().ToString()); + _messageFactory.SendCustomerEmailValidationMessage(currentCustomer, _workContext.WorkingLanguage.Id); - return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredEmailValidation); - } + return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredEmailValidation); + } case UserRegistrationType.AdminApproval: - { - return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredAdminApproval); - } + { + return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredAdminApproval); + } case UserRegistrationType.Standard: - { - // send customer welcome message - _messageFactory.SendCustomerWelcomeMessage(currentCustomer, _workContext.WorkingLanguage.Id); + { + // send customer welcome message + _messageFactory.SendCustomerWelcomeMessage(currentCustomer, _workContext.WorkingLanguage.Id); - return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredStandard); - } + return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredStandard); + } default: break; } diff --git a/src/Libraries/SmartStore.Services/Authentication/External/IExternalAuthenticationMethod.cs b/src/Libraries/SmartStore.Services/Authentication/External/IExternalAuthenticationMethod.cs index 6e8029ce4a..e7eed71acb 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/IExternalAuthenticationMethod.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/IExternalAuthenticationMethod.cs @@ -9,7 +9,7 @@ namespace SmartStore.Services.Authentication.External /// Provides an interface for creating external authentication methods /// public partial interface IExternalAuthenticationMethod : IProvider, IUserEditable - { + { /// /// Gets a route for displaying plugin in public store /// diff --git a/src/Libraries/SmartStore.Services/Authentication/External/IExternalProviderAuthorizer.cs b/src/Libraries/SmartStore.Services/Authentication/External/IExternalProviderAuthorizer.cs index d46a5ba1db..afc3348198 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/IExternalProviderAuthorizer.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/IExternalProviderAuthorizer.cs @@ -5,12 +5,12 @@ namespace SmartStore.Services.Authentication.External { public partial interface IExternalProviderAuthorizer { - /// - /// Authorize response - /// - /// Return URL - /// true - Verify response;false - request authentication;null - determine automatically - /// Authorize state - AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null); + /// + /// Authorize response + /// + /// Return URL + /// true - Verify response;false - request authentication;null - determine automatically + /// Authorize state + AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationParameters.cs b/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationParameters.cs index 1348bcadd3..e8a84d18b3 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationParameters.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationParameters.cs @@ -14,9 +14,6 @@ public abstract partial class OpenAuthenticationParameters public string OAuthToken { get; set; } public string OAuthAccessToken { get; set; } - public virtual IList UserClaims - { - get { return new List(0); } - } + public virtual IList UserClaims => new List(0); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationService.cs b/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationService.cs index aad91d8ff2..1cbca5a9f4 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthenticationService.cs @@ -17,34 +17,34 @@ public partial class OpenAuthenticationService : IOpenAuthenticationService private readonly IPluginFinder _pluginFinder; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; private readonly IRepository _externalAuthenticationRecordRepository; - private readonly ISettingService _settingService; - private readonly IProviderManager _providerManager; + private readonly ISettingService _settingService; + private readonly IProviderManager _providerManager; public OpenAuthenticationService( - IRepository externalAuthenticationRecordRepository, + IRepository externalAuthenticationRecordRepository, IPluginFinder pluginFinder, ExternalAuthenticationSettings externalAuthenticationSettings, ICustomerService customerService, - ISettingService settingService, - IProviderManager providerManager) + ISettingService settingService, + IProviderManager providerManager) { this._externalAuthenticationRecordRepository = externalAuthenticationRecordRepository; this._pluginFinder = pluginFinder; this._externalAuthenticationSettings = externalAuthenticationSettings; this._customerService = customerService; - this._settingService = settingService; - this._providerManager = providerManager; + this._settingService = settingService; + this._providerManager = providerManager; } - /// - /// Load all external authentication methods - /// - /// Load records allows only in specified store; pass 0 to load all records - /// External authentication methods - public virtual IEnumerable> LoadAllExternalAuthenticationMethods(int storeId = 0) - { - return _providerManager.GetAllProviders(storeId); - } + /// + /// Load all external authentication methods + /// + /// Load records allows only in specified store; pass 0 to load all records + /// External authentication methods + public virtual IEnumerable> LoadAllExternalAuthenticationMethods(int storeId = 0) + { + return _providerManager.GetAllProviders(storeId); + } /// /// Load active external authentication methods @@ -53,11 +53,11 @@ public virtual IEnumerable> LoadAllExter /// Payment methods public virtual IEnumerable> LoadActiveExternalAuthenticationMethods(int storeId = 0) { - var allMethods = LoadAllExternalAuthenticationMethods(storeId); - var activeMethods = allMethods - .Where(p => _externalAuthenticationSettings.ActiveAuthenticationMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)); + var allMethods = LoadAllExternalAuthenticationMethods(storeId); + var activeMethods = allMethods + .Where(p => _externalAuthenticationSettings.ActiveAuthenticationMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)); - return activeMethods; + return activeMethods; } /// @@ -67,7 +67,7 @@ public virtual IEnumerable> LoadActiveEx /// Found external authentication method public virtual Provider LoadExternalAuthenticationMethodBySystemName(string systemName, int storeId = 0) { - return _providerManager.GetProvider(systemName, storeId); + return _providerManager.GetProvider(systemName, storeId); } @@ -83,11 +83,11 @@ public virtual void AssociateExternalAccountWithUser(Customer customer, OpenAuth if (parameters.UserClaims != null) foreach (var userClaim in parameters.UserClaims .Where(x => x.Contact != null && !String.IsNullOrEmpty(x.Contact.Email))) - { - //found - email = userClaim.Contact.Email; - break; - } + { + //found + email = userClaim.Contact.Email; + break; + } var externalAuthenticationRecord = new ExternalAuthenticationRecord() { diff --git a/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthentificationExtentions.cs b/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthentificationExtentions.cs index 1db598a637..41295dfd59 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthentificationExtentions.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/OpenAuthentificationExtentions.cs @@ -7,7 +7,7 @@ namespace SmartStore.Services.Authentication.External { public static class OpenAuthenticationExtentions { - public static bool IsMethodActive(this Provider method, ExternalAuthenticationSettings settings) + public static bool IsMethodActive(this Provider method, ExternalAuthenticationSettings settings) { if (method == null) throw new ArgumentNullException("method"); @@ -18,7 +18,7 @@ public static bool IsMethodActive(this Provider m if (settings.ActiveAuthenticationMethodSystemNames == null) return false; - return settings.ActiveAuthenticationMethodSystemNames.Contains(method.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); + return settings.ActiveAuthenticationMethodSystemNames.Contains(method.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); } } } diff --git a/src/Libraries/SmartStore.Services/Authentication/External/UserClaims.cs b/src/Libraries/SmartStore.Services/Authentication/External/UserClaims.cs index 5e2ab5f934..c080e859c0 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/UserClaims.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/UserClaims.cs @@ -142,6 +142,6 @@ public partial class BirthDateClaims public DateTime? WholeBirthDate { get; set; } public int Year { get; set; } public string Raw { get; set; } - public DateTime GeneratedBirthDate { get { return new DateTime(Year, Month, DayOfMonth); } } + public DateTime GeneratedBirthDate => new DateTime(Year, Month, DayOfMonth); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs index ca39006dfe..626d002326 100644 --- a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Web; using System.Web.Security; using SmartStore.Core; @@ -12,43 +14,50 @@ public partial class FormsAuthenticationService : IAuthenticationService private readonly HttpContextBase _httpContext; private readonly ICustomerService _customerService; private readonly CustomerSettings _customerSettings; + private readonly PrivacySettings _privacySettings; private readonly TimeSpan _expirationTimeSpan; private Customer _cachedCustomer; - public FormsAuthenticationService(HttpContextBase httpContext, ICustomerService customerService, CustomerSettings customerSettings) + public FormsAuthenticationService( + HttpContextBase httpContext, + ICustomerService customerService, + CustomerSettings customerSettings, + PrivacySettings privacySettings) { _httpContext = httpContext; _customerService = customerService; _customerSettings = customerSettings; + _privacySettings = privacySettings; _expirationTimeSpan = FormsAuthentication.Timeout; } public virtual void SignIn(Customer customer, bool createPersistentCookie) { var now = DateTime.UtcNow.ToLocalTime(); - var name = _customerSettings.CustomerLoginType != CustomerLoginType.Email ? customer.Username : customer.Email; + var name = _customerSettings.CustomerLoginType != CustomerLoginType.Email ? customer.Username : customer.Email; - var ticket = new FormsAuthenticationTicket( + var ticket = new FormsAuthenticationTicket( 1 /*version*/, - name, + name, now, now.Add(_expirationTimeSpan), createPersistentCookie, - name, + name, FormsAuthentication.FormsCookiePath); var encryptedTicket = FormsAuthentication.Encrypt(ticket); - var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) - { - HttpOnly = true, - Secure = FormsAuthentication.RequireSSL, - Path = FormsAuthentication.FormsCookiePath - }; + var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) + { + HttpOnly = true, + Path = FormsAuthentication.FormsCookiePath, + Secure = FormsAuthentication.RequireSSL, + SameSite = FormsAuthentication.RequireSSL ? (SameSiteMode)_privacySettings.SameSiteMode : SameSiteMode.Lax + }; - if (ticket.IsPersistent) + if (ticket.IsPersistent) { cookie.Expires = ticket.Expiration; } @@ -70,13 +79,13 @@ public virtual void SignOut() public virtual Customer GetAuthenticatedCustomer() { - if (_cachedCustomer != null) - return _cachedCustomer; + if (_cachedCustomer != null) + return _cachedCustomer; - if (_httpContext?.Request == null || !_httpContext.Request.IsAuthenticated) - return null; + if (_httpContext?.Request == null || !_httpContext.Request.IsAuthenticated) + return null; - Customer customer = null; + Customer customer = null; if (_httpContext.User.Identity is FormsIdentity formsIdent) { @@ -88,38 +97,50 @@ public virtual Customer GetAuthenticatedCustomer() } if (customer != null && customer.Active && !customer.Deleted && customer.IsRegistered()) - { - _cachedCustomer = customer; - } + { + _cachedCustomer = customer; + } - return _cachedCustomer; - } + return _cachedCustomer; + } public virtual Customer GetAuthenticatedCustomerFromTicket(FormsAuthenticationTicket ticket) { - Guard.NotNull(ticket, nameof(ticket)); + Guard.NotNull(ticket, nameof(ticket)); var usernameOrEmail = ticket.UserData; - if (string.IsNullOrWhiteSpace(usernameOrEmail)) + if (!usernameOrEmail.HasValue()) return null; - Customer customer = null; + List> customerResolvers = new List>(2); if (_customerSettings.CustomerLoginType == CustomerLoginType.Email) { - customer = _customerService.GetCustomerByEmail(usernameOrEmail); + customerResolvers.Add(_customerService.GetCustomerByEmail); } else if (_customerSettings.CustomerLoginType == CustomerLoginType.Username) { - customer = _customerService.GetCustomerByUsername(usernameOrEmail); + customerResolvers.Add(_customerService.GetCustomerByUsername); } else { - customer = _customerService.GetCustomerByEmail(usernameOrEmail) ?? _customerService.GetCustomerByUsername(usernameOrEmail); + customerResolvers.Add(_customerService.GetCustomerByUsername); + var mayBeEmail = usernameOrEmail.IndexOf('@') > -1; + if (mayBeEmail) + { + customerResolvers.Insert(0, _customerService.GetCustomerByEmail); + } + else + { + customerResolvers.Add(_customerService.GetCustomerByEmail); + } } - return customer; + return customerResolvers + .Select(x => x.Invoke(usernameOrEmail)) + .Where(x => x != null) + .FirstOrDefault(); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/IAuthenticationService.cs b/src/Libraries/SmartStore.Services/Authentication/IAuthenticationService.cs index 16248f3ac8..6ebbb2baf7 100644 --- a/src/Libraries/SmartStore.Services/Authentication/IAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/IAuthenticationService.cs @@ -5,7 +5,7 @@ namespace SmartStore.Services.Authentication /// /// Authentication service interface /// - public partial interface IAuthenticationService + public partial interface IAuthenticationService { void SignIn(Customer customer, bool createPersistentCookie); void SignOut(); diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogExtensions.cs b/src/Libraries/SmartStore.Services/Blogs/BlogExtensions.cs index 172050bd20..1c54dec053 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogExtensions.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogExtensions.cs @@ -20,7 +20,7 @@ public static class BlogExtensions public static IList GetPostsByDate(this IList source, DateTime dateFrom, DateTime dateTo) { - var list = source.ToList().FindAll(delegate(BlogPost p) + var list = source.ToList().FindAll(delegate (BlogPost p) { return (dateFrom.Date <= p.CreatedOnUtc && p.CreatedOnUtc.Date <= dateTo); }); diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs index 7ef5c96706..32e138fe47 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs @@ -5,15 +5,15 @@ namespace SmartStore.Services.Blogs { - public static class BlogMessageFactoryExtensions - { - /// - /// Sends a blog comment notification message to a store owner - /// - public static CreateMessageResult SendBlogCommentNotificationMessage(this IMessageFactory factory, BlogComment blogComment, int languageId = 0) - { - Guard.NotNull(blogComment, nameof(blogComment)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.BlogCommentStoreOwner, languageId, customer: blogComment.Customer), true, blogComment); - } - } + public static class BlogMessageFactoryExtensions + { + /// + /// Sends a blog comment notification message to a store owner + /// + public static CreateMessageResult SendBlogCommentNotificationMessage(this IMessageFactory factory, BlogComment blogComment, int languageId = 0) + { + Guard.NotNull(blogComment, nameof(blogComment)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.BlogCommentStoreOwner, languageId, customer: blogComment.Customer), true, blogComment); + } + } } diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs index e6a1c32cf6..1dc0977884 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs @@ -4,52 +4,105 @@ using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Blogs; -using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Seo; -using SmartStore.Utilities; +using SmartStore.Core.Domain.Stores; using SmartStore.Services.Seo; +using SmartStore.Utilities; namespace SmartStore.Services.Blogs { public partial class BlogService : IBlogService, IXmlSitemapPublisher { private readonly IRepository _blogPostRepository; - private readonly IRepository _storeMappingRepository; + private readonly IRepository _storeMappingRepository; private readonly SeoSettings _seoSettings; - public BlogService(IRepository blogPostRepository, - IRepository storeMappingRepository, + public BlogService( + IRepository blogPostRepository, + IRepository storeMappingRepository, SeoSettings seoSettings) { _blogPostRepository = blogPostRepository; - _storeMappingRepository = storeMappingRepository; + _storeMappingRepository = storeMappingRepository; _seoSettings = seoSettings; + } + + public DbQuerySettings QuerySettings { get; set; } = DbQuerySettings.Default; - this.QuerySettings = DbQuerySettings.Default; + public virtual void InsertBlogPost(BlogPost blogPost) + { + Guard.NotNull(blogPost, nameof(blogPost)); + + _blogPostRepository.Insert(blogPost); } - public DbQuerySettings QuerySettings { get; set; } + public virtual void UpdateBlogPost(BlogPost blogPost) + { + Guard.NotNull(blogPost, nameof(blogPost)); + + _blogPostRepository.Update(blogPost); + } public virtual void DeleteBlogPost(BlogPost blogPost) { - if (blogPost == null) - throw new ArgumentNullException("blogPost"); + if (blogPost != null) + { + _blogPostRepository.Delete(blogPost); + } + } - _blogPostRepository.Delete(blogPost); + public virtual void UpdateCommentTotals(BlogPost blogPost) + { + Guard.NotNull(blogPost, nameof(blogPost)); + + var approvedCommentCount = 0; + var notApprovedCommentCount = 0; + var blogComments = blogPost.BlogComments; + + foreach (var bc in blogComments) + { + if (bc.IsApproved) + { + approvedCommentCount++; + } + else + { + notApprovedCommentCount++; + } + } + + blogPost.ApprovedCommentCount = approvedCommentCount; + blogPost.NotApprovedCommentCount = notApprovedCommentCount; + + UpdateBlogPost(blogPost); } public virtual BlogPost GetBlogPostById(int blogPostId) { if (blogPostId == 0) + { return null; + } - return _blogPostRepository.GetById(blogPostId); + return _blogPostRepository.GetById(blogPostId); } - public virtual IPagedList GetAllBlogPosts(int storeId, int languageId, - DateTime? dateFrom, DateTime? dateTo, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null) + public virtual IPagedList GetAllBlogPosts( + int storeId, + DateTime? dateFrom, + DateTime? dateTo, + int pageIndex, + int pageSize, + int languageId = 0, + bool showHidden = false, + DateTime? maxAge = null, + string title = "", + string intro = "", + string body = "", + string tag = "", + bool untracked = true) { - var query = _blogPostRepository.Table; + var query = untracked ? _blogPostRepository.TableUntracked : _blogPostRepository.Table; if (dateFrom.HasValue) query = query.Where(b => dateFrom.Value <= b.CreatedOnUtc); @@ -57,11 +110,25 @@ public virtual IPagedList GetAllBlogPosts(int storeId, int languageId, if (dateTo.HasValue) query = query.Where(b => dateTo.Value >= b.CreatedOnUtc); - if (languageId > 0) - query = query.Where(b => languageId == b.LanguageId); + if (maxAge.HasValue) + query = query.Where(b => b.CreatedOnUtc >= maxAge.Value); + + if (title.HasValue()) + query = query.Where(b => b.Title.Contains(title)); - if (maxAge.HasValue) - query = query.Where(b => b.CreatedOnUtc >= maxAge.Value); + if (intro.HasValue()) + query = query.Where(b => b.Intro.Contains(intro)); + + if (body.HasValue()) + query = query.Where(b => b.Body.Contains(body)); + + if (tag.HasValue()) + query = query.Where(b => b.Tags.Contains(tag)); + + if (languageId != 0) + { + query = query.Where(b => !b.LanguageId.HasValue || b.LanguageId == languageId); + } if (!showHidden) { @@ -71,44 +138,44 @@ public virtual IPagedList GetAllBlogPosts(int storeId, int languageId, query = query.Where(b => b.IsPublished); } - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - //Store mapping - query = from bp in query - join sm in _storeMappingRepository.Table - on new { c1 = bp.Id, c2 = "BlogPost" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into bp_sm - from sm in bp_sm.DefaultIfEmpty() - where !bp.LimitedToStores || storeId == sm.StoreId - select bp; - - //only distinct blog posts (group by ID) - query = from bp in query - group bp by bp.Id into bpGroup - orderby bpGroup.Key - select bpGroup.FirstOrDefault(); - } + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + // Store mapping. + query = from bp in query + join sm in _storeMappingRepository.Table + on new { c1 = bp.Id, c2 = "BlogPost" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into bp_sm + from sm in bp_sm.DefaultIfEmpty() + where !bp.LimitedToStores || storeId == sm.StoreId + select bp; + + // Only distinct blog posts (group by ID). + query = from bp in query + group bp by bp.Id into bpGroup + orderby bpGroup.Key + select bpGroup.FirstOrDefault(); + } query = query.OrderByDescending(b => b.CreatedOnUtc); - + var blogPosts = new PagedList(query, pageIndex, pageSize); return blogPosts; } - public virtual IPagedList GetAllBlogPostsByTag( - int storeId, - int languageId, - string tag, - int pageIndex, - int pageSize, - bool showHidden = false, + public virtual IPagedList GetAllBlogPostsByTag( + int storeId, + string tag, + int pageIndex, + int pageSize, + int languageId = 0, + bool showHidden = false, DateTime? maxAge = null) { tag = tag.Trim(); - //we laod all records and only then filter them by tag - var blogPostsAll = GetAllBlogPosts(storeId, languageId, null, null, 0, int.MaxValue, showHidden, maxAge); + // We load all records and only then filter them by tag. + var blogPostsAll = GetAllBlogPosts(storeId, null, null, 0, int.MaxValue, languageId, showHidden, maxAge); var taggedBlogPosts = new List(); - + foreach (var blogPost in blogPostsAll) { var tags = blogPost.ParseTags().Select(x => SeoHelper.GetSeName(x, @@ -121,16 +188,15 @@ public virtual IPagedList GetAllBlogPostsByTag( taggedBlogPosts.Add(blogPost); } - //server-side paging var result = new PagedList(taggedBlogPosts, pageIndex, pageSize); return result; } - public virtual IList GetAllBlogPostTags(int storeId, int languageId, bool showHidden = false) + public virtual IList GetAllBlogPostTags(int storeId, int languageId = 0, bool showHidden = false) { var blogPostTags = new List(); - var blogPosts = GetAllBlogPosts(storeId, languageId, null, null, 0, int.MaxValue, showHidden); + var blogPosts = GetAllBlogPosts(storeId, null, null, 0, int.MaxValue, languageId, showHidden); foreach (var blogPost in blogPosts) { var tags = blogPost.ParseTags(); @@ -139,7 +205,7 @@ public virtual IList GetAllBlogPostTags(int storeId, int languageId var foundBlogPostTag = blogPostTags.Find(bpt => bpt.Name.Equals(tag, StringComparison.InvariantCultureIgnoreCase)); if (foundBlogPostTag == null) { - foundBlogPostTag = new BlogPostTag() + foundBlogPostTag = new BlogPostTag { Name = tag, BlogPostCount = 1 @@ -147,58 +213,23 @@ public virtual IList GetAllBlogPostTags(int storeId, int languageId blogPostTags.Add(foundBlogPostTag); } else + { foundBlogPostTag.BlogPostCount++; + } } } return blogPostTags; } - public virtual void InsertBlogPost(BlogPost blogPost) - { - if (blogPost == null) - throw new ArgumentNullException("blogPost"); - - _blogPostRepository.Insert(blogPost); - } - - public virtual void UpdateBlogPost(BlogPost blogPost) - { - if (blogPost == null) - throw new ArgumentNullException("blogPost"); - - _blogPostRepository.Update(blogPost); - } - - public virtual void UpdateCommentTotals(BlogPost blogPost) - { - if (blogPost == null) - throw new ArgumentNullException("blogPost"); - - int approvedCommentCount = 0; - int notApprovedCommentCount = 0; - var blogComments = blogPost.BlogComments; - foreach (var bc in blogComments) - { - if (bc.IsApproved) - approvedCommentCount++; - else - notApprovedCommentCount++; - } - - blogPost.ApprovedCommentCount = approvedCommentCount; - blogPost.NotApprovedCommentCount = notApprovedCommentCount; - UpdateBlogPost(blogPost); - } - #region XML Sitemap public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) { if (!context.LoadSetting().XmlSitemapIncludesBlog || !context.LoadSetting().Enabled) return null; - - var query = GetAllBlogPosts(context.RequestStoreId, 0, null, null, 0, int.MaxValue).SourceQuery; + + var query = GetAllBlogPosts(context.RequestStoreId, null, null, 0, int.MaxValue).SourceQuery; return new BlogPostXmlSitemapResult { Query = query }; } @@ -214,8 +245,9 @@ public override int GetTotalCount() public override IEnumerable Enlist() { - var topics = Query.Select(x => new { x.Id, x.CreatedOnUtc, x.LanguageId }).ToList(); - foreach (var x in topics) + var blogPosts = Query.Select(x => new { x.Id, x.CreatedOnUtc, x.LanguageId }).ToList(); + + foreach (var x in blogPosts) { yield return new NamedEntity { EntityName = "BlogPost", Id = x.Id, LastMod = x.CreatedOnUtc, LanguageId = x.LanguageId }; } diff --git a/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs b/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs index eeb1d8e890..9e483667d7 100644 --- a/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs @@ -6,76 +6,99 @@ namespace SmartStore.Services.Blogs { /// - /// Blog service interface + /// Blog service interface. /// public partial interface IBlogService { /// - /// Deletes a blog post + /// Inserts an blog post. /// - /// Blog post - void DeleteBlogPost(BlogPost blogPost); + /// Blog post. + void InsertBlogPost(BlogPost blogPost); /// - /// Gets a blog post + /// Updates the blog post. /// - /// Blog post identifier - /// Blog post - BlogPost GetBlogPostById(int blogPostId); + /// Blog post. + void UpdateBlogPost(BlogPost blogPost); /// - /// Gets all blog posts + /// Deletes a blog post. /// - /// The store identifier; pass 0 to load all records - /// Language identifier; 0 if you want to get all records - /// Filter by created date; null if you want to get all records - /// Filter by created date; null if you want to get all records - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// The maximum age of returned blog posts - /// Blog posts - IPagedList GetAllBlogPosts(int storeId, int languageId, - DateTime? dateFrom, DateTime? dateTo, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null); + /// Blog post. + void DeleteBlogPost(BlogPost blogPost); /// - /// Gets all blog posts + /// Update blog post comment totals. /// - /// The store identifier; pass 0 to load all records - /// Language identifier. 0 if you want to get all news - /// Tag - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Blog posts - IPagedList GetAllBlogPostsByTag(int storeId, int languageId, string tag, - int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null); + /// Blog post. + void UpdateCommentTotals(BlogPost blogPost); /// - /// Gets all blog post tags + /// Gets a blog post. /// - /// The store identifier; pass 0 to load all records - /// Language identifier. 0 if you want to get all news - /// A value indicating whether to show hidden records - /// Blog post tags - IList GetAllBlogPostTags(int storeId, int languageId, bool showHidden = false); + /// Blog post identifier. + /// Blog post. + BlogPost GetBlogPostById(int blogPostId); /// - /// Inserts an blog post + /// Gets all blog posts. /// - /// Blog post - void InsertBlogPost(BlogPost blogPost); + /// The store identifier; pass 0 to load all records. + /// Filter by created date; null if you want to get all records. + /// Filter by created date; null if you want to get all records. + /// Page index. + /// Page size. + /// Filter by a language identifier. 0 to get all blog posts. + /// A value indicating whether to show hidden records. + /// The maximum age of returned blog posts. + /// Search for a term or phrase in the title. + /// Search for a term or phrase in the intro. + /// Search for a term or phrase in the full description. + /// Search for a term or phrase in the tags field. + /// Indicates whether to load entities tracked or untracked. + /// Blog posts. + IPagedList GetAllBlogPosts( + int storeId, + DateTime? dateFrom, + DateTime? dateTo, + int pageIndex, + int pageSize, + int languageId = 0, + bool showHidden = false, + DateTime? maxAge = null, + string title = "", + string intro = "", + string body = "", + string tag = "", + bool untracked = true); /// - /// Updates the blog post + /// Gets all blog posts. /// - /// Blog post - void UpdateBlogPost(BlogPost blogPost); + /// The store identifier; pass 0 to load all records. + /// Tag. + /// Page index. + /// Page size. + /// Filter by a language identifier. 0 to get all blog posts. + /// A value indicating whether to show hidden records. + /// Blog posts. + IPagedList GetAllBlogPostsByTag( + int storeId, + string tag, + int pageIndex, + int pageSize, + int languageId = 0, + bool showHidden = false, + DateTime? maxAge = null); /// - /// Update blog post comment totals + /// Gets all blog post tags. /// - /// Blog post - void UpdateCommentTotals(BlogPost blogPost); + /// The store identifier; pass 0 to load all records. + /// Filter by a language identifier. 0 to get all blog posts. + /// A value indicating whether to show hidden records. + /// Blog post tags. + IList GetAllBlogPostTags(int storeId, int languageId = 0, bool showHidden = false); } } diff --git a/src/Libraries/SmartStore.Services/Caching/ClearCacheTask.cs b/src/Libraries/SmartStore.Services/Caching/ClearCacheTask.cs index 2ebfdd303e..cbb5717df4 100644 --- a/src/Libraries/SmartStore.Services/Caching/ClearCacheTask.cs +++ b/src/Libraries/SmartStore.Services/Caching/ClearCacheTask.cs @@ -10,19 +10,19 @@ namespace SmartStore.Services.Caching /// public partial class ClearCacheTask : ITask { - private readonly ICacheManager _cacheManager; - - public ClearCacheTask(ICacheManager cacheManager) - { - _cacheManager = cacheManager; - } - - /// + private readonly ICacheManager _cacheManager; + + public ClearCacheTask(ICacheManager cacheManager) + { + _cacheManager = cacheManager; + } + + /// /// Executes a task /// - public void Execute(TaskExecutionContext ctx) + public void Execute(TaskExecutionContext ctx) { - _cacheManager.Clear(); + _cacheManager.Clear(); } } } diff --git a/src/Libraries/SmartStore.Services/Cart/RenderingOrderTotalsEvent.cs b/src/Libraries/SmartStore.Services/Cart/RenderingOrderTotalsEvent.cs new file mode 100644 index 0000000000..907a20c53f --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cart/RenderingOrderTotalsEvent.cs @@ -0,0 +1,15 @@ +using SmartStore.Core.Domain.Customers; + +namespace SmartStore.Services.Cart +{ + public class RenderingOrderTotalsEvent + { + public RenderingOrderTotalsEvent() + { + } + + public Customer Customer { get; set; } + + public int? StoreId { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleContext.cs b/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleContext.cs index a3940fdbaa..529c7e6414 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleContext.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleContext.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Stores; @@ -11,8 +7,18 @@ namespace SmartStore.Services.Cart.Rules { public class CartRuleContext { + private readonly Func _sessionKeyBuilder; + private object _sessionKey; + + internal CartRuleContext(Func sessionKeyBuilder) + { + _sessionKeyBuilder = sessionKeyBuilder; + } + public Customer Customer { get; set; } public Store Store { get; set; } public IWorkContext WorkContext { get; set; } + + public object SessionKey => _sessionKey ?? (_sessionKey = _sessionKeyBuilder?.Invoke() ?? 0); } } diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleProvider.cs b/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleProvider.cs index a8f3738c19..3fc46b2574 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleProvider.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/CartRuleProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Autofac; +using SmartStore.Core.Localization; using SmartStore.Rules; using SmartStore.Rules.Domain; using SmartStore.Services.Cart.Rules.Impl; @@ -33,6 +34,8 @@ public CartRuleProvider(IRuleFactory ruleFactory, IComponentContext componentCon _services = services; } + public Localizer T { get; set; } = NullLocalizer.Instance; + public RuleExpressionGroup CreateExpressionGroup(int ruleSetId) { return _ruleFactory.CreateExpressionGroup(ruleSetId, this) as RuleExpressionGroup; @@ -128,7 +131,7 @@ public bool RuleMatches(RuleExpression[] expressions, LogicalRuleOperator logica group.AddExpressions(expressions); } - var context = new CartRuleContext + var context = new CartRuleContext(() => group.GetHashCode()) { Customer = _services.WorkContext.CurrentCustomer, Store = _services.StoreContext.CurrentStore, @@ -142,15 +145,15 @@ public bool RuleMatches(RuleExpression[] expressions, LogicalRuleOperator logica public IRule GetProcessor(RuleExpression expression) { + var group = expression as RuleExpressionGroup; var descriptor = expression.Descriptor as CartRuleDescriptor; - if (descriptor == null) + + if (group == null && descriptor == null) { - // TODO: ErrHandling - throw new InvalidOperationException(); + throw new InvalidOperationException($"Missing cart rule descriptor for expression {expression.Id} ('{expression.RawValue.EmptyNull()}')."); } IRule instance; - var group = expression as RuleExpressionGroup; if (group == null && descriptor.ProcessorType != typeof(CompositeRule)) { @@ -166,129 +169,143 @@ public IRule GetProcessor(RuleExpression expression) protected override IEnumerable LoadDescriptors() { + var language = _services.WorkContext.WorkingLanguage; + var currencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; + var stores = _services.StoreService.GetAllStores() .Select(x => new RuleValueSelectListOption { Value = x.Id.ToString(), Text = x.Name }) .ToArray(); + var cartItemQuantity = new CartRuleDescriptor + { + Name = "CartItemQuantity", + DisplayName = T("Admin.Rules.FilterDescriptor.CartItemQuantity"), + RuleType = RuleType.String, + ProcessorType = typeof(CartItemQuantityRule), + Operators = new[] { RuleOperator.IsEqualTo } + }; + cartItemQuantity.Metadata["ValueTemplateName"] = "ValueTemplates/CartItemQuantity"; + cartItemQuantity.Metadata["ProductRuleDescriptor"] = new CartRuleDescriptor + { + Name = "CartItemQuantity-Product", + RuleType = RuleType.Int, + ProcessorType = typeof(CartItemQuantityRule), + Operators = new[] { RuleOperator.IsEqualTo }, + SelectList = new RemoteRuleValueSelectList("Product") + }; + var descriptors = new List { new CartRuleDescriptor { Name = "Currency", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Currency"), + DisplayName = T("Admin.Rules.FilterDescriptor.Currency"), RuleType = RuleType.IntArray, ProcessorType = typeof(CurrencyRule), - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Currency") { Multiple = true } }, new CartRuleDescriptor { Name = "Language", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Language"), + DisplayName = T("Admin.Rules.FilterDescriptor.Language"), RuleType = RuleType.IntArray, ProcessorType = typeof(LanguageRule), - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Language") { Multiple = true } }, new CartRuleDescriptor { Name = "Store", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Store"), + DisplayName = T("Admin.Rules.FilterDescriptor.Store"), RuleType = RuleType.IntArray, ProcessorType = typeof(StoreRule), - Constraints = new IRuleConstraint[0], SelectList = new LocalRuleValueSelectList(stores) { Multiple = true } }, new CartRuleDescriptor { Name = "IPCountry", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.IPCountry"), + DisplayName = T("Admin.Rules.FilterDescriptor.IPCountry"), RuleType = RuleType.StringArray, ProcessorType = typeof(IPCountryRule), - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } }, new CartRuleDescriptor { - Name = "UserAgent.IsMobile", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.MobileDevice"), - RuleType = RuleType.Boolean, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(IsMobileRule) + Name = "Weekday", + DisplayName = T("Admin.Rules.FilterDescriptor.Weekday"), + RuleType = RuleType.IntArray, + ProcessorType = typeof(WeekdayRule), + SelectList = new LocalRuleValueSelectList(WeekdayRule.GetDefaultValues(language)) { Multiple = true } }, + new CartRuleDescriptor { - Name = "UserAgent.Device", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.DeviceFamily"), - RuleType = RuleType.StringArray, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(DeviceRule) + Name = "CustomerRole", + DisplayName = T("Admin.Rules.FilterDescriptor.IsInCustomerRole"), + RuleType = RuleType.IntArray, + ProcessorType = typeof(CustomerRoleRule), + SelectList = new RemoteRuleValueSelectList("CustomerRole") { Multiple = true }, + IsComparingSequences = true }, new CartRuleDescriptor { - Name = "UserAgent.OS", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.OperatingSystem"), - RuleType = RuleType.StringArray, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(OSRule) + Name = "CartBillingCountry", + DisplayName = T("Admin.Rules.FilterDescriptor.BillingCountry"), + RuleType = RuleType.IntArray, + ProcessorType = typeof(BillingCountryRule), + SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } }, new CartRuleDescriptor { - Name = "UserAgent.Browser", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.BrowserName"), - RuleType = RuleType.StringArray, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(BrowserRule) + Name = "CartShippingCountry", + DisplayName = T("Admin.Rules.FilterDescriptor.ShippingCountry"), + RuleType = RuleType.IntArray, + ProcessorType = typeof(ShippingCountryRule), + SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } }, new CartRuleDescriptor { - Name = "UserAgent.BrowserMajorVersion", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.BrowserMajorVersion"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(BrowserMajorVersionRule) + Name = "CartShippingMethod", + DisplayName = T("Admin.Rules.FilterDescriptor.ShippingMethod"), + RuleType = RuleType.IntArray, + ProcessorType = typeof(ShippingMethodRule), + SelectList = new RemoteRuleValueSelectList("ShippingMethod") { Multiple = true } }, new CartRuleDescriptor { - Name = "UserAgent.BrowserMinorVersion", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.BrowserMinorVersion"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(BrowserMinorVersionRule) + Name = "CartPaymentMethod", + DisplayName = T("Admin.Rules.FilterDescriptor.PaymentMethod"), + RuleType = RuleType.StringArray, + ProcessorType = typeof(PaymentMethodRule), + SelectList = new RemoteRuleValueSelectList("PaymentMethod") { Multiple = true } }, - new CartRuleDescriptor - { - Name = "CustomerRole", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.IsInCustomerRole"), - RuleType = RuleType.IntArray, - ProcessorType = typeof(CustomerRoleRule), - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("CustomerRole") { Multiple = true }, - IsComparingSequences = true - }, new CartRuleDescriptor { Name = "CartTotal", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.CartTotal"), + DisplayName = T("Admin.Rules.FilterDescriptor.CartTotal"), RuleType = RuleType.Money, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(CartTotalRule) }, new CartRuleDescriptor { Name = "CartSubtotal", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.CartSubtotal"), + DisplayName = T("Admin.Rules.FilterDescriptor.CartSubtotal"), RuleType = RuleType.Money, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(CartSubtotalRule) }, new CartRuleDescriptor + { + Name = "CartProductCount", + DisplayName = T("Admin.Rules.FilterDescriptor.CartProductCount"), + RuleType = RuleType.Int, + ProcessorType = typeof(CartProductCountRule) + }, + cartItemQuantity, + new CartRuleDescriptor { Name = "ProductInCart", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ProductInCart"), + DisplayName = T("Admin.Rules.FilterDescriptor.ProductInCart"), RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(ProductInCartRule), SelectList = new RemoteRuleValueSelectList("Product") { Multiple = true }, IsComparingSequences = true @@ -296,9 +313,8 @@ protected override IEnumerable LoadDescriptors() new CartRuleDescriptor { Name = "ProductFromCategoryInCart", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ProductFromCategoryInCart"), + DisplayName = T("Admin.Rules.FilterDescriptor.ProductFromCategoryInCart"), RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(ProductFromCategoryInCartRule), SelectList = new RemoteRuleValueSelectList("Category") { Multiple = true }, IsComparingSequences = true @@ -306,9 +322,8 @@ protected override IEnumerable LoadDescriptors() new CartRuleDescriptor { Name = "ProductFromManufacturerInCart", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ProductFromManufacturerInCart"), + DisplayName = T("Admin.Rules.FilterDescriptor.ProductFromManufacturerInCart"), RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(ProductFromManufacturerInCartRule), SelectList = new RemoteRuleValueSelectList("Manufacturer") { Multiple = true }, IsComparingSequences = true @@ -316,9 +331,8 @@ protected override IEnumerable LoadDescriptors() new CartRuleDescriptor { Name = "ProductInWishlist", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ProductOnWishlist"), + DisplayName = T("Admin.Rules.FilterDescriptor.ProductOnWishlist"), RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(ProductOnWishlistRule), SelectList = new RemoteRuleValueSelectList("Product") { Multiple = true }, IsComparingSequences = true @@ -326,68 +340,59 @@ protected override IEnumerable LoadDescriptors() new CartRuleDescriptor { Name = "ProductReviewCount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ProductReviewCount"), + DisplayName = T("Admin.Rules.FilterDescriptor.ProductReviewCount"), RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(ProductReviewCountRule) }, new CartRuleDescriptor { Name = "RewardPointsBalance", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.RewardPointsBalance"), + DisplayName = T("Admin.Rules.FilterDescriptor.RewardPointsBalance"), RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(RewardPointsBalanceRule) }, new CartRuleDescriptor { - Name = "CartBillingCountry", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.BillingCountry"), - RuleType = RuleType.IntArray, - ProcessorType = typeof(BillingCountryRule), - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } - }, - new CartRuleDescriptor - { - Name = "CartShippingCountry", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ShippingCountry"), - RuleType = RuleType.IntArray, - ProcessorType = typeof(ShippingCountryRule), - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } - }, - new CartRuleDescriptor - { - Name = "CartShippingMethod", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ShippingMethod"), - RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(ShippingMethodRule), - SelectList = new RemoteRuleValueSelectList("ShippingMethod") { Multiple = true } + Name = "RuleSet", + DisplayName = T("Admin.Rules.FilterDescriptor.RuleSet"), + RuleType = RuleType.Int, + ProcessorType = typeof(RuleSetRule), + Operators = new[] { RuleOperator.IsEqualTo, RuleOperator.IsNotEqualTo }, + SelectList = new RemoteRuleValueSelectList("CartRule"), }, + new CartRuleDescriptor { Name = "CartOrderCount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.OrderCount"), + DisplayName = T("Admin.Rules.FilterDescriptor.OrderCount"), + GroupKey = "Admin.Orders", RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(OrderCountRule) }, new CartRuleDescriptor { Name = "CartSpentAmount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.SpentAmount"), + DisplayName = T("Admin.Rules.FilterDescriptor.SpentAmount"), + GroupKey = "Admin.Orders", RuleType = RuleType.Money, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(SpentAmountRule) }, new CartRuleDescriptor + { + Name = "CartPaidBy", + DisplayName = T("Admin.Rules.FilterDescriptor.PaidBy"), + GroupKey = "Admin.Orders", + RuleType = RuleType.StringArray, + ProcessorType = typeof(PaidByRule), + SelectList = new RemoteRuleValueSelectList("PaymentMethod") { Multiple = true }, + IsComparingSequences = true + }, + new CartRuleDescriptor { Name = "CartPurchasedProduct", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.PurchasedProduct"), + DisplayName = T("Admin.Rules.FilterDescriptor.PurchasedProduct"), + GroupKey = "Admin.Orders", RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(PurchasedProductRule), SelectList = new RemoteRuleValueSelectList("Product") { Multiple = true }, IsComparingSequences = true @@ -395,47 +400,70 @@ protected override IEnumerable LoadDescriptors() new CartRuleDescriptor { Name = "CartPurchasedFromManufacturer", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.PurchasedFromManufacturer"), + DisplayName = T("Admin.Rules.FilterDescriptor.PurchasedFromManufacturer"), + GroupKey = "Admin.Orders", RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], ProcessorType = typeof(PurchasedFromManufacturerRule), SelectList = new RemoteRuleValueSelectList("Manufacturer") { Multiple = true }, IsComparingSequences = true }, + new CartRuleDescriptor { - Name = "CartPaymentMethod", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.PaymentMethod"), + Name = "UserAgent.IsMobile", + DisplayName = T("Admin.Rules.FilterDescriptor.MobileDevice"), + GroupKey = "Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", + RuleType = RuleType.Boolean, + ProcessorType = typeof(IsMobileRule) + }, + new CartRuleDescriptor + { + Name = "UserAgent.Device", + DisplayName = T("Admin.Rules.FilterDescriptor.DeviceFamily"), + GroupKey = "Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", RuleType = RuleType.StringArray, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(PaymentMethodRule), - SelectList = new RemoteRuleValueSelectList("PaymentMethod") { Multiple = true } + ProcessorType = typeof(DeviceRule), + SelectList = new LocalRuleValueSelectList(DeviceRule.GetDefaultValues()) { Multiple = true, Tags = true } }, new CartRuleDescriptor { - Name = "CartPaidBy", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.PaidBy"), + Name = "UserAgent.OS", + DisplayName = T("Admin.Rules.FilterDescriptor.OperatingSystem"), + GroupKey = "Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", RuleType = RuleType.StringArray, - Constraints = new IRuleConstraint[0], - ProcessorType = typeof(PaidByRule), - SelectList = new RemoteRuleValueSelectList("PaymentMethod") { Multiple = true }, - IsComparingSequences = true + ProcessorType = typeof(OSRule), + SelectList = new LocalRuleValueSelectList(OSRule.GetDefaultValues()) { Multiple = true, Tags = true } }, new CartRuleDescriptor { - Name = "RuleSet", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.RuleSet"), + Name = "UserAgent.Browser", + DisplayName = T("Admin.Rules.FilterDescriptor.BrowserName"), + GroupKey = "Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", + RuleType = RuleType.StringArray, + ProcessorType = typeof(BrowserRule), + SelectList = new LocalRuleValueSelectList(BrowserRule.GetDefaultValues()) { Multiple = true, Tags = true } + }, + new CartRuleDescriptor + { + Name = "UserAgent.BrowserMajorVersion", + DisplayName = T("Admin.Rules.FilterDescriptor.BrowserMajorVersion"), + GroupKey = "Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", RuleType = RuleType.Int, - ProcessorType = typeof(RuleSetRule), - Operators = new[] { RuleOperator.IsEqualTo, RuleOperator.IsNotEqualTo }, - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("CartRule"), - } + ProcessorType = typeof(BrowserMajorVersionRule) + }, + new CartRuleDescriptor + { + Name = "UserAgent.BrowserMinorVersion", + DisplayName = T("Admin.Rules.FilterDescriptor.BrowserMinorVersion"), + GroupKey = "Admin.Rules.FilterDescriptor.Group.BrowserUserAgent", + RuleType = RuleType.Int, + ProcessorType = typeof(BrowserMinorVersionRule) + }, }; descriptors .Where(x => x.RuleType == RuleType.Money) - .Each(x => x.Metadata["postfix"] = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode); + .Each(x => x.Metadata["postfix"] = currencyCode); return descriptors; } diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartItemQuantityRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartItemQuantityRule.cs new file mode 100644 index 0000000000..b15f586972 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartItemQuantityRule.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Logging; +using SmartStore.Rules; +using SmartStore.Services.Orders; + +namespace SmartStore.Services.Cart.Rules.Impl +{ + public class CartItemQuantityRule : IRule + { + private readonly IShoppingCartService _shoppingCartService; + + public CartItemQuantityRule(IShoppingCartService shoppingCartService) + { + _shoppingCartService = shoppingCartService; + } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public bool Match(CartRuleContext context, RuleExpression expression) + { + int productId = 0; + int? minQuantity = null; + int? maxQuantity = null; + + try + { + var rawValue = expression.Value as string; + if (rawValue.HasValue()) + { + dynamic json = JObject.Parse(rawValue); + productId = ((string)json.ProductId).ToInt(); + + var str = (string)json.MinQuantity; + if (str.HasValue()) + { + minQuantity = str.ToInt(); + } + + str = (string)json.MaxQuantity; + if (str.HasValue()) + { + maxQuantity = str.ToInt(); + } + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + + if (productId == 0) + { + return false; + } + + var cart = _shoppingCartService.GetCartItems(context.Customer, ShoppingCartType.ShoppingCart, context.Store.Id); + var items = cart.Where(x => x.Item.ProductId == productId); + if (!items.Any()) + { + return false; + } + + var quantity = items.Sum(x => x.Item.Quantity); + if (quantity == 0) + { + return false; + } + + if (minQuantity.HasValue && maxQuantity.HasValue) + { + if (minQuantity == maxQuantity) + { + return quantity == minQuantity; + } + else + { + return quantity >= minQuantity && quantity <= maxQuantity; + } + } + else if (minQuantity.HasValue) + { + return quantity >= minQuantity; + } + else if (maxQuantity.HasValue) + { + return quantity <= maxQuantity; + } + + return false; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartProductCountRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartProductCountRule.cs new file mode 100644 index 0000000000..f2fed96f95 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartProductCountRule.cs @@ -0,0 +1,24 @@ +using SmartStore.Core.Domain.Orders; +using SmartStore.Rules; +using SmartStore.Services.Orders; + +namespace SmartStore.Services.Cart.Rules.Impl +{ + public class CartProductCountRule : IRule + { + private readonly IShoppingCartService _shoppingCartService; + + public CartProductCountRule(IShoppingCartService shoppingCartService) + { + _shoppingCartService = shoppingCartService; + } + + public bool Match(CartRuleContext context, RuleExpression expression) + { + var cart = _shoppingCartService.GetCartItems(context.Customer, ShoppingCartType.ShoppingCart, context.Store.Id); + var productCount = cart.GetTotalProducts(); + + return expression.Operator.Match(productCount, expression.Value); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartSubtotalRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartSubtotalRule.cs index 9f2bacbfe4..96c0c8a728 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartSubtotalRule.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartSubtotalRule.cs @@ -1,7 +1,7 @@ -using System.Threading; -using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Orders; using SmartStore.Rules; using SmartStore.Services.Orders; +using SmartStore.Utilities.Threading; namespace SmartStore.Services.Cart.Rules.Impl { @@ -9,11 +9,8 @@ public class CartSubtotalRule : IRule { private readonly IShoppingCartService _shoppingCartService; private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private int _reentrancyNum = 0; - public CartSubtotalRule( - IShoppingCartService shoppingCartService, - IOrderTotalCalculationService orderTotalCalculationService) + public CartSubtotalRule(IShoppingCartService shoppingCartService, IOrderTotalCalculationService orderTotalCalculationService) { _shoppingCartService = shoppingCartService; _orderTotalCalculationService = orderTotalCalculationService; @@ -21,31 +18,30 @@ public CartSubtotalRule( public bool Match(CartRuleContext context, RuleExpression expression) { - var result = true; + var sessionKey = context.SessionKey; + var lockKey = "rule:cart:cartsubtotalrule:" + sessionKey.ToString(); - // We must prevent the rule from indirectly calling itself. It would cause a stack overflow on cart page - // and wrong discount calculation (due to MergeWithCombination, if the cart contains a product several times). - if (Interlocked.CompareExchange(ref _reentrancyNum, 1, 0) == 0) + if (KeyedLock.IsLockHeld(lockKey)) { - try - { - var cart = _shoppingCartService.GetCartItems(context.Customer, ShoppingCartType.ShoppingCart, context.Store.Id); - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, out _, out _, out var cartSubtotal, out _); - - // Currency values must be rounded, otherwise unexpected results may occur. - var money = new Money(cartSubtotal, context.WorkContext.WorkingCurrency); - cartSubtotal = money.RoundedAmount; - - result = expression.Operator.Match(cartSubtotal, expression.Value); - } - finally - { - _reentrancyNum = 0; - } + //$"locked expression {expression.Id}: {lockKey}".Dump(); + return false; } - return result; + // We must prevent the rule from indirectly calling itself. It would cause a stack overflow on cart page. + using (KeyedLock.Lock(lockKey)) + { + var cart = _shoppingCartService.GetCartItems(context.Customer, ShoppingCartType.ShoppingCart, context.Store.Id); + + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, out _, out _, out var cartSubtotal, out _); + + // Currency values must be rounded, otherwise unexpected results may occur. + var money = new Money(cartSubtotal, context.WorkContext.WorkingCurrency); + cartSubtotal = money.RoundedAmount; + + var result = expression.Operator.Match(cartSubtotal, expression.Value); + //$"unlocked expression {expression.Id}: {lockKey}".Dump(); + return result; + } } } } diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartTotalRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartTotalRule.cs index 1746b0a8be..163535831b 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartTotalRule.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/CartTotalRule.cs @@ -1,7 +1,7 @@ -using System.Threading; -using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Orders; using SmartStore.Rules; using SmartStore.Services.Orders; +using SmartStore.Utilities.Threading; namespace SmartStore.Services.Cart.Rules.Impl { @@ -9,7 +9,6 @@ public class CartTotalRule : IRule { private readonly IShoppingCartService _shoppingCartService; private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private int _reentrancyNum = 0; public CartTotalRule( IShoppingCartService shoppingCartService, @@ -21,31 +20,28 @@ public CartTotalRule( public bool Match(CartRuleContext context, RuleExpression expression) { - var result = true; + var sessionKey = context.SessionKey; + var lockKey = "rule:cart:carttotalrule:" + sessionKey.ToString(); - // We must prevent the rule from indirectly calling itself. It would cause a stack overflow on cart page - // and wrong discount calculation (due to MergeWithCombination, if the cart contains a product several times). - if (Interlocked.CompareExchange(ref _reentrancyNum, 1, 0) == 0) + if (KeyedLock.IsLockHeld(lockKey)) { - try - { - var cart = _shoppingCartService.GetCartItems(context.Customer, ShoppingCartType.ShoppingCart, context.Store.Id); - - var cartTotal = ((decimal?)_orderTotalCalculationService.GetShoppingCartTotal(cart)) ?? decimal.Zero; - - // Currency values must be rounded, otherwise unexpected results may occur. - var money = new Money(cartTotal, context.WorkContext.WorkingCurrency); - cartTotal = money.RoundedAmount; - - result = expression.Operator.Match(cartTotal, expression.Value); - } - finally - { - _reentrancyNum = 0; - } + return false; } - return result; + // We must prevent the rule from indirectly calling itself. It would cause a stack overflow on cart page. + using (KeyedLock.Lock(lockKey)) + { + var cart = _shoppingCartService.GetCartItems(context.Customer, ShoppingCartType.ShoppingCart, context.Store.Id); + + var cartTotal = ((decimal?)_orderTotalCalculationService.GetShoppingCartTotal(cart)) ?? decimal.Zero; + + // Currency values must be rounded, otherwise unexpected results may occur. + var money = new Money(cartTotal, context.WorkContext.WorkingCurrency); + cartTotal = money.RoundedAmount; + + var result = expression.Operator.Match(cartTotal, expression.Value); + return result; + } } } } diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/ProductReviewCountRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/ProductReviewCountRule.cs index 30dbbd2241..cdf8c9b12e 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/ProductReviewCountRule.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/ProductReviewCountRule.cs @@ -1,4 +1,5 @@ -using SmartStore.Core.Domain.Catalog; +using System.Linq; +using SmartStore.Core.Domain.Catalog; using SmartStore.Rules; using SmartStore.Services.Customers; @@ -15,9 +16,10 @@ public ProductReviewCountRule(ICustomerContentService customerContentService) public bool Match(CartRuleContext context, RuleExpression expression) { - var reviews = _customerContentService.GetAllCustomerContent(context.Customer.Id, true); + var query = _customerContentService.GetAllCustomerContent(context.Customer.Id, true).SourceQuery; + var reviewsCount = query.Count(); - return expression.Operator.Match(reviews.Count, expression.Value); + return expression.Operator.Match(reviewsCount, expression.Value); } } } diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/BrowserRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/BrowserRule.cs index 341700cecf..eb29c02821 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/BrowserRule.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/BrowserRule.cs @@ -1,4 +1,6 @@ -using SmartStore.Services.Common; +using System.Linq; +using SmartStore.Rules; +using SmartStore.Services.Common; namespace SmartStore.Services.Cart.Rules.Impl { @@ -11,6 +13,28 @@ public BrowserRule(IUserAgent userAgent) _userAgent = userAgent; } + public static RuleValueSelectListOption[] GetDefaultValues() + { + return new[] + { + "Chrome", + "Chrome Mobile", + "Edge", + "Firefox", + "Firefox Mobile", + "IE", + "IE Mobile", + "Mobile Safari", + "Opera", + "Opera Mobile", + "Opera Mini", + "Safari", + "Samsung Internet" + } + .Select(x => new RuleValueSelectListOption { Value = x, Text = x }) + .ToArray(); + } + protected override string GetValue(CartRuleContext context) { return _userAgent.UserAgent.Family.NullEmpty(); diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/DeviceRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/DeviceRule.cs index 3e75ac32c2..b82f61c452 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/DeviceRule.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/DeviceRule.cs @@ -1,4 +1,6 @@ -using SmartStore.Services.Common; +using System.Linq; +using SmartStore.Rules; +using SmartStore.Services.Common; namespace SmartStore.Services.Cart.Rules.Impl { @@ -11,6 +13,31 @@ public DeviceRule(IUserAgent userAgent) _userAgent = userAgent; } + public static RuleValueSelectListOption[] GetDefaultValues() + { + return new[] + { + "BlackBerry", + "Generic Feature Phone", + "Generic Smartphone", + "Generic Tablet", + "HP TouchPad", + "iPad", + "iPhone", + "iPod", + "Kindle", + "Kindle Fire", + "Lumia", + "Motorola", + "Nokia", + "Palm", + "Spider", + "Other" + } + .Select(x => new RuleValueSelectListOption { Value = x, Text = x }) + .ToArray(); + } + protected override string GetValue(CartRuleContext context) { return _userAgent.Device.Family.NullEmpty(); diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/OSRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/OSRule.cs index 2dafaa538c..5fe72b37ad 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/OSRule.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/UserAgent/OSRule.cs @@ -1,4 +1,6 @@ -using SmartStore.Services.Common; +using System.Linq; +using SmartStore.Rules; +using SmartStore.Services.Common; namespace SmartStore.Services.Cart.Rules.Impl { @@ -11,6 +13,31 @@ public OSRule(IUserAgent userAgent) _userAgent = userAgent; } + public static RuleValueSelectListOption[] GetDefaultValues() + { + return new[] + { + "Android", + "BlackBerry OS", + "BlackBerry Tablet OS", + "Chrome OS", + "Firefox OS", + "iOS", + "Kindle", + "Linux", + "Mac OS X", + "Symbian OS", + "Ubuntu", + "webOS", + "Windows", + "Windows Mobile", + "Windows Phone", + "Windows CE" + } + .Select(x => new RuleValueSelectListOption { Value = x, Text = x }) + .ToArray(); + } + protected override string GetValue(CartRuleContext context) { return _userAgent.OS.Family.NullEmpty(); diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/Impl/WeekdayRule.cs b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/WeekdayRule.cs new file mode 100644 index 0000000000..0cce47603d --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cart/Rules/Impl/WeekdayRule.cs @@ -0,0 +1,36 @@ +using System; +using System.Globalization; +using System.Linq; +using SmartStore.Core.Domain.Localization; +using SmartStore.Rules; + +namespace SmartStore.Services.Cart.Rules.Impl +{ + public class WeekdayRule : ListRuleBase + { + public static RuleValueSelectListOption[] GetDefaultValues(Language language) + { + CultureInfo cultureInfo = null; + + try + { + cultureInfo = CultureInfo.GetCultureInfo(language.LanguageCulture); + } + catch { } + + var dtif = cultureInfo?.DateTimeFormat ?? DateTimeFormatInfo.InvariantInfo; + + var options = Enum.GetValues(typeof(DayOfWeek)) + .Cast() + .Select(x => new RuleValueSelectListOption { Value = ((int)x).ToString(), Text = dtif.GetDayName(x) }) + .ToArray(); + + return options; + } + + protected override int GetValue(CartRuleContext context) + { + return (int)DateTime.Now.DayOfWeek; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Cart/Rules/ListRuleBase.cs b/src/Libraries/SmartStore.Services/Cart/Rules/ListRuleBase.cs index 17f0636689..93c301ee8c 100644 --- a/src/Libraries/SmartStore.Services/Cart/Rules/ListRuleBase.cs +++ b/src/Libraries/SmartStore.Services/Cart/Rules/ListRuleBase.cs @@ -13,9 +13,15 @@ protected ListRuleBase(IEqualityComparer comparer = null) _comparer = comparer; } - protected virtual T GetValue(CartRuleContext context) => default; + protected virtual T GetValue(CartRuleContext context) + { + return default; + } - protected virtual IEnumerable GetValues(CartRuleContext context) => default; + protected virtual IEnumerable GetValues(CartRuleContext context) + { + return default; + } public virtual bool Match(CartRuleContext context, RuleExpression expression) { diff --git a/src/Libraries/SmartStore.Services/Cart/ValidatingCartEvent.cs b/src/Libraries/SmartStore.Services/Cart/ValidatingCartEvent.cs new file mode 100644 index 0000000000..b17f20784b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cart/ValidatingCartEvent.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; + +namespace SmartStore.Services.Cart +{ + public class ValidatingCartEvent + { + public ValidatingCartEvent( + Customer customer, + IList warnings, + IList cart) + { + Customer = customer; + Warnings = warnings; + Cart = cart; + } + + public Customer Customer { get; set; } + + public IList Cart { get; set; } + + public IList Warnings { get; set; } + + public ActionResult Result { get; set; } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Catalog/AdjustInventoryResult.cs b/src/Libraries/SmartStore.Services/Catalog/AdjustInventoryResult.cs index 91867ebe55..aac4efb6ea 100644 --- a/src/Libraries/SmartStore.Services/Catalog/AdjustInventoryResult.cs +++ b/src/Libraries/SmartStore.Services/Catalog/AdjustInventoryResult.cs @@ -1,50 +1,32 @@  namespace SmartStore.Services.Catalog { - public class AdjustInventoryResult - { - private int? _stockQuantityOld; - private int? _stockQuantityNew; + public class AdjustInventoryResult + { + private int? _stockQuantityOld; + private int? _stockQuantityNew; - /// - /// The stock quantity before adjustment - /// - public int StockQuantityOld - { - get - { - return _stockQuantityOld ?? 0; - } - set - { - _stockQuantityOld = value; - } - } + /// + /// The stock quantity before adjustment + /// + public int StockQuantityOld + { + get => _stockQuantityOld ?? 0; + set => _stockQuantityOld = value; + } - /// - /// The stock quantity after adjustment - /// - public int StockQuantityNew - { - get - { - return _stockQuantityNew ?? 0; - } - set - { - _stockQuantityNew = value; - } - } + /// + /// The stock quantity after adjustment + /// + public int StockQuantityNew + { + get => _stockQuantityNew ?? 0; + set => _stockQuantityNew = value; + } - /// - /// Determines whether the adjustment resulted in a clear, unique stock quantity update. For instance false for bundle products. - /// - public bool HasClearStockQuantityResult - { - get - { - return _stockQuantityOld.HasValue && _stockQuantityNew.HasValue; - } - } - } + /// + /// Determines whether the adjustment resulted in a clear, unique stock quantity update. For instance false for bundle products. + /// + public bool HasClearStockQuantityResult => _stockQuantityOld.HasValue && _stockQuantityNew.HasValue; + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs b/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs index 38a945b869..53feaaacaa 100644 --- a/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs @@ -19,22 +19,22 @@ public partial class BackInStockSubscriptionService : IBackInStockSubscriptionSe private readonly IRepository _backInStockSubscriptionRepository; private readonly IMessageFactory _messageFactory; - private readonly IWorkContext _workContext; + private readonly IWorkContext _workContext; private readonly IEventPublisher _eventPublisher; #endregion - + #region Ctor public BackInStockSubscriptionService( - IRepository backInStockSubscriptionRepository, - IMessageFactory messageFactory, - IWorkContext workContext, + IRepository backInStockSubscriptionRepository, + IMessageFactory messageFactory, + IWorkContext workContext, IEventPublisher eventPublisher) { _backInStockSubscriptionRepository = backInStockSubscriptionRepository; _messageFactory = messageFactory; - _workContext = workContext; + _workContext = workContext; _eventPublisher = eventPublisher; } @@ -67,9 +67,9 @@ public virtual IPagedList GetAllSubscriptionsByCustomer var query = _backInStockSubscriptionRepository.Table; //customer query = query.Where(biss => biss.CustomerId == customerId); - //store - if (storeId > 0) - query = query.Where(biss => biss.StoreId == storeId); + //store + if (storeId > 0) + query = query.Where(biss => biss.StoreId == storeId); //product query = query.Where(biss => !biss.Product.Deleted); query = query.OrderByDescending(biss => biss.CreatedOnUtc); @@ -86,14 +86,14 @@ public virtual IPagedList GetAllSubscriptionsByCustomer /// Page size /// Subscriptions public virtual IPagedList GetAllSubscriptionsByProductId(int productId, - int storeId, int pageIndex, int pageSize) + int storeId, int pageIndex, int pageSize) { var query = _backInStockSubscriptionRepository.Table; //product query = query.Where(biss => biss.ProductId == productId); - //store - if (storeId > 0) - query = query.Where(biss => biss.StoreId == storeId); + //store + if (storeId > 0) + query = query.Where(biss => biss.StoreId == storeId); //customer query = query.Where(biss => !biss.Customer.Deleted); query = query.OrderByDescending(biss => biss.CreatedOnUtc); @@ -109,11 +109,11 @@ public virtual IPagedList GetAllSubscriptionsByProductI /// Subscriptions public virtual BackInStockSubscription FindSubscription(int customerId, int productId, int storeId) { - var query = - from biss in _backInStockSubscriptionRepository.Table - orderby biss.CreatedOnUtc descending - where biss.CustomerId == customerId && biss.ProductId == productId && biss.StoreId == storeId - select biss; + var query = + from biss in _backInStockSubscriptionRepository.Table + orderby biss.CreatedOnUtc descending + where biss.CustomerId == customerId && biss.ProductId == productId && biss.StoreId == storeId + select biss; var subscription = query.FirstOrDefault(); return subscription; @@ -164,17 +164,17 @@ public virtual void UpdateSubscription(BackInStockSubscription subscription) /// Number of sent email public virtual int SendNotificationsToSubscribers(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + if (product == null) + throw new ArgumentNullException("product"); int result = 0; - var subscriptions = GetAllSubscriptionsByProductId(product.Id, 0, 0, int.MaxValue); + var subscriptions = GetAllSubscriptionsByProductId(product.Id, 0, 0, int.MaxValue); foreach (var subscription in subscriptions) { // Ensure that customer is registered (simple and fast way) - if (subscription.Customer.Email.IsEmail()) + if (subscription.Customer.Email.IsEmail()) { - _messageFactory.SendBackInStockNotification(subscription); + _messageFactory.SendBackInStockNotification(subscription); result++; } } @@ -182,7 +182,7 @@ public virtual int SendNotificationsToSubscribers(Product product) DeleteSubscription(subscriptions[i]); return result; } - + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs index cb0fb7fb1d..9fa9ee35e3 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs @@ -7,73 +7,77 @@ namespace SmartStore.Services.Catalog { - public static class CatalogMessageFactoryExtensions - { - /// - /// Sends "email a friend" message - /// - public static CreateMessageResult SendShareProductMessage(this IMessageFactory factory, Customer customer, Product product, - string fromEmail, string toEmail, string personalMessage, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(product, nameof(product)); + public static class CatalogMessageFactoryExtensions + { + /// + /// Sends "email a friend" message + /// + public static CreateMessageResult SendShareProductMessage(this IMessageFactory factory, Customer customer, Product product, + string fromEmail, string toEmail, string personalMessage, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(product, nameof(product)); - var model = new NamedModelPart("Message") - { - ["Body"] = personalMessage.NullEmpty(), - ["From"] = fromEmail.NullEmpty(), - ["To"] = toEmail.NullEmpty() - }; + var model = new NamedModelPart("Message") + { + ["Body"] = personalMessage.NullEmpty(), + ["From"] = fromEmail.NullEmpty(), + ["To"] = toEmail.NullEmpty() + }; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShareProduct, languageId, customer: customer), true, product, model); - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShareProduct, languageId, customer: customer), true, product, model); + } public static CreateMessageResult SendProductQuestionMessage(this IMessageFactory factory, Customer customer, Product product, - string senderEmail, string senderName, string senderPhone, string question, int languageId = 0) + string senderEmail, string senderName, string senderPhone, string question, + string attributes, string productUrl, bool isQuoteRequest, int languageId = 0) { Guard.NotNull(customer, nameof(customer)); Guard.NotNull(product, nameof(product)); var model = new NamedModelPart("Message") { + ["ProductUrl"] = productUrl.NullEmpty(), + ["IsQuoteRequest"] = isQuoteRequest, + ["ProductAttributes"] = attributes.NullEmpty(), ["Message"] = question.NullEmpty(), ["SenderEmail"] = senderEmail.NullEmpty(), ["SenderName"] = senderName.NullEmpty(), ["SenderPhone"] = senderPhone.NullEmpty() }; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ProductQuestion, languageId, customer: customer), true, product, model); - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ProductQuestion, languageId, customer: customer), true, product, model); + } - /// - /// Sends a product review notification message to a store owner - /// - public static CreateMessageResult SendProductReviewNotificationMessage(this IMessageFactory factory, ProductReview productReview, int languageId = 0) - { - Guard.NotNull(productReview, nameof(productReview)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ProductReviewStoreOwner, languageId, customer: productReview.Customer), true, productReview, productReview.Product); - } + /// + /// Sends a product review notification message to a store owner + /// + public static CreateMessageResult SendProductReviewNotificationMessage(this IMessageFactory factory, ProductReview productReview, int languageId = 0) + { + Guard.NotNull(productReview, nameof(productReview)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ProductReviewStoreOwner, languageId, customer: productReview.Customer), true, productReview, productReview.Product); + } - /// - /// Sends a "quantity below" notification to a store owner - /// - public static CreateMessageResult SendQuantityBelowStoreOwnerNotification(this IMessageFactory factory, Product product, int languageId = 0) - { - Guard.NotNull(product, nameof(product)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.QuantityBelowStoreOwner, languageId), true, product); - } + /// + /// Sends a "quantity below" notification to a store owner + /// + public static CreateMessageResult SendQuantityBelowStoreOwnerNotification(this IMessageFactory factory, Product product, int languageId = 0) + { + Guard.NotNull(product, nameof(product)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.QuantityBelowStoreOwner, languageId), true, product); + } - /// - /// Sends a 'Back in stock' notification message to a customer - /// - public static CreateMessageResult SendBackInStockNotification(this IMessageFactory factory, BackInStockSubscription subscription) - { - Guard.NotNull(subscription, nameof(subscription)); + /// + /// Sends a 'Back in stock' notification message to a customer + /// + public static CreateMessageResult SendBackInStockNotification(this IMessageFactory factory, BackInStockSubscription subscription) + { + Guard.NotNull(subscription, nameof(subscription)); - var customer = subscription.Customer; + var customer = subscription.Customer; var languageId = customer.GetAttribute(SystemCustomerAttributeNames.LanguageId); return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.BackInStockCustomer, languageId, subscription.StoreId, customer), true, subscription.Product); - } - } + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs index d7977f972b..b62bb38a57 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs @@ -132,70 +132,70 @@ private IEnumerable SortInternal(int parentId, ILookup lookup) /// A ProductCategory that has the specified values; otherwise null public static ProductCategory FindProductCategory(this IList source, int productId, int categoryId) { - foreach (var productCategory in source) - { - if (productCategory.ProductId == productId && productCategory.CategoryId == categoryId) - return productCategory; - } + foreach (var productCategory in source) + { + if (productCategory.ProductId == productId && productCategory.CategoryId == categoryId) + return productCategory; + } return null; } - public static string GetCategoryNameIndented(this TreeNode treeNode, - string indentWith = "--", - int? languageId = null, - bool withAlias = true) - { - Guard.NotNull(treeNode, nameof(treeNode)); - - var sb = new StringBuilder(); - var indentSize = treeNode.Depth - 1; - for (int i = 0; i < indentSize; i++) - { - sb.Append(indentWith); - } - - var cat = treeNode.Value; - - var name = languageId.HasValue - ? cat.GetLocalized(n => n.Name, languageId.Value) - : cat.Name; - - sb.Append(name); - - if (withAlias && cat.Alias.HasValue()) - { - sb.Append(" ("); - sb.Append(cat.Alias); - sb.Append(")"); - } - - return sb.ToString(); - } - - /// - /// Builds a category breadcrumb (path) for a particular category node - /// - /// The category node - /// The id of language. Pass null to skip localization. - /// true appends the category alias - if specified - to the name - /// The separator string - /// Category breadcrumb path - public static string GetCategoryPath(this ICategoryNode categoryNode, - ICategoryService categoryService, - int? languageId = null, - string aliasPattern = null, - string separator = " ") - { - Guard.NotNull(categoryNode, nameof(categoryNode)); - - var treeNode = categoryService.GetCategoryTree(categoryNode.Id, true); - if (treeNode != null) - { - return categoryService.GetCategoryPath(treeNode, languageId, aliasPattern, separator); - } - - return string.Empty; - } - } + public static string GetCategoryNameIndented(this TreeNode treeNode, + string indentWith = "--", + int? languageId = null, + bool withAlias = true) + { + Guard.NotNull(treeNode, nameof(treeNode)); + + var sb = new StringBuilder(); + var indentSize = treeNode.Depth - 1; + for (int i = 0; i < indentSize; i++) + { + sb.Append(indentWith); + } + + var cat = treeNode.Value; + + var name = languageId.HasValue + ? cat.GetLocalized(n => n.Name, languageId.Value) + : cat.Name; + + sb.Append(name); + + if (withAlias && cat.Alias.HasValue()) + { + sb.Append(" ("); + sb.Append(cat.Alias); + sb.Append(")"); + } + + return sb.ToString(); + } + + /// + /// Builds a category breadcrumb (path) for a particular category node + /// + /// The category node + /// The id of language. Pass null to skip localization. + /// true appends the category alias - if specified - to the name + /// The separator string + /// Category breadcrumb path + public static string GetCategoryPath(this ICategoryNode categoryNode, + ICategoryService categoryService, + int? languageId = null, + string aliasPattern = null, + string separator = " ") + { + Guard.NotNull(categoryNode, nameof(categoryNode)); + + var treeNode = categoryService.GetCategoryTree(categoryNode.Id, true); + if (treeNode != null) + { + return categoryService.GetCategoryPath(treeNode, languageId, aliasPattern, separator); + } + + return string.Empty; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index 2964d244d1..e4f1c20899 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -20,327 +20,362 @@ using SmartStore.Utilities.ObjectPools; using SmartStore.Services.Seo; using SmartStore.Core.Domain.Seo; +using SmartStore.Data.Utilities; namespace SmartStore.Services.Catalog { - public partial class CategoryService : ICategoryService, IXmlSitemapPublisher - { - internal static TimeSpan CategoryTreeCacheDuration = TimeSpan.FromHours(6); + public partial class CategoryService : ICategoryService, IXmlSitemapPublisher + { + internal static TimeSpan CategoryTreeCacheDuration = TimeSpan.FromHours(6); - // {0} = IncludeHidden, {1} = CustomerRoleIds, {2} = StoreId - internal const string CATEGORY_TREE_KEY = "category:tree-{0}-{1}-{2}"; - internal const string CATEGORY_TREE_PATTERN_KEY = "category:tree-*"; + // {0} = IncludeHidden, {1} = CustomerRoleIds, {2} = StoreId + internal const string CATEGORY_TREE_KEY = "category:tree-{0}-{1}-{2}"; + internal const string CATEGORY_TREE_PATTERN_KEY = "category:tree-*"; - private const string CATEGORIES_BY_PARENT_CATEGORY_ID_KEY = "category:byparent-{0}-{1}-{2}-{3}"; - private const string PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY = "productcategory:allbycategoryid-{0}-{1}-{2}-{3}-{4}-{5}"; - private const string PRODUCTCATEGORIES_ALLBYPRODUCTID_KEY = "productcategory:allbyproductid-{0}-{1}-{2}-{3}"; - private const string CATEGORIES_PATTERN_KEY = "category:*"; - private const string PRODUCTCATEGORIES_PATTERN_KEY = "productcategory:*"; + private const string CATEGORIES_BY_PARENT_CATEGORY_ID_KEY = "category:byparent-{0}-{1}-{2}-{3}"; + private const string PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY = "productcategory:allbycategoryid-{0}-{1}-{2}-{3}-{4}-{5}"; + private const string PRODUCTCATEGORIES_ALLBYPRODUCTID_KEY = "productcategory:allbyproductid-{0}-{1}-{2}-{3}"; + private const string CATEGORIES_PATTERN_KEY = "category:*"; + private const string PRODUCTCATEGORIES_PATTERN_KEY = "productcategory:*"; private readonly IRepository _categoryRepository; private readonly IRepository _productCategoryRepository; private readonly IRepository _productRepository; private readonly IRepository _aclRepository; - private readonly IRepository _storeMappingRepository; + private readonly IRepository _storeMappingRepository; private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; + private readonly IStoreContext _storeContext; private readonly IEventPublisher _eventPublisher; private readonly IRequestCache _requestCache; - private readonly ICacheManager _cache; - private readonly IStoreMappingService _storeMappingService; - private readonly IAclService _aclService; + private readonly ICacheManager _cache; + private readonly IStoreMappingService _storeMappingService; + private readonly IAclService _aclService; private readonly ICustomerService _customerService; private readonly IStoreService _storeService; - private readonly ICatalogSearchService _catalogSearchService; + private readonly ICatalogSearchService _catalogSearchService; - public CategoryService(IRequestCache requestCache, - ICacheManager cache, - IRepository categoryRepository, + public CategoryService(IRequestCache requestCache, + ICacheManager cache, + IRepository categoryRepository, IRepository productCategoryRepository, IRepository productRepository, IRepository aclRepository, - IRepository storeMappingRepository, + IRepository storeMappingRepository, IWorkContext workContext, - IStoreContext storeContext, + IStoreContext storeContext, IEventPublisher eventPublisher, - IStoreMappingService storeMappingService, - IAclService aclService, + IStoreMappingService storeMappingService, + IAclService aclService, ICustomerService customerService, IStoreService storeService, - ICatalogSearchService catalogSearchService) + ICatalogSearchService catalogSearchService) { _requestCache = requestCache; - _cache = cache; + _cache = cache; _categoryRepository = categoryRepository; _productCategoryRepository = productCategoryRepository; _productRepository = productRepository; _aclRepository = aclRepository; - _storeMappingRepository = storeMappingRepository; + _storeMappingRepository = storeMappingRepository; _workContext = workContext; - _storeContext = storeContext; + _storeContext = storeContext; _eventPublisher = eventPublisher; - _storeMappingService = storeMappingService; - _aclService = aclService; + _storeMappingService = storeMappingService; + _aclService = aclService; _customerService = customerService; _storeService = storeService; - _catalogSearchService = catalogSearchService; + _catalogSearchService = catalogSearchService; - QuerySettings = DbQuerySettings.Default; + QuerySettings = DbQuerySettings.Default; } - public DbQuerySettings QuerySettings { get; set; } - - private void DeleteAllCategories(IList categories, bool delete) - { - foreach (var category in categories) - { - if (delete) - { - category.Deleted = true; - } - else - { - category.ParentCategoryId = 0; - } - - UpdateCategory(category); - - var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); - DeleteAllCategories(childCategories, delete); + public DbQuerySettings QuerySettings { get; set; } + + private void DeleteAllCategories(IList categories, bool delete) + { + foreach (var category in categories) + { + if (delete) + { + category.Deleted = true; + } + else + { + category.ParentCategoryId = 0; + } + + UpdateCategory(category); + + var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); + DeleteAllCategories(childCategories, delete); } - } + } public virtual void InheritAclIntoChildren( - int categoryId, + int categoryId, bool touchProductsWithMultipleCategories = false, bool touchExistingAcls = false, bool categoriesOnly = false) { - var category = GetCategoryById(categoryId); - var subcategories = GetAllCategoriesByParentCategoryId(categoryId, true); - var allCustomerRoles = _customerService.GetAllCustomerRoles(true); - var categoryCustomerRoles = _aclService.GetCustomerRoleIdsWithAccessTo(category); + var allCustomerRoles = _customerService.GetAllCustomerRoles(true); + var referenceCategory = GetCategoryById(categoryId); + var referenceRoleIds = _aclService.GetCustomerRoleIdsWithAccessTo(referenceCategory); - var categoryIds = new HashSet(subcategories.Select(x => x.Id)); - categoryIds.Add(categoryId); + using (var scope = new DbContextScope(ctx: _aclRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + ProcessCategory(scope, referenceCategory); + } - var searchQuery = new CatalogSearchQuery() - .WithCategoryIds(null, categoryIds.ToArray()); + _cache.RemoveByPattern(AclService.ACL_SEGMENT_PATTERN); - var query = _catalogSearchService.PrepareQuery(searchQuery); - var products = query.OrderBy(p => p.Id).ToList(); - using (var scope = new DbContextScope(ctx: _aclRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + void ProcessCategory(DbContextScope scope, Category c) { - _aclRepository.AutoCommitEnabled = false; + // Process sub-categories. + var subCategories = GetAllCategoriesByParentCategoryId(c.Id, true); - foreach (var subcategory in subcategories) + foreach (var subCategory in subCategories) { - if (subcategory.SubjectToAcl != category.SubjectToAcl) + if (subCategory.SubjectToAcl != referenceCategory.SubjectToAcl) { - subcategory.SubjectToAcl = category.SubjectToAcl; - _categoryRepository.Update(subcategory); + subCategory.SubjectToAcl = referenceCategory.SubjectToAcl; + _categoryRepository.Update(subCategory); } - var existingAclRecords = _aclService.GetAclRecords(subcategory).ToDictionarySafe(x => x.CustomerRoleId); - - foreach (var customerRole in allCustomerRoles) + var existingAclRecords = _aclService.GetAclRecords(subCategory).ToDictionarySafe(x => x.CustomerRoleId); + + foreach (var role in allCustomerRoles) { - if (categoryCustomerRoles.Contains(customerRole.Id)) + if (referenceRoleIds.Contains(role.Id)) { - if (!existingAclRecords.ContainsKey(customerRole.Id)) + if (!existingAclRecords.ContainsKey(role.Id)) { - _aclRepository.Insert(new AclRecord { CustomerRole = customerRole, CustomerRoleId = customerRole.Id, EntityId = subcategory.Id, EntityName = "Category" }); + _aclRepository.Insert(new AclRecord { CustomerRoleId = role.Id, EntityId = subCategory.Id, EntityName = "Category" }); } } - else + else if (existingAclRecords.TryGetValue(role.Id, out var aclRecordToDelete)) { - if (existingAclRecords.TryGetValue(customerRole.Id, out var aclRecordToDelete)) - { - _aclRepository.Delete(aclRecordToDelete); - } + _aclRepository.Delete(aclRecordToDelete); } } } - _aclRepository.Context.SaveChanges(); + scope.Commit(); + + // Process products. + var categoryIds = new HashSet(subCategories.Select(x => x.Id)); + categoryIds.Add(c.Id); - foreach (var product in products) + var searchQuery = new CatalogSearchQuery().WithCategoryIds(null, categoryIds.ToArray()); + var productsQuery = _catalogSearchService.PrepareQuery(searchQuery); + var productsPager = new FastPager(productsQuery, 500); + + while (productsPager.ReadNextPage(out var products)) { - if (product.SubjectToAcl != category.SubjectToAcl) + foreach (var product in products) { - product.SubjectToAcl = category.SubjectToAcl; - _productRepository.Update(product); - } + if (product.SubjectToAcl != referenceCategory.SubjectToAcl) + { + product.SubjectToAcl = referenceCategory.SubjectToAcl; + _productRepository.Update(product); + } - var existingAclRecords = _aclService.GetAclRecords(product).ToDictionarySafe(x => x.CustomerRoleId); + var existingAclRecords = _aclService.GetAclRecords(product).ToDictionarySafe(x => x.CustomerRoleId); - foreach (var customerRole in allCustomerRoles) - { - if (categoryCustomerRoles.Contains(customerRole.Id)) + foreach (var role in allCustomerRoles) { - if (!existingAclRecords.ContainsKey(customerRole.Id)) + if (referenceRoleIds.Contains(role.Id)) { - _aclRepository.Insert(new AclRecord { CustomerRole = customerRole, CustomerRoleId = customerRole.Id, EntityId = product.Id, EntityName = "Product" }); + if (!existingAclRecords.ContainsKey(role.Id)) + { + _aclRepository.Insert(new AclRecord { CustomerRoleId = role.Id, EntityId = product.Id, EntityName = "Product" }); + } + } + else if (existingAclRecords.TryGetValue(role.Id, out var aclRecordToDelete)) + { + _aclRepository.Delete(aclRecordToDelete); } } - else - { - if (existingAclRecords.TryGetValue(customerRole.Id, out var aclRecordToDelete)) - { - _aclRepository.Delete(aclRecordToDelete); - } - } } + + scope.Commit(); + } + + scope.Commit(); + + try + { + scope.DbContext.DetachEntities(x => x is Product || x is Category || x is AclRecord, false); } + catch { } - _aclRepository.Context.SaveChanges(); + foreach (var subCategory in subCategories) + { + ProcessCategory(scope, subCategory); + } } } public virtual void InheritStoresIntoChildren( - int categoryId, + int categoryId, bool touchProductsWithMultipleCategories = false, bool touchExistingAcls = false, bool categoriesOnly = false) { - var category = GetCategoryById(categoryId); - var subcategories = GetAllCategoriesByParentCategoryId(categoryId, true); - var allStores = _storeService.GetAllStores(); - var categoryStoreMappings = _storeMappingService.GetStoresIdsWithAccess(category); + var allStores = _storeService.GetAllStores(); + var referenceCategory = GetCategoryById(categoryId); + var referenceStoreMappings = _storeMappingService.GetStoresIdsWithAccess(referenceCategory); - var categoryIds = new HashSet(subcategories.Select(x => x.Id)); - categoryIds.Add(categoryId); - - var searchQuery = new CatalogSearchQuery() - .WithCategoryIds(null, categoryIds.ToArray()); + using (var scope = new DbContextScope(ctx: _storeMappingRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + ProcessCategory(scope, referenceCategory); + } - var query = _catalogSearchService.PrepareQuery(searchQuery); - var products = query.OrderBy(p => p.Id).ToList(); + _cache.RemoveByPattern(StoreMappingService.STOREMAPPING_SEGMENT_PATTERN); - using (var scope = new DbContextScope(ctx: _storeMappingRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + + void ProcessCategory(DbContextScope scope, Category c) { - _storeMappingRepository.AutoCommitEnabled = false; + // Process sub-categories. + var subCategories = GetAllCategoriesByParentCategoryId(c.Id, true); - foreach (var subcategory in subcategories) + foreach (var subCategory in subCategories) { - if (subcategory.LimitedToStores != category.LimitedToStores) + if (subCategory.LimitedToStores != referenceCategory.LimitedToStores) { - subcategory.LimitedToStores = category.LimitedToStores; - _categoryRepository.Update(subcategory); + subCategory.LimitedToStores = referenceCategory.LimitedToStores; + _categoryRepository.Update(subCategory); } - var existingStoreMappingsRecords = _storeMappingService.GetStoreMappings(subcategory).ToDictionary(x => x.StoreId); + var existingStoreMappingsRecords = _storeMappingService.GetStoreMappings(subCategory).ToDictionary(x => x.StoreId); foreach (var store in allStores) { - if (categoryStoreMappings.Contains(store.Id)) + if (referenceStoreMappings.Contains(store.Id)) { if (!existingStoreMappingsRecords.ContainsKey(store.Id)) { - _storeMappingRepository.Insert(new StoreMapping { StoreId = store.Id, EntityId = subcategory.Id, EntityName = "Category" }); + _storeMappingRepository.Insert(new StoreMapping { StoreId = store.Id, EntityId = subCategory.Id, EntityName = "Category" }); } } - else + else if (existingStoreMappingsRecords.TryGetValue(store.Id, out var storeMappingToDelete)) { - if (existingStoreMappingsRecords.TryGetValue(store.Id, out var storeMappingToDelete)) - { - _storeMappingRepository.Delete(storeMappingToDelete); - } - } + _storeMappingRepository.Delete(storeMappingToDelete); + } } } - _storeMappingRepository.Context.SaveChanges(); + scope.Commit(); + + // Process products. + var categoryIds = new HashSet(subCategories.Select(x => x.Id)); + categoryIds.Add(c.Id); - foreach (var product in products) + var searchQuery = new CatalogSearchQuery().WithCategoryIds(null, categoryIds.ToArray()); + var productsQuery = _catalogSearchService.PrepareQuery(searchQuery); + var productsPager = new FastPager(productsQuery, 500); + + while (productsPager.ReadNextPage(out var products)) { - if (product.LimitedToStores != category.LimitedToStores) + foreach (var product in products) { - product.LimitedToStores = category.LimitedToStores; - _productRepository.Update(product); - } + if (product.LimitedToStores != referenceCategory.LimitedToStores) + { + product.LimitedToStores = referenceCategory.LimitedToStores; + _productRepository.Update(product); + } - var existingStoreMappingsRecords = _storeMappingService.GetStoreMappings(product).ToDictionary(x => x.StoreId); + var existingStoreMappingsRecords = _storeMappingService.GetStoreMappings(product).ToDictionary(x => x.StoreId); - foreach (var store in allStores) - { - if (categoryStoreMappings.Contains(store.Id)) + foreach (var store in allStores) { - if (!existingStoreMappingsRecords.ContainsKey(store.Id)) + if (referenceStoreMappings.Contains(store.Id)) + { + if (!existingStoreMappingsRecords.ContainsKey(store.Id)) + { + _storeMappingRepository.Insert(new StoreMapping { StoreId = store.Id, EntityId = product.Id, EntityName = "Product" }); + } + } + else if (existingStoreMappingsRecords.TryGetValue(store.Id, out var storeMappingToDelete)) { - _storeMappingRepository.Insert(new StoreMapping { StoreId = store.Id, EntityId = product.Id, EntityName = "Product" }); + _storeMappingRepository.Delete(storeMappingToDelete); } } - else - { - if (existingStoreMappingsRecords.TryGetValue(store.Id, out var storeMappingToDelete)) - { - _storeMappingRepository.Delete(storeMappingToDelete); - } - } } + + scope.Commit(); + } + + scope.Commit(); + + try + { + scope.DbContext.DetachEntities(x => x is Product || x is Category || x is StoreMapping, false); } + catch { } - _storeMappingRepository.Context.SaveChanges(); + foreach (var subCategory in subCategories) + { + ProcessCategory(scope, subCategory); + } } } - public virtual void DeleteCategory(Category category, bool deleteChilds = false) + public virtual void DeleteCategory(Category category, bool deleteChilds = false) { - Guard.NotNull(category, nameof(category)); + Guard.NotNull(category, nameof(category)); - category.Deleted = true; + category.Deleted = true; UpdateCategory(category); - var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); - DeleteAllCategories(childCategories, deleteChilds); + var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); + DeleteAllCategories(childCategories, deleteChilds); } - public virtual IQueryable BuildCategoriesQuery( - string categoryName = "", - bool showHidden = false, - string alias = null, - int storeId = 0) - { - var query = _categoryRepository.Table; + public virtual IQueryable BuildCategoriesQuery( + string categoryName = "", + bool showHidden = false, + string alias = null, + int storeId = 0) + { + var query = _categoryRepository.Table; - if (!showHidden) - query = query.Where(c => c.Published); + query = query.Where(c => !c.Deleted); - if (categoryName.HasValue()) - query = query.Where(c => c.Name.Contains(categoryName) || c.FullName.Contains(categoryName)); + if (!showHidden) + query = query.Where(c => c.Published); - if (alias.HasValue()) - query = query.Where(c => c.Alias.Contains(alias)); + if (categoryName.HasValue()) + query = query.Where(c => c.Name.Contains(categoryName) || c.FullName.Contains(categoryName)); - query = ApplyHiddenCategoriesFilter(query, storeId, showHidden); + if (alias.HasValue()) + query = query.Where(c => c.Alias.Contains(alias)); - query = query.Where(c => !c.Deleted); + query = ApplyHiddenCategoriesFilter(query, storeId, showHidden); - return query; - } + return query; + } public virtual IPagedList GetAllCategories( - string categoryName = "", - int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false, - string alias = null, + string categoryName = "", + int pageIndex = 0, + int pageSize = int.MaxValue, + bool showHidden = false, + string alias = null, bool ignoreDetachedCategories = true, int storeId = 0) { - var query = BuildCategoriesQuery(categoryName, showHidden, alias, storeId); + var query = BuildCategoriesQuery(categoryName, showHidden, alias, storeId); - query = query - .OrderBy(x => x.ParentCategoryId) - .ThenBy(x => x.DisplayOrder) - .ThenBy(x => x.Name); + query = query + .OrderBy(x => x.ParentCategoryId) + .ThenBy(x => x.DisplayOrder) + .ThenBy(x => x.Name); var unsortedCategories = query.ToList(); // Sort categories - var sortedCategories = unsortedCategories.SortCategoryNodesForTree(ignoreDetachedCategories: ignoreDetachedCategories); + var sortedCategories = unsortedCategories.SortCategoryNodesForTree(ignoreDetachedCategories: ignoreDetachedCategories); // Paging return new PagedList(sortedCategories, pageIndex, pageSize); @@ -348,8 +383,8 @@ public virtual IPagedList GetAllCategories( public IList GetAllCategoriesByParentCategoryId(int parentCategoryId, bool showHidden = false) { - int storeId = _storeContext.CurrentStore.Id; - string key = string.Format(CATEGORIES_BY_PARENT_CATEGORY_ID_KEY, parentCategoryId, showHidden, _workContext.CurrentCustomer.Id, storeId); + int storeId = _storeContext.CurrentStore.Id; + string key = string.Format(CATEGORIES_BY_PARENT_CATEGORY_ID_KEY, parentCategoryId, showHidden, _workContext.CurrentCustomer.Id, storeId); return _requestCache.Get(key, () => { var query = _categoryRepository.Table; @@ -359,56 +394,57 @@ public IList GetAllCategoriesByParentCategoryId(int parentCategoryId, query = query.Where(c => c.ParentCategoryId == parentCategoryId && !c.Deleted); - query = ApplyHiddenCategoriesFilter(query, storeId, showHidden); + query = ApplyHiddenCategoriesFilter(query, storeId, showHidden); var categories = query.OrderBy(x => x.DisplayOrder).ToList(); return categories; }); } - protected virtual IQueryable ApplyHiddenCategoriesFilter(IQueryable query, int storeId = 0, bool showHidden = false) + protected virtual IQueryable ApplyHiddenCategoriesFilter(IQueryable query, int storeId = 0, bool showHidden = false) { - var entityName = nameof(Category); - var grouping = false; - - // Store mapping - if (!showHidden && storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = from c in query - join m in _storeMappingRepository.Table - on new { c1 = c.Id, c2 = entityName } equals new { c1 = m.EntityId, c2 = m.EntityName } into cm - from m in cm.DefaultIfEmpty() - where !c.LimitedToStores || storeId == m.StoreId - select c; - - grouping = true; - } - - // ACL (access control list) - if (!showHidden && !QuerySettings.IgnoreAcl) - { + var entityName = nameof(Category); + var grouping = false; + + // Store mapping + if (!showHidden && storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = from c in query + join m in _storeMappingRepository.Table + on new { c1 = c.Id, c2 = entityName } equals new { c1 = m.EntityId, c2 = m.EntityName } into cm + from m in cm.DefaultIfEmpty() + where !c.LimitedToStores || storeId == m.StoreId + select c; + + grouping = true; + } + + // ACL (access control list) + if (!showHidden && !QuerySettings.IgnoreAcl) + { var allowedCustomerRolesIds = _workContext.CurrentCustomer.GetRoleIds(); - query = from c in query - join a in _aclRepository.Table - on new { c1 = c.Id, c2 = entityName } equals new { c1 = a.EntityId, c2 = a.EntityName } into ca - from a in ca.DefaultIfEmpty() - where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(a.CustomerRoleId) - select c; - - grouping = true; - } - - if (grouping) - { - // Only distinct categories (group by ID) - query = from c in query - group c by c.Id into cGroup - orderby cGroup.Key - select cGroup.FirstOrDefault(); - } - - return query; + query = from c in query + join a in _aclRepository.Table + on new { c1 = c.Id, c2 = entityName } equals new { c1 = a.EntityId, c2 = a.EntityName } into ca + from a in ca.DefaultIfEmpty() + where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(a.CustomerRoleId) + select c; + + grouping = true; + } + + if (grouping) + { + // Only distinct categories (group by ID). + // SQL Server CE: where-clause beyond this statement produces "SELECT @p__linq__0 AS[p__linq__0]..." which fails in CE. + query = from c in query + group c by c.Id into cGroup + orderby cGroup.Key + select cGroup.FirstOrDefault(); + } + + return query; } public virtual IList GetAllCategoriesDisplayedOnHomePage() @@ -416,7 +452,7 @@ public virtual IList GetAllCategoriesDisplayedOnHomePage() var query = from c in _categoryRepository.Table orderby c.DisplayOrder where c.Published && - !c.Deleted && + !c.Deleted && c.ShowOnHomePage select c; @@ -429,8 +465,8 @@ public virtual Category GetCategoryById(int categoryId) if (categoryId == 0) return null; - return _categoryRepository.GetByIdCached(categoryId, "db.category.id-" + categoryId); - } + return _categoryRepository.GetByIdCached(categoryId, "db.category.id-" + categoryId); + } public virtual IList GetCategoriesByIds(int[] categoryIds) { @@ -451,9 +487,9 @@ where categoryIds.Contains(c.Id) public virtual void InsertCategory(Category category) { - Guard.NotNull(category, nameof(category)); + Guard.NotNull(category, nameof(category)); - _categoryRepository.Insert(category); + _categoryRepository.Insert(category); _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); @@ -461,7 +497,7 @@ public virtual void InsertCategory(Category category) public virtual void UpdateCategory(Category category) { - Guard.NotNull(category, nameof(category)); + Guard.NotNull(category, nameof(category)); //validate category hierarchy var parentCategory = GetCategoryById(category.ParentCategoryId); @@ -483,17 +519,17 @@ public virtual void UpdateCategory(Category category) public virtual void UpdateHasDiscountsApplied(Category category) { - Guard.NotNull(category, nameof(category)); + Guard.NotNull(category, nameof(category)); - category.HasDiscountsApplied = category.AppliedDiscounts.Count > 0; + category.HasDiscountsApplied = category.AppliedDiscounts.Count > 0; UpdateCategory(category); } public virtual void DeleteProductCategory(ProductCategory productCategory) { - Guard.NotNull(productCategory, nameof(productCategory)); + Guard.NotNull(productCategory, nameof(productCategory)); - _productCategoryRepository.Delete(productCategory); + _productCategoryRepository.Delete(productCategory); //cache _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); @@ -505,8 +541,8 @@ public virtual IPagedList GetProductCategoriesByCategoryId(int if (categoryId == 0) return new PagedList(new List(), pageIndex, pageSize); - int storeId = _storeContext.CurrentStore.Id; - string key = string.Format(PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, pageSize, _workContext.CurrentCustomer.Id, storeId); + int storeId = _storeContext.CurrentStore.Id; + string key = string.Format(PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, pageSize, _workContext.CurrentCustomer.Id, storeId); return _requestCache.Get(key, () => { @@ -520,9 +556,9 @@ join p in _productRepository.Table on pc.ProductId equals p.Id query = ApplyHiddenProductCategoriesFilter(query, storeId); } - query = query - .OrderBy(pc => pc.DisplayOrder) - .ThenBy(pc => pc.Id); // required for paging! + query = query + .OrderBy(pc => pc.DisplayOrder) + .ThenBy(pc => pc.Id); // required for paging! var productCategories = new PagedList(query, pageIndex, pageSize); @@ -535,132 +571,132 @@ public virtual IList GetProductCategoriesByProductId(int produc if (productId == 0) return new List(); - string key = string.Format(PRODUCTCATEGORIES_ALLBYPRODUCTID_KEY, showHidden, productId, _workContext.CurrentCustomer.Id, _storeContext.CurrentStore.Id); + string key = string.Format(PRODUCTCATEGORIES_ALLBYPRODUCTID_KEY, showHidden, productId, _workContext.CurrentCustomer.Id, _storeContext.CurrentStore.Id); return _requestCache.Get(key, () => { - var query = from pc in _productCategoryRepository.Table + var query = from pc in _productCategoryRepository.Table join c in _categoryRepository.Table on pc.CategoryId equals c.Id where pc.ProductId == productId && - !c.Deleted && - (showHidden || c.Published) + !c.Deleted && + (showHidden || c.Published) orderby pc.DisplayOrder select pc; - query = query.Include(x => x.Category); - - var allProductCategories = query.ToList(); - var result = new List(); - if (!showHidden) - { - foreach (var pc in allProductCategories) - { - // ACL (access control list) and store mapping - var category = pc.Category; - if (_aclService.Authorize(category) && _storeMappingService.Authorize(category)) - result.Add(pc); - } - } - else - { - // No filtering - result.AddRange(allProductCategories); - } - return result; + query = query.Include(x => x.Category); + + var allProductCategories = query.ToList(); + var result = new List(); + if (!showHidden) + { + foreach (var pc in allProductCategories) + { + // ACL (access control list) and store mapping + var category = pc.Category; + if (_aclService.Authorize(category) && _storeMappingService.Authorize(category)) + result.Add(pc); + } + } + else + { + // No filtering + result.AddRange(allProductCategories); + } + return result; }); } - public virtual Multimap GetProductCategoriesByProductIds(int[] productIds, bool? hasDiscountsApplied = null, bool showHidden = false) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual Multimap GetProductCategoriesByProductIds(int[] productIds, bool? hasDiscountsApplied = null, bool showHidden = false) + { + Guard.NotNull(productIds, nameof(productIds)); if (!productIds.Any()) { return new Multimap(); } - var query = - from pc in _productCategoryRepository.TableUntracked - join c in _categoryRepository.Table on pc.CategoryId equals c.Id - where productIds.Contains(pc.ProductId) && !c.Deleted && (showHidden || c.Published) - orderby pc.DisplayOrder - select pc; + var query = + from pc in _productCategoryRepository.TableUntracked + join c in _categoryRepository.Table on pc.CategoryId equals c.Id + where productIds.Contains(pc.ProductId) && !c.Deleted && (showHidden || c.Published) + orderby pc.DisplayOrder + select pc; - query = query.Include(x => x.Category.MediaFile); + query = query.Include(x => x.Category.MediaFile); - if (hasDiscountsApplied.HasValue) - { - query = query.Where(x => x.Category.HasDiscountsApplied == hasDiscountsApplied); - } + if (hasDiscountsApplied.HasValue) + { + query = query.Where(x => x.Category.HasDiscountsApplied == hasDiscountsApplied); + } - var list = query.ToList(); + var list = query.ToList(); - if (!showHidden) - { - list = list.Where(x => _aclService.Authorize(x.Category) && _storeMappingService.Authorize(x.Category)).ToList(); - } + if (!showHidden) + { + list = list.Where(x => _aclService.Authorize(x.Category) && _storeMappingService.Authorize(x.Category)).ToList(); + } - var map = list.ToMultimap(x => x.ProductId, x => x); + var map = list.ToMultimap(x => x.ProductId, x => x); - return map; - } + return map; + } - public virtual Multimap GetProductCategoriesByCategoryIds(int[] categoryIds) - { - Guard.NotNull(categoryIds, nameof(categoryIds)); + public virtual Multimap GetProductCategoriesByCategoryIds(int[] categoryIds) + { + Guard.NotNull(categoryIds, nameof(categoryIds)); - var query = _productCategoryRepository.TableUntracked - .Where(x => categoryIds.Contains(x.CategoryId)) - .OrderBy(x => x.DisplayOrder); + var query = _productCategoryRepository.TableUntracked + .Where(x => categoryIds.Contains(x.CategoryId)) + .OrderBy(x => x.DisplayOrder); - var map = query - .ToList() - .ToMultimap(x => x.CategoryId, x => x); + var map = query + .ToList() + .ToMultimap(x => x.CategoryId, x => x); - return map; - } + return map; + } - protected virtual IQueryable ApplyHiddenProductCategoriesFilter(IQueryable query, int storeId = 0) + protected virtual IQueryable ApplyHiddenProductCategoriesFilter(IQueryable query, int storeId = 0) { - bool group = false; + bool group = false; //ACL (access control list) - if (!QuerySettings.IgnoreAcl) - { - group = true; + if (!QuerySettings.IgnoreAcl) + { + group = true; var allowedCustomerRolesIds = _workContext.CurrentCustomer.GetRoleIds(); - query = from pc in query - join c in _categoryRepository.Table on pc.CategoryId equals c.Id - join acl in _aclRepository.Table - on new { c1 = c.Id, c2 = "Category" } equals new { c1 = acl.EntityId, c2 = acl.EntityName } into c_acl - from acl in c_acl.DefaultIfEmpty() - where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(acl.CustomerRoleId) - select pc; - } + query = from pc in query + join c in _categoryRepository.Table on pc.CategoryId equals c.Id + join acl in _aclRepository.Table + on new { c1 = c.Id, c2 = "Category" } equals new { c1 = acl.EntityId, c2 = acl.EntityName } into c_acl + from acl in c_acl.DefaultIfEmpty() + where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(acl.CustomerRoleId) + select pc; + } //Store mapping - if (!QuerySettings.IgnoreMultiStore && storeId > 0) - { - group = true; - query = from pc in query - join c in _categoryRepository.Table on pc.CategoryId equals c.Id - join sm in _storeMappingRepository.Table - on new { c1 = c.Id, c2 = "Category" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into c_sm - from sm in c_sm.DefaultIfEmpty() - where !c.LimitedToStores || storeId == sm.StoreId - select pc; - } - - if (group) - { - //only distinct categories (group by ID) - query = from pc in query - group pc by pc.Id into pcGroup - orderby pcGroup.Key - select pcGroup.FirstOrDefault(); - } - - return query; + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + group = true; + query = from pc in query + join c in _categoryRepository.Table on pc.CategoryId equals c.Id + join sm in _storeMappingRepository.Table + on new { c1 = c.Id, c2 = "Category" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into c_sm + from sm in c_sm.DefaultIfEmpty() + where !c.LimitedToStores || storeId == sm.StoreId + select pc; + } + + if (group) + { + //only distinct categories (group by ID) + query = from pc in query + group pc by pc.Id into pcGroup + orderby pcGroup.Key + select pcGroup.FirstOrDefault(); + } + + return query; } public virtual ProductCategory GetProductCategoryById(int productCategoryId) @@ -693,118 +729,118 @@ public virtual void UpdateProductCategory(ProductCategory productCategory) _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); } - public virtual IEnumerable GetCategoryTrail(ICategoryNode node) - { - Guard.NotNull(node, nameof(node)); + public virtual IEnumerable GetCategoryTrail(ICategoryNode node) + { + Guard.NotNull(node, nameof(node)); - var treeNode = GetCategoryTree(node.Id, true); + var treeNode = GetCategoryTree(node.Id, true); - if (treeNode == null) - { - return Enumerable.Empty(); - } + if (treeNode == null) + { + return Enumerable.Empty(); + } - return treeNode.Trail - .Where(x => !x.IsRoot) - //.TakeWhile(x => x.Value.Published) // TBD: (mc) do we need this? - .Select(x => x.Value); - } + return treeNode.Trail + .Where(x => !x.IsRoot) + //.TakeWhile(x => x.Value.Published) // TBD: (mc) do we need this? + .Select(x => x.Value); + } - public virtual string GetCategoryPath( - TreeNode treeNode, - int? languageId = null, - string aliasPattern = null, - string separator = " ") - { - Guard.NotNull(treeNode, nameof(treeNode)); + public virtual string GetCategoryPath( + TreeNode treeNode, + int? languageId = null, + string aliasPattern = null, + string separator = " ") + { + Guard.NotNull(treeNode, nameof(treeNode)); - var lookupKey = "Path.{0}.{1}.{2}".FormatInvariant(separator, languageId ?? 0, aliasPattern.HasValue()); - var cachedPath = treeNode.GetMetadata(lookupKey, false); + var lookupKey = "Path.{0}.{1}.{2}".FormatInvariant(separator, languageId ?? 0, aliasPattern.HasValue()); + var cachedPath = treeNode.GetMetadata(lookupKey, false); - if (cachedPath != null) - { - return cachedPath; - } + if (cachedPath != null) + { + return cachedPath; + } - var trail = treeNode.Trail; + var trail = treeNode.Trail; var psb = PooledStringBuilder.Rent(); var sb = (StringBuilder)psb; - foreach (var node in trail) - { - if (!node.IsRoot) - { - var cat = node.Value; - - var name = languageId.HasValue - ? cat.GetLocalized(n => n.Name, languageId.Value) - : cat.Name; - - sb.Append(name); - - if (aliasPattern.HasValue() && cat.Alias.HasValue()) - { - sb.Append(" "); - sb.Append(string.Format(aliasPattern, cat.Alias)); - } - - if (node != treeNode) - { - // Is not self (trail end) - sb.Append(separator); - } - } - } - - var path = psb.ToStringAndReturn(); - treeNode.SetThreadMetadata(lookupKey, path); - return path; - } - - public TreeNode GetCategoryTree(int rootCategoryId = 0, bool includeHidden = false, int storeId = 0) - { - var storeToken = QuerySettings.IgnoreMultiStore ? "0" : storeId.ToString(); - var rolesToken = QuerySettings.IgnoreAcl || includeHidden ? "0" : _workContext.CurrentCustomer.GetRolesIdent(); - var cacheKey = CATEGORY_TREE_KEY.FormatInvariant(includeHidden.ToString().ToLower(), rolesToken, storeToken); - - var root = _cache.Get(cacheKey, () => - { - // (Perf) don't fetch every field from db - var query = from x in BuildCategoriesQuery(showHidden: includeHidden, storeId: storeId) - orderby x.ParentCategoryId, x.DisplayOrder, x.Name - select new - { - x.Id, - x.ParentCategoryId, - x.Name, + foreach (var node in trail) + { + if (!node.IsRoot) + { + var cat = node.Value; + + var name = languageId.HasValue + ? cat.GetLocalized(n => n.Name, languageId.Value) + : cat.Name; + + sb.Append(name); + + if (aliasPattern.HasValue() && cat.Alias.HasValue()) + { + sb.Append(" "); + sb.Append(string.Format(aliasPattern, cat.Alias)); + } + + if (node != treeNode) + { + // Is not self (trail end) + sb.Append(separator); + } + } + } + + var path = psb.ToStringAndReturn(); + treeNode.SetThreadMetadata(lookupKey, path); + return path; + } + + public TreeNode GetCategoryTree(int rootCategoryId = 0, bool includeHidden = false, int storeId = 0) + { + var storeToken = QuerySettings.IgnoreMultiStore ? "0" : storeId.ToString(); + var rolesToken = QuerySettings.IgnoreAcl || includeHidden ? "0" : _workContext.CurrentCustomer.GetRolesIdent(); + var cacheKey = CATEGORY_TREE_KEY.FormatInvariant(includeHidden.ToString().ToLower(), rolesToken, storeToken); + + var root = _cache.Get(cacheKey, () => + { + // (Perf) don't fetch every field from db + var query = from x in BuildCategoriesQuery(showHidden: includeHidden, storeId: storeId) + orderby x.ParentCategoryId, x.DisplayOrder, x.Name + select new + { + x.Id, + x.ParentCategoryId, + x.Name, x.ExternalLink, - x.Alias, - x.MediaFileId, - x.Published, - x.DisplayOrder, - x.UpdatedOnUtc, - x.BadgeText, - x.BadgeStyle, - x.LimitedToStores, - x.SubjectToAcl - }; - - var unsortedNodes = query.ToList().Select(x => new CategoryNode + x.Alias, + x.MediaFileId, + x.Published, + x.DisplayOrder, + x.UpdatedOnUtc, + x.BadgeText, + x.BadgeStyle, + x.LimitedToStores, + x.SubjectToAcl + }; + + var unsortedNodes = query.ToList().Select(x => new CategoryNode { - Id = x.Id, - ParentCategoryId = x.ParentCategoryId, - Name = x.Name, + Id = x.Id, + ParentCategoryId = x.ParentCategoryId, + Name = x.Name, ExternalLink = x.ExternalLink, - Alias = x.Alias, - MediaFileId = x.MediaFileId, - Published = x.Published, - DisplayOrder = x.DisplayOrder, - UpdatedOnUtc = x.UpdatedOnUtc, - BadgeText = x.BadgeText, - BadgeStyle = x.BadgeStyle, - LimitedToStores = x.LimitedToStores, - SubjectToAcl = x.SubjectToAcl - }); + Alias = x.Alias, + MediaFileId = x.MediaFileId, + Published = x.Published, + DisplayOrder = x.DisplayOrder, + UpdatedOnUtc = x.UpdatedOnUtc, + BadgeText = x.BadgeText, + BadgeStyle = x.BadgeStyle, + LimitedToStores = x.LimitedToStores, + SubjectToAcl = x.SubjectToAcl + }); var nodeMap = unsortedNodes.ToMultimap(x => x.ParentCategoryId, x => x); var curParent = new TreeNode(new CategoryNode { Name = "Home" }); @@ -812,15 +848,15 @@ public TreeNode GetCategoryTree(int rootCategoryId = 0, bool incl AddChildTreeNodes(curParent, 0, nodeMap); return curParent.Root; - }, CategoryTreeCacheDuration); + }, CategoryTreeCacheDuration); - if (rootCategoryId > 0) - { - root = root.SelectNodeById(rootCategoryId); - } + if (rootCategoryId > 0) + { + root = root.SelectNodeById(rootCategoryId); + } - return root; - } + return root; + } private void AddChildTreeNodes(TreeNode parentNode, int parentItemId, Multimap nodeMap) { @@ -846,46 +882,46 @@ private void AddChildTreeNodes(TreeNode parentNode, int parentIte } } - public int CountAllCategories() - { - var query = _categoryRepository.Table; - query = query.Where(x => !x.Deleted); + public int CountAllCategories() + { + var query = _categoryRepository.Table; + query = query.Where(x => !x.Deleted); - return query.Count(); - } + return query.Count(); + } - #region XML Sitemap + #region XML Sitemap - public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) - { - if (!context.LoadSetting().XmlSitemapIncludesCategories) - return null; + public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) + { + if (!context.LoadSetting().XmlSitemapIncludesCategories) + return null; - var query = BuildCategoriesQuery(showHidden: false, storeId: context.RequestStoreId); - return new CategoryXmlSitemapResult { Query = query }; - } + var query = BuildCategoriesQuery(showHidden: false, storeId: context.RequestStoreId); + return new CategoryXmlSitemapResult { Query = query }; + } - class CategoryXmlSitemapResult : XmlSitemapProvider - { - public IQueryable Query { get; set; } + class CategoryXmlSitemapResult : XmlSitemapProvider + { + public IQueryable Query { get; set; } - public override int GetTotalCount() - { - return Query.Count(); - } + public override int GetTotalCount() + { + return Query.Count(); + } - public override IEnumerable Enlist() - { - var topics = Query.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); - foreach (var x in topics) - { - yield return new NamedEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }; - } - } + public override IEnumerable Enlist() + { + var topics = Query.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); + foreach (var x in topics) + { + yield return new NamedEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } + } - public override int Order => int.MinValue; - } + public override int Order => int.MinValue; + } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs index c455ddc0c6..ff50dcd3b9 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs @@ -12,7 +12,7 @@ public partial class CategoryTemplateService : ICategoryTemplateService private readonly IRepository _categoryTemplateRepository; private readonly IEventPublisher _eventPublisher; - public CategoryTemplateService(IRepository categoryTemplateRepository, IEventPublisher eventPublisher) + public CategoryTemplateService(IRepository categoryTemplateRepository, IEventPublisher eventPublisher) { _categoryTemplateRepository = categoryTemplateRepository; _eventPublisher = eventPublisher; @@ -35,7 +35,7 @@ orderby pt.DisplayOrder var templates = query.ToList(); return templates; } - + public virtual CategoryTemplate GetCategoryTemplateById(int categoryTemplateId) { if (categoryTemplateId == 0) diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs index 7bd3a3ab3d..df7cd1a0af 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs @@ -12,281 +12,280 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Core.Search; +using SmartStore.Data; namespace SmartStore.Services.Catalog { - public enum CategoryTreeChangeReason - { - ElementCounts, - Data, - Localization, - StoreMapping, - Acl, - Hierarchy - } + public enum CategoryTreeChangeReason + { + ElementCounts, + Data, + Localization, + StoreMapping, + Acl, + Hierarchy + } - public class CategoryTreeChangedEvent - { - public CategoryTreeChangedEvent(CategoryTreeChangeReason reason) - { - Reason = reason; - } + public class CategoryTreeChangedEvent + { + public CategoryTreeChangedEvent(CategoryTreeChangeReason reason) + { + Reason = reason; + } - public CategoryTreeChangeReason Reason { get; private set; } - } + public CategoryTreeChangeReason Reason { get; private set; } + } - public class CategoryTreeChangeHook : IDbSaveHook, IConsumer - { - private readonly ICommonServices _services; - private readonly ICategoryService _categoryService; + public class CategoryTreeChangeHook : IDbSaveHook, IConsumer + { + private readonly ICommonServices _services; - private readonly bool[] _handledReasons = new bool[(int)CategoryTreeChangeReason.Hierarchy + 1]; - private bool _invalidated; + private readonly bool[] _handledReasons = new bool[(int)CategoryTreeChangeReason.Hierarchy + 1]; + private bool _invalidated; - // Hierarchy affecting category prop names - private static readonly string[] _h = new string[] { "ParentCategoryId", "Published", "Deleted", "DisplayOrder" }; - // Visibility affecting category prop names - private static readonly string[] _a = new string[] { "LimitedToStores", "SubjectToAcl" }; - // Data affecting category prop names - private static readonly string[] _d = new string[] { "Name", "Alias", "ExternalLink", "PictureId", "BadgeText", "BadgeStyle" }; + // Hierarchy affecting category prop names + private static readonly string[] _h = new string[] { "ParentCategoryId", "Published", "Deleted", "DisplayOrder" }; + // Visibility affecting category prop names + private static readonly string[] _a = new string[] { "LimitedToStores", "SubjectToAcl" }; + // Data affecting category prop names + private static readonly string[] _d = new string[] { "Name", "Alias", "ExternalLink", "MediaFileId", "BadgeText", "BadgeStyle" }; - private static readonly HashSet _candidateTypes = new HashSet - { - typeof(Product), - typeof(Category), - typeof(ProductCategory), - typeof(Setting), - typeof(Language), - typeof(LocalizedProperty), - typeof(StoreMapping), - typeof(AclRecord) - }; + private static readonly HashSet _candidateTypes = new HashSet + { + typeof(Product), + typeof(Category), + typeof(ProductCategory), + typeof(Setting), + typeof(Language), + typeof(LocalizedProperty), + typeof(StoreMapping), + typeof(AclRecord) + }; - public CategoryTreeChangeHook(ICommonServices services, ICategoryService categoryService) - { - _services = services; - _categoryService = categoryService; - } + public CategoryTreeChangeHook(ICommonServices services) + { + _services = services; + } - public void OnBeforeSave(IHookedEntity entry) - { - if (_invalidated) - return; + public void OnBeforeSave(IHookedEntity entry) + { + if (_invalidated) + return; - if (entry.InitialState != EntityState.Modified) - throw new NotSupportedException(); + if (entry.InitialState != EntityState.Modified) + throw new NotSupportedException(); - var cache = _services.Cache; - var entity = entry.Entity; - - if (entity is Product) - { - var modProps = _services.DbContext.GetModifiedProperties(entity); - var toxicPropNames = Product.GetVisibilityAffectingPropertyNames(); - if (modProps.Keys.Any(x => toxicPropNames.Contains(x))) - { - // No eviction, just notification - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - } - else if (entity is ProductCategory) - { - var modProps = _services.DbContext.GetModifiedProperties(entity); - if (modProps.ContainsKey("CategoryId")) - { - // No eviction, just notification - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - } - else if (entity is Category category) - { - var modProps = _services.DbContext.GetModifiedProperties(entity); + var cache = _services.Cache; + var entity = entry.Entity; - if (modProps.Keys.Any(x => _h.Contains(x))) - { - // Hierarchy affecting properties has changed. Nuke each tree. - cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); - PublishEvent(CategoryTreeChangeReason.Hierarchy); - _invalidated = true; - } - else if (modProps.Keys.Any(x => _a.Contains(x))) - { - if (modProps.ContainsKey("LimitedToStores")) - { - // Don't nuke store agnostic trees - cache.RemoveByPattern(BuildCacheKeyPattern("*", "*", "[^0]*")); - PublishEvent(CategoryTreeChangeReason.StoreMapping); - } - if (modProps.ContainsKey("SubjectToAcl")) - { - // Don't nuke ACL agnostic trees - cache.RemoveByPattern(BuildCacheKeyPattern("*", "[^0]*", "*")); - PublishEvent(CategoryTreeChangeReason.Acl); - } - } - else if (modProps.Keys.Any(x => _d.Contains(x))) - { - // Only data has changed. Don't nuke trees, update corresponding cache entries instead. - var keys = cache.Keys(CategoryService.CATEGORY_TREE_PATTERN_KEY).ToArray(); - foreach (var key in keys) - { - var tree = cache.Get>(key); - if (tree != null) - { - var node = tree.SelectNodeById(entity.Id); - if (node != null) - { - var value = node.Value as CategoryNode; - if (value == null) - { - // Cannot update. Nuke tree. - cache.Remove(key); - } - else - { - value.Name = category.Name; + if (entity is Product) + { + var modProps = _services.DbContext.GetModifiedProperties(entity); + var toxicPropNames = Product.GetVisibilityAffectingPropertyNames(); + if (modProps.Keys.Any(x => toxicPropNames.Contains(x))) + { + // No eviction, just notification + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is ProductCategory) + { + var modProps = _services.DbContext.GetModifiedProperties(entity); + if (modProps.ContainsKey("CategoryId")) + { + // No eviction, just notification + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is Category category) + { + var modProps = _services.DbContext.GetModifiedProperties(entity); + + if (modProps.Keys.Any(x => _h.Contains(x))) + { + // Hierarchy affecting properties has changed. Nuke each tree. + cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); + PublishEvent(CategoryTreeChangeReason.Hierarchy); + _invalidated = true; + } + else if (modProps.Keys.Any(x => _a.Contains(x))) + { + if (modProps.ContainsKey("LimitedToStores")) + { + // Don't nuke store agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "*", "[^0]*")); + PublishEvent(CategoryTreeChangeReason.StoreMapping); + } + if (modProps.ContainsKey("SubjectToAcl")) + { + // Don't nuke ACL agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "[^0]*", "*")); + PublishEvent(CategoryTreeChangeReason.Acl); + } + } + else if (modProps.Keys.Any(x => _d.Contains(x))) + { + // Only data has changed. Don't nuke trees, update corresponding cache entries instead. + var keys = cache.Keys(CategoryService.CATEGORY_TREE_PATTERN_KEY).ToArray(); + foreach (var key in keys) + { + var tree = cache.Get>(key); + if (tree != null) + { + var node = tree.SelectNodeById(entity.Id); + if (node != null) + { + var value = node.Value as CategoryNode; + if (value == null) + { + // Cannot update. Nuke tree. + cache.Remove(key); + } + else + { + value.Name = category.Name; value.ExternalLink = category.ExternalLink; - value.Alias = category.Alias; - value.MediaFileId = category.MediaFileId; - value.BadgeText = category.BadgeText; - value.BadgeStyle = category.BadgeStyle; + value.Alias = category.Alias; + value.MediaFileId = category.MediaFileId; + value.BadgeText = category.BadgeText; + value.BadgeStyle = category.BadgeStyle; - // Persist to cache store - cache.Put(key, tree, CategoryService.CategoryTreeCacheDuration); - } - } - } - } + // Persist to cache store + cache.Put(key, tree, CategoryService.CategoryTreeCacheDuration); + } + } + } + } - // Publish event only once - PublishEvent(CategoryTreeChangeReason.Data); - } - } - else - { - throw new NotSupportedException(); - } - } + // Publish event only once + PublishEvent(CategoryTreeChangeReason.Data); + } + } + else + { + throw new NotSupportedException(); + } + } - public void OnAfterSave(IHookedEntity entry) - { - if (_invalidated) - return; + public void OnAfterSave(IHookedEntity entry) + { + if (_invalidated) + return; - if (!_candidateTypes.Contains(entry.EntityType)) - throw new NotSupportedException(); + if (!_candidateTypes.Contains(entry.EntityType)) + throw new NotSupportedException(); - // INFO: Acl & StoreMapping affect element counts + // INFO: Acl & StoreMapping affect element counts - var cache = _services.Cache; - var isNewOrDeleted = entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Deleted; - var entity = entry.Entity; + var cache = _services.Cache; + var isNewOrDeleted = entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Deleted; + var entity = entry.Entity; - if (entity is Product) - { - // INFO: 'Modified' case already handled in 'OnBeforeSave()' - if (entry.InitialState == EntityState.Deleted || (entry.InitialState == EntityState.Added && ((Product)entity).Published)) - { - // No eviction, just notification, but for PUBLISHED products only - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - } - else if (entity is ProductCategory && isNewOrDeleted) - { - // INFO: 'Modified' case already handled in 'OnBeforeSave()' - // New or deleted product category mappings affect counts - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - else if (entity is Category && isNewOrDeleted) - { - // INFO: 'Modified' case already handled in 'OnBeforeSave()' - // Hierarchy affecting change, nuke all. - cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); - PublishEvent(CategoryTreeChangeReason.Hierarchy); - _invalidated = true; - } - else if (entity is Setting) - { - var name = (entity as Setting).Name.ToLowerInvariant(); - if (name == "catalogsettings.showcategoryproductnumber" || name == "catalogsettings.showcategoryproductnumberincludingsubcategories") - { - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - } - else if (entity is Language && entry.InitialState == EntityState.Deleted) - { - PublishEvent(CategoryTreeChangeReason.Localization); - } - else if (entity is LocalizedProperty) - { - var lp = entity as LocalizedProperty; - var key = lp.LocaleKey; - if (lp.LocaleKeyGroup == "Category" && (key == "Name" || key == "BadgeText")) - { - PublishEvent(CategoryTreeChangeReason.Localization); - } - } - else if (entity is StoreMapping) - { - var stm = entity as StoreMapping; - if (stm.EntityName == "Product") - { - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - else if (stm.EntityName == "Category") - { - // Don't nuke store agnostic trees - cache.RemoveByPattern(BuildCacheKeyPattern("*", "*", "[^0]*")); - PublishEvent(CategoryTreeChangeReason.StoreMapping); - } - } - else if (entity is AclRecord) - { - var acl = entity as AclRecord; - if (!acl.IsIdle) - { - if (acl.EntityName == "Product") - { - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - else if (acl.EntityName == "Category") - { - // Don't nuke ACL agnostic trees - cache.RemoveByPattern(BuildCacheKeyPattern("*", "[^0]*", "*")); - PublishEvent(CategoryTreeChangeReason.Acl); - } - } - } - } + if (entity is Product) + { + // INFO: 'Modified' case already handled in 'OnBeforeSave()' + if (entry.InitialState == EntityState.Deleted || (entry.InitialState == EntityState.Added && ((Product)entity).Published)) + { + // No eviction, just notification, but for PUBLISHED products only + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is ProductCategory && isNewOrDeleted) + { + // INFO: 'Modified' case already handled in 'OnBeforeSave()' + // New or deleted product category mappings affect counts + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + else if (entity is Category && isNewOrDeleted) + { + // INFO: 'Modified' case already handled in 'OnBeforeSave()' + // Hierarchy affecting change, nuke all. + cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); + PublishEvent(CategoryTreeChangeReason.Hierarchy); + _invalidated = true; + } + else if (entity is Setting) + { + var name = (entity as Setting).Name.ToLowerInvariant(); + if (name == "catalogsettings.showcategoryproductnumber" || name == "catalogsettings.showcategoryproductnumberincludingsubcategories") + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is Language && entry.InitialState == EntityState.Deleted) + { + PublishEvent(CategoryTreeChangeReason.Localization); + } + else if (entity is LocalizedProperty) + { + var lp = entity as LocalizedProperty; + var key = lp.LocaleKey; + if (lp.LocaleKeyGroup == "Category" && (key == "Name" || key == "BadgeText")) + { + PublishEvent(CategoryTreeChangeReason.Localization); + } + } + else if (entity is StoreMapping) + { + var stm = entity as StoreMapping; + if (stm.EntityName == "Product") + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + else if (stm.EntityName == "Category") + { + // Don't nuke store agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "*", "[^0]*")); + PublishEvent(CategoryTreeChangeReason.StoreMapping); + } + } + else if (entity is AclRecord) + { + var acl = entity as AclRecord; + if (!acl.IsIdle) + { + if (acl.EntityName == "Product") + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + else if (acl.EntityName == "Category") + { + // Don't nuke ACL agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "[^0]*", "*")); + PublishEvent(CategoryTreeChangeReason.Acl); + } + } + } + } - public void HandleEvent(IndexingCompletedEvent message) - { - if (message.IndexInfo.IsModified) - { - PublishEvent(CategoryTreeChangeReason.ElementCounts); - } - } + public void HandleEvent(IndexingCompletedEvent message) + { + if (message.IndexInfo.IsModified) + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } - private void PublishEvent(CategoryTreeChangeReason reason) - { - if (_handledReasons[(int)reason] == false) - { - _services.EventPublisher.Publish(new CategoryTreeChangedEvent(reason)); - _handledReasons[(int)reason] = true; - } - } + private void PublishEvent(CategoryTreeChangeReason reason) + { + if (_handledReasons[(int)reason] == false) + { + _services.EventPublisher.Publish(new CategoryTreeChangedEvent(reason)); + _handledReasons[(int)reason] = true; + } + } - private string BuildCacheKeyPattern(string includeHiddenToken = "*", string rolesToken = "*", string storeToken = "*") - { - return CategoryService.CATEGORY_TREE_KEY.FormatInvariant(includeHiddenToken, rolesToken, storeToken); - } + private string BuildCacheKeyPattern(string includeHiddenToken = "*", string rolesToken = "*", string storeToken = "*") + { + return CategoryService.CATEGORY_TREE_KEY.FormatInvariant(includeHiddenToken, rolesToken, storeToken); + } - public void OnBeforeSaveCompleted() - { - } + public void OnBeforeSaveCompleted() + { + } - public void OnAfterSaveCompleted() - { - } - } + public void OnAfterSaveCompleted() + { + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CompareProductsService.cs b/src/Libraries/SmartStore.Services/Catalog/CompareProductsService.cs index 6368d7dae5..1d5730b812 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CompareProductsService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CompareProductsService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Web; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; using SmartStore.Services.Search; namespace SmartStore.Services.Catalog @@ -16,16 +17,19 @@ public partial class CompareProductsService : ICompareProductsService private readonly HttpContextBase _httpContext; private readonly IProductService _productService; - private readonly ICatalogSearchService _catalogSearchService; - - public CompareProductsService( - HttpContextBase httpContext, - IProductService productService, - ICatalogSearchService catalogSearchService) + private readonly ICatalogSearchService _catalogSearchService; + private readonly PrivacySettings _privacySettings; + + public CompareProductsService( + HttpContextBase httpContext, + IProductService productService, + ICatalogSearchService catalogSearchService, + PrivacySettings privacySettings) { _httpContext = httpContext; _productService = productService; - _catalogSearchService = catalogSearchService; + _catalogSearchService = catalogSearchService; + _privacySettings = privacySettings; } #region Utilities @@ -66,6 +70,10 @@ public virtual void ClearCompareProducts() { compareCookie.Values.Clear(); compareCookie.Expires = DateTime.Now.AddYears(-1); + compareCookie.HttpOnly = true; + compareCookie.Secure = _httpContext.Request.IsHttps(); + compareCookie.SameSite = compareCookie.Secure ? (SameSiteMode)_privacySettings.SameSiteMode : SameSiteMode.Lax; + _httpContext.Response.Cookies.Set(compareCookie); } } @@ -85,20 +93,19 @@ public virtual IList GetComparedProducts() public virtual int GetComparedProductsCount() { - var productIds = GetComparedProductIds(); - if (productIds.Count == 0) - return 0; + var productIds = GetComparedProductIds(); + if (productIds.Count == 0) + return 0; - var searchQuery = new CatalogSearchQuery() - .VisibleOnly() - .WithProductIds(productIds.ToArray()) - .BuildHits(false); + var searchQuery = new CatalogSearchQuery() + .VisibleOnly() + .WithProductIds(productIds.ToArray()) + .BuildHits(false); - var result = _catalogSearchService.Search(searchQuery); + var result = _catalogSearchService.Search(searchQuery); return result.TotalHitsCount; } - /// /// Removes a product from a "compare products" list /// @@ -112,11 +119,21 @@ public virtual void RemoveProductFromCompareList(int productId) var compareCookie = _httpContext.Request.Cookies.Get(COMPARE_PRODUCTS_COOKIE_NAME); if ((compareCookie == null) || (compareCookie.Values == null)) + { return; + } + compareCookie.Values.Clear(); foreach (int newProductId in newProductIds) + { compareCookie.Values.Add("CompareProductIds", newProductId.ToString()); + } + compareCookie.Expires = DateTime.Now.AddDays(10.0); + compareCookie.HttpOnly = true; + compareCookie.Secure = _httpContext.Request.IsHttps(); + compareCookie.SameSite = compareCookie.Secure ? (SameSiteMode)_privacySettings.SameSiteMode : SameSiteMode.Lax; + _httpContext.Response.Cookies.Set(compareCookie); } @@ -129,21 +146,16 @@ public virtual void AddProductToCompareList(int productId) var oldProductIds = GetComparedProductIds(); var newProductIds = new List(); newProductIds.Add(productId); - - foreach (int oldProductId in oldProductIds) - { - if (oldProductId != productId) - newProductIds.Add(oldProductId); - } - var compareCookie = _httpContext.Request.Cookies.Get(COMPARE_PRODUCTS_COOKIE_NAME); - if (compareCookie == null) - { - compareCookie = new HttpCookie(COMPARE_PRODUCTS_COOKIE_NAME); - compareCookie.HttpOnly = true; - } + foreach (int oldProductId in oldProductIds) + { + if (oldProductId != productId) + newProductIds.Add(oldProductId); + } + var compareCookie = _httpContext.Request.Cookies.Get(COMPARE_PRODUCTS_COOKIE_NAME) ?? new HttpCookie(COMPARE_PRODUCTS_COOKIE_NAME); compareCookie.Values.Clear(); + int maxProducts = 4; int i = 1; foreach (int newProductId in newProductIds) @@ -153,7 +165,12 @@ public virtual void AddProductToCompareList(int productId) break; i++; } + compareCookie.Expires = DateTime.Now.AddDays(10.0); + compareCookie.HttpOnly = true; + compareCookie.Secure = _httpContext.Request.IsHttps(); + compareCookie.SameSite = compareCookie.Secure ? (SameSiteMode)_privacySettings.SameSiteMode : SameSiteMode.Lax; + _httpContext.Response.Cookies.Set(compareCookie); } diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 44c4ddb02f..395b2f0e2e 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -18,208 +18,209 @@ namespace SmartStore.Services.Catalog { public partial class CopyProductService : ICopyProductService { - private readonly IRepository _productRepository; - private readonly IRepository _relatedProductRepository; - private readonly IRepository _crossSellProductRepository; - private readonly ICommonServices _services; - private readonly IProductService _productService; + private readonly IRepository _productRepository; + private readonly IRepository _relatedProductRepository; + private readonly IRepository _crossSellProductRepository; + private readonly ICommonServices _services; + private readonly IProductService _productService; private readonly IProductAttributeService _productAttributeService; private readonly ILanguageService _languageService; private readonly ILocalizedEntityService _localizedEntityService; private readonly IDownloadService _downloadService; private readonly IProductAttributeParser _productAttributeParser; private readonly IUrlRecordService _urlRecordService; - private readonly IStoreMappingService _storeMappingService; - private readonly ICatalogSearchService _catalogSearchService; - private readonly SeoSettings _seoSettings; - - public CopyProductService( - IRepository productRepository, - IRepository relatedProductRepository, - IRepository crossSellProductRepository, - ICommonServices services, - IProductService productService, + private readonly IStoreMappingService _storeMappingService; + private readonly ICatalogSearchService _catalogSearchService; + private readonly SeoSettings _seoSettings; + + public CopyProductService( + IRepository productRepository, + IRepository relatedProductRepository, + IRepository crossSellProductRepository, + ICommonServices services, + IProductService productService, IProductAttributeService productAttributeService, - ILanguageService languageService, + ILanguageService languageService, ILocalizedEntityService localizedEntityService, IDownloadService downloadService, IProductAttributeParser productAttributeParser, - IUrlRecordService urlRecordService, - IStoreMappingService storeMappingService, - ICatalogSearchService catalogSearchService, - SeoSettings seoSettings) + IUrlRecordService urlRecordService, + IStoreMappingService storeMappingService, + ICatalogSearchService catalogSearchService, + SeoSettings seoSettings) { - _productRepository = productRepository; - _relatedProductRepository = relatedProductRepository; - _crossSellProductRepository = crossSellProductRepository; - _services = services; - _productService = productService; + _productRepository = productRepository; + _relatedProductRepository = relatedProductRepository; + _crossSellProductRepository = crossSellProductRepository; + _services = services; + _productService = productService; _productAttributeService = productAttributeService; _languageService = languageService; _localizedEntityService = localizedEntityService; _downloadService = downloadService; _productAttributeParser = productAttributeParser; - _urlRecordService = urlRecordService; - _storeMappingService = storeMappingService; - _catalogSearchService = catalogSearchService; - _seoSettings = seoSettings; + _urlRecordService = urlRecordService; + _storeMappingService = storeMappingService; + _catalogSearchService = catalogSearchService; + _seoSettings = seoSettings; } - public Localizer T { get; set; } = NullLocalizer.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; public virtual Product CopyProduct( - Product product, - string newName, - bool isPublished, - bool copyAssociatedProducts = true) + Product product, + string newName, + bool isPublished, + bool copyAssociatedProducts = true) { - Guard.NotNull(product, nameof(product)); - Guard.NotEmpty(newName, nameof(newName)); - - using (_services.Chronometer.Step("Copy product " + product.Id)) - { - Product clone = null; - var utcNow = DateTime.UtcNow; - var languages = _languageService.GetAllLanguages(true); - int? sampleDownloadId = null; - - using (var scope = new DbContextScope(ctx: _productRepository.Context, - autoCommit: false, - autoDetectChanges: false, - proxyCreation: true, - validateOnSave: false, - forceNoTracking: true, - hooksEnabled: false)) - { - if (product.HasSampleDownload) - { + Guard.NotNull(product, nameof(product)); + Guard.NotEmpty(newName, nameof(newName)); + + using (_services.Chronometer.Step("Copy product " + product.Id)) + { + Product clone = null; + var utcNow = DateTime.UtcNow; + var languages = _languageService.GetAllLanguages(true); + int? sampleDownloadId = null; + + using (var scope = new DbContextScope(ctx: _productRepository.Context, + autoCommit: false, + autoDetectChanges: false, + proxyCreation: true, + validateOnSave: false, + forceNoTracking: true, + hooksEnabled: false)) + { + if (product.HasSampleDownload) + { var sampleDownload = _downloadService.GetDownloadById((int)product.SampleDownloadId); - var sampleDownloadClone = CopyDownload(sampleDownload); + var sampleDownloadClone = CopyDownload(sampleDownload); sampleDownloadId = sampleDownloadClone.Id; } - // Product - clone = new Product - { - ProductTypeId = product.ProductTypeId, - ParentGroupedProductId = product.ParentGroupedProductId, + // Product + clone = new Product + { + ProductTypeId = product.ProductTypeId, + ParentGroupedProductId = product.ParentGroupedProductId, Visibility = product.Visibility, Condition = product.Condition, - Name = newName, - ShortDescription = product.ShortDescription, - FullDescription = product.FullDescription, - ProductTemplateId = product.ProductTemplateId, - AdminComment = product.AdminComment, - ShowOnHomePage = product.ShowOnHomePage, - HomePageDisplayOrder = product.HomePageDisplayOrder, - MetaKeywords = product.MetaKeywords, - MetaDescription = product.MetaDescription, - MetaTitle = product.MetaTitle, - AllowCustomerReviews = product.AllowCustomerReviews, - LimitedToStores = product.LimitedToStores, - Sku = product.Sku, - ManufacturerPartNumber = product.ManufacturerPartNumber, - Gtin = product.Gtin, - IsGiftCard = product.IsGiftCard, - GiftCardType = product.GiftCardType, - RequireOtherProducts = product.RequireOtherProducts, - RequiredProductIds = product.RequiredProductIds, - AutomaticallyAddRequiredProducts = product.AutomaticallyAddRequiredProducts, - IsDownload = product.IsDownload, - UnlimitedDownloads = product.UnlimitedDownloads, - MaxNumberOfDownloads = product.MaxNumberOfDownloads, - DownloadExpirationDays = product.DownloadExpirationDays, - DownloadActivationType = product.DownloadActivationType, - HasSampleDownload = product.HasSampleDownload, - SampleDownloadId = sampleDownloadId, - HasUserAgreement = product.HasUserAgreement, - UserAgreementText = product.UserAgreementText, - IsRecurring = product.IsRecurring, - RecurringCycleLength = product.RecurringCycleLength, - RecurringCyclePeriod = product.RecurringCyclePeriod, - RecurringTotalCycles = product.RecurringTotalCycles, - IsShipEnabled = product.IsShipEnabled, - IsFreeShipping = product.IsFreeShipping, - AdditionalShippingCharge = product.AdditionalShippingCharge, - IsEsd = product.IsEsd, - IsTaxExempt = product.IsTaxExempt, - TaxCategoryId = product.TaxCategoryId, - ManageInventoryMethod = product.ManageInventoryMethod, - StockQuantity = product.StockQuantity, - DisplayStockAvailability = product.DisplayStockAvailability, - DisplayStockQuantity = product.DisplayStockQuantity, - MinStockQuantity = product.MinStockQuantity, - LowStockActivityId = product.LowStockActivityId, - NotifyAdminForQuantityBelow = product.NotifyAdminForQuantityBelow, - BackorderMode = product.BackorderMode, - AllowBackInStockSubscriptions = product.AllowBackInStockSubscriptions, - OrderMinimumQuantity = product.OrderMinimumQuantity, - OrderMaximumQuantity = product.OrderMaximumQuantity, - QuantityStep = product.QuantityStep, - QuantiyControlType = product.QuantiyControlType, - HideQuantityControl = product.HideQuantityControl, - AllowedQuantities = product.AllowedQuantities, - DisableBuyButton = product.DisableBuyButton, - DisableWishlistButton = product.DisableWishlistButton, - AvailableForPreOrder = product.AvailableForPreOrder, - CallForPrice = product.CallForPrice, - Price = product.Price, - OldPrice = product.OldPrice, - ProductCost = product.ProductCost, - SpecialPrice = product.SpecialPrice, - SpecialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc, - SpecialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc, - CustomerEntersPrice = product.CustomerEntersPrice, - MinimumCustomerEnteredPrice = product.MinimumCustomerEnteredPrice, - MaximumCustomerEnteredPrice = product.MaximumCustomerEnteredPrice, - LowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice, - Weight = product.Weight, - Length = product.Length, - Width = product.Width, - Height = product.Height, - AvailableStartDateTimeUtc = product.AvailableStartDateTimeUtc, - AvailableEndDateTimeUtc = product.AvailableEndDateTimeUtc, - DisplayOrder = product.DisplayOrder, - Published = isPublished, - Deleted = product.Deleted, - IsSystemProduct = product.IsSystemProduct, - DeliveryTimeId = product.DeliveryTimeId, - QuantityUnitId = product.QuantityUnitId, - BasePriceEnabled = product.BasePriceEnabled, - BasePriceMeasureUnit = product.BasePriceMeasureUnit, - BasePriceAmount = product.BasePriceAmount, - BasePriceBaseAmount = product.BasePriceBaseAmount, - BundleTitleText = product.BundleTitleText, - BundlePerItemShipping = product.BundlePerItemShipping, - BundlePerItemPricing = product.BundlePerItemPricing, - BundlePerItemShoppingCart = product.BundlePerItemShoppingCart, - CustomsTariffNumber = product.CustomsTariffNumber, - CountryOfOriginId = product.CountryOfOriginId, - CreatedOnUtc = utcNow, - UpdatedOnUtc = utcNow - }; - - // Category mappings - foreach (var pc in product.ProductCategories) - { - clone.ProductCategories.Add(new ProductCategory - { - CategoryId = pc.CategoryId, - IsFeaturedProduct = pc.IsFeaturedProduct, - DisplayOrder = pc.DisplayOrder - }); - } - - // Manufacturer mappings - foreach (var pm in product.ProductManufacturers) - { - clone.ProductManufacturers.Add(new ProductManufacturer - { - ManufacturerId = pm.ManufacturerId, - IsFeaturedProduct = pm.IsFeaturedProduct, - DisplayOrder = pm.DisplayOrder - }); - } + Name = newName, + ShortDescription = product.ShortDescription, + FullDescription = product.FullDescription, + ProductTemplateId = product.ProductTemplateId, + AdminComment = product.AdminComment, + ShowOnHomePage = product.ShowOnHomePage, + HomePageDisplayOrder = product.HomePageDisplayOrder, + MetaKeywords = product.MetaKeywords, + MetaDescription = product.MetaDescription, + MetaTitle = product.MetaTitle, + AllowCustomerReviews = product.AllowCustomerReviews, + LimitedToStores = product.LimitedToStores, + Sku = product.Sku, + ManufacturerPartNumber = product.ManufacturerPartNumber, + Gtin = product.Gtin, + IsGiftCard = product.IsGiftCard, + GiftCardType = product.GiftCardType, + RequireOtherProducts = product.RequireOtherProducts, + RequiredProductIds = product.RequiredProductIds, + AutomaticallyAddRequiredProducts = product.AutomaticallyAddRequiredProducts, + IsDownload = product.IsDownload, + UnlimitedDownloads = product.UnlimitedDownloads, + MaxNumberOfDownloads = product.MaxNumberOfDownloads, + DownloadExpirationDays = product.DownloadExpirationDays, + DownloadActivationType = product.DownloadActivationType, + HasSampleDownload = product.HasSampleDownload, + SampleDownloadId = sampleDownloadId, + HasUserAgreement = product.HasUserAgreement, + UserAgreementText = product.UserAgreementText, + IsRecurring = product.IsRecurring, + RecurringCycleLength = product.RecurringCycleLength, + RecurringCyclePeriod = product.RecurringCyclePeriod, + RecurringTotalCycles = product.RecurringTotalCycles, + IsShipEnabled = product.IsShipEnabled, + IsFreeShipping = product.IsFreeShipping, + AdditionalShippingCharge = product.AdditionalShippingCharge, + IsEsd = product.IsEsd, + IsTaxExempt = product.IsTaxExempt, + TaxCategoryId = product.TaxCategoryId, + ManageInventoryMethod = product.ManageInventoryMethod, + StockQuantity = product.StockQuantity, + DisplayStockAvailability = product.DisplayStockAvailability, + DisplayStockQuantity = product.DisplayStockQuantity, + MinStockQuantity = product.MinStockQuantity, + LowStockActivityId = product.LowStockActivityId, + NotifyAdminForQuantityBelow = product.NotifyAdminForQuantityBelow, + BackorderMode = product.BackorderMode, + AllowBackInStockSubscriptions = product.AllowBackInStockSubscriptions, + OrderMinimumQuantity = product.OrderMinimumQuantity, + OrderMaximumQuantity = product.OrderMaximumQuantity, + QuantityStep = product.QuantityStep, + QuantiyControlType = product.QuantiyControlType, + HideQuantityControl = product.HideQuantityControl, + AllowedQuantities = product.AllowedQuantities, + DisableBuyButton = product.DisableBuyButton, + DisableWishlistButton = product.DisableWishlistButton, + AvailableForPreOrder = product.AvailableForPreOrder, + CallForPrice = product.CallForPrice, + Price = product.Price, + OldPrice = product.OldPrice, + ProductCost = product.ProductCost, + SpecialPrice = product.SpecialPrice, + SpecialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc, + SpecialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc, + CustomerEntersPrice = product.CustomerEntersPrice, + MinimumCustomerEnteredPrice = product.MinimumCustomerEnteredPrice, + MaximumCustomerEnteredPrice = product.MaximumCustomerEnteredPrice, + LowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice, + AttributeChoiceBehaviour = product.AttributeChoiceBehaviour, + Weight = product.Weight, + Length = product.Length, + Width = product.Width, + Height = product.Height, + AvailableStartDateTimeUtc = product.AvailableStartDateTimeUtc, + AvailableEndDateTimeUtc = product.AvailableEndDateTimeUtc, + DisplayOrder = product.DisplayOrder, + Published = isPublished, + Deleted = product.Deleted, + IsSystemProduct = product.IsSystemProduct, + DeliveryTimeId = product.DeliveryTimeId, + QuantityUnitId = product.QuantityUnitId, + BasePriceEnabled = product.BasePriceEnabled, + BasePriceMeasureUnit = product.BasePriceMeasureUnit, + BasePriceAmount = product.BasePriceAmount, + BasePriceBaseAmount = product.BasePriceBaseAmount, + BundleTitleText = product.BundleTitleText, + BundlePerItemShipping = product.BundlePerItemShipping, + BundlePerItemPricing = product.BundlePerItemPricing, + BundlePerItemShoppingCart = product.BundlePerItemShoppingCart, + CustomsTariffNumber = product.CustomsTariffNumber, + CountryOfOriginId = product.CountryOfOriginId, + CreatedOnUtc = utcNow, + UpdatedOnUtc = utcNow + }; + + // Category mappings + foreach (var pc in product.ProductCategories) + { + clone.ProductCategories.Add(new ProductCategory + { + CategoryId = pc.CategoryId, + IsFeaturedProduct = pc.IsFeaturedProduct, + DisplayOrder = pc.DisplayOrder + }); + } + + // Manufacturer mappings + foreach (var pm in product.ProductManufacturers) + { + clone.ProductManufacturers.Add(new ProductManufacturer + { + ManufacturerId = pm.ManufacturerId, + IsFeaturedProduct = pm.IsFeaturedProduct, + DisplayOrder = pm.DisplayOrder + }); + } // Picture mappings foreach (var pp in product.ProductPictures) @@ -238,121 +239,121 @@ public virtual Product CopyProduct( // Product specifications foreach (var psa in product.ProductSpecificationAttributes) - { - clone.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - SpecificationAttributeOptionId = psa.SpecificationAttributeOptionId, - AllowFiltering = psa.AllowFiltering, - ShowOnProductPage = psa.ShowOnProductPage, - DisplayOrder = psa.DisplayOrder - }); - } - - // Tier prices - foreach (var tp in product.TierPrices) - { - clone.TierPrices.Add(new TierPrice - { - StoreId = tp.StoreId, - CustomerRoleId = tp.CustomerRoleId, - Quantity = tp.Quantity, - Price = tp.Price, - CalculationMethod = tp.CalculationMethod - }); + { + clone.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + SpecificationAttributeOptionId = psa.SpecificationAttributeOptionId, + AllowFiltering = psa.AllowFiltering, + ShowOnProductPage = psa.ShowOnProductPage, + DisplayOrder = psa.DisplayOrder + }); + } + + // Tier prices + foreach (var tp in product.TierPrices) + { + clone.TierPrices.Add(new TierPrice + { + StoreId = tp.StoreId, + CustomerRoleId = tp.CustomerRoleId, + Quantity = tp.Quantity, + Price = tp.Price, + CalculationMethod = tp.CalculationMethod + }); clone.HasTierPrices = true; } - // Discount mapping - foreach (var discount in product.AppliedDiscounts) - { + // Discount mapping + foreach (var discount in product.AppliedDiscounts) + { clone.AppliedDiscounts.Add(discount); clone.HasDiscountsApplied = true; } - // Tags - foreach (var tag in product.ProductTags) - { - clone.ProductTags.Add(tag); - } - - // >>>>>>> Put clone to db (from here on we need the product clone's ID) - _productRepository.Insert(clone); - Commit(); - - // Related products mapping - foreach (var rp in _productService.GetRelatedProductsByProductId1(product.Id, true)) - { - _relatedProductRepository.Insert(new RelatedProduct - { - ProductId1 = clone.Id, - ProductId2 = rp.ProductId2, - DisplayOrder = rp.DisplayOrder - }); - } - - // CrossSell products mapping - foreach (var csp in _productService.GetCrossSellProductsByProductId1(product.Id, true)) - { - _crossSellProductRepository.Insert(new CrossSellProduct - { - ProductId1 = clone.Id, - ProductId2 = csp.ProductId2 - }); - } - - // Store mapping - var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(product); - foreach (var id in selectedStoreIds) - { - _storeMappingService.InsertStoreMapping(clone, id); - } - - // SEO - ProcessSlug(clone); - - // Localization - ProcessLocalization(product, clone, languages); - - // Attributes and attribute combinations. + // Tags + foreach (var tag in product.ProductTags) + { + clone.ProductTags.Add(tag); + } + + // >>>>>>> Put clone to db (from here on we need the product clone's ID) + _productRepository.Insert(clone); + Commit(); + + // Related products mapping + foreach (var rp in _productService.GetRelatedProductsByProductId1(product.Id, true)) + { + _relatedProductRepository.Insert(new RelatedProduct + { + ProductId1 = clone.Id, + ProductId2 = rp.ProductId2, + DisplayOrder = rp.DisplayOrder + }); + } + + // CrossSell products mapping + foreach (var csp in _productService.GetCrossSellProductsByProductId1(product.Id, true)) + { + _crossSellProductRepository.Insert(new CrossSellProduct + { + ProductId1 = clone.Id, + ProductId2 = csp.ProductId2 + }); + } + + // Store mapping + var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(product); + foreach (var id in selectedStoreIds) + { + _storeMappingService.InsertStoreMapping(clone, id); + } + + // SEO + ProcessSlug(clone); + + // Localization + ProcessLocalization(product, clone, languages); + + // Attributes and attribute combinations. ProcessAttributes(product, clone, languages); // Update computed properties. clone.LowestAttributeCombinationPrice = _productAttributeService.GetLowestCombinationPrice(clone.Id); - // Associated products. - if (copyAssociatedProducts && product.ProductType != ProductType.BundledProduct) - { - ProcessAssociatedProducts(product, clone, isPublished); - } + // Associated products. + if (copyAssociatedProducts && product.ProductType != ProductType.BundledProduct) + { + ProcessAssociatedProducts(product, clone, isPublished); + } - // Bundle items - ProcessBundleItems(product, clone); + // Bundle items + ProcessBundleItems(product, clone); // Downloads CopyDownloads(product.Id, clone.Id, "Product"); - + // >>>>>>> Our final commit Commit(); - } + } - return clone; - } + return clone; + } } - private void Commit() - { - _services.DbContext.SaveChanges(); - } + private void Commit() + { + _services.DbContext.SaveChanges(); + } - private Dictionary CopyDownloads(int productId, int cloneId, string entityName = "") - { - var downloads = _downloadService.GetDownloadsFor(productId, entityName); + private Dictionary CopyDownloads(int productId, int cloneId, string entityName = "") + { + var downloads = _downloadService.GetDownloadsFor(productId, entityName); var clonedDownloads = new Dictionary(); if (!downloads.Any()) - { - return null; - } + { + return null; + } using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) { @@ -362,10 +363,10 @@ private Dictionary CopyDownloads(int productId, int cloneId, stri var clone = CopyDownload(download); clonedDownloads[clone.Id] = clone; } - } - - return clonedDownloads; - } + } + + return clonedDownloads; + } private Download CopyDownload(Download download) { @@ -374,7 +375,7 @@ private Download CopyDownload(Download download) DownloadGuid = Guid.NewGuid(), UseDownloadUrl = download.UseDownloadUrl, DownloadUrl = download.DownloadUrl, - MediaFile = download.MediaFile, + MediaFile = download.MediaFile, UpdatedOnUtc = DateTime.UtcNow, EntityId = download.EntityId, EntityName = download.EntityName, @@ -382,96 +383,96 @@ private Download CopyDownload(Download download) FileVersion = download.FileVersion }; - _services.DbContext.Set().Add(clone); - _services.DbContext.SaveChanges(); + _services.DbContext.Set().Add(clone); + _services.DbContext.SaveChanges(); return clone; } private void ProcessSlug(Product clone) - { - using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) - { - var slug = clone.ValidateSeName("", clone.Name, true, _urlRecordService, _seoSettings); - _urlRecordService.SaveSlug(clone, slug, 0); - } - } - - private void ProcessLocalization(Product product, Product clone, IEnumerable languages) - { - using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) - { - foreach (var lang in languages) - { - var name = product.GetLocalized(x => x.Name, lang, false, false); - if (!String.IsNullOrEmpty(name)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.Name, name, lang.Id); - - var shortDescription = product.GetLocalized(x => x.ShortDescription, lang, false, false); - if (!String.IsNullOrEmpty(shortDescription)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.ShortDescription, shortDescription, lang.Id); - - var fullDescription = product.GetLocalized(x => x.FullDescription, lang, false, false); - if (!String.IsNullOrEmpty(fullDescription)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.FullDescription, fullDescription, lang.Id); - - var metaKeywords = product.GetLocalized(x => x.MetaKeywords, lang, false, false); - if (!String.IsNullOrEmpty(metaKeywords)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaKeywords, metaKeywords, lang.Id); - - var metaDescription = product.GetLocalized(x => x.MetaDescription, lang, false, false); - if (!String.IsNullOrEmpty(metaDescription)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaDescription, metaDescription, lang.Id); - - var metaTitle = product.GetLocalized(x => x.MetaTitle, lang, false, false); - if (!String.IsNullOrEmpty(metaTitle)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaTitle, metaTitle, lang.Id); - - var bundleTitleText = product.GetLocalized(x => x.BundleTitleText, lang, false, false); - if (!String.IsNullOrEmpty(bundleTitleText)) - _localizedEntityService.SaveLocalizedValue(clone, x => x.BundleTitleText, bundleTitleText, lang.Id); - - // Search engine name. - var slug = clone.ValidateSeName("", name, false, _urlRecordService, _seoSettings, lang.Id); - _urlRecordService.SaveSlug(clone, slug, lang.Id); - } - } - } - - private void ProcessAssociatedProducts(Product product, Product clone, bool isPublished) - { - var searchQuery = new CatalogSearchQuery().HasParentGroupedProduct(product.Id); - - var query = _catalogSearchService.PrepareQuery(searchQuery); - var associatedProducts = query.OrderBy(p => p.DisplayOrder).ToList(); - - foreach (var associatedProduct in associatedProducts) - { - var associatedProductCopy = CopyProduct(associatedProduct, T("Admin.Common.CopyOf", associatedProduct.Name), isPublished, false); - associatedProductCopy.ParentGroupedProductId = clone.Id; - } - } - - private void ProcessBundleItems(Product product, Product clone) - { - var bundledItems = _productService.GetBundleItems(product.Id, true); - - foreach (var bundleItem in bundledItems) - { - var newBundleItem = bundleItem.Item.Clone(); - newBundleItem.BundleProductId = clone.Id; - - _productService.InsertBundleItem(newBundleItem); - - foreach (var itemFilter in bundleItem.Item.AttributeFilters) - { - var newItemFilter = itemFilter.Clone(); - newItemFilter.BundleItemId = newBundleItem.Id; - - _productAttributeService.InsertProductBundleItemAttributeFilter(newItemFilter); - } - } - } + { + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) + { + var slug = clone.ValidateSeName("", clone.Name, true, _urlRecordService, _seoSettings); + _urlRecordService.SaveSlug(clone, slug, 0); + } + } + + private void ProcessLocalization(Product product, Product clone, IEnumerable languages) + { + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) + { + foreach (var lang in languages) + { + var name = product.GetLocalized(x => x.Name, lang, false, false); + if (!String.IsNullOrEmpty(name)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.Name, name, lang.Id); + + var shortDescription = product.GetLocalized(x => x.ShortDescription, lang, false, false); + if (!String.IsNullOrEmpty(shortDescription)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.ShortDescription, shortDescription, lang.Id); + + var fullDescription = product.GetLocalized(x => x.FullDescription, lang, false, false); + if (!String.IsNullOrEmpty(fullDescription)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.FullDescription, fullDescription, lang.Id); + + var metaKeywords = product.GetLocalized(x => x.MetaKeywords, lang, false, false); + if (!String.IsNullOrEmpty(metaKeywords)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaKeywords, metaKeywords, lang.Id); + + var metaDescription = product.GetLocalized(x => x.MetaDescription, lang, false, false); + if (!String.IsNullOrEmpty(metaDescription)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaDescription, metaDescription, lang.Id); + + var metaTitle = product.GetLocalized(x => x.MetaTitle, lang, false, false); + if (!String.IsNullOrEmpty(metaTitle)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaTitle, metaTitle, lang.Id); + + var bundleTitleText = product.GetLocalized(x => x.BundleTitleText, lang, false, false); + if (!String.IsNullOrEmpty(bundleTitleText)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.BundleTitleText, bundleTitleText, lang.Id); + + // Search engine name. + var slug = clone.ValidateSeName("", name, false, _urlRecordService, _seoSettings, lang.Id); + _urlRecordService.SaveSlug(clone, slug, lang.Id); + } + } + } + + private void ProcessAssociatedProducts(Product product, Product clone, bool isPublished) + { + var searchQuery = new CatalogSearchQuery().HasParentGroupedProduct(product.Id); + + var query = _catalogSearchService.PrepareQuery(searchQuery); + var associatedProducts = query.OrderBy(p => p.DisplayOrder).ToList(); + + foreach (var associatedProduct in associatedProducts) + { + var associatedProductCopy = CopyProduct(associatedProduct, T("Admin.Common.CopyOf", associatedProduct.Name), isPublished, false); + associatedProductCopy.ParentGroupedProductId = clone.Id; + } + } + + private void ProcessBundleItems(Product product, Product clone) + { + var bundledItems = _productService.GetBundleItems(product.Id, true); + + foreach (var bundleItem in bundledItems) + { + var newBundleItem = bundleItem.Item.Clone(); + newBundleItem.BundleProductId = clone.Id; + + _productService.InsertBundleItem(newBundleItem); + + foreach (var itemFilter in bundleItem.Item.AttributeFilters) + { + var newItemFilter = itemFilter.Clone(); + newItemFilter.BundleItemId = newBundleItem.Id; + + _productAttributeService.InsertProductBundleItemAttributeFilter(newItemFilter); + } + } + } private void ProcessAttributes(Product product, Product clone, IEnumerable languages) { @@ -622,5 +623,5 @@ private void ProcessAttributes(Product product, Product clone, IEnumerable>>>>> Commit combinations. Commit(); } - } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs index ce2b90d9d2..7c7f4df528 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs @@ -14,7 +14,7 @@ namespace SmartStore.Services.Catalog.Extensions { - public class ProductUrlHelper + public class ProductUrlHelper { private readonly HttpRequestBase _httpRequest; private readonly ICommonServices _services; diff --git a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelperExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelperExtensions.cs index 296b46b366..249d8eec8c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelperExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelperExtensions.cs @@ -8,45 +8,45 @@ namespace SmartStore.Services.Catalog.Extensions { - public static class ProductUrlHelperExtensions - { - /// - /// Creates a product URL including variant query string. - /// - /// Product URL helper - /// Product identifier - /// Product SEO name - /// Bundle item identifier. Use 0 if it's not a bundle item. - /// Variant values - /// Product URL - public static string GetProductUrl( - this ProductUrlHelper helper, - int productId, - string productSeName, - int bundleItemId, - params ProductVariantAttributeValue[] variantValues) - { - Guard.NotZero(productId, nameof(productId)); - - var query = new ProductVariantQuery(); - - foreach (var value in variantValues) - { - var attribute = value.ProductVariantAttribute; - - query.AddVariant(new ProductVariantQueryItem(value.Id.ToString()) - { - ProductId = productId, - BundleItemId = bundleItemId, - AttributeId = attribute.ProductAttributeId, - VariantAttributeId = attribute.Id, - Alias = attribute.ProductAttribute.Alias, - ValueAlias = value.Alias - }); - } - - return helper.GetProductUrl(query, productSeName); - } + public static class ProductUrlHelperExtensions + { + /// + /// Creates a product URL including variant query string. + /// + /// Product URL helper + /// Product identifier + /// Product SEO name + /// Bundle item identifier. Use 0 if it's not a bundle item. + /// Variant values + /// Product URL + public static string GetProductUrl( + this ProductUrlHelper helper, + int productId, + string productSeName, + int bundleItemId, + params ProductVariantAttributeValue[] variantValues) + { + Guard.NotZero(productId, nameof(productId)); + + var query = new ProductVariantQuery(); + + foreach (var value in variantValues) + { + var attribute = value.ProductVariantAttribute; + + query.AddVariant(new ProductVariantQueryItem(value.Id.ToString()) + { + ProductId = productId, + BundleItemId = bundleItemId, + AttributeId = attribute.ProductAttributeId, + VariantAttributeId = attribute.Id, + Alias = attribute.ProductAttribute.Alias, + ValueAlias = value.Alias + }); + } + + return helper.GetProductUrl(query, productSeName); + } /// /// Creates a product URL including variant query string. @@ -57,75 +57,75 @@ public static string GetProductUrl( /// Product URL [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetProductUrl( - this ProductUrlHelper helper, - Product product, - params ProductVariantAttributeValue[] variantValues) - { - Guard.NotNull(product, nameof(product)); - - return helper.GetProductUrl(product.Id, product.GetSeName(), 0, variantValues); - } - - /// - /// Creates a product URL including variant query string. - /// - /// Product URL helper - /// Product SEO name - /// Organized shopping cart item - /// Product URL - public static string GetProductUrl( - this ProductUrlHelper helper, - string productSeName, - OrganizedShoppingCartItem cartItem) - { - Guard.NotNull(cartItem, nameof(cartItem)); - - var query = new ProductVariantQuery(); - var product = cartItem.Item.Product; - - if (product.ProductType != ProductType.BundledProduct) - { - helper.DeserializeQuery(query, product.Id, cartItem.Item.AttributesXml); - } - else if (cartItem.ChildItems != null && product.BundlePerItemPricing) - { - foreach (var childItem in cartItem.ChildItems.Where(x => x.Item.Id != cartItem.Item.Id)) - { - helper.DeserializeQuery(query, childItem.Item.ProductId, childItem.Item.AttributesXml, childItem.BundleItemData.Item.Id); - } - } - - return helper.GetProductUrl(query, productSeName); - } - - /// - /// Creates a product URL including variant query string. - /// - /// Product URL helper - /// Product SEO name - /// Order item - /// Product URL - public static string GetProductUrl( - this ProductUrlHelper helper, - string productSeName, - OrderItem orderItem) - { - Guard.NotNull(orderItem, nameof(orderItem)); - - var query = new ProductVariantQuery(); - - if (orderItem.Product.ProductType != ProductType.BundledProduct) - { - helper.DeserializeQuery(query, orderItem.ProductId, orderItem.AttributesXml); - } - else if (orderItem.Product.BundlePerItemPricing && orderItem.BundleData.HasValue()) - { - var bundleData = orderItem.GetBundleData(); - - bundleData.ForEach(x => helper.DeserializeQuery(query, x.ProductId, x.AttributesXml, x.BundleItemId)); - } - - return helper.GetProductUrl(query, productSeName); - } - } + this ProductUrlHelper helper, + Product product, + params ProductVariantAttributeValue[] variantValues) + { + Guard.NotNull(product, nameof(product)); + + return helper.GetProductUrl(product.Id, product.GetSeName(), 0, variantValues); + } + + /// + /// Creates a product URL including variant query string. + /// + /// Product URL helper + /// Product SEO name + /// Organized shopping cart item + /// Product URL + public static string GetProductUrl( + this ProductUrlHelper helper, + string productSeName, + OrganizedShoppingCartItem cartItem) + { + Guard.NotNull(cartItem, nameof(cartItem)); + + var query = new ProductVariantQuery(); + var product = cartItem.Item.Product; + + if (product.ProductType != ProductType.BundledProduct) + { + helper.DeserializeQuery(query, product.Id, cartItem.Item.AttributesXml); + } + else if (cartItem.ChildItems != null && product.BundlePerItemPricing) + { + foreach (var childItem in cartItem.ChildItems.Where(x => x.Item.Id != cartItem.Item.Id)) + { + helper.DeserializeQuery(query, childItem.Item.ProductId, childItem.Item.AttributesXml, childItem.BundleItemData.Item.Id); + } + } + + return helper.GetProductUrl(query, productSeName); + } + + /// + /// Creates a product URL including variant query string. + /// + /// Product URL helper + /// Product SEO name + /// Order item + /// Product URL + public static string GetProductUrl( + this ProductUrlHelper helper, + string productSeName, + OrderItem orderItem) + { + Guard.NotNull(orderItem, nameof(orderItem)); + + var query = new ProductVariantQuery(); + + if (orderItem.Product.ProductType != ProductType.BundledProduct) + { + helper.DeserializeQuery(query, orderItem.ProductId, orderItem.AttributesXml); + } + else if (orderItem.Product.BundlePerItemPricing && orderItem.BundleData.HasValue()) + { + var bundleData = orderItem.GetBundleData(); + + bundleData.ForEach(x => helper.DeserializeQuery(query, x.ProductId, x.AttributesXml, x.BundleItemId)); + } + + return helper.GetProductUrl(query, productSeName); + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductVariantQueryExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductVariantQueryExtensions.cs index e58af6d6ed..da76ad1190 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductVariantQueryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductVariantQueryExtensions.cs @@ -10,130 +10,130 @@ namespace SmartStore.Services.Catalog.Extensions { - public static class ProductVariantQueryExtensions - { - public static string CreateSelectedAttributesXml( - this ProductVariantQuery query, - int productId, - int bundleItemId, - IEnumerable variantAttributes, - IProductAttributeParser productAttributeParser, - ILocalizationService localization, - IDownloadService downloadService, - CatalogSettings catalogSettings, - HttpRequestBase request, - List warnings) - { - var result = string.Empty; + public static class ProductVariantQueryExtensions + { + public static string CreateSelectedAttributesXml( + this ProductVariantQuery query, + int productId, + int bundleItemId, + IEnumerable variantAttributes, + IProductAttributeParser productAttributeParser, + ILocalizationService localization, + IDownloadService downloadService, + CatalogSettings catalogSettings, + HttpRequestBase request, + List warnings) + { + var result = string.Empty; - foreach (var pva in variantAttributes) - { - var selectedItems = query.Variants.Where(x => - x.ProductId == productId && - x.BundleItemId == bundleItemId && - x.AttributeId == pva.ProductAttributeId && - x.VariantAttributeId == pva.Id); + foreach (var pva in variantAttributes) + { + var selectedItems = query.Variants.Where(x => + x.ProductId == productId && + x.BundleItemId == bundleItemId && + x.AttributeId == pva.ProductAttributeId && + x.VariantAttributeId == pva.Id); - switch (pva.AttributeControlType) - { - case AttributeControlType.DropdownList: - case AttributeControlType.RadioList: - case AttributeControlType.Boxes: - { - var firstItemValue = selectedItems.FirstOrDefault()?.Value; - if (firstItemValue.HasValue()) - { - var selectedAttributeId = firstItemValue.SplitSafe(",").SafeGet(0).ToInt(); - if (selectedAttributeId > 0) - { - result = productAttributeParser.AddProductAttribute(result, pva, selectedAttributeId.ToString()); - } - } - } - break; + switch (pva.AttributeControlType) + { + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + case AttributeControlType.Boxes: + { + var firstItemValue = selectedItems.FirstOrDefault()?.Value; + if (firstItemValue.HasValue()) + { + var selectedAttributeId = firstItemValue.SplitSafe(",").SafeGet(0).ToInt(); + if (selectedAttributeId > 0) + { + result = productAttributeParser.AddProductAttribute(result, pva, selectedAttributeId.ToString()); + } + } + } + break; - case AttributeControlType.Checkboxes: - foreach (var item in selectedItems) - { - var selectedAttributeId = item.Value.SplitSafe(",").SafeGet(0).ToInt(); - if (selectedAttributeId > 0) - { - result = productAttributeParser.AddProductAttribute(result, pva, selectedAttributeId.ToString()); - } - } - break; + case AttributeControlType.Checkboxes: + foreach (var item in selectedItems) + { + var selectedAttributeId = item.Value.SplitSafe(",").SafeGet(0).ToInt(); + if (selectedAttributeId > 0) + { + result = productAttributeParser.AddProductAttribute(result, pva, selectedAttributeId.ToString()); + } + } + break; - case AttributeControlType.TextBox: - case AttributeControlType.MultilineTextbox: - { - var selectedValue = string.Join(",", selectedItems.Select(x => x.Value)); - if (selectedValue.HasValue()) - { - result = productAttributeParser.AddProductAttribute(result, pva, selectedValue); - } - } - break; + case AttributeControlType.TextBox: + case AttributeControlType.MultilineTextbox: + { + var selectedValue = string.Join(",", selectedItems.Select(x => x.Value)); + if (selectedValue.HasValue()) + { + result = productAttributeParser.AddProductAttribute(result, pva, selectedValue); + } + } + break; - case AttributeControlType.Datepicker: - { - var firstItemDate = selectedItems.FirstOrDefault()?.Date; - if (firstItemDate.HasValue) - { - result = productAttributeParser.AddProductAttribute(result, pva, firstItemDate.Value.ToString("D")); - } - } - break; + case AttributeControlType.Datepicker: + { + var firstItemDate = selectedItems.FirstOrDefault()?.Date; + if (firstItemDate.HasValue) + { + result = productAttributeParser.AddProductAttribute(result, pva, firstItemDate.Value.ToString("D")); + } + } + break; - case AttributeControlType.FileUpload: - if (request == null) - { - var firstItemValue = selectedItems.FirstOrDefault()?.Value; - Guid.TryParse(firstItemValue, out var downloadGuid); + case AttributeControlType.FileUpload: + if (request == null) + { + var firstItemValue = selectedItems.FirstOrDefault()?.Value; + Guid.TryParse(firstItemValue, out var downloadGuid); - var download = downloadService.GetDownloadByGuid(downloadGuid); - if (download != null) - { + var download = downloadService.GetDownloadByGuid(downloadGuid); + if (download != null) + { if (download.IsTransient) { download.IsTransient = false; downloadService.UpdateDownload(download); } - result = productAttributeParser.AddProductAttribute(result, pva, download.DownloadGuid.ToString()); - } - } - else - { + result = productAttributeParser.AddProductAttribute(result, pva, download.DownloadGuid.ToString()); + } + } + else + { var postedFile = request.Files[ProductVariantQueryItem.CreateKey(productId, bundleItemId, pva.ProductAttributeId, pva.Id)]; - if (postedFile != null && postedFile.FileName.HasValue()) - { - if (postedFile.ContentLength > catalogSettings.FileUploadMaximumSizeBytes) - { - warnings.Add(localization.GetResource("ShoppingCart.MaximumUploadedFileSize").FormatInvariant((int)(catalogSettings.FileUploadMaximumSizeBytes / 1024))); - } - else - { - var download = new Download - { - DownloadGuid = Guid.NewGuid(), - UseDownloadUrl = false, - DownloadUrl = "", - UpdatedOnUtc = DateTime.UtcNow, + if (postedFile != null && postedFile.FileName.HasValue()) + { + if (postedFile.ContentLength > catalogSettings.FileUploadMaximumSizeBytes) + { + warnings.Add(localization.GetResource("ShoppingCart.MaximumUploadedFileSize").FormatInvariant((int)(catalogSettings.FileUploadMaximumSizeBytes / 1024))); + } + else + { + var download = new Download + { + DownloadGuid = Guid.NewGuid(), + UseDownloadUrl = false, + DownloadUrl = "", + UpdatedOnUtc = DateTime.UtcNow, EntityId = productId, EntityName = "ProductAttribute" }; downloadService.InsertDownload(download, postedFile.InputStream, postedFile.FileName); - result = productAttributeParser.AddProductAttribute(result, pva, download.DownloadGuid.ToString()); - } - } - } - break; - } - } + result = productAttributeParser.AddProductAttribute(result, pva, download.DownloadGuid.ToString()); + } + } + } + break; + } + } - return result; - } - } + return result; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IBackInStockSubscriptionService.cs b/src/Libraries/SmartStore.Services/Catalog/IBackInStockSubscriptionService.cs index eb802ed5cf..5fb2732286 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IBackInStockSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IBackInStockSubscriptionService.cs @@ -23,7 +23,7 @@ public partial interface IBackInStockSubscriptionService /// Page size /// Subscriptions IPagedList GetAllSubscriptionsByCustomerId(int customerId, - int storeId, int pageIndex, int pageSize); + int storeId, int pageIndex, int pageSize); /// /// Gets all subscriptions @@ -34,7 +34,7 @@ IPagedList GetAllSubscriptionsByCustomerId(int customer /// Page size /// Subscriptions IPagedList GetAllSubscriptionsByProductId(int productId, - int storeId, int pageIndex, int pageSize); + int storeId, int pageIndex, int pageSize); /// /// Gets all subscriptions diff --git a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs index bd7261d40e..c7659168e7 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs @@ -44,40 +44,40 @@ void InheritStoresIntoChildren(int categoryId, /// Whether to delete child categories or to set them to no parent. void DeleteCategory(Category category, bool deleteChilds = false); - /// - /// Builds LINQ query for categories - /// - /// Category name filter - /// A value indicating whether to show hidden records - /// Alias filter - /// Store identifier; 0 to load all records - /// Category query - IQueryable BuildCategoriesQuery( - string categoryName = "", - bool showHidden = false, - string alias = null, - int storeId = 0); + /// + /// Builds LINQ query for categories + /// + /// Category name filter + /// A value indicating whether to show hidden records + /// Alias filter + /// Store identifier; 0 to load all records + /// Category query + IQueryable BuildCategoriesQuery( + string categoryName = "", + bool showHidden = false, + string alias = null, + int storeId = 0); - /// - /// Gets all categories - /// - /// Category name filter - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Alias filter - /// (Obsolete) Whether to apply instances to the actual categories query. Never applied when is true - /// A value indicating whether categories without parent category in provided category list (source) should be ignored - /// Store identifier; 0 to load all records - /// Categories - IPagedList GetAllCategories( - string categoryName = "", - int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false, - string alias = null, - bool ignoreDetachedCategories = true, - int storeId = 0); + /// + /// Gets all categories + /// + /// Category name filter + /// Page index + /// Page size + /// A value indicating whether to show hidden records + /// Alias filter + /// (Obsolete) Whether to apply instances to the actual categories query. Never applied when is true + /// A value indicating whether categories without parent category in provided category list (source) should be ignored + /// Store identifier; 0 to load all records + /// Categories + IPagedList GetAllCategories( + string categoryName = "", + int pageIndex = 0, + int pageSize = int.MaxValue, + bool showHidden = false, + string alias = null, + bool ignoreDetachedCategories = true, + int storeId = 0); /// /// Gets all categories filtered by parent category identifier @@ -92,7 +92,7 @@ IPagedList GetAllCategories( /// /// Categories IList GetAllCategoriesDisplayedOnHomePage(); - + /// /// Gets a category /// @@ -140,10 +140,10 @@ IPagedList GetAllCategories( /// A value indicating whether to show hidden records /// Product a category mapping collection IPagedList GetProductCategoriesByCategoryId( - int categoryId, - int pageIndex, - int pageSize, - bool showHidden = false); + int categoryId, + int pageIndex, + int pageSize, + bool showHidden = false); /// /// Gets a product category mapping collection @@ -153,21 +153,21 @@ IPagedList GetProductCategoriesByCategoryId( /// Product category mapping collection IList GetProductCategoriesByProductId(int productId, bool showHidden = false); - /// - /// Gets product category mappings - /// - /// Product identifiers - /// A value indicating whether to filter categories with applied discounts - /// A value indicating whether to show hidden records - /// Map with product category mappings - Multimap GetProductCategoriesByProductIds(int[] productIds, bool? hasDiscountsApplied = null, bool showHidden = false); + /// + /// Gets product category mappings + /// + /// Product identifiers + /// A value indicating whether to filter categories with applied discounts + /// A value indicating whether to show hidden records + /// Map with product category mappings + Multimap GetProductCategoriesByProductIds(int[] productIds, bool? hasDiscountsApplied = null, bool showHidden = false); - /// - /// Gets product category mappings - /// - /// Category identifiers - /// Map with product category mappings - Multimap GetProductCategoriesByCategoryIds(int[] categoryIds); + /// + /// Gets product category mappings + /// + /// Category identifiers + /// Map with product category mappings + Multimap GetProductCategoriesByCategoryIds(int[] categoryIds); /// /// Gets a product category mapping @@ -188,84 +188,84 @@ IPagedList GetProductCategoriesByCategoryId( /// >Product category mapping void UpdateProductCategory(ProductCategory productCategory); - /// - /// Gets the category trail - /// - /// The category node - /// Trail - IEnumerable GetCategoryTrail(ICategoryNode node); + /// + /// Gets the category trail + /// + /// The category node + /// Trail + IEnumerable GetCategoryTrail(ICategoryNode node); - /// - /// Builds a category breadcrumb (path) for a particular category node - /// - /// The category node - /// The id of language. Pass null to skip localization. - /// How the category alias - if specified - should be appended to the category name (e.g. ({0})) - /// The separator string - /// Category breadcrumb path - string GetCategoryPath( - TreeNode treeNode, - int? languageId = null, - string aliasPattern = null, - string separator = " "); + /// + /// Builds a category breadcrumb (path) for a particular category node + /// + /// The category node + /// The id of language. Pass null to skip localization. + /// How the category alias - if specified - should be appended to the category name (e.g. ({0})) + /// The separator string + /// Category breadcrumb path + string GetCategoryPath( + TreeNode treeNode, + int? languageId = null, + string aliasPattern = null, + string separator = " "); - /// - /// Gets the tree representation of categories - /// - /// Specifies which node to return as root - /// false excludes unpublished and ACL-inaccessible categories - /// > 0 = apply store mapping, 0 to bypass store mapping - /// The category tree representation - /// - /// This method puts the tree result into application cache, so subsequent calls are very fast. - /// Localization is up to the caller because the nodes only contain unlocalized data. - /// Subscribe to the CategoryTreeChanged event if you need to evict cache entries which depend - /// on this method's result. - /// - TreeNode GetCategoryTree( - int rootCategoryId = 0, - bool includeHidden = false, - int storeId = 0); + /// + /// Gets the tree representation of categories + /// + /// Specifies which node to return as root + /// false excludes unpublished and ACL-inaccessible categories + /// > 0 = apply store mapping, 0 to bypass store mapping + /// The category tree representation + /// + /// This method puts the tree result into application cache, so subsequent calls are very fast. + /// Localization is up to the caller because the nodes only contain unlocalized data. + /// Subscribe to the CategoryTreeChanged event if you need to evict cache entries which depend + /// on this method's result. + /// + TreeNode GetCategoryTree( + int rootCategoryId = 0, + bool includeHidden = false, + int storeId = 0); - /// - /// Get count of all categories - /// - /// Count - int CountAllCategories(); - } + /// + /// Get count of all categories + /// + /// Count + int CountAllCategories(); + } - public static class ICategoryServiceExtensions - { - /// - /// Builds a category breadcrumb for a particular product - /// - /// The product - /// The id of language. Pass null to skip localization. - /// The id of store. Pass null to skip store filtering. - /// The separator string - /// Category breadcrumb for product - public static string GetCategoryPath(this ICategoryService categoryService, - Product product, - int? languageId = null, - int? storeId = null, - string separator = " ") - { - Guard.NotNull(product, nameof(product)); + public static class ICategoryServiceExtensions + { + /// + /// Builds a category breadcrumb for a particular product + /// + /// The product + /// The id of language. Pass null to skip localization. + /// The id of store. Pass null to skip store filtering. + /// The separator string + /// Category breadcrumb for product + public static string GetCategoryPath(this ICategoryService categoryService, + Product product, + int? languageId = null, + int? storeId = null, + string separator = " ") + { + Guard.NotNull(product, nameof(product)); - string result = string.Empty; + string result = string.Empty; - var pc = categoryService.GetProductCategoriesByProductId(product.Id).FirstOrDefault(); - if (pc != null) - { - var node = categoryService.GetCategoryTree(pc.CategoryId, false, storeId ?? 0); - if (node != null) - { - result = categoryService.GetCategoryPath(node, languageId, null, separator); - } - } + var pc = categoryService.GetProductCategoriesByProductId(product.Id).FirstOrDefault(); + if (pc != null) + { + var node = categoryService.GetCategoryTree(pc.CategoryId, false, storeId ?? 0); + if (node != null) + { + result = categoryService.GetCategoryPath(node, languageId, null, separator); + } + } - return result; - } - } + return result; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs index d07be96577..3d02e61910 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs @@ -7,18 +7,18 @@ namespace SmartStore.Services.Catalog /// public partial interface ICopyProductService { - /// - /// Creates a duplicate of a product with all it's dependencies. - /// - /// The product to copy. - /// The name of the product duplicate. - /// A value indicating whether the product duplicate should be published. - /// A value indicating whether to copy associated products. - /// Product duplicate. - Product CopyProduct( - Product product, - string newName, - bool isPublished, - bool copyAssociatedProducts = true); + /// + /// Creates a duplicate of a product with all it's dependencies. + /// + /// The product to copy. + /// The name of the product duplicate. + /// A value indicating whether the product duplicate should be published. + /// A value indicating whether to copy associated products. + /// Product duplicate. + Product CopyProduct( + Product product, + string newName, + bool isPublished, + bool copyAssociatedProducts = true); } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs index bd389fa21e..480c9196b5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs @@ -6,10 +6,10 @@ namespace SmartStore.Services.Catalog { - /// - /// Manufacturer service - /// - public partial interface IManufacturerService + /// + /// Manufacturer service + /// + public partial interface IManufacturerService { /// /// Deletes a manufacturer @@ -17,13 +17,13 @@ public partial interface IManufacturerService /// Manufacturer void DeleteManufacturer(Manufacturer manufacturer); - /// - /// Get manufacturer query - /// - /// A value indicating whether to show hidden records - /// Store identifier - /// Manufacturer query - IQueryable GetManufacturers(bool showHidden = false, int storeId = 0); + /// + /// Get manufacturer query + /// + /// A value indicating whether to show hidden records + /// Store identifier + /// Manufacturer query + IQueryable GetManufacturers(bool showHidden = false, int storeId = 0); /// /// Gets all manufacturers @@ -32,25 +32,25 @@ public partial interface IManufacturerService /// Manufacturer collection IList GetAllManufacturers(bool showHidden = false); - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// Whether to filter result by store identifier - /// A value indicating whether to show hidden records - /// Manufacturer collection - IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false); - - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// Page index - /// Page size - /// Whether to filter result by store identifier - /// A value indicating whether to show hidden records - /// Manufacturers - IPagedList GetAllManufacturers(string manufacturerName, + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturer collection + IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false); + + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Page index + /// Page size + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturers + IPagedList GetAllManufacturers(string manufacturerName, int pageIndex, int pageSize, int storeId = 0, bool showHidden = false); /// @@ -79,18 +79,18 @@ IPagedList GetAllManufacturers(string manufacturerName, /// Manufacturer void UpdateManufacturer(Manufacturer manufacturer); - /// - /// Update HasDiscountsApplied property (used for performance optimization) - /// - /// Manufacturer - void UpdateHasDiscountsApplied(Manufacturer manufacturer); - - /// - /// Deletes a product manufacturer mapping - /// - /// Product manufacturer mapping - void DeleteProductManufacturer(ProductManufacturer productManufacturer); - + /// + /// Update HasDiscountsApplied property (used for performance optimization) + /// + /// Manufacturer + void UpdateHasDiscountsApplied(Manufacturer manufacturer); + + /// + /// Deletes a product manufacturer mapping + /// + /// Product manufacturer mapping + void DeleteProductManufacturer(ProductManufacturer productManufacturer); + /// /// Gets product manufacturer collection /// diff --git a/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs index 9ae18bbdb1..90daa5573a 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs @@ -7,10 +7,10 @@ namespace SmartStore.Services.Catalog { - /// - /// Price calculation service - /// - public partial interface IPriceCalculationService + /// + /// Price calculation service + /// + public partial interface IPriceCalculationService { /// /// Creates a price calculation context @@ -22,183 +22,183 @@ public partial interface IPriceCalculationService /// Price calculation context PriceCalculationContext CreatePriceCalculationContext(IEnumerable products = null, Customer customer = null, int? storeId = null, bool includeHidden = true); - /// - /// Get product special price (is valid) - /// - /// Product - /// Product special price - decimal? GetSpecialPrice(Product product); - - /// - /// Gets the final price - /// - /// Product - /// A value indicating whether include discounts or not for final price computation - /// Final price - decimal GetFinalPrice(Product product, bool includeDiscounts); - - /// - /// Gets the final price - /// - /// Product - /// The customer - /// A value indicating whether include discounts or not for final price computation - /// Final price - decimal GetFinalPrice(Product product, - Customer customer, - bool includeDiscounts); - - /// - /// Gets the final price - /// - /// Product - /// The customer - /// Additional charge - /// A value indicating whether include discounts or not for final price computation - /// Final price - decimal GetFinalPrice(Product product, - Customer customer, - decimal additionalCharge, - bool includeDiscounts); - - /// - /// Gets the final price - /// - /// Product - /// The customer - /// Additional charge - /// A value indicating whether include discounts or not for final price computation - /// Shopping cart item quantity - /// A product bundle item - /// Object with cargo data for better performance - /// Final price - decimal GetFinalPrice(Product product, - Customer customer, - decimal additionalCharge, - bool includeDiscounts, - int quantity, - ProductBundleItemData bundleItem = null, - PriceCalculationContext context = null, + /// + /// Get product special price (is valid) + /// + /// Product + /// Product special price + decimal? GetSpecialPrice(Product product); + + /// + /// Gets the final price + /// + /// Product + /// A value indicating whether include discounts or not for final price computation + /// Final price + decimal GetFinalPrice(Product product, bool includeDiscounts); + + /// + /// Gets the final price + /// + /// Product + /// The customer + /// A value indicating whether include discounts or not for final price computation + /// Final price + decimal GetFinalPrice(Product product, + Customer customer, + bool includeDiscounts); + + /// + /// Gets the final price + /// + /// Product + /// The customer + /// Additional charge + /// A value indicating whether include discounts or not for final price computation + /// Final price + decimal GetFinalPrice(Product product, + Customer customer, + decimal additionalCharge, + bool includeDiscounts); + + /// + /// Gets the final price + /// + /// Product + /// The customer + /// Additional charge + /// A value indicating whether include discounts or not for final price computation + /// Shopping cart item quantity + /// A product bundle item + /// Object with cargo data for better performance + /// Final price + decimal GetFinalPrice(Product product, + Customer customer, + decimal additionalCharge, + bool includeDiscounts, + int quantity, + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null, bool isTierPrice = false); - /// - /// Gets the final price including bundle per-item pricing - /// - /// Product - /// Bundle items - /// The customer - /// Additional charge - /// A value indicating whether include discounts or not for final price computation - /// Shopping cart item quantity - /// A product bundle item - /// Object with cargo data for better performance - /// Final price - decimal GetFinalPrice(Product product, - IEnumerable bundleItems, - Customer customer, - decimal additionalCharge, - bool includeDiscounts, - int quantity, - ProductBundleItemData bundleItem = null, - PriceCalculationContext context = null); - - /// - /// Get the lowest possible price for a product. - /// - /// Product - /// The customer - /// Object with cargo data for better performance - /// Whether to display the from message. - /// The lowest price. - decimal GetLowestPrice(Product product, Customer customer, PriceCalculationContext context, out bool displayFromMessage); - - /// - /// Get the lowest price of a grouped product. - /// - /// Grouped product - /// The customer - /// Object with cargo data for better performance - /// Products associated to product - /// The associated product with the lowest price - /// The lowest price. - decimal? GetLowestPrice(Product product, Customer customer, PriceCalculationContext context, IEnumerable associatedProducts, out Product lowestPriceProduct); - - /// - /// Get the initial price including preselected attributes - /// - /// Product - /// The customer - /// The currency - /// Object with cargo data for better performance - /// Preselected price - decimal GetPreselectedPrice(Product product, Customer customer, Currency currency, PriceCalculationContext context); - - /// - /// Gets the product cost - /// - /// Product - /// Shopping cart item attributes in XML - /// Product cost - decimal GetProductCost(Product product, string attributesXml); - - /// - /// Gets discount amount - /// - /// Product - /// Discount amount - decimal GetDiscountAmount(Product product); - - /// - /// Gets discount amount - /// - /// Product - /// The customer - /// Discount amount - decimal GetDiscountAmount(Product product, - Customer customer); - - /// - /// Gets discount amount - /// - /// Product - /// The customer - /// Additional charge - /// Discount amount - decimal GetDiscountAmount(Product product, - Customer customer, - decimal additionalCharge); - - /// - /// Gets discount amount - /// - /// Product - /// The customer - /// Additional charge - /// Applied discount - /// Discount amount - decimal GetDiscountAmount(Product product, - Customer customer, - decimal additionalCharge, - out Discount appliedDiscount); - - /// - /// Gets discount amount - /// - /// Product - /// The customer - /// Additional charge - /// Product quantity - /// Applied discount - /// A product bundle item - /// Object with cargo data for better performance + /// + /// Gets the final price including bundle per-item pricing + /// + /// Product + /// Bundle items + /// The customer + /// Additional charge + /// A value indicating whether include discounts or not for final price computation + /// Shopping cart item quantity + /// A product bundle item + /// Object with cargo data for better performance + /// Final price + decimal GetFinalPrice(Product product, + IEnumerable bundleItems, + Customer customer, + decimal additionalCharge, + bool includeDiscounts, + int quantity, + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null); + + /// + /// Get the lowest possible price for a product. + /// + /// Product + /// The customer + /// Object with cargo data for better performance + /// Whether to display the from message. + /// The lowest price. + decimal GetLowestPrice(Product product, Customer customer, PriceCalculationContext context, out bool displayFromMessage); + + /// + /// Get the lowest price of a grouped product. + /// + /// Grouped product + /// The customer + /// Object with cargo data for better performance + /// Products associated to product + /// The associated product with the lowest price + /// The lowest price. + decimal? GetLowestPrice(Product product, Customer customer, PriceCalculationContext context, IEnumerable associatedProducts, out Product lowestPriceProduct); + + /// + /// Get the initial price including preselected attributes + /// + /// Product + /// The customer + /// The currency + /// Object with cargo data for better performance + /// Preselected price + decimal GetPreselectedPrice(Product product, Customer customer, Currency currency, PriceCalculationContext context); + + /// + /// Gets the product cost + /// + /// Product + /// Shopping cart item attributes in XML + /// Product cost + decimal GetProductCost(Product product, string attributesXml); + + /// + /// Gets discount amount + /// + /// Product + /// Discount amount + decimal GetDiscountAmount(Product product); + + /// + /// Gets discount amount + /// + /// Product + /// The customer + /// Discount amount + decimal GetDiscountAmount(Product product, + Customer customer); + + /// + /// Gets discount amount + /// + /// Product + /// The customer + /// Additional charge + /// Discount amount + decimal GetDiscountAmount(Product product, + Customer customer, + decimal additionalCharge); + + /// + /// Gets discount amount + /// + /// Product + /// The customer + /// Additional charge + /// Applied discount + /// Discount amount + decimal GetDiscountAmount(Product product, + Customer customer, + decimal additionalCharge, + out Discount appliedDiscount); + + /// + /// Gets discount amount + /// + /// Product + /// The customer + /// Additional charge + /// Product quantity + /// Applied discount + /// A product bundle item + /// Object with cargo data for better performance /// Final product price without discount. - /// Discount amount - decimal GetDiscountAmount(Product product, - Customer customer, - decimal additionalCharge, - int quantity, - out Discount appliedDiscount, - ProductBundleItemData bundleItem = null, - PriceCalculationContext context = null, + /// Discount amount + decimal GetDiscountAmount(Product product, + Customer customer, + decimal additionalCharge, + int quantity, + out Discount appliedDiscount, + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null, decimal? finalPrice = null); @@ -217,7 +217,7 @@ decimal GetDiscountAmount(Product product, /// A value indicating whether include discounts or not for price computation /// Shopping cart unit price (one item) decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, bool includeDiscounts); - + @@ -226,7 +226,7 @@ decimal GetDiscountAmount(Product product, /// /// The shopping cart item /// Discount amount - decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem); + decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem); /// /// Gets discount amount @@ -237,12 +237,12 @@ decimal GetDiscountAmount(Product product, decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem, out Discount appliedDiscount); - /// - /// Gets the price adjustment of a variant attribute value - /// - /// Product variant attribute value - /// Price adjustment of a variant attribute value - decimal GetProductVariantAttributeValuePriceAdjustment(ProductVariantAttributeValue attributeValue, + /// + /// Gets the price adjustment of a variant attribute value + /// + /// Product variant attribute value + /// Price adjustment of a variant attribute value + decimal GetProductVariantAttributeValuePriceAdjustment(ProductVariantAttributeValue attributeValue, Product product, Customer customer, PriceCalculationContext context, int productQuantity = 1); } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IPriceFormatter.cs b/src/Libraries/SmartStore.Services/Catalog/IPriceFormatter.cs index 584c3f40e9..fd814f7798 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IPriceFormatter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IPriceFormatter.cs @@ -66,17 +66,17 @@ public partial interface IPriceFormatter /// Price string FormatPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax); - /// - /// Formats the price - /// - /// Price - /// A value indicating whether to show a currency - /// Target currency - /// Language - /// A value indicating whether price includes tax - /// A value indicating whether to show tax suffix - /// Price - string FormatPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax); + /// + /// Formats the price + /// + /// Price + /// A value indicating whether to show a currency + /// Target currency + /// Language + /// A value indicating whether price includes tax + /// A value indicating whether to show tax suffix + /// Price + string FormatPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax); /// /// Formats the price @@ -110,19 +110,19 @@ public partial interface IPriceFormatter /// Price string FormatShippingPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax); - /// - /// Formats the shipping price - /// - /// Price - /// A value indicating whether to show a currency - /// Target currency code - /// Language - /// A value indicating whether price includes tax - /// A value indicating whether to show tax suffix - /// Price - string FormatShippingPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax); - - /// + /// + /// Formats the shipping price + /// + /// Price + /// A value indicating whether to show a currency + /// Target currency code + /// Language + /// A value indicating whether price includes tax + /// A value indicating whether to show tax suffix + /// Price + string FormatShippingPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax); + + /// /// Formats the shipping price /// /// Price @@ -133,7 +133,7 @@ public partial interface IPriceFormatter /// A value indicating whether to show tax suffix /// Price string FormatShippingPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax, bool showTax); - + /// /// Formats the shipping price /// @@ -166,17 +166,17 @@ public partial interface IPriceFormatter /// Price string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax); - /// - /// Formats the payment method additional fee - /// - /// Price - /// A value indicating whether to show a currency - /// Target currency code - /// Language - /// A value indicating whether price includes tax - /// A value indicating whether to show tax suffix - /// Price - string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax); + /// + /// Formats the payment method additional fee + /// + /// Price + /// A value indicating whether to show a currency + /// Target currency code + /// Language + /// A value indicating whether price includes tax + /// A value indicating whether to show tax suffix + /// Price + string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax); /// /// Formats the payment method additional fee diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeFormatter.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeFormatter.cs index d91cea4f4c..20cc18f050 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeFormatter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeFormatter.cs @@ -36,7 +36,7 @@ string FormatAttributes( string separator = "
", bool htmlEncode = true, bool renderPrices = true, - bool renderProductAttributes = true, + bool renderProductAttributes = true, bool renderGiftCardAttributes = true, bool allowHyperlinks = true); } diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs index 0017183f46..ce76d9bf37 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs @@ -1,36 +1,37 @@ +using System; using System.Collections.Generic; using SmartStore.Collections; using SmartStore.Core.Domain.Catalog; namespace SmartStore.Services.Catalog { - /// - /// Product attribute parser interface - /// - public partial interface IProductAttributeParser + /// + /// Product attribute parser interface + /// + public partial interface IProductAttributeParser { - #region Product attributes + #region Product attributes - /// - /// Parses, prefetches & caches all passed attribute definitions for the current request - /// - /// All attribute definitions to prefetch + /// + /// Parses, prefetches & caches all passed attribute definitions for the current request + /// + /// All attribute definitions to prefetch /// Number of fetched attributes - int PrefetchProductVariantAttributes(IEnumerable attributesXml); + int PrefetchProductVariantAttributes(IEnumerable attributesXml); - /// - /// Gets selected product variant attributes as a map of integer ids with their corresponding values. - /// - /// XML formatted attributes - /// The deserialized map - Multimap DeserializeProductVariantAttributes(string attributesXml); + /// + /// Gets selected product variant attributes as a map of integer ids with their corresponding values. + /// + /// XML formatted attributes + /// The deserialized map + Multimap DeserializeProductVariantAttributes(string attributesXml); - /// - /// Gets selected product variant attributes - /// - /// XML formatted attributes - /// Selected product variant attributes - IList ParseProductVariantAttributes(string attributesXml); + /// + /// Gets selected product variant attributes + /// + /// XML formatted attributes + /// Selected product variant attributes + IList ParseProductVariantAttributes(string attributesXml); /// /// Get product variant attribute values @@ -52,13 +53,13 @@ public partial interface IProductAttributeParser /// Collection of product variant attribute values ICollection ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes); - /// - /// Gets selected product variant attribute value - /// - /// XML formatted attributes - /// Product variant attribute identifier - /// Product variant attribute value - IList ParseValues(string attributesXml, int productVariantAttributeId); + /// + /// Gets selected product variant attribute value + /// + /// XML formatted attributes + /// Product variant attribute identifier + /// Product variant attribute value + IList ParseValues(string attributesXml, int productVariantAttributeId); /// /// Adds an attribute @@ -69,44 +70,59 @@ public partial interface IProductAttributeParser /// Attributes string AddProductAttribute(string attributesXml, ProductVariantAttribute pva, string value); - /// - /// Creates formatted xml for a list of product variant attribute values - /// - /// The attributes map - /// Attributes XML - string CreateAttributesXml(Multimap attributes); - - /// - /// Are attributes equal - /// - /// The attributes of the first product - /// The attributes of the second product - /// Result - bool AreProductAttributesEqual(string attributeXml1, string attributeXml2); - - /// - /// Finds a product variant attribute combination by attributes stored in XML - /// - /// Product identifier - /// XML formatted attributes - /// Found product variant attribute combination - ProductVariantAttributeCombination FindProductVariantAttributeCombination(int productId, string attributesXml); + /// + /// Creates formatted xml for a list of product variant attribute values + /// + /// The attributes map + /// Attributes XML + string CreateAttributesXml(Multimap attributes); - #endregion + /// + /// Are attributes equal + /// + /// The attributes of the first product + /// The attributes of the second product + /// Result + bool AreProductAttributesEqual(string attributeXml1, string attributeXml2); - #region Gift card attributes + /// + /// Finds a product variant attribute combination by attributes stored in XML + /// + /// Product identifier + /// XML formatted attributes + /// Found product variant attribute combination + ProductVariantAttributeCombination FindProductVariantAttributeCombination(int productId, string attributesXml); - /// - /// Add gift card attrbibutes - /// - /// XML formatted attributes - /// Recipient name - /// Recipient email - /// Sender name - /// Sender email - /// Message - /// Attributes - string AddGiftCardAttribute(string attributesXml, string recipientName, + /// + /// Returns informations about the availability of an attribute combination. + /// + /// Product. + /// All product attributes of the specified product. null to test availability of . + /// The attribute values of the currently selected attribute combination. + /// The current attribute value. null to test availability of . + /// Informations about the attribute combination's availability. null if the combination is available. + CombinationAvailabilityInfo IsCombinationAvailable( + Product product, + IEnumerable attributes, + IEnumerable selectedValues, + ProductVariantAttributeValue currentValue); + + #endregion + + #region Gift card attributes + + /// + /// Add gift card attrbibutes + /// + /// XML formatted attributes + /// Recipient name + /// Recipient email + /// Sender name + /// Sender email + /// Message + /// Attributes + /// Attributes + string AddGiftCardAttribute(string attributesXml, string recipientName, string recipientEmail, string senderName, string senderEmail, string giftCardMessage); /// @@ -124,4 +140,12 @@ void GetGiftCardAttribute(string attributesXml, out string recipientName, #endregion } + + + [Serializable] + public class CombinationAvailabilityInfo + { + public bool IsActive { get; set; } + public bool IsOutOfStock { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs index 396acb9a8a..9053d4dec3 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs @@ -46,101 +46,101 @@ public partial interface IProductAttributeService /// Product attribute void UpdateProductAttribute(ProductAttribute productAttribute); - /// - /// Gets the export mappings for a given field prefix. - /// - /// The export field prefix, e.g. gmc. - /// A multimap with export field names to ProductAttribute.Id mappings. - Multimap GetExportFieldMappings(string fieldPrefix); - - #endregion - - #region Product attribute options - - /// - /// Gets an attribute option by id - /// - /// Product attribute option identifier - /// Product attribute option - ProductAttributeOption GetProductAttributeOptionById(int id); - - /// - /// Gets all attribute options by options set identifier - /// - /// Attribute options set identifier - /// List of attribute options - IList GetProductAttributeOptionsByOptionsSetId(int optionsSetId); - - /// - /// Gets all attribute options by attribute identifier - /// - /// Attribute identifier - /// List of attribute options - IList GetProductAttributeOptionsByAttributeId(int attributeId); - - /// - /// Deletes an attribute option - /// - /// Product attribute option - void DeleteProductAttributeOption(ProductAttributeOption productAttributeOption); - - /// - /// Inserts an attribute option - /// - /// Product attribute option - void InsertProductAttributeOption(ProductAttributeOption productAttributeOption); - - /// - /// Updates an attribute option - /// - /// Product attribute option - void UpdateProductAttributeOption(ProductAttributeOption productAttributeOption); - - #endregion - - #region Product attribute options sets - - /// - /// Gets an attribute options set by id - /// - /// Product attribute options set identifier - /// Product attribute options set - ProductAttributeOptionsSet GetProductAttributeOptionsSetById(int id); - - /// - /// Gets all attribute options sets by attribute identifier - /// - /// - /// - IList GetProductAttributeOptionsSetsByAttributeId(int productAttributeId); - - /// - /// Deletes an attribute options set - /// - /// Product attribute options set - void DeleteProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet); - - /// - /// Inserts an attribute options set - /// - /// Product attribute options set - void InsertProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet); - - /// - /// Updates an attribute options set - /// - /// Product attribute options set - void UpdateProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet); - - #endregion - - #region Product variant attributes mappings (ProductVariantAttribute) - - /// - /// Deletes a product variant attribute mapping - /// - /// Product variant attribute mapping - void DeleteProductVariantAttribute(ProductVariantAttribute productVariantAttribute); + /// + /// Gets the export mappings for a given field prefix. + /// + /// The export field prefix, e.g. gmc. + /// A multimap with export field names to ProductAttribute.Id mappings. + Multimap GetExportFieldMappings(string fieldPrefix); + + #endregion + + #region Product attribute options + + /// + /// Gets an attribute option by id + /// + /// Product attribute option identifier + /// Product attribute option + ProductAttributeOption GetProductAttributeOptionById(int id); + + /// + /// Gets all attribute options by options set identifier + /// + /// Attribute options set identifier + /// List of attribute options + IList GetProductAttributeOptionsByOptionsSetId(int optionsSetId); + + /// + /// Gets all attribute options by attribute identifier + /// + /// Attribute identifier + /// List of attribute options + IList GetProductAttributeOptionsByAttributeId(int attributeId); + + /// + /// Deletes an attribute option + /// + /// Product attribute option + void DeleteProductAttributeOption(ProductAttributeOption productAttributeOption); + + /// + /// Inserts an attribute option + /// + /// Product attribute option + void InsertProductAttributeOption(ProductAttributeOption productAttributeOption); + + /// + /// Updates an attribute option + /// + /// Product attribute option + void UpdateProductAttributeOption(ProductAttributeOption productAttributeOption); + + #endregion + + #region Product attribute options sets + + /// + /// Gets an attribute options set by id + /// + /// Product attribute options set identifier + /// Product attribute options set + ProductAttributeOptionsSet GetProductAttributeOptionsSetById(int id); + + /// + /// Gets all attribute options sets by attribute identifier + /// + /// + /// + IList GetProductAttributeOptionsSetsByAttributeId(int productAttributeId); + + /// + /// Deletes an attribute options set + /// + /// Product attribute options set + void DeleteProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet); + + /// + /// Inserts an attribute options set + /// + /// Product attribute options set + void InsertProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet); + + /// + /// Updates an attribute options set + /// + /// Product attribute options set + void UpdateProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet); + + #endregion + + #region Product variant attributes mappings (ProductVariantAttribute) + + /// + /// Deletes a product variant attribute mapping + /// + /// Product variant attribute mapping + void DeleteProductVariantAttribute(ProductVariantAttribute productVariantAttribute); /// /// Gets product variant attribute mappings by product identifier @@ -149,13 +149,13 @@ public partial interface IProductAttributeService /// Product variant attribute mapping collection IList GetProductVariantAttributesByProductId(int productId); - /// - /// Gets product variant attribute mappings by multiple product identifiers - /// - /// The product identifiers - /// An optional control type filter. null loads all controls regardless of type. - /// A map with product id as key and a collection of variant attributes as value. - Multimap GetProductVariantAttributesByProductIds(int[] productIds, AttributeControlType? controlType); + /// + /// Gets product variant attribute mappings by multiple product identifiers + /// + /// The product identifiers + /// An optional control type filter. null loads all controls regardless of type. + /// A map with product id as key and a collection of variant attributes as value. + Multimap GetProductVariantAttributesByProductIds(int[] productIds, AttributeControlType? controlType); /// /// Gets a product variant attribute mapping @@ -164,13 +164,13 @@ public partial interface IProductAttributeService /// Product variant attribute mapping ProductVariantAttribute GetProductVariantAttributeById(int productVariantAttributeId); - /// - /// Gets product variant attribute mappings - /// - /// Enumerable of product variant attribute mapping identifiers - /// Collection of already loaded product attribute mappings to reduce database round trips - /// - IList GetProductVariantAttributesByIds(IEnumerable productVariantAttributeIds, IEnumerable attributes = null); + /// + /// Gets product variant attribute mappings + /// + /// Enumerable of product variant attribute mapping identifiers + /// Collection of already loaded product attribute mappings to reduce database round trips + /// + IList GetProductVariantAttributesByIds(IEnumerable productVariantAttributeIds, IEnumerable attributes = null); /// /// Inserts a product variant attribute mapping @@ -184,24 +184,24 @@ public partial interface IProductAttributeService /// The product variant attribute mapping void UpdateProductVariantAttribute(ProductVariantAttribute productVariantAttribute); - /// - /// Copies attribute options (if any) to product variant attribute values. Existing values are ignored (identified by name field). - /// - /// The product variant attribute mapping - /// Product attribute options set identifier - /// Indicates whether to delete all existing product variant attribute values - /// Number of inserted product variant attribute values - int CopyAttributeOptions(ProductVariantAttribute productVariantAttribute, int productAttributeOptionsSetId, bool deleteExistingValues); + /// + /// Copies attribute options (if any) to product variant attribute values. Existing values are ignored (identified by name field). + /// + /// The product variant attribute mapping + /// Product attribute options set identifier + /// Indicates whether to delete all existing product variant attribute values + /// Number of inserted product variant attribute values + int CopyAttributeOptions(ProductVariantAttribute productVariantAttribute, int productAttributeOptionsSetId, bool deleteExistingValues); - #endregion + #endregion - #region Product variant attribute values (ProductVariantAttributeValue) + #region Product variant attribute values (ProductVariantAttributeValue) - /// - /// Deletes a product variant attribute value - /// - /// Product variant attribute value - void DeleteProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue); + /// + /// Deletes a product variant attribute value + /// + /// Product variant attribute value + void DeleteProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue); /// /// Gets product variant attribute values by product identifier @@ -246,37 +246,37 @@ public partial interface IProductAttributeService /// Product variant attribute combination void DeleteProductVariantAttributeCombination(ProductVariantAttributeCombination combination); - /// - /// Gets all product variant attribute combinations - /// - /// Product identifier - /// Page index - /// Page size - /// Specifies whether loaded entities should be tracked by the state manager - /// Product variant attribute combination collection - IPagedList GetAllProductVariantAttributeCombinations(int productId, int pageIndex, int pageSize, bool untracked = true); - - /// - /// Gets a distinct list of picture identifiers. - /// Only pictures that are explicitly assigned to combinations are taken into account. - /// - /// Product id - /// Picture ids - IList GetAllProductVariantAttributeCombinationPictureIds(int productId); - - /// - /// Gets product variant attribute combinations by multiple product identifiers - /// - /// The product identifiers - /// A map with product id as key and a collection of product variant attribute combinations as value. - Multimap GetProductVariantAttributeCombinations(int[] productIds); - - /// - /// Get the lowest price of all combinations for a product - /// - /// Product identifier - /// Lowest price - decimal? GetLowestCombinationPrice(int productId); + /// + /// Gets all product variant attribute combinations + /// + /// Product identifier + /// Page index + /// Page size + /// Specifies whether loaded entities should be tracked by the state manager + /// Product variant attribute combination collection + IPagedList GetAllProductVariantAttributeCombinations(int productId, int pageIndex, int pageSize, bool untracked = true); + + /// + /// Gets a distinct list of picture identifiers. + /// Only pictures that are explicitly assigned to combinations are taken into account. + /// + /// Product id + /// Picture ids + IList GetAllProductVariantAttributeCombinationPictureIds(int productId); + + /// + /// Gets product variant attribute combinations by multiple product identifiers + /// + /// The product identifiers + /// A map with product id as key and a collection of product variant attribute combinations as value. + Multimap GetProductVariantAttributeCombinations(int[] productIds); + + /// + /// Get the lowest price of all combinations for a product + /// + /// Product identifier + /// Lowest price + decimal? GetLowestCombinationPrice(int productId); /// /// Gets a product variant attribute combination by identifier @@ -285,12 +285,12 @@ public partial interface IProductAttributeService /// Product variant attribute combination ProductVariantAttributeCombination GetProductVariantAttributeCombinationById(int productVariantAttributeCombinationId); - /// - /// Gets a product variant attribute combination by SKU - /// - /// SKU - /// Product variant attribute combination - ProductVariantAttributeCombination GetProductVariantAttributeCombinationBySku(string sku); + /// + /// Gets a product variant attribute combination by SKU + /// + /// SKU + /// Product variant attribute combination + ProductVariantAttributeCombination GetProductVariantAttributeCombinationBySku(string sku); /// /// Gets a product variant attribute combination by GTIN. @@ -318,10 +318,10 @@ public partial interface IProductAttributeService /// Product variant attribute combination void UpdateProductVariantAttributeCombination(ProductVariantAttributeCombination combination); - /// - /// Creates all variant attributes combinations - /// - void CreateAllProductVariantAttributeCombinations(Product product); + /// + /// Creates all variant attributes combinations + /// + void CreateAllProductVariantAttributeCombinations(Product product); /// /// Gets a value indicating the existence of any attribute combination for a product @@ -330,58 +330,58 @@ public partial interface IProductAttributeService #endregion - #region Product bundle item attribute filter - - /// - /// Inserts a product bundle item attribute filter - /// - /// Product bundle item attribute filter - void InsertProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter); - - /// - /// Updates the product bundle item attribute filter - /// - /// Product bundle item attribute filter - void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter); - - /// - /// Deletes a product bundle item attribute filter - /// - /// Product bundle item attribute filter - void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter); - - /// - /// Deletes all attribute filters of a bundle item - /// - /// Bundle item - void DeleteProductBundleItemAttributeFilter(ProductBundleItem bundleItem); - - /// - /// Deletes product bundle item attribute filters - /// - /// Attribute identifier - /// Attribute value identifier - void DeleteProductBundleItemAttributeFilter(int attributeId, int attributeValueId); - - /// - /// Deletes product bundle item attribute filters - /// - /// Attribute identifier - void DeleteProductBundleItemAttributeFilter(int attributeId); - - #endregion + #region Product bundle item attribute filter + + /// + /// Inserts a product bundle item attribute filter + /// + /// Product bundle item attribute filter + void InsertProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter); + + /// + /// Updates the product bundle item attribute filter + /// + /// Product bundle item attribute filter + void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter); + + /// + /// Deletes a product bundle item attribute filter + /// + /// Product bundle item attribute filter + void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter); + + /// + /// Deletes all attribute filters of a bundle item + /// + /// Bundle item + void DeleteProductBundleItemAttributeFilter(ProductBundleItem bundleItem); + + /// + /// Deletes product bundle item attribute filters + /// + /// Attribute identifier + /// Attribute value identifier + void DeleteProductBundleItemAttributeFilter(int attributeId, int attributeValueId); + + /// + /// Deletes product bundle item attribute filters + /// + /// Attribute identifier + void DeleteProductBundleItemAttributeFilter(int attributeId); + + #endregion } - public static class IProductAttributeServiceExtensions - { - /// - /// Gets all product variant attribute combinations - /// - /// Product identifier - /// Product variant attribute combination collection - public static IList GetAllProductVariantAttributeCombinations(this IProductAttributeService service, int productId) - { - return service.GetAllProductVariantAttributeCombinations(productId, 0, int.MaxValue, true); - } - } + public static class IProductAttributeServiceExtensions + { + /// + /// Gets all product variant attribute combinations + /// + /// Product identifier + /// Product variant attribute combination collection + public static IList GetAllProductVariantAttributeCombinations(this IProductAttributeService service, int productId) + { + return service.GetAllProductVariantAttributeCombinations(productId, 0, int.MaxValue, true); + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs index 9c0c054f53..1f8adbf816 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs @@ -39,7 +39,7 @@ public partial interface IProductService /// /// Product collection IList GetAllProductsDisplayedOnHomePage(); - + /// /// Gets product /// @@ -55,18 +55,18 @@ public partial interface IProductService /// Products IList GetProductsByIds(int[] productIds, ProductLoadFlags flags = ProductLoadFlags.None); - /// - /// Get product by system name. - /// - /// System name - /// Product entity. - Product GetProductBySystemName(string systemName); + /// + /// Get product by system name. + /// + /// System name + /// Product entity. + Product GetProductBySystemName(string systemName); - /// - /// Inserts a product - /// - /// Product - void InsertProduct(Product product); + /// + /// Inserts a product + /// + /// Product + void InsertProduct(Product product); /// /// Updates the product @@ -79,7 +79,7 @@ public partial interface IProductService /// /// Product void UpdateProductReviewTotals(Product product); - + /// /// Get low stock products /// @@ -100,36 +100,50 @@ public partial interface IProductService /// Product Product GetProductByGtin(string gtin); - /// - /// Gets a product by manufacturer part number (MPN) - /// - /// Manufacturer part number - /// Product - Product GetProductByManufacturerPartNumber(string manufacturerPartNumber); - - /// - /// Gets a product by name - /// - /// Product name - /// Product - Product GetProductByName(string name); - - /// - /// Adjusts inventory - /// - /// Shopping cart item - /// A value indicating whether to increase or descrease product stock quantity - /// Adjust inventory result - AdjustInventoryResult AdjustInventory(OrganizedShoppingCartItem sci, bool decrease); + /// + /// Gets a product by manufacturer part number (MPN) + /// + /// Manufacturer part number + /// Product + Product GetProductByManufacturerPartNumber(string manufacturerPartNumber); - /// - /// Adjusts inventory - /// - /// Order item - /// A value indicating whether to increase or descrease product stock quantity - /// Quantity - /// Adjust inventory result - AdjustInventoryResult AdjustInventory(OrderItem orderItem, bool decrease, int quantity); + /// + /// Gets a product by SKU, GTIN or MPN. + /// + /// SKU, GTIN or MPN. + /// Returns the attribute combination if one with the identification number exists. + /// Whether to include hidden products. + /// Specifies whether loaded entities should be tracked by the state manager. + /// Product. + Product GetProductByIdentificationNumber( + string identificationNumber, + out ProductVariantAttributeCombination attributeCombination, + bool includeHidden = false, + bool untracked = true); + + /// + /// Gets a product by name + /// + /// Product name + /// Product + Product GetProductByName(string name); + + /// + /// Adjusts inventory + /// + /// Shopping cart item + /// A value indicating whether to increase or descrease product stock quantity + /// Adjust inventory result + AdjustInventoryResult AdjustInventory(OrganizedShoppingCartItem sci, bool decrease); + + /// + /// Adjusts inventory + /// + /// Order item + /// A value indicating whether to increase or descrease product stock quantity + /// Quantity + /// Adjust inventory result + AdjustInventoryResult AdjustInventory(OrderItem orderItem, bool decrease, int quantity); /// /// Adjusts inventory @@ -147,11 +161,11 @@ public partial interface IProductService /// Product void UpdateHasTierPricesProperty(Product product); - /// - /// Update LowestAttributeCombinationPrice property (used for performance optimization) - /// - /// Product - void UpdateLowestAttributeCombinationPriceProperty(Product product); + /// + /// Update LowestAttributeCombinationPrice property (used for performance optimization) + /// + /// Product + void UpdateLowestAttributeCombinationPriceProperty(Product product); /// /// Update HasDiscountsApplied property (used for performance optimization) @@ -219,12 +233,12 @@ public partial interface IProductService /// Related product void UpdateRelatedProduct(RelatedProduct relatedProduct); - /// - /// Ensure existence of all mutually related products - /// - /// First product identifier - /// Number of inserted related products - int EnsureMutuallyRelatedProducts(int productId1); + /// + /// Ensure existence of all mutually related products + /// + /// First product identifier + /// Number of inserted related products + int EnsureMutuallyRelatedProducts(int productId1); #endregion @@ -244,20 +258,20 @@ public partial interface IProductService /// Cross-sell product collection IList GetCrossSellProductsByProductId1(int productId1, bool showHidden = false); - /// - /// Gets a cross-sell product collection by many product identifiers - /// - /// A sequence of alpha product identifiers - /// A value indicating whether to show hidden records - /// Cross-sell product collection - IList GetCrossSellProductsByProductIds(IEnumerable productIds, bool showHidden = false); + /// + /// Gets a cross-sell product collection by many product identifiers + /// + /// A sequence of alpha product identifiers + /// A value indicating whether to show hidden records + /// Cross-sell product collection + IList GetCrossSellProductsByProductIds(IEnumerable productIds, bool showHidden = false); - /// - /// Gets a cross-sell product - /// - /// Cross-sell product identifier - /// Cross-sell product - CrossSellProduct GetCrossSellProductById(int crossSellProductId); + /// + /// Gets a cross-sell product + /// + /// Cross-sell product identifier + /// Cross-sell product + CrossSellProduct GetCrossSellProductById(int crossSellProductId); /// /// Inserts a cross-sell product @@ -270,24 +284,24 @@ public partial interface IProductService /// /// Cross-sell product void UpdateCrossSellProduct(CrossSellProduct crossSellProduct); - + /// /// Gets a cross-sells /// /// Shopping cart /// Number of products to return /// Cross-sells - IList GetCrosssellProductsByShoppingCart(IList cart, int numberOfProducts); + IList GetCrosssellProductsByShoppingCart(IList cart, int numberOfProducts); - /// - /// Ensure existence of all mutually cross selling products - /// - /// First product identifier - /// Number of inserted cross selling products - int EnsureMutuallyCrossSellProducts(int productId1); + /// + /// Ensure existence of all mutually cross selling products + /// + /// First product identifier + /// Number of inserted cross selling products + int EnsureMutuallyCrossSellProducts(int productId1); #endregion - + #region Tier prices /// @@ -303,14 +317,14 @@ public partial interface IProductService /// Tier price TierPrice GetTierPriceById(int tierPriceId); - /// - /// Gets tier prices by product identifiers - /// - /// Product identifiers - /// Filter tier prices by customer - /// Filter tier prices by store - /// Map of tier prices - Multimap GetTierPricesByProductIds(int[] productIds, Customer customer = null, int storeId = 0); + /// + /// Gets tier prices by product identifiers + /// + /// Product identifiers + /// Filter tier prices by customer + /// Filter tier prices by store + /// Map of tier prices + Multimap GetTierPricesByProductIds(int[] productIds, Customer customer = null, int storeId = 0); /// /// Inserts a tier price @@ -375,48 +389,48 @@ Multimap GetProductPicturesByProductIds( #endregion - #region Bundled products - - /// - /// Inserts a product bundle item - /// - /// Product bundle item - void InsertBundleItem(ProductBundleItem bundleItem); - - /// - /// Updates a product bundle item - /// - /// Product bundle item - void UpdateBundleItem(ProductBundleItem bundleItem); - - /// - /// Deletes a product bundle item - /// - /// Product bundle item - void DeleteBundleItem(ProductBundleItem bundleItem); - - /// - /// Get a product bundle item by item identifier - /// - /// Product bundle item identifier - /// Product bundle item - ProductBundleItem GetBundleItemById(int bundleItemId); - - /// - /// Gets a list of bundle items for a particular product identifier - /// - /// Product identifier - /// A value indicating whether to show hidden records - /// List of bundle items - IList GetBundleItems(int bundleProductId, bool showHidden = false); - - /// - /// Get bundle items by product identifiers - /// - /// Product identifiers - /// A value indicating whether to show hidden records - /// Map of bundle items - Multimap GetBundleItemsByProductIds(int[] productIds, bool showHidden = false); + #region Bundled products + + /// + /// Inserts a product bundle item + /// + /// Product bundle item + void InsertBundleItem(ProductBundleItem bundleItem); + + /// + /// Updates a product bundle item + /// + /// Product bundle item + void UpdateBundleItem(ProductBundleItem bundleItem); + + /// + /// Deletes a product bundle item + /// + /// Product bundle item + void DeleteBundleItem(ProductBundleItem bundleItem); + + /// + /// Get a product bundle item by item identifier + /// + /// Product bundle item identifier + /// Product bundle item + ProductBundleItem GetBundleItemById(int bundleItemId); + + /// + /// Gets a list of bundle items for a particular product identifier + /// + /// Product identifier + /// A value indicating whether to show hidden records + /// List of bundle items + IList GetBundleItems(int bundleProductId, bool showHidden = false); + + /// + /// Get bundle items by product identifiers + /// + /// Product identifiers + /// A value indicating whether to show hidden records + /// Map of bundle items + Multimap GetBundleItemsByProductIds(int[] productIds, bool showHidden = false); /// /// Checks whether a product is a bundle item. @@ -428,23 +442,23 @@ Multimap GetProductPicturesByProductIds( #endregion } - [Flags] - public enum ProductLoadFlags - { - None = 0, - WithCategories = 1 << 0, - WithManufacturers = 1 << 1, - WithPictures = 1 << 2, - WithReviews = 1 << 3, - WithSpecificationAttributes = 1 << 4, - WithAttributes = 1 << 5, - WithAttributeValues = 1 << 7, - WithAttributeCombinations = 1 << 8, - WithTags = 1 << 9, - WithTierPrices = 1 << 10, - WithDiscounts = 1 << 11, - WithBundleItems = 1 << 12, - WithDeliveryTime = 1 << 13, - Full = WithCategories | WithManufacturers | WithPictures | WithReviews | WithSpecificationAttributes | WithAttributes | WithAttributeValues | WithAttributeCombinations | WithTags | WithTierPrices | WithDiscounts | WithBundleItems | WithDeliveryTime - } + [Flags] + public enum ProductLoadFlags + { + None = 0, + WithCategories = 1 << 0, + WithManufacturers = 1 << 1, + WithPictures = 1 << 2, + WithReviews = 1 << 3, + WithSpecificationAttributes = 1 << 4, + WithAttributes = 1 << 5, + WithAttributeValues = 1 << 7, + WithAttributeCombinations = 1 << 8, + WithTags = 1 << 9, + WithTierPrices = 1 << 10, + WithDiscounts = 1 << 11, + WithBundleItems = 1 << 12, + WithDeliveryTime = 1 << 13, + Full = WithCategories | WithManufacturers | WithPictures | WithReviews | WithSpecificationAttributes | WithAttributes | WithAttributeValues | WithAttributeCombinations | WithTags | WithTierPrices | WithDiscounts | WithBundleItems | WithDeliveryTime + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductTagService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductTagService.cs index 38d8504af3..70476562ae 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductTagService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductTagService.cs @@ -33,7 +33,7 @@ public partial interface IProductTagService : IScopedService /// Product tag identifier /// Product tag ProductTag GetProductTagById(int productTagId); - + /// /// Gets product tag by name /// diff --git a/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs index 63bd9f6256..f6a6d13873 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs @@ -25,12 +25,12 @@ public partial interface ISpecificationAttributeService /// Specification attribute query IQueryable GetSpecificationAttributes(); - /// - /// Gets specification attributes by identifier - /// - /// Identifiers - /// Specification attribute query - IQueryable GetSpecificationAttributesByIds(int[] ids); + /// + /// Gets specification attributes by identifier + /// + /// Identifiers + /// Specification attribute query + IQueryable GetSpecificationAttributesByIds(int[] ids); /// /// Deletes a specification attribute diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs index 4a8721b441..0e7e928198 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Data.Entity; using System.IO; using System.Linq; using System.Linq.Expressions; using SmartStore.Core.Async; +using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Media; using SmartStore.Core.IO; using SmartStore.Services.DataExchange.Import; using SmartStore.Services.DataExchange.Import.Events; @@ -17,177 +20,197 @@ namespace SmartStore.Services.Catalog.Importer { public class CategoryImporter : EntityImporterBase - { - private readonly IRepository _categoryRepository; - private readonly ICategoryTemplateService _categoryTemplateService; + { + private readonly IRepository _categoryRepository; + private readonly ICategoryTemplateService _categoryTemplateService; private readonly IMediaService _mediaService; + private readonly IFolderService _folderService; private readonly ILocalizedEntityService _localizedEntityService; - private readonly FileDownloadManager _fileDownloadManager; - - private static readonly Dictionary>> _localizableProperties = new Dictionary>> - { - { "Name", x => x.Name }, - { "FullName", x => x.FullName }, - { "Description", x => x.Description }, - { "BottomDescription", x => x.BottomDescription }, - { "MetaKeywords", x => x.MetaKeywords }, - { "MetaDescription", x => x.MetaDescription }, - { "MetaTitle", x => x.MetaTitle } - }; - - public CategoryImporter( - IRepository categoryRepository, - ICategoryTemplateService categoryTemplateService, + private readonly FileDownloadManager _fileDownloadManager; + private readonly ICacheManager _cache; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "FullName", x => x.FullName }, + { "Description", x => x.Description }, + { "BottomDescription", x => x.BottomDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle } + }; + + public CategoryImporter( + IRepository categoryRepository, + ICategoryTemplateService categoryTemplateService, IMediaService mediaService, + IFolderService folderService, ILocalizedEntityService localizedEntityService, - FileDownloadManager fileDownloadManager) - { - _categoryRepository = categoryRepository; - _categoryTemplateService = categoryTemplateService; + FileDownloadManager fileDownloadManager, + ICacheManager cache) + { + _categoryRepository = categoryRepository; + _categoryTemplateService = categoryTemplateService; _mediaService = mediaService; - _localizedEntityService = localizedEntityService; - _fileDownloadManager = fileDownloadManager; - } - - protected override void Import(ImportExecuteContext context) - { - var srcToDestId = new Dictionary(); - - var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); - - using (var scope = new DbContextScope(ctx: context.Services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) - { - var segmenter = context.DataSegmenter; - - Initialize(context); - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.GetCurrentBatch(); - - // Perf: detach all entities - _categoryRepository.Context.DetachAll(true); - - context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); - - try - { - ProcessCategories(context, batch, templateViewPaths, srcToDestId); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessCategories"); - } - - // Reduce batch to saved (valid) products. - // No need to perform import operations on errored products. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); - - // Update result object. - context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); - context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); - - // Process slugs. - if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) - { - try - { - _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessSlugs(context, batch, typeof(Category).Name); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessSlugs"); - } - finally - { - _categoryRepository.Context.AutoDetectChangesEnabled = false; - } - } - - // Process store mappings. - if (segmenter.HasColumn("StoreIds")) - { - try - { - ProcessStoreMappings(context, batch); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessStoreMappings"); - } - } - - // Localizations. - try - { - ProcessLocalizations(context, batch, _localizableProperties); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessLocalizedProperties"); - } - - // Process pictures. - if (segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) - { - try - { - _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessPictures(context, batch); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessPictures"); - } - finally - { - _categoryRepository.Context.AutoDetectChangesEnabled = false; - } - } + _folderService = folderService; + _localizedEntityService = localizedEntityService; + _fileDownloadManager = fileDownloadManager; + _cache = cache; + } + + protected override void Import(ImportExecuteContext context) + { + var srcToDestId = new Dictionary(); + + var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); + + using (var scope = new DbContextScope(ctx: context.Services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _categoryRepository.Context.DetachAll(true); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + try + { + ProcessCategories(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessCategories"); + } + + // Reduce batch to saved (valid) products. + // No need to perform import operations on errored products. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // Update result object. + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // Process slugs. + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Category).Name); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessSlugs"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } + + // Process store mappings. + if (segmenter.HasColumn("StoreIds")) + { + try + { + ProcessStoreMappings(context, batch); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessStoreMappings"); + } + } + + // Localizations. + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessLocalizedProperties"); + } + + // Process pictures. + if (segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessPictures(context, batch); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessPictures"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); } - // Map parent id of inserted categories. - if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId") && !segmenter.IsIgnored("ParentCategoryId")) - { - segmenter.Reset(); - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.GetCurrentBatch(); - _categoryRepository.Context.DetachAll(unchangedEntitiesOnly: false); - - try - { - ProcessParentMappings(context, batch, srcToDestId); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessParentMappings"); - } - } - } - } - } - - protected virtual int ProcessPictures( - ImportExecuteContext context, - IEnumerable> batch) - { - foreach (var row in batch) - { - try - { - var srcId = row.GetDataValue("Id"); - var imageUrl = row.GetDataValue("ImageUrl"); + // Map parent id of inserted categories. + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId") && !segmenter.IsIgnored("ParentCategoryId")) + { + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + _categoryRepository.Context.DetachAll(unchangedEntitiesOnly: false); + + try + { + ProcessParentMappings(context, batch, srcToDestId); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessParentMappings"); + } + } + } + } + + // Hooks are disabled but category tree may have changed. + _cache.Clear(); + } + + protected virtual int ProcessPictures(ImportExecuteContext context, IEnumerable> batch) + { + var allFileIds = batch + .Where(row => row.HasDataValue("ImageUrl") && row.Entity.MediaFileId > 0) + .Select(row => row.Entity.MediaFileId.Value) + .Distinct() + .ToArray(); + + var allFiles = _mediaService.GetFilesByIds(allFileIds).ToDictionary(x => x.Id, x => x.File); + var catalogAlbumId = _folderService.GetNodeByPath(SystemAlbumProvider.Catalog).Value.Id; + + foreach (var row in batch) + { + try + { + var imageUrl = row.GetDataValue("ImageUrl"); if (imageUrl.IsEmpty()) { continue; } var image = CreateDownloadImage(context, imageUrl, 1); + if (image == null) + { + continue; + } + if (image.Url.HasValue() && !image.Success.HasValue) { AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); @@ -200,26 +223,27 @@ protected virtual int ProcessPictures( { if ((stream?.Length ?? 0) > 0) { - var currentFiles = new List(); - var file = _mediaService.GetFileById(row.Entity.MediaFileId ?? 0, MediaLoadFlags.AsNoTracking); - if (file != null) + var assignedFile = allFiles.Get(row.Entity.MediaFileId ?? 0); + MediaFile sourceFile = null; + + if (assignedFile != null && _mediaService.FindEqualFile(stream, new[] { assignedFile }, true, out var _)) { - currentFiles.Add(file); + context.Result.AddInfo($"Found equal image in data store for '{image.FileName}'. Skipping file.", row.GetRowInfo(), "ImageUrl"); } - - if (!_mediaService.FindEqualFile(stream, currentFiles.Select(x => x.File), true, out var _)) + else if (_mediaService.FindEqualFile(stream, image.FileName, catalogAlbumId, true, out sourceFile)) { - var path = _mediaService.CombinePaths(SystemAlbumProvider.Categories, image.FileName.ToValidFileName()); - var newFile = _mediaService.SaveFile(path, stream, false, DuplicateFileHandling.Rename); - if ((newFile?.Id ?? 0) != 0) - { - row.Entity.MediaFileId = newFile.Id; - _categoryRepository.Update(row.Entity); - } + context.Result.AddInfo($"Found equal image in catalog album for '{image.FileName}'. Assigning existing file instead.", row.GetRowInfo(), "ImageUrl"); } else { - context.Result.AddInfo("Found equal image in data store. Skipping field.", row.GetRowInfo(), "ImageUrls"); + var path = _mediaService.CombinePaths(SystemAlbumProvider.Catalog, image.FileName); + sourceFile = _mediaService.SaveFile(path, stream, false, DuplicateFileHandling.Rename)?.File; + } + + if (sourceFile?.Id > 0) + { + row.Entity.MediaFileId = sourceFile.Id; + _categoryRepository.Update(row.Entity); } } } @@ -228,229 +252,217 @@ protected virtual int ProcessPictures( { context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls"); } - } - catch (Exception ex) - { - context.Result.AddWarning(ex.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); - } - } - - var num = _categoryRepository.Context.SaveChanges(); - return num; - } - - protected virtual int ProcessLocalizations( - ImportExecuteContext context, - IEnumerable> batch, - string[] localizedProperties) - { - if (localizedProperties.Length == 0) - { - return 0; - } - - bool shouldSave = false; - - foreach (var row in batch) - { - foreach (var prop in localizedProperties) - { - var lambda = _localizableProperties[prop]; - foreach (var lang in context.Languages) - { - var code = lang.UniqueSeoCode; - string value; - - if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) - { - _localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); - shouldSave = true; - } - } - } - } - - if (shouldSave) - { - // commit whole batch at once - return context.Services.DbContext.SaveChanges(); - } - - return 0; - } - - protected virtual int ProcessParentMappings( - ImportExecuteContext context, - IEnumerable> batch, - Dictionary srcToDestId) - { - foreach (var row in batch) - { - var id = row.GetDataValue("Id"); - var rawParentId = row.GetDataValue("ParentCategoryId"); - var parentId = rawParentId.ToInt(-1); - - if (id != 0 && parentId != -1 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentId)) - { - // only touch hierarchical data if child and parent were inserted - if (srcToDestId[id].Inserted && srcToDestId[parentId].Inserted && srcToDestId[id].DestinationId != 0) - { - var category = _categoryRepository.GetById(srcToDestId[id].DestinationId); - if (category != null) - { - category.ParentCategoryId = srcToDestId[parentId].DestinationId; - - _categoryRepository.Update(category); - } - } - } - } - - var num = _categoryRepository.Context.SaveChanges(); - - return num; - } - - protected virtual int ProcessCategories( - ImportExecuteContext context, - IEnumerable> batch, - Dictionary templateViewPaths, - Dictionary srcToDestId) - { - _categoryRepository.AutoCommitEnabled = true; - - var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; + } + catch (Exception ex) + { + context.Result.AddWarning(ex.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); + } + } + + var num = _categoryRepository.Context.SaveChanges(); + return num; + } + + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + string[] localizedProperties) + { + if (localizedProperties.Length == 0) + { + return 0; + } + + bool shouldSave = false; + + foreach (var row in batch) + { + foreach (var prop in localizedProperties) + { + var lambda = _localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + _localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } + + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + return 0; + } + + protected virtual int ProcessParentMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + var rawParentId = row.GetDataValue("ParentCategoryId"); + var parentId = rawParentId.ToInt(-1); + + if (id != 0 && parentId != -1 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentId)) + { + // only touch hierarchical data if child and parent were inserted + if (srcToDestId[id].Inserted && srcToDestId[parentId].Inserted && srcToDestId[id].DestinationId != 0) + { + var category = _categoryRepository.GetById(srcToDestId[id].DestinationId); + if (category != null) + { + category.ParentCategoryId = srcToDestId[parentId].DestinationId; + + _categoryRepository.Update(category); + } + } + } + } + + var num = _categoryRepository.Context.SaveChanges(); + + return num; + } + + protected virtual int ProcessCategories( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _categoryRepository.AutoCommitEnabled = true; + + var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; var hasNameColumn = context.DataSegmenter.HasColumn("Name"); foreach (var row in batch) - { - Category category = null; - var id = row.GetDataValue("Id"); - var name = row.GetDataValue("Name"); - - foreach (var keyName in context.KeyFieldNames) - { - switch (keyName) - { - case "Id": - if (id != 0) - category = _categoryRepository.GetById(id); - break; - case "Name": - if (name.HasValue()) - category = _categoryRepository.Table.FirstOrDefault(x => x.Name == name); - break; - } - - if (category != null) - break; - } - - if (category == null) - { - if (context.UpdateOnly) - { - ++context.Result.SkippedRecords; - continue; - } + { + Category category = null; + var id = row.GetDataValue("Id"); + var name = row.GetDataValue("Name"); + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Id": + if (id != 0) + category = _categoryRepository.GetById(id); + break; + case "Name": + if (name.HasValue()) + category = _categoryRepository.Table.FirstOrDefault(x => x.Name == name); + break; + } + + if (category != null) + break; + } + + if (category == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } // A name is required for new categories. if (!row.HasDataValue("Name")) { - ++context.Result.SkippedRecords; - context.Result.AddError("The 'Name' field is required for new categories. Skipping row.", row.GetRowInfo(), "Name"); - continue; - } - - category = new Category(); - } - - row.Initialize(category, name ?? category.Name); - - if (!row.IsNew && hasNameColumn && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - { - // Perf: use this later for SeName updates. - row.NameChanged = true; - } - - row.SetProperty(context.Result, (x) => x.Name); - row.SetProperty(context.Result, (x) => x.FullName); - row.SetProperty(context.Result, (x) => x.Description); - row.SetProperty(context.Result, (x) => x.BottomDescription); - row.SetProperty(context.Result, (x) => x.MetaKeywords); - row.SetProperty(context.Result, (x) => x.MetaDescription); - row.SetProperty(context.Result, (x) => x.MetaTitle); - row.SetProperty(context.Result, (x) => x.PageSize); - row.SetProperty(context.Result, (x) => x.AllowCustomersToSelectPageSize); - row.SetProperty(context.Result, (x) => x.PageSizeOptions); - row.SetProperty(context.Result, (x) => x.ShowOnHomePage); - row.SetProperty(context.Result, (x) => x.HasDiscountsApplied); - row.SetProperty(context.Result, (x) => x.Published, true); - row.SetProperty(context.Result, (x) => x.DisplayOrder); - row.SetProperty(context.Result, (x) => x.Alias); - row.SetProperty(context.Result, (x) => x.DefaultViewMode); - // With new entities, "LimitedToStores" is an implicit field, meaning - // it has to be set to true by code if it's absent but "StoreIds" exists. - row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); - - string tvp; - if (row.TryGetDataValue("CategoryTemplateViewPath", out tvp, row.IsTransient)) - { - category.CategoryTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); - } - - if (id != 0 && !srcToDestId.ContainsKey(id)) - { - srcToDestId.Add(id, new ImportCategoryMapping { Inserted = row.IsTransient }); - } - - if (row.IsTransient) - { - _categoryRepository.Insert(category); - } - else - { - _categoryRepository.Update(category); - } - } - - // Commit whole batch at once. - var num = _categoryRepository.Context.SaveChanges(); - - // Get new category ids. - foreach (var row in batch) - { - var id = row.GetDataValue("Id"); + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new categories. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + category = new Category(); + } + + row.Initialize(category, name ?? category.Name); + + if (!row.IsNew && hasNameColumn && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.FullName); + row.SetProperty(context.Result, (x) => x.Description); + row.SetProperty(context.Result, (x) => x.BottomDescription); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.PageSize); + row.SetProperty(context.Result, (x) => x.AllowCustomersToSelectPageSize); + row.SetProperty(context.Result, (x) => x.PageSizeOptions); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HasDiscountsApplied); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.Alias); + row.SetProperty(context.Result, (x) => x.DefaultViewMode); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("CategoryTemplateViewPath", out tvp, row.IsTransient)) + { + category.CategoryTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportCategoryMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _categoryRepository.Insert(category); + } + else + { + _categoryRepository.Update(category); + } + } + + // Commit whole batch at once. + var num = _categoryRepository.Context.SaveChanges(); + + // Get new category ids. + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); if (id != 0 && srcToDestId.ContainsKey(id)) { srcToDestId[id].DestinationId = row.Entity.Id; } - } - - return num; - } - - public static string[] SupportedKeyFields - { - get - { - return new string[] { "Id", "Name" }; - } - } - - public static string[] DefaultKeyFields - { - get - { - return new string[] { "Name", "Id" }; - } - } - - public class ImportCategoryMapping - { - public int DestinationId { get; set; } - public bool Inserted { get; set; } - } - } + } + + return num; + } + + public static string[] SupportedKeyFields => new string[] { "Id", "Name" }; + + public static string[] DefaultKeyFields => new string[] { "Name", "Id" }; + + public class ImportCategoryMapping + { + public int DestinationId { get; set; } + public bool Inserted { get; set; } + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs index dc9be33ca0..7da3f102c3 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -33,6 +33,7 @@ public class ProductImporter : EntityImporterBase private readonly IRepository _attributeValueRepository; private readonly IRepository _attributeCombinationRepository; private readonly IMediaService _mediaService; + private readonly IFolderService _folderService; private readonly IManufacturerService _manufacturerService; private readonly ICategoryService _categoryService; private readonly IProductService _productService; @@ -60,6 +61,7 @@ public ProductImporter( IRepository attributeValueRepository, IRepository attributeCombinationRepository, IMediaService mediaService, + IFolderService folderService, IManufacturerService manufacturerService, ICategoryService categoryService, IProductService productService, @@ -75,6 +77,7 @@ public ProductImporter( _attributeValueRepository = attributeValueRepository; _attributeCombinationRepository = attributeCombinationRepository; _mediaService = mediaService; + _folderService = folderService; _manufacturerService = manufacturerService; _categoryService = categoryService; _productService = productService; @@ -114,7 +117,7 @@ protected override void Import(ImportExecuteContext context) } protected virtual void ImportProducts(ImportExecuteContext context) - { + { var segmenter = context.DataSegmenter; var srcToDestId = new Dictionary(); var templateViewPaths = _productTemplateService.GetAllProductTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); @@ -598,183 +601,187 @@ protected virtual void ImportAttributeCombinations(ImportExecuteContext context) } protected virtual int ProcessProducts( - ImportExecuteContext context, - IEnumerable> batch, - Dictionary templateViewPaths, - Dictionary srcToDestId) - { - _productRepository.AutoCommitEnabled = false; - - var defaultTemplateId = templateViewPaths["Product"]; + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + var defaultTemplateId = templateViewPaths["Product"]; var hasNameColumn = context.DataSegmenter.HasColumn("Name"); - + foreach (var row in batch) - { - Product product = null; - var id = row.GetDataValue("Id"); - - foreach (var keyName in context.KeyFieldNames) - { - var keyValue = row.GetDataValue(keyName); - - if (keyValue.HasValue() || id > 0) - { - switch (keyName) - { - case "Id": - product = _productRepository.GetById(id); // get it uncached - break; - case "Sku": - product = _productService.GetProductBySku(keyValue); - break; - case "Gtin": - product = _productService.GetProductByGtin(keyValue); - break; - case "ManufacturerPartNumber": - product = _productService.GetProductByManufacturerPartNumber(keyValue); - break; - case "Name": - product = _productService.GetProductByName(keyValue); - break; - } - } - - if (product != null) - break; - } - - if (product == null) - { - if (context.UpdateOnly) - { - ++context.Result.SkippedRecords; - continue; - } - - // A name is required for new products. - if (!row.HasDataValue("Name")) - { - ++context.Result.SkippedRecords; - context.Result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); - continue; - } - - product = new Product(); - } - - var name = row.GetDataValue("Name"); - - row.Initialize(product, name ?? product.Name); - - if (!row.IsNew && hasNameColumn) - { - if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - { - // Perf: use this later for SeName updates. - row.NameChanged = true; - } - } - - row.SetProperty(context.Result, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); - row.SetProperty(context.Result, (x) => x.Name); - row.SetProperty(context.Result, (x) => x.ShortDescription); - row.SetProperty(context.Result, (x) => x.FullDescription); - row.SetProperty(context.Result, (x) => x.AdminComment); - row.SetProperty(context.Result, (x) => x.ShowOnHomePage); - row.SetProperty(context.Result, (x) => x.HomePageDisplayOrder); - row.SetProperty(context.Result, (x) => x.MetaKeywords); - row.SetProperty(context.Result, (x) => x.MetaDescription); - row.SetProperty(context.Result, (x) => x.MetaTitle); - row.SetProperty(context.Result, (x) => x.AllowCustomerReviews, true); - row.SetProperty(context.Result, (x) => x.ApprovedRatingSum); - row.SetProperty(context.Result, (x) => x.NotApprovedRatingSum); - row.SetProperty(context.Result, (x) => x.ApprovedTotalReviews); - row.SetProperty(context.Result, (x) => x.NotApprovedTotalReviews); - row.SetProperty(context.Result, (x) => x.Published, true); - row.SetProperty(context.Result, (x) => x.Sku); - row.SetProperty(context.Result, (x) => x.ManufacturerPartNumber); - row.SetProperty(context.Result, (x) => x.Gtin); - row.SetProperty(context.Result, (x) => x.IsGiftCard); - row.SetProperty(context.Result, (x) => x.GiftCardTypeId); - row.SetProperty(context.Result, (x) => x.RequireOtherProducts); - row.SetProperty(context.Result, (x) => x.RequiredProductIds); // TODO: global scope - row.SetProperty(context.Result, (x) => x.AutomaticallyAddRequiredProducts); - row.SetProperty(context.Result, (x) => x.IsDownload); - //row.SetProperty(context.Result, (x) => x.DownloadId); - //row.SetProperty(context.Result, (x) => x.UnlimitedDownloads, true); - row.SetProperty(context.Result, (x) => x.MaxNumberOfDownloads, 10); - row.SetProperty(context.Result, (x) => x.DownloadExpirationDays); - row.SetProperty(context.Result, (x) => x.DownloadActivationTypeId, 1); - row.SetProperty(context.Result, (x) => x.HasSampleDownload); - row.SetProperty(context.Result, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); // TODO: global scope - row.SetProperty(context.Result, (x) => x.HasUserAgreement); - row.SetProperty(context.Result, (x) => x.UserAgreementText); - row.SetProperty(context.Result, (x) => x.IsRecurring); - row.SetProperty(context.Result, (x) => x.RecurringCycleLength, 100); - row.SetProperty(context.Result, (x) => x.RecurringCyclePeriodId); - row.SetProperty(context.Result, (x) => x.RecurringTotalCycles, 10); - row.SetProperty(context.Result, (x) => x.IsShipEnabled, true); - row.SetProperty(context.Result, (x) => x.IsFreeShipping); - row.SetProperty(context.Result, (x) => x.AdditionalShippingCharge); - row.SetProperty(context.Result, (x) => x.IsEsd); - row.SetProperty(context.Result, (x) => x.IsTaxExempt); - row.SetProperty(context.Result, (x) => x.TaxCategoryId, 1); // TODO: global scope - row.SetProperty(context.Result, (x) => x.ManageInventoryMethodId); - row.SetProperty(context.Result, (x) => x.StockQuantity, 10000); - row.SetProperty(context.Result, (x) => x.DisplayStockAvailability); - row.SetProperty(context.Result, (x) => x.DisplayStockQuantity); - row.SetProperty(context.Result, (x) => x.MinStockQuantity); - row.SetProperty(context.Result, (x) => x.LowStockActivityId); - row.SetProperty(context.Result, (x) => x.NotifyAdminForQuantityBelow, 1); - row.SetProperty(context.Result, (x) => x.BackorderModeId); - row.SetProperty(context.Result, (x) => x.AllowBackInStockSubscriptions); - row.SetProperty(context.Result, (x) => x.OrderMinimumQuantity, 1); - row.SetProperty(context.Result, (x) => x.OrderMaximumQuantity, 100); - row.SetProperty(context.Result, (x) => x.QuantityStep, 1); - row.SetProperty(context.Result, (x) => x.HideQuantityControl); + { + Product product = null; + var id = row.GetDataValue("Id"); + + foreach (var keyName in context.KeyFieldNames) + { + var keyValue = row.GetDataValue(keyName); + + if (keyValue.HasValue() || id > 0) + { + switch (keyName) + { + case "Id": + product = _productRepository.GetById(id); // get it uncached + break; + case "Sku": + product = _productService.GetProductBySku(keyValue); + break; + case "Gtin": + product = _productService.GetProductByGtin(keyValue); + break; + case "ManufacturerPartNumber": + product = _productService.GetProductByManufacturerPartNumber(keyValue); + break; + case "Name": + product = _productService.GetProductByName(keyValue); + break; + } + } + + if (product != null) + break; + } + + if (product == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + // A name is required for new products. + if (!row.HasDataValue("Name")) + { + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + product = new Product(); + } + + var name = row.GetDataValue("Name"); + + row.Initialize(product, name ?? product.Name); + + if (!row.IsNew && hasNameColumn) + { + if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + } + + row.SetProperty(context.Result, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.ShortDescription); + row.SetProperty(context.Result, (x) => x.FullDescription); + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HomePageDisplayOrder); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.AllowCustomerReviews, true); + row.SetProperty(context.Result, (x) => x.ApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.NotApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.ApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.NotApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.Sku); + row.SetProperty(context.Result, (x) => x.ManufacturerPartNumber); + row.SetProperty(context.Result, (x) => x.Gtin); + row.SetProperty(context.Result, (x) => x.IsGiftCard); + row.SetProperty(context.Result, (x) => x.GiftCardTypeId); + row.SetProperty(context.Result, (x) => x.RequireOtherProducts); + row.SetProperty(context.Result, (x) => x.RequiredProductIds); // TODO: global scope + row.SetProperty(context.Result, (x) => x.AutomaticallyAddRequiredProducts); + row.SetProperty(context.Result, (x) => x.IsDownload); + //row.SetProperty(context.Result, (x) => x.DownloadId); + //row.SetProperty(context.Result, (x) => x.UnlimitedDownloads, true); + row.SetProperty(context.Result, (x) => x.MaxNumberOfDownloads, 10); + row.SetProperty(context.Result, (x) => x.DownloadExpirationDays); + row.SetProperty(context.Result, (x) => x.DownloadActivationTypeId, 1); + row.SetProperty(context.Result, (x) => x.HasSampleDownload); + row.SetProperty(context.Result, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); // TODO: global scope + row.SetProperty(context.Result, (x) => x.HasUserAgreement); + row.SetProperty(context.Result, (x) => x.UserAgreementText); + row.SetProperty(context.Result, (x) => x.IsRecurring); + row.SetProperty(context.Result, (x) => x.RecurringCycleLength, 100); + row.SetProperty(context.Result, (x) => x.RecurringCyclePeriodId); + row.SetProperty(context.Result, (x) => x.RecurringTotalCycles, 10); + row.SetProperty(context.Result, (x) => x.IsShipEnabled, true); + row.SetProperty(context.Result, (x) => x.IsFreeShipping); + row.SetProperty(context.Result, (x) => x.AdditionalShippingCharge); + row.SetProperty(context.Result, (x) => x.IsEsd); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.TaxCategoryId, 1); // TODO: global scope + row.SetProperty(context.Result, (x) => x.ManageInventoryMethodId); + row.SetProperty(context.Result, (x) => x.StockQuantity, 10000); + row.SetProperty(context.Result, (x) => x.DisplayStockAvailability); + row.SetProperty(context.Result, (x) => x.DisplayStockQuantity); + row.SetProperty(context.Result, (x) => x.MinStockQuantity); + row.SetProperty(context.Result, (x) => x.LowStockActivityId); + row.SetProperty(context.Result, (x) => x.NotifyAdminForQuantityBelow, 1); + row.SetProperty(context.Result, (x) => x.BackorderModeId); + row.SetProperty(context.Result, (x) => x.AllowBackInStockSubscriptions); + row.SetProperty(context.Result, (x) => x.OrderMinimumQuantity, 1); + row.SetProperty(context.Result, (x) => x.OrderMaximumQuantity, 100); + row.SetProperty(context.Result, (x) => x.QuantityStep, 1); + row.SetProperty(context.Result, (x) => x.HideQuantityControl); row.SetProperty(context.Result, (x) => x.AllowedQuantities); - row.SetProperty(context.Result, (x) => x.DisableBuyButton); - row.SetProperty(context.Result, (x) => x.DisableWishlistButton); - row.SetProperty(context.Result, (x) => x.AvailableForPreOrder); - row.SetProperty(context.Result, (x) => x.CallForPrice); - row.SetProperty(context.Result, (x) => x.Price); - row.SetProperty(context.Result, (x) => x.OldPrice); - row.SetProperty(context.Result, (x) => x.ProductCost); - row.SetProperty(context.Result, (x) => x.SpecialPrice); - row.SetProperty(context.Result, (x) => x.SpecialPriceStartDateTimeUtc); - row.SetProperty(context.Result, (x) => x.SpecialPriceEndDateTimeUtc); - row.SetProperty(context.Result, (x) => x.CustomerEntersPrice); - row.SetProperty(context.Result, (x) => x.MinimumCustomerEnteredPrice); - row.SetProperty(context.Result, (x) => x.MaximumCustomerEnteredPrice, 1000); - // HasTierPrices... ignore as long as no tier prices are imported - // LowestAttributeCombinationPrice... ignore as long as no combinations are imported - row.SetProperty(context.Result, (x) => x.Weight); - row.SetProperty(context.Result, (x) => x.Length); - row.SetProperty(context.Result, (x) => x.Width); - row.SetProperty(context.Result, (x) => x.Height); - row.SetProperty(context.Result, (x) => x.DisplayOrder); - row.SetProperty(context.Result, (x) => x.DeliveryTimeId); // TODO: global scope - row.SetProperty(context.Result, (x) => x.QuantityUnitId); // TODO: global scope - row.SetProperty(context.Result, (x) => x.BasePriceEnabled); - row.SetProperty(context.Result, (x) => x.BasePriceMeasureUnit); - row.SetProperty(context.Result, (x) => x.BasePriceAmount); - row.SetProperty(context.Result, (x) => x.BasePriceBaseAmount); - row.SetProperty(context.Result, (x) => x.BundleTitleText); - row.SetProperty(context.Result, (x) => x.BundlePerItemShipping); - row.SetProperty(context.Result, (x) => x.BundlePerItemPricing); - row.SetProperty(context.Result, (x) => x.BundlePerItemShoppingCart); - row.SetProperty(context.Result, (x) => x.AvailableStartDateTimeUtc); - row.SetProperty(context.Result, (x) => x.AvailableEndDateTimeUtc); - // With new entities, "LimitedToStores" is an implicit field, meaning - // it has to be set to true by code if it's absent but "StoreIds" exists. - row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); - row.SetProperty(context.Result, (x) => x.CustomsTariffNumber); - row.SetProperty(context.Result, (x) => x.CountryOfOriginId); + row.SetProperty(context.Result, (x) => x.DisableBuyButton); + row.SetProperty(context.Result, (x) => x.DisableWishlistButton); + row.SetProperty(context.Result, (x) => x.AvailableForPreOrder); + row.SetProperty(context.Result, (x) => x.CallForPrice); + row.SetProperty(context.Result, (x) => x.Price); + row.SetProperty(context.Result, (x) => x.OldPrice); + row.SetProperty(context.Result, (x) => x.ProductCost); + row.SetProperty(context.Result, (x) => x.SpecialPrice); + row.SetProperty(context.Result, (x) => x.SpecialPriceStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.SpecialPriceEndDateTimeUtc); + row.SetProperty(context.Result, (x) => x.CustomerEntersPrice); + row.SetProperty(context.Result, (x) => x.MinimumCustomerEnteredPrice); + row.SetProperty(context.Result, (x) => x.MaximumCustomerEnteredPrice, 1000); + // HasTierPrices... ignore as long as no tier prices are imported + // LowestAttributeCombinationPrice... ignore as long as no combinations are imported + row.SetProperty(context.Result, (x) => x.Weight); + row.SetProperty(context.Result, (x) => x.Length); + row.SetProperty(context.Result, (x) => x.Width); + row.SetProperty(context.Result, (x) => x.Height); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.DeliveryTimeId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.QuantityUnitId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.BasePriceEnabled); + row.SetProperty(context.Result, (x) => x.BasePriceMeasureUnit); + row.SetProperty(context.Result, (x) => x.BasePriceAmount); + row.SetProperty(context.Result, (x) => x.BasePriceBaseAmount); + row.SetProperty(context.Result, (x) => x.BundleTitleText); + row.SetProperty(context.Result, (x) => x.BundlePerItemShipping); + row.SetProperty(context.Result, (x) => x.BundlePerItemPricing); + row.SetProperty(context.Result, (x) => x.BundlePerItemShoppingCart); + row.SetProperty(context.Result, (x) => x.AvailableStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.AvailableEndDateTimeUtc); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + row.SetProperty(context.Result, (x) => x.CustomsTariffNumber); + row.SetProperty(context.Result, (x) => x.CountryOfOriginId); if (row.TryGetDataValue("QuantiyControlType", out int qct)) { product.QuantiyControlType = (QuantityControlType)qct; } + if (row.TryGetDataValue("AttributeChoiceBehaviour", out int attributeChoiceBehaviour)) + { + product.AttributeChoiceBehaviour = (AttributeChoiceBehaviour)attributeChoiceBehaviour; + } if (row.TryGetDataValue("Visibility", out int visibilityValue)) { product.Visibility = (ProductVisibility)visibilityValue; @@ -784,126 +791,141 @@ protected virtual int ProcessProducts( product.Condition = (ProductCondition)conditionValue; } - if (row.TryGetDataValue("ProductTemplateViewPath", out string tvp, row.IsTransient)) - { - product.ProductTemplateId = tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId; - } - - if (id != 0 && !srcToDestId.ContainsKey(id)) - { - srcToDestId.Add(id, new ImportProductMapping { Inserted = row.IsTransient }); - } - - if (row.IsTransient) - { - _productRepository.Insert(product); - } - else - { - //_productRepository.Update(product); // unnecessary: we use DetectChanges() - } - } - - // Commit whole batch at once. - var num = _productRepository.Context.SaveChanges(); - - // Get new product ids. - foreach (var row in batch) - { - var id = row.GetDataValue("Id"); + if (row.TryGetDataValue("ProductTemplateViewPath", out string tvp, row.IsTransient)) + { + product.ProductTemplateId = tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId; + } + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportProductMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _productRepository.Insert(product); + } + else + { + product.UpdatedOnUtc = DateTime.UtcNow; + //_productRepository.Update(product); // unnecessary: we use DetectChanges() + } + } + + // Commit whole batch at once. + var num = _productRepository.Context.SaveChanges(); + + // Get new product ids. + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); if (id != 0 && srcToDestId.ContainsKey(id)) { srcToDestId[id].DestinationId = row.Entity.Id; } - } - - return num; - } - - protected virtual int ProcessProductMappings( - ImportExecuteContext context, - IEnumerable> batch, - Dictionary srcToDestId) - { - _productRepository.AutoCommitEnabled = false; - - foreach (var row in batch) - { - var id = row.GetDataValue("Id"); - var parentGroupedProductId = row.GetDataValue("ParentGroupedProductId"); - - if (id != 0 && parentGroupedProductId != 0 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentGroupedProductId)) - { - if (srcToDestId[id].DestinationId != 0) - { - var product = _productRepository.GetById(srcToDestId[id].DestinationId); - if (product != null) - { - product.ParentGroupedProductId = srcToDestId[parentGroupedProductId].DestinationId; - //_productRepository.Update(product); // unnecessary: we use DetectChanges() - } - } - } - } - - var num = _productRepository.Context.SaveChanges(); - return num; - } - - protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnumerable> batch) - { + } + + return num; + } + + protected virtual int ProcessProductMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + var parentGroupedProductId = row.GetDataValue("ParentGroupedProductId"); + + if (id != 0 && parentGroupedProductId != 0 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentGroupedProductId)) + { + if (srcToDestId[id].DestinationId != 0) + { + var product = _productRepository.GetById(srcToDestId[id].DestinationId); + if (product != null) + { + product.ParentGroupedProductId = srcToDestId[parentGroupedProductId].DestinationId; + //_productRepository.Update(product); // unnecessary: we use DetectChanges() + } + } + } + } + + var num = _productRepository.Context.SaveChanges(); + return num; + } + + protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnumerable> batch) + { _productRepository.AutoCommitEnabled = false; var numberOfPictures = context.ExtraData.NumberOfPictures ?? int.MaxValue; + var productIds = batch.Select(x => x.Entity.Id).ToArray(); + var tmpFileMap = _productService.GetProductPicturesByProductIds(productIds, null, MediaLoadFlags.None); - foreach (var row in batch) - { - var imageUrls = row.GetDataValue>("ImageUrls"); - if (imageUrls.IsNullOrEmpty()) + foreach (var row in batch) + { + var rawImageUrls = row.GetDataValue("ImageUrls"); + + // Force pipe symbol as separator because file names can contain commas or semicolons. + var imageUrls = rawImageUrls.SplitSafe("|"); + if (imageUrls.Length == 0) { continue; } - + var productId = row.Entity.Id; var imageNumber = 0; - var displayOrder = -1; - var imageFiles = new List(); - - // Collect required image file infos. - foreach (var urlOrPath in imageUrls) - { - var image = CreateDownloadImage(context, urlOrPath, ++imageNumber); - - if (image != null) - imageFiles.Add(image); - - if (imageFiles.Count >= numberOfPictures) - break; - } - - // Download images. - if (imageFiles.Any(x => x.Url.HasValue())) - { - // Async downloading in batch processing is inefficient cause only the image processing benefits from async, - // not the record processing itself. A per record processing may speed up the import. - - AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, imageFiles.Where(x => x.Url.HasValue() && !x.Success.HasValue))); - } - - // Import images. - foreach (var image in imageFiles.OrderBy(x => x.DisplayOrder)) - { - try - { - if ((image.Success ?? false) && File.Exists(image.Path)) - { - Succeeded(image); + var displayOrder = -1; + var imageFiles = new List(); + var catalogAlbumId = _folderService.GetNodeByPath(SystemAlbumProvider.Catalog).Value.Id; + + // Collect required image file infos. + foreach (var urlOrPath in imageUrls) + { + var image = CreateDownloadImage(context, urlOrPath, ++imageNumber); + if (image != null) + { + imageFiles.Add(image); + + if (imageFiles.Count >= numberOfPictures) + break; + } + } + + // Download images. + if (imageFiles.Any(x => x.Url.HasValue())) + { + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, imageFiles.Where(x => x.Url.HasValue() && !x.Success.HasValue))); + + var hasDuplicateFileNames = imageFiles + .Where(x => x.Url.HasValue()) + .Select(x => x.FileName) + .GroupBy(x => x) + .Any(x => x.Count() > 1); + + if (hasDuplicateFileNames) + { + context.Result.AddWarning($"Found duplicate names (not supported yet). File names in URLs have to be unique!", row.GetRowInfo(), "ImageUrls"); + } + } + + // Import images. + foreach (var image in imageFiles.OrderBy(x => x.DisplayOrder)) + { + try + { + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); using (var stream = File.OpenRead(image.Path)) { if ((stream?.Length ?? 0) > 0) { - var tmpFileMap = _productService.GetProductPicturesByProductIds(new int[] { productId }, null, MediaLoadFlags.None); - + MediaFile sourceFile = null; var currentFiles = tmpFileMap.ContainsKey(productId) ? tmpFileMap[productId] : Enumerable.Empty(); @@ -913,132 +935,142 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu displayOrder = currentFiles.Any() ? currentFiles.Select(x => x.DisplayOrder).Max() : 0; } - if (!_mediaService.FindEqualFile(stream, currentFiles.Select(x => x.MediaFile), true, out var _)) + if (_mediaService.FindEqualFile(stream, currentFiles.Select(x => x.MediaFile), true, out var _)) { - var path = _mediaService.CombinePaths(SystemAlbumProvider.Products, image.FileName.ToValidFileName()); - var newFile = _mediaService.SaveFile(path, stream, false, DuplicateFileHandling.Rename); - if ((newFile?.Id ?? 0) != 0) - { - _productService.InsertProductPicture(new ProductMediaFile - { - ProductId = productId, - MediaFileId = newFile.Id, - DisplayOrder = ++displayOrder - }); - - // Update for FixProductMainPictureIds. - row.Entity.UpdatedOnUtc = DateTime.UtcNow; - // Required otherwise nothing updated. - _productRepository.Update(row.Entity); - } + context.Result.AddInfo($"Found equal file in product data for '{image.FileName}'. Skipping file.", row.GetRowInfo(), "ImageUrls"); + } + else if (_mediaService.FindEqualFile(stream, image.FileName, catalogAlbumId, true, out sourceFile)) + { + context.Result.AddInfo($"Found equal file in catalog album for '{image.FileName}'. Assigning existing file instead.", row.GetRowInfo(), "ImageUrls"); } else { - context.Result.AddInfo($"Found equal image in data store for {image.Url}. Skipping field.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + var path = _mediaService.CombinePaths(SystemAlbumProvider.Catalog, image.FileName); + sourceFile = _mediaService.SaveFile(path, stream, false, DuplicateFileHandling.Rename)?.File; + } + + if (sourceFile?.Id > 0) + { + var productMediaFile = new ProductMediaFile + { + ProductId = productId, + MediaFileId = sourceFile.Id, + DisplayOrder = ++displayOrder + }; + + _productService.InsertProductPicture(productMediaFile); + + productMediaFile.MediaFile = sourceFile; + + tmpFileMap.Add(productId, productMediaFile); + // Update for FixProductMainPictureIds. + row.Entity.UpdatedOnUtc = DateTime.UtcNow; + + //_productRepository.Update(row.Entity); } } } - } - else if (image.Url.HasValue()) - { - context.Result.AddInfo($"Download failed for image {image.Url}.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); - } - } - catch (Exception ex) - { - context.Result.AddWarning(ex.ToAllMessages(), row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); - } - } - } + } + else if (image.Url.HasValue()) + { + context.Result.AddInfo($"Download failed for image {image.Url}.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + catch (Exception ex) + { + context.Result.AddWarning(ex.ToAllMessages(), row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + } _productRepository.Context.SaveChanges(); } - protected virtual int ProcessProductManufacturers(ImportExecuteContext context, IEnumerable> batch) - { - _productManufacturerRepository.AutoCommitEnabled = false; - - foreach (var row in batch) - { - var manufacturerIds = row.GetDataValue>("ManufacturerIds"); - if (!manufacturerIds.IsNullOrEmpty()) - { - try - { - foreach (var id in manufacturerIds) - { - if (_productManufacturerRepository.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.ManufacturerId == id).FirstOrDefault() == null) - { - // Ensure that manufacturer exists. - var manufacturer = _manufacturerService.GetManufacturerById(id); - if (manufacturer != null) - { - var productManufacturer = new ProductManufacturer - { - ProductId = row.Entity.Id, - ManufacturerId = manufacturer.Id, - IsFeaturedProduct = false, - DisplayOrder = 1 - }; - _productManufacturerRepository.Insert(productManufacturer); - } - } - } - } - catch (Exception ex) - { - context.Result.AddWarning(ex.Message, row.GetRowInfo(), "ManufacturerIds"); - } - } - } - - // Commit whole batch at once. - var num = _productManufacturerRepository.Context.SaveChanges(); - return num; - } - - protected virtual int ProcessProductCategories(ImportExecuteContext context, IEnumerable> batch) - { - _productCategoryRepository.AutoCommitEnabled = false; - - foreach (var row in batch) - { - var categoryIds = row.GetDataValue>("CategoryIds"); - if (!categoryIds.IsNullOrEmpty()) - { - try - { - foreach (var id in categoryIds) - { - if (!_productCategoryRepository.TableUntracked.Any(x => x.ProductId == row.Entity.Id && x.CategoryId == id)) - { - // Ensure that category exists. - var category = _categoryService.GetCategoryById(id); - if (category != null) - { - var productCategory = new ProductCategory - { - ProductId = row.Entity.Id, - CategoryId = category.Id, - IsFeaturedProduct = false, - DisplayOrder = 1 - }; - _productCategoryRepository.Insert(productCategory); - } - } - } - } - catch (Exception ex) - { - context.Result.AddWarning(ex.Message, row.GetRowInfo(), "CategoryIds"); - } - } - } - - // Commit whole batch at once, - var num = _productCategoryRepository.Context.SaveChanges(); - return num; - } + protected virtual int ProcessProductManufacturers(ImportExecuteContext context, IEnumerable> batch) + { + _productManufacturerRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var manufacturerIds = row.GetDataValue>("ManufacturerIds"); + if (!manufacturerIds.IsNullOrEmpty()) + { + try + { + foreach (var id in manufacturerIds) + { + if (_productManufacturerRepository.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.ManufacturerId == id).FirstOrDefault() == null) + { + // Ensure that manufacturer exists. + var manufacturer = _manufacturerService.GetManufacturerById(id); + if (manufacturer != null) + { + var productManufacturer = new ProductManufacturer + { + ProductId = row.Entity.Id, + ManufacturerId = manufacturer.Id, + IsFeaturedProduct = false, + DisplayOrder = 1 + }; + _productManufacturerRepository.Insert(productManufacturer); + } + } + } + } + catch (Exception ex) + { + context.Result.AddWarning(ex.Message, row.GetRowInfo(), "ManufacturerIds"); + } + } + } + + // Commit whole batch at once. + var num = _productManufacturerRepository.Context.SaveChanges(); + return num; + } + + protected virtual int ProcessProductCategories(ImportExecuteContext context, IEnumerable> batch) + { + _productCategoryRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var categoryIds = row.GetDataValue>("CategoryIds"); + if (!categoryIds.IsNullOrEmpty()) + { + try + { + foreach (var id in categoryIds) + { + if (!_productCategoryRepository.TableUntracked.Any(x => x.ProductId == row.Entity.Id && x.CategoryId == id)) + { + // Ensure that category exists. + var category = _categoryService.GetCategoryById(id); + if (category != null) + { + var productCategory = new ProductCategory + { + ProductId = row.Entity.Id, + CategoryId = category.Id, + IsFeaturedProduct = false, + DisplayOrder = 1 + }; + _productCategoryRepository.Insert(productCategory); + } + } + } + } + catch (Exception ex) + { + context.Result.AddWarning(ex.Message, row.GetRowInfo(), "CategoryIds"); + } + } + } + + // Commit whole batch at once, + var num = _productCategoryRepository.Context.SaveChanges(); + return num; + } protected virtual void ProcessProductTags(ImportExecuteContext context, IEnumerable> batch) { @@ -1130,35 +1162,23 @@ protected virtual void ProcessProductTags(ImportExecuteContext context, IEnumera } private int? ZeroToNull(object value, CultureInfo culture) - { - if (CommonHelper.TryConvert(value, culture, out int result) && result > 0) - { - return result; - } - - return (int?)null; - } - - public static string[] SupportedKeyFields - { - get - { - return new string[] { "Id", "Sku", "Gtin", "ManufacturerPartNumber", "Name" }; - } - } - - public static string[] DefaultKeyFields - { - get - { - return new string[] { "Sku", "Gtin", "ManufacturerPartNumber" }; - } - } - - public class ImportProductMapping - { - public int DestinationId { get; set; } - public bool Inserted { get; set; } - } - } + { + if (CommonHelper.TryConvert(value, culture, out int result) && result > 0) + { + return result; + } + + return (int?)null; + } + + public static string[] SupportedKeyFields => new string[] { "Id", "Sku", "Gtin", "ManufacturerPartNumber", "Name" }; + + public static string[] DefaultKeyFields => new string[] { "Sku", "Gtin", "ManufacturerPartNumber" }; + + public class ImportProductMapping + { + public int DestinationId { get; set; } + public bool Inserted { get; set; } + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs index b2bca811ef..218387e4cc 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs @@ -25,46 +25,46 @@ public partial class ManufacturerService : IManufacturerService, IXmlSitemapPubl private readonly IRepository _manufacturerRepository; private readonly IRepository _productManufacturerRepository; private readonly IRepository _productRepository; - private readonly IRepository _storeMappingRepository; + private readonly IRepository _storeMappingRepository; private readonly IRepository _aclRepository; private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; + private readonly IStoreContext _storeContext; private readonly IRequestCache _requestCache; - public ManufacturerService(IRequestCache requestCache, + public ManufacturerService(IRequestCache requestCache, IRepository manufacturerRepository, IRepository productManufacturerRepository, IRepository productRepository, - IRepository storeMappingRepository, + IRepository storeMappingRepository, IRepository aclRepository, IWorkContext workContext, - IStoreContext storeContext) + IStoreContext storeContext) { _requestCache = requestCache; _manufacturerRepository = manufacturerRepository; _productManufacturerRepository = productManufacturerRepository; _productRepository = productRepository; - _storeMappingRepository = storeMappingRepository; + _storeMappingRepository = storeMappingRepository; _aclRepository = aclRepository; - _workContext = workContext; - _storeContext = storeContext; + _workContext = workContext; + _storeContext = storeContext; - QuerySettings = DbQuerySettings.Default; - } + QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } public virtual void DeleteManufacturer(Manufacturer manufacturer) { if (manufacturer == null) throw new ArgumentNullException("manufacturer"); - + manufacturer.Deleted = true; UpdateManufacturer(manufacturer); } - public virtual IQueryable GetManufacturers(bool showHidden = false, int storeId = 0) - { + public virtual IQueryable GetManufacturers(bool showHidden = false, int storeId = 0) + { var grouping = false; var entityName = nameof(Manufacturer); var query = _manufacturerRepository.Table.Where(m => !m.Deleted); @@ -76,16 +76,16 @@ public virtual IQueryable GetManufacturers(bool showHidden = false // Store mapping. if (!showHidden && storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = from m in query - join sm in _storeMappingRepository.Table - on new { m1 = m.Id, m2 = entityName } equals new { m1 = sm.EntityId, m2 = sm.EntityName } into m_sm - from sm in m_sm.DefaultIfEmpty() - where !m.LimitedToStores || storeId == sm.StoreId - select m; + { + query = from m in query + join sm in _storeMappingRepository.Table + on new { m1 = m.Id, m2 = entityName } equals new { m1 = sm.EntityId, m2 = sm.EntityName } into m_sm + from sm in m_sm.DefaultIfEmpty() + where !m.LimitedToStores || storeId == sm.StoreId + select m; grouping = true; - } + } // ACL (access control list). if (!showHidden && !QuerySettings.IgnoreAcl) @@ -104,7 +104,7 @@ from a in ma.DefaultIfEmpty() if (grouping) { - query = + query = from m in query group m by m.Id into mGroup orderby mGroup.Key @@ -112,7 +112,7 @@ orderby mGroup.Key } return query; - } + } public virtual IList GetAllManufacturers(bool showHidden = false) { @@ -121,19 +121,19 @@ public virtual IList GetAllManufacturers(bool showHidden = false) public virtual IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false) { - var query = GetManufacturers(showHidden, storeId); + var query = GetManufacturers(showHidden, storeId); - if (manufacturerName.HasValue()) - query = query.Where(m => m.Name.Contains(manufacturerName)); + if (manufacturerName.HasValue()) + query = query.Where(m => m.Name.Contains(manufacturerName)); - query = query.OrderBy(m => m.DisplayOrder) - .ThenBy(m => m.Name); + query = query.OrderBy(m => m.DisplayOrder) + .ThenBy(m => m.Name); var manufacturers = query.ToList(); return manufacturers; } - public virtual IPagedList GetAllManufacturers(string manufacturerName, + public virtual IPagedList GetAllManufacturers(string manufacturerName, int pageIndex, int pageSize, int storeId = 0, bool showHidden = false) { var manufacturers = GetAllManufacturers(manufacturerName, storeId, showHidden); @@ -145,8 +145,8 @@ public virtual Manufacturer GetManufacturerById(int manufacturerId) if (manufacturerId == 0) return null; - return _manufacturerRepository.GetByIdCached(manufacturerId, "db.manu.id-" + manufacturerId); - } + return _manufacturerRepository.GetByIdCached(manufacturerId, "db.manu.id-" + manufacturerId); + } public virtual IList GetManufacturersByIds(int[] manufacturerIds) { @@ -189,15 +189,15 @@ public virtual void UpdateManufacturer(Manufacturer manufacturer) _requestCache.RemoveByPattern(PRODUCTMANUFACTURERS_PATTERN_KEY); } - public virtual void UpdateHasDiscountsApplied(Manufacturer manufacturer) - { - Guard.NotNull(manufacturer, nameof(manufacturer)); + public virtual void UpdateHasDiscountsApplied(Manufacturer manufacturer) + { + Guard.NotNull(manufacturer, nameof(manufacturer)); - manufacturer.HasDiscountsApplied = manufacturer.AppliedDiscounts.Count > 0; - UpdateManufacturer(manufacturer); - } + manufacturer.HasDiscountsApplied = manufacturer.AppliedDiscounts.Count > 0; + UpdateManufacturer(manufacturer); + } - public virtual void DeleteProductManufacturer(ProductManufacturer productManufacturer) + public virtual void DeleteProductManufacturer(ProductManufacturer productManufacturer) { if (productManufacturer == null) throw new ArgumentNullException("productManufacturer"); @@ -248,65 +248,65 @@ public virtual IList GetProductManufacturersByProductId(int var key = string.Format(PRODUCTMANUFACTURERS_ALLBYPRODUCTID_KEY, showHidden, productId, rolesToken, storeToken); return _requestCache.Get(key, () => - { - var query = from pm in _productManufacturerRepository.Table - join m in _manufacturerRepository.Table on pm.ManufacturerId equals m.Id - where pm.ProductId == productId && !m.Deleted && (showHidden || m.Published) - select pm; + { + var query = from pm in _productManufacturerRepository.Table + join m in _manufacturerRepository.Table on pm.ManufacturerId equals m.Id + where pm.ProductId == productId && !m.Deleted && (showHidden || m.Published) + select pm; query = ApplyHiddenProductManufacturerFilter(query, storeId, showHidden); query = query.OrderBy(pm => pm.DisplayOrder); query = query.Include(pm => pm.Manufacturer.MediaFile); var productManufacturers = query.ToList(); - return productManufacturers; - }); + return productManufacturers; + }); } - public virtual Multimap GetProductManufacturersByManufacturerIds(int[] manufacturerIds, bool showHidden = false) - { - Guard.NotNull(manufacturerIds, nameof(manufacturerIds)); + public virtual Multimap GetProductManufacturersByManufacturerIds(int[] manufacturerIds, bool showHidden = false) + { + Guard.NotNull(manufacturerIds, nameof(manufacturerIds)); var query = _productManufacturerRepository.TableUntracked - .Where(x => manufacturerIds.Contains(x.ManufacturerId)); + .Where(x => manufacturerIds.Contains(x.ManufacturerId)); query = ApplyHiddenProductManufacturerFilter(query, _storeContext.CurrentStore.Id, showHidden); query = query.OrderBy(pm => pm.DisplayOrder); var map = query - .ToList() - .ToMultimap(x => x.ManufacturerId, x => x); + .ToList() + .ToMultimap(x => x.ManufacturerId, x => x); - return map; - } + return map; + } - public virtual Multimap GetProductManufacturersByProductIds(int[] productIds, bool showHidden = false) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual Multimap GetProductManufacturersByProductIds(int[] productIds, bool showHidden = false) + { + Guard.NotNull(productIds, nameof(productIds)); if (!productIds.Any()) { return new Multimap(); } - var query = - from pm in _productManufacturerRepository.TableUntracked - //join m in _manufacturerRepository.TableUntracked on pm.ManufacturerId equals m.Id // Eager loading does not work with this join - where !pm.Manufacturer.Deleted && productIds.Contains(pm.ProductId) - select pm; + var query = + from pm in _productManufacturerRepository.TableUntracked + //join m in _manufacturerRepository.TableUntracked on pm.ManufacturerId equals m.Id // Eager loading does not work with this join + where !pm.Manufacturer.Deleted && productIds.Contains(pm.ProductId) + select pm; query = ApplyHiddenProductManufacturerFilter(query, _storeContext.CurrentStore.Id, showHidden); query = query.Include(x => x.Manufacturer.MediaFile); - var map = query - .OrderBy(x => x.ProductId) - .ThenBy(x => x.DisplayOrder) - .ToList() - .ToMultimap(x => x.ProductId, x => x); + var map = query + .OrderBy(x => x.ProductId) + .ThenBy(x => x.DisplayOrder) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } - return map; - } - public virtual ProductManufacturer GetProductManufacturerById(int productManufacturerId) { if (productManufacturerId == 0) @@ -346,7 +346,7 @@ protected virtual IQueryable ApplyHiddenProductManufacturer // Store mapping. if (!showHidden && storeId > 0 && !QuerySettings.IgnoreMultiStore) { - query = + query = from pm in query join m in _manufacturerRepository.Table on pm.ManufacturerId equals m.Id join sm in _storeMappingRepository.Table @@ -390,35 +390,35 @@ orderby pmGroup.Key #region XML Sitemap public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) - { - if (!context.LoadSetting().XmlSitemapIncludesManufacturers) - return null; - - var query = GetManufacturers(false, context.RequestStoreId).OrderBy(x => x.DisplayOrder).ThenBy(x => x.Name); - return new ManufacturerXmlSitemapResult { Query = query }; - } - - class ManufacturerXmlSitemapResult : XmlSitemapProvider - { - public IQueryable Query { get; set; } - - public override int GetTotalCount() - { - return Query.Count(); - } - - public override IEnumerable Enlist() - { - var topics = Query.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); - foreach (var x in topics) - { - yield return new NamedEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }; - } - } - - public override int Order => int.MinValue + 100; - } - - #endregion - } + { + if (!context.LoadSetting().XmlSitemapIncludesManufacturers) + return null; + + var query = GetManufacturers(false, context.RequestStoreId).OrderBy(x => x.DisplayOrder).ThenBy(x => x.Name); + return new ManufacturerXmlSitemapResult { Query = query }; + } + + class ManufacturerXmlSitemapResult : XmlSitemapProvider + { + public IQueryable Query { get; set; } + + public override int GetTotalCount() + { + return Query.Count(); + } + + public override IEnumerable Enlist() + { + var topics = Query.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); + foreach (var x in topics) + { + yield return new NamedEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } + } + + public override int Order => int.MinValue + 100; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs index eae06162de..82df97a567 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs @@ -35,7 +35,7 @@ orderby pt.DisplayOrder var templates = query.ToList(); return templates; } - + public virtual ManufacturerTemplate GetManufacturerTemplateById(int manufacturerTemplateId) { if (manufacturerTemplateId == 0) diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs index cc35919d93..81014679e0 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs @@ -2,48 +2,48 @@ namespace SmartStore.Services.Catalog.Modelling { - public class CheckoutAttributeQueryItem - { - public CheckoutAttributeQueryItem(int attributeId, string value) - { - Value = value ?? string.Empty; - AttributeId = attributeId; - } + public class CheckoutAttributeQueryItem + { + public CheckoutAttributeQueryItem(int attributeId, string value) + { + Value = value ?? string.Empty; + AttributeId = attributeId; + } - /// - /// Key used for form names. - /// - /// Checkout attribute identifier - /// Key - public static string CreateKey(int attributeId) - { - return $"cattr{attributeId}"; - } + /// + /// Key used for form names. + /// + /// Checkout attribute identifier + /// Key + public static string CreateKey(int attributeId) + { + return $"cattr{attributeId}"; + } - public string Value { get; private set; } - public int AttributeId { get; private set; } - public DateTime? Date { get; set; } - public bool IsFile { get; set; } - public bool IsText { get; set; } + public string Value { get; private set; } + public int AttributeId { get; private set; } + public DateTime? Date { get; set; } + public bool IsFile { get; set; } + public bool IsText { get; set; } - public override string ToString() - { - var key = CreateKey(AttributeId); + public override string ToString() + { + var key = CreateKey(AttributeId); - if (Date.HasValue) - { - return key + "-date"; - } - else if (IsFile) - { - return key + "-file"; - } - else if (IsText) - { - return key + "-text"; - } + if (Date.HasValue) + { + return key + "-date"; + } + else if (IsFile) + { + return key + "-file"; + } + else if (IsText) + { + return key + "-text"; + } - return key; - } - } + return key; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/GiftCardQueryItem.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/GiftCardQueryItem.cs index df61daf13b..26cc81b71e 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/GiftCardQueryItem.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/GiftCardQueryItem.cs @@ -1,40 +1,40 @@ namespace SmartStore.Services.Catalog.Modelling { - public class GiftCardQueryItem - { - public GiftCardQueryItem(string name, string value) - { - Guard.NotEmpty(name, nameof(name)); + public class GiftCardQueryItem + { + public GiftCardQueryItem(string name, string value) + { + Guard.NotEmpty(name, nameof(name)); - Name = name.ToLower(); - Value = value ?? string.Empty; + Name = name.ToLower(); + Value = value ?? string.Empty; - if (Name.StartsWith(".")) - { - Name = Name.Substring(1); - } - } + if (Name.StartsWith(".")) + { + Name = Name.Substring(1); + } + } - public static string CreateKey(int productId, int bundleItemId, string name) - { - if (name.HasValue()) - { - return $"giftcard{productId}-{bundleItemId}-.{name.EmptyNull().ToLower()}"; - } + public static string CreateKey(int productId, int bundleItemId, string name) + { + if (name.HasValue()) + { + return $"giftcard{productId}-{bundleItemId}-.{name.EmptyNull().ToLower()}"; + } - // Just return field prefix for partial views. - return $"giftcard{productId}-{bundleItemId}-"; - } + // Just return field prefix for partial views. + return $"giftcard{productId}-{bundleItemId}-"; + } - public string Name { get; private set; } - public string Value { get; private set; } + public string Name { get; private set; } + public string Value { get; private set; } - public int ProductId { get; set; } - public int BundleItemId { get; set; } + public int ProductId { get; set; } + public int BundleItemId { get; set; } - public override string ToString() - { - return CreateKey(ProductId, BundleItemId, Name); - } - } + public override string ToString() + { + return CreateKey(ProductId, BundleItemId, Name); + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/IProductVariantQueryFactory.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/IProductVariantQueryFactory.cs index 119a770832..178e8d4efc 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/IProductVariantQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/IProductVariantQueryFactory.cs @@ -1,17 +1,17 @@ namespace SmartStore.Services.Catalog.Modelling { - public interface IProductVariantQueryFactory - { - /// - /// The last created product variant query. The MVC model binder uses this property to avoid repeated binding. - /// - ProductVariantQuery Current { get; } + public interface IProductVariantQueryFactory + { + /// + /// The last created product variant query. The MVC model binder uses this property to avoid repeated binding. + /// + ProductVariantQuery Current { get; } - /// - /// Creates a name value collection with product variants from the current - /// by looking up corresponding keys in posted form and/or query string - /// - /// Product variant query - ProductVariantQuery CreateFromQuery(); - } + /// + /// Creates a name value collection with product variants from the current + /// by looking up corresponding keys in posted form and/or query string + /// + /// Product variant query + ProductVariantQuery CreateFromQuery(); + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQuery.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQuery.cs index e8fe50918a..cf035adc29 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQuery.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQuery.cs @@ -3,79 +3,72 @@ namespace SmartStore.Services.Catalog.Modelling { - public class ProductVariantQuery - { - private readonly List _variants; - private readonly List _giftCards; - private readonly List _checkoutAttributes; + public class ProductVariantQuery + { + private readonly List _variants; + private readonly List _giftCards; + private readonly List _checkoutAttributes; - public ProductVariantQuery() - { - _variants = new List(); - _giftCards = new List(); - _checkoutAttributes = new List(); - } + public ProductVariantQuery() + { + _variants = new List(); + _giftCards = new List(); + _checkoutAttributes = new List(); + } public int VariantCombinationId { get; set; } - public IReadOnlyList Variants - { - get - { - return _variants; - } - } + public IReadOnlyList Variants => _variants; - public IReadOnlyList GiftCards - { - get - { - return _giftCards; - } - } + public IReadOnlyList GiftCards => _giftCards; - public IReadOnlyList CheckoutAttributes - { - get - { - return _checkoutAttributes; - } - } + public IReadOnlyList CheckoutAttributes => _checkoutAttributes; - public void AddVariant(ProductVariantQueryItem item) - { - _variants.Add(item); - } + public void AddVariant(ProductVariantQueryItem item) + { + var exists = _variants.Any(x => + x.ProductId == item.ProductId && + x.BundleItemId == item.BundleItemId && + x.AttributeId == item.AttributeId && + x.VariantAttributeId == item.VariantAttributeId && + x.Value == item.Value + ); - public void AddGiftCard(GiftCardQueryItem item) - { - _giftCards.Add(item); - } + if (!exists) + { + _variants.Add(item); + } + } - public void AddCheckoutAttribute(CheckoutAttributeQueryItem item) - { - _checkoutAttributes.Add(item); - } + public void AddGiftCard(GiftCardQueryItem item) + { + _giftCards.Add(item); + } - public string GetGiftCardValue(int productId, int bundleItemId, string name) - { - return _giftCards.FirstOrDefault(x => - x.ProductId == productId && - x.BundleItemId == bundleItemId && - x.Name.IsCaseInsensitiveEqual(name)) - ?.Value; - } + public void AddCheckoutAttribute(CheckoutAttributeQueryItem item) + { + _checkoutAttributes.Add(item); + } - public override string ToString() - { - var groups = new string[] - { - string.Join("&", Variants.Select(x => x.ToString())), - string.Join("&", GiftCards.Select(x => x.ToString())), - string.Join("&", CheckoutAttributes.Select(x => x.ToString())) - }; + public string GetGiftCardValue(int productId, int bundleItemId, string name) + { + return _giftCards.FirstOrDefault(x => + x.ProductId == productId && + x.BundleItemId == bundleItemId && + x.Name.IsCaseInsensitiveEqual(name)) + ?.Value; + } - return string.Join("&", groups.Where(x => x.HasValue())); - } - } + public override string ToString() + { + var groups = new string[] + { + string.Join("&", Variants.Select(x => x.ToString())), + string.Join("&", GiftCards.Select(x => x.ToString())), + string.Join("&", CheckoutAttributes.Select(x => x.ToString())) + }; + + return string.Join("&", groups.Where(x => x.HasValue())); + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs index 04543438ff..c7cb504693 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs @@ -9,68 +9,66 @@ namespace SmartStore.Services.Catalog.Modelling { public class ProductVariantQueryFactory : IProductVariantQueryFactory - { - internal static readonly Regex IsVariantKey = new Regex(@"pvari[0-9]+-[0-9]+-[0-9]+-[0-9]+", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); - internal static readonly Regex IsVariantAliasKey = new Regex(@"\w+-[0-9]+-[0-9]+-[0-9]+", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); - internal static readonly Regex IsGiftCardKey = new Regex(@"giftcard[0-9]+-[0-9]+-\.\w+$", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); - internal static readonly Regex IsCheckoutAttributeKey = new Regex(@"cattr[0-9]+", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); - - protected readonly HttpContextBase _httpContext; - protected readonly ICommonServices _services; - protected readonly ICatalogSearchQueryAliasMapper _catalogSearchQueryAliasMapper; - private Multimap _queryItems; - - public ProductVariantQueryFactory( - HttpContextBase httpContext, - ICommonServices services, - ICatalogSearchQueryAliasMapper catalogSearchQueryAliasMapper) - { - _httpContext = httpContext; - _services = services; - _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; - } - - protected Multimap QueryItems - { - get - { - if (_queryItems == null) - { - _queryItems = new Multimap(); - - if (_httpContext.Request?.Unvalidated != null) - { - var form = _httpContext.Request.Unvalidated.Form; - - if (form != null) - { - foreach (var key in form.AllKeys) - { - if (key.HasValue()) - { - _queryItems.AddRange(key, form[key].SplitSafe(",")); - } - } - } + { + internal static readonly Regex IsVariantKey = new Regex(@"pvari[0-9]+-[0-9]+-[0-9]+-[0-9]+", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + internal static readonly Regex IsVariantAliasKey = new Regex(@"\w+-[0-9]+-[0-9]+-[0-9]+", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + internal static readonly Regex IsGiftCardKey = new Regex(@"giftcard[0-9]+-[0-9]+-\.\w+$", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + internal static readonly Regex IsCheckoutAttributeKey = new Regex(@"cattr[0-9]+", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + + protected readonly HttpContextBase _httpContext; + protected readonly ICommonServices _services; + protected readonly ICatalogSearchQueryAliasMapper _catalogSearchQueryAliasMapper; + private Multimap _queryItems; + + public ProductVariantQueryFactory( + HttpContextBase httpContext, + ICommonServices services, + ICatalogSearchQueryAliasMapper catalogSearchQueryAliasMapper) + { + _httpContext = httpContext; + _services = services; + _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; + } - var query = _httpContext.Request.Unvalidated.QueryString; + protected Multimap QueryItems + { + get + { + if (_queryItems == null) + { + _queryItems = new Multimap(); + if (_httpContext.Request?.Unvalidated != null) + { + var form = _httpContext.Request.Unvalidated.Form; + if (form != null) + { + foreach (var key in form.AllKeys) + { + if (key.HasValue()) + { + _queryItems.AddRange(key, form[key].SplitSafe(",")); + } + } + } + + var query = _httpContext.Request.Unvalidated.QueryString; if (query != null) - { - foreach (var key in query.AllKeys) - { - if (key.HasValue()) - { - _queryItems.AddRange(key, query[key].SplitSafe(",")); - } - } - } - } - } - - return _queryItems; - } - } + { + foreach (var key in query.AllKeys) + { + if (key.HasValue()) + { + _queryItems.AddRange(key, query[key].SplitSafe(",")); + } + } + } + } + } + + return _queryItems; + } + } public ProductVariantQuery Current { get; private set; } @@ -123,53 +121,65 @@ public ProductVariantQuery CreateFromQuery() } private DateTime? ConvertToDate(string key, string value) - { - var year = 0; - var month = 0; - var day = 0; - - if (key.EndsWith("-date")) - { - // Convert from one query string item. - var dateItems = value.SplitSafe("-"); - year = dateItems.SafeGet(0).ToInt(); - month = dateItems.SafeGet(1).ToInt(); - day = dateItems.SafeGet(2).ToInt(); - } - else if (key.EndsWith("-year")) - { - // Convert from three form controls. - var dateKey = key.Replace("-year", ""); - year = value.ToInt(); - month = QueryItems[dateKey + "-month"].FirstOrDefault()?.ToInt() ?? 0; - day = QueryItems[dateKey + "-day"].FirstOrDefault()?.ToInt() ?? 0; - } - - if (year > 0 && month > 0 && day > 0) - { - try - { - return new DateTime(year, month, day); - } - catch { } - } - - return null; - } - - protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICollection values) - { - var ids = key.Replace("pvari", "").SplitSafe("-"); - if (ids.Length < 4) - return; - - var isDate = key.EndsWith("-date") || key.EndsWith("-year"); - var isFile = key.EndsWith("-file"); - var isText = key.EndsWith("-text"); - - if (isDate || isFile || isText) - { - var value = isText ? string.Join(",", values) : values.First(); + { + var year = 0; + var month = 0; + var day = 0; + + if (key.EndsWith("-date")) + { + // Convert from one query string item. + var dateItems = value.SplitSafe("-"); + year = dateItems.SafeGet(0).ToInt(); + month = dateItems.SafeGet(1).ToInt(); + day = dateItems.SafeGet(2).ToInt(); + } + else if (key.EndsWith("-year")) + { + // Convert from three form controls. + var dateKey = key.Replace("-year", ""); + year = value.ToInt(); + + if (QueryItems.ContainsKey(dateKey + "-month")) + { + var str = QueryItems[dateKey + "-month"].FirstOrDefault(); + month = str?.ToInt() ?? 0; + } + + if (QueryItems.ContainsKey(dateKey + "-day")) + { + var str = QueryItems[dateKey + "-day"].FirstOrDefault(); + day = str?.ToInt() ?? 0; + } + } + + if (year > 0 && month > 0 && day > 0) + { + try + { + return new DateTime(year, month, day); + } + catch + { + } + } + + return null; + } + + protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICollection values) + { + var ids = key.Replace("pvari", "").SplitSafe("-"); + if (ids.Length < 4) + return; + + var isDate = key.EndsWith("-date") || key.EndsWith("-year"); + var isFile = key.EndsWith("-file"); + var isText = key.EndsWith("-text"); + + if (isDate || isFile || isText) + { + var value = isText ? string.Join(",", values) : values.First(); var variant = new ProductVariantQueryItem(value) { ProductId = ids[0].ToInt(), @@ -181,16 +191,16 @@ protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICo }; if (isDate) - { - variant.Date = ConvertToDate(key, value); - } - - query.AddVariant(variant); - } - else - { - foreach (var value in values) - { + { + variant.Date = ConvertToDate(key, value); + } + + query.AddVariant(variant); + } + else + { + foreach (var value in values) + { var variant = new ProductVariantQueryItem(value) { ProductId = ids[0].ToInt(), @@ -200,42 +210,42 @@ protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICo }; query.AddVariant(variant); - } - } - } - - protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key, ICollection values, int languageId) - { - var ids = key.SplitSafe("-"); - var len = ids.Length; - if (len < 4) - return; - - var isDate = key.EndsWith("-date") || key.EndsWith("-year"); - var isFile = key.EndsWith("-file"); - var isText = key.EndsWith("-text"); - - if (isDate || isFile || isText) - { - ids = ids.Take(len - 1).ToArray(); - len = ids.Length; - } - - var alias = string.Join("-", ids.Take(len - 3)); - var attributeId = _catalogSearchQueryAliasMapper.GetVariantIdByAlias(alias, languageId); - if (attributeId == 0) - return; - - var productId = ids.SafeGet(len - 3).ToInt(); - var bundleItemId = ids.SafeGet(len - 2).ToInt(); - var variantAttributeId = ids.SafeGet(len - 1).ToInt(); - - if (productId == 0 || variantAttributeId == 0) - return; - - if (isDate || isFile || isText) - { - var value = isText ? string.Join(",", values) : values.First(); + } + } + } + + protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key, ICollection values, int languageId) + { + var ids = key.SplitSafe("-"); + var len = ids.Length; + if (len < 4) + return; + + var isDate = key.EndsWith("-date") || key.EndsWith("-year"); + var isFile = key.EndsWith("-file"); + var isText = key.EndsWith("-text"); + + if (isDate || isFile || isText) + { + ids = ids.Take(len - 1).ToArray(); + len = ids.Length; + } + + var alias = string.Join("-", ids.Take(len - 3)); + var attributeId = _catalogSearchQueryAliasMapper.GetVariantIdByAlias(alias, languageId); + if (attributeId == 0) + return; + + var productId = ids.SafeGet(len - 3).ToInt(); + var bundleItemId = ids.SafeGet(len - 2).ToInt(); + var variantAttributeId = ids.SafeGet(len - 1).ToInt(); + + if (productId == 0 || variantAttributeId == 0) + return; + + if (isDate || isFile || isText) + { + var value = isText ? string.Join(",", values) : values.First(); var variant = new ProductVariantQueryItem(value) { ProductId = productId, @@ -248,27 +258,27 @@ protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key }; if (isDate) - { - variant.Date = ConvertToDate(key, value); - } - - query.AddVariant(variant); - } - else - { - foreach (var value in values) - { - // We cannot use GetVariantOptionIdByAlias. It doesn't necessarily provide a ProductVariantAttributeValue.Id associated with this product. - //var optionId = _catalogSearchQueryAliasMapper.GetVariantOptionIdByAlias(value, attributeId, languageId); - var optionId = 0; - string valueAlias = null; - - var valueIds = value.SplitSafe("-"); - if (valueIds.Length >= 2) - { - optionId = valueIds.SafeGet(valueIds.Length - 1).ToInt(); - valueAlias = string.Join("-", valueIds.Take(valueIds.Length - 1)); - } + { + variant.Date = ConvertToDate(key, value); + } + + query.AddVariant(variant); + } + else + { + foreach (var value in values) + { + // We cannot use GetVariantOptionIdByAlias. It doesn't necessarily provide a ProductVariantAttributeValue.Id associated with this product. + //var optionId = _catalogSearchQueryAliasMapper.GetVariantOptionIdByAlias(value, attributeId, languageId); + var optionId = 0; + string valueAlias = null; + + var valueIds = value.SplitSafe("-"); + if (valueIds.Length >= 2) + { + optionId = valueIds.SafeGet(valueIds.Length - 1).ToInt(); + valueAlias = string.Join("-", valueIds.Take(valueIds.Length - 1)); + } var variant = new ProductVariantQueryItem(optionId == 0 ? value : optionId.ToString()) { @@ -280,42 +290,42 @@ protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key }; if (optionId != 0) - { - variant.ValueAlias = valueAlias; - } - - query.AddVariant(variant); - } - } - } - - protected virtual void ConvertGiftCard(ProductVariantQuery query, string key, string value) - { - var elements = key.Replace("giftcard", "").SplitSafe("-"); - if (elements.Length > 2) - { - var giftCard = new GiftCardQueryItem(elements[2], value); - giftCard.ProductId = elements[0].ToInt(); - giftCard.BundleItemId = elements[1].ToInt(); - - query.AddGiftCard(giftCard); - } - } - - protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, string key, ICollection values) - { - var ids = key.Replace("cattr", "").SplitSafe("-"); - if (ids.Length <= 0) - return; - - var attributeId = ids[0].ToInt(); - var isDate = key.EndsWith("-date") || key.EndsWith("-year"); - var isFile = key.EndsWith("-file"); - var isText = key.EndsWith("-text"); - - if (isDate || isFile || isText) - { - var value = isText ? string.Join(",", values) : values.First(); + { + variant.ValueAlias = valueAlias; + } + + query.AddVariant(variant); + } + } + } + + protected virtual void ConvertGiftCard(ProductVariantQuery query, string key, string value) + { + var elements = key.Replace("giftcard", "").SplitSafe("-"); + if (elements.Length > 2) + { + var giftCard = new GiftCardQueryItem(elements[2], value); + giftCard.ProductId = elements[0].ToInt(); + giftCard.BundleItemId = elements[1].ToInt(); + + query.AddGiftCard(giftCard); + } + } + + protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, string key, ICollection values) + { + var ids = key.Replace("cattr", "").SplitSafe("-"); + if (ids.Length <= 0) + return; + + var attributeId = ids[0].ToInt(); + var isDate = key.EndsWith("-date") || key.EndsWith("-year"); + var isFile = key.EndsWith("-file"); + var isText = key.EndsWith("-text"); + + if (isDate || isFile || isText) + { + var value = isText ? string.Join(",", values) : values.First(); var attribute = new CheckoutAttributeQueryItem(attributeId, value) { IsFile = isFile, @@ -323,23 +333,23 @@ protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, strin }; if (isDate) - { - attribute.Date = ConvertToDate(key, value); - } - - query.AddCheckoutAttribute(attribute); - } - else - { - foreach (var value in values) - { - query.AddCheckoutAttribute(new CheckoutAttributeQueryItem(attributeId, value)); - } - } - } - - protected virtual void ConvertItems(HttpRequestBase request, ProductVariantQuery query, string key, ICollection values) - { - } - } + { + attribute.Date = ConvertToDate(key, value); + } + + query.AddCheckoutAttribute(attribute); + } + else + { + foreach (var value in values) + { + query.AddCheckoutAttribute(new CheckoutAttributeQueryItem(attributeId, value)); + } + } + } + + protected virtual void ConvertItems(HttpRequestBase request, ProductVariantQuery query, string key, ICollection values) + { + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs index a41f3ea64b..aacf8f9997 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs @@ -2,59 +2,59 @@ namespace SmartStore.Services.Catalog.Modelling { - public class ProductVariantQueryItem - { - public ProductVariantQueryItem(string value) - { - Value = value ?? string.Empty; - } - - /// - /// Key used for form names. - /// - /// Product identifier - /// Bundle item identifier. 0 if not a bundle item. - /// Product attribute identifier - /// Product variant attribute identifier - /// Key - public static string CreateKey(int productId, int bundleItemId, int attributeId, int variantAttributeId) - { - return $"pvari{productId}-{bundleItemId}-{attributeId}-{variantAttributeId}"; - } - - public string Value { get; private set; } - - public int ProductId { get; set; } - public int BundleItemId { get; set; } - public int AttributeId { get; set; } - public int VariantAttributeId { get; set; } - public DateTime? Date { get; set; } - public bool IsFile { get; set; } - public bool IsText { get; set; } - - public string Alias { get; set; } - public string ValueAlias { get; set; } - - public override string ToString() - { - var key = Alias.HasValue() - ? $"{Alias}-{ProductId}-{BundleItemId}-{VariantAttributeId}" - : CreateKey(ProductId, BundleItemId, AttributeId, VariantAttributeId); - - if (Date.HasValue) - { - return key + "-date"; - } - else if (IsFile) - { - return key + "-file"; - } - else if (IsText) - { - return key + "-text"; - } - - return key; - } - } + public class ProductVariantQueryItem + { + public ProductVariantQueryItem(string value) + { + Value = value ?? string.Empty; + } + + /// + /// Key used for form names. + /// + /// Product identifier + /// Bundle item identifier. 0 if not a bundle item. + /// Product attribute identifier + /// Product variant attribute identifier + /// Key + public static string CreateKey(int productId, int bundleItemId, int attributeId, int variantAttributeId) + { + return $"pvari{productId}-{bundleItemId}-{attributeId}-{variantAttributeId}"; + } + + public string Value { get; private set; } + + public int ProductId { get; set; } + public int BundleItemId { get; set; } + public int AttributeId { get; set; } + public int VariantAttributeId { get; set; } + public DateTime? Date { get; set; } + public bool IsFile { get; set; } + public bool IsText { get; set; } + + public string Alias { get; set; } + public string ValueAlias { get; set; } + + public override string ToString() + { + var key = Alias.HasValue() + ? $"{Alias}-{ProductId}-{BundleItemId}-{VariantAttributeId}" + : CreateKey(ProductId, BundleItemId, AttributeId, VariantAttributeId); + + if (Date.HasValue) + { + return key + "-date"; + } + else if (IsFile) + { + return key + "-file"; + } + else if (IsText) + { + return key + "-text"; + } + + return key; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryModelBinder.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryModelBinder.cs index 536553d3ff..ebb79e92b8 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryModelBinder.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryModelBinder.cs @@ -3,39 +3,39 @@ namespace SmartStore.Services.Catalog.Modelling { - [ModelBinderType(typeof(ProductVariantQuery))] - public class ProductAttributeQueryModelBinder : IModelBinder - { - private readonly IProductVariantQueryFactory _factory; + [ModelBinderType(typeof(ProductVariantQuery))] + public class ProductAttributeQueryModelBinder : IModelBinder + { + private readonly IProductVariantQueryFactory _factory; - public ProductAttributeQueryModelBinder(IProductVariantQueryFactory factory) - { - _factory = factory; - } + public ProductAttributeQueryModelBinder(IProductVariantQueryFactory factory) + { + _factory = factory; + } - public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) - { - if (_factory.Current != null) - { - // Don't bind again for current request - return _factory.Current; - } + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (_factory.Current != null) + { + // Don't bind again for current request + return _factory.Current; + } - if (controllerContext.IsChildAction) - { - // Never attempt to bind in child actions. We require the binding to happen in a parent action. - return _factory.Current; - } + if (controllerContext.IsChildAction) + { + // Never attempt to bind in child actions. We require the binding to happen in a parent action. + return _factory.Current; + } - var modelType = bindingContext.ModelType; + var modelType = bindingContext.ModelType; - if (modelType != typeof(ProductVariantQuery)) - { - return new ProductVariantQuery(); - } + if (modelType != typeof(ProductVariantQuery)) + { + return new ProductVariantQuery(); + } - var query = _factory.CreateFromQuery(); - return query; - } - } + var query = _factory.CreateFromQuery(); + return query; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs index eea8195b4f..14a4cf1495 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs @@ -11,72 +11,72 @@ namespace SmartStore.Services.Catalog /// Cargo data to reduce database round trips during price calculation /// public class PriceCalculationContext - { - protected List _productIds; - private List _productIdsTierPrices; - private List _productIdsAppliedDiscounts; - private List _bundledProductIds; + { + protected List _productIds; + private List _productIdsTierPrices; + private List _productIdsAppliedDiscounts; + private List _bundledProductIds; private List _groupedProductIds; private Func> _funcAttributes; - private Func> _funcAttributeCombinations; - private Func> _funcTierPrices; - private Func> _funcProductCategories; - private Func> _funcProductManufacturers; - private Func> _funcAppliedDiscounts; - private Func> _funcProductBundleItems; + private Func> _funcAttributeCombinations; + private Func> _funcTierPrices; + private Func> _funcProductCategories; + private Func> _funcProductManufacturers; + private Func> _funcAppliedDiscounts; + private Func> _funcProductBundleItems; private Func> _funcAssociatedProducts; private LazyMultimap _attributes; - private LazyMultimap _attributeCombinations; - private LazyMultimap _tierPrices; - private LazyMultimap _productCategories; - private LazyMultimap _productManufacturers; - private LazyMultimap _appliedDiscounts; - private LazyMultimap _productBundleItems; + private LazyMultimap _attributeCombinations; + private LazyMultimap _tierPrices; + private LazyMultimap _productCategories; + private LazyMultimap _productManufacturers; + private LazyMultimap _appliedDiscounts; + private LazyMultimap _productBundleItems; private LazyMultimap _associatedProducts; public PriceCalculationContext(IEnumerable products, - Func> attributes, - Func> attributeCombinations, - Func> tierPrices, - Func> productCategories, - Func> productManufacturers, - Func> appliedDiscounts, - Func> productBundleItems, + Func> attributes, + Func> attributeCombinations, + Func> tierPrices, + Func> productCategories, + Func> productManufacturers, + Func> appliedDiscounts, + Func> productBundleItems, Func> associatedProducts) - { - if (products == null) - { - _productIds = new List(); - _productIdsTierPrices = new List(); - _productIdsAppliedDiscounts = new List(); - _bundledProductIds = new List(); + { + if (products == null) + { + _productIds = new List(); + _productIdsTierPrices = new List(); + _productIdsAppliedDiscounts = new List(); + _bundledProductIds = new List(); _groupedProductIds = new List(); - } - else - { - _productIds = new List(products.Select(x => x.Id)); - _productIdsTierPrices = new List(products.Where(x => x.HasTierPrices).Select(x => x.Id)); - _productIdsAppliedDiscounts = new List(products.Where(x => x.HasDiscountsApplied).Select(x => x.Id)); - _bundledProductIds = new List(products.Where(x => x.ProductType == ProductType.BundledProduct).Select(x => x.Id)); + } + else + { + _productIds = new List(products.Select(x => x.Id)); + _productIdsTierPrices = new List(products.Where(x => x.HasTierPrices).Select(x => x.Id)); + _productIdsAppliedDiscounts = new List(products.Where(x => x.HasDiscountsApplied).Select(x => x.Id)); + _bundledProductIds = new List(products.Where(x => x.ProductType == ProductType.BundledProduct).Select(x => x.Id)); _groupedProductIds = new List(products.Where(x => x.ProductType == ProductType.GroupedProduct).Select(x => x.Id)); } - _funcAttributes = attributes; - _funcAttributeCombinations = attributeCombinations; - _funcTierPrices = tierPrices; - _funcProductCategories = productCategories; - _funcProductManufacturers = productManufacturers; - _funcAppliedDiscounts = appliedDiscounts; - _funcProductBundleItems = productBundleItems; + _funcAttributes = attributes; + _funcAttributeCombinations = attributeCombinations; + _funcTierPrices = tierPrices; + _funcProductCategories = productCategories; + _funcProductManufacturers = productManufacturers; + _funcAppliedDiscounts = appliedDiscounts; + _funcProductBundleItems = productBundleItems; _funcAssociatedProducts = associatedProducts; - } + } - public IReadOnlyList ProductIds => _productIds; + public IReadOnlyList ProductIds => _productIds; - public void Clear() - { + public void Clear() + { _attributes?.Clear(); _attributeCombinations?.Clear(); _tierPrices?.Clear(); @@ -88,91 +88,91 @@ public void Clear() _bundledProductIds.Clear(); _groupedProductIds.Clear(); - } - - public LazyMultimap Attributes - { - get - { - if (_attributes == null) - { - _attributes = new LazyMultimap(keys => _funcAttributes(keys), _productIds); - } - return _attributes; - } - } - - public LazyMultimap AttributeCombinations - { - get - { - if (_attributeCombinations == null) - { - _attributeCombinations = new LazyMultimap(keys => _funcAttributeCombinations(keys), _productIds); - } - return _attributeCombinations; - } - } - - public LazyMultimap TierPrices - { - get - { - if (_tierPrices == null) - { - _tierPrices = new LazyMultimap(keys => _funcTierPrices(keys), _productIdsTierPrices); - } - return _tierPrices; - } - } - - public LazyMultimap ProductCategories - { - get - { - if (_productCategories == null) - { - _productCategories = new LazyMultimap(keys => _funcProductCategories(keys), _productIds); - } - return _productCategories; - } - } - - public LazyMultimap ProductManufacturers - { - get - { - if (_productManufacturers == null) - { - _productManufacturers = new LazyMultimap(keys => _funcProductManufacturers(keys), _productIds); - } - return _productManufacturers; - } - } - - public LazyMultimap AppliedDiscounts - { - get - { - if (_appliedDiscounts == null) - { - _appliedDiscounts = new LazyMultimap(keys => _funcAppliedDiscounts(keys), _productIdsAppliedDiscounts); - } - return _appliedDiscounts; - } - } - - public LazyMultimap ProductBundleItems - { - get - { - if (_productBundleItems == null) - { - _productBundleItems = new LazyMultimap(keys => _funcProductBundleItems(keys), _bundledProductIds); - } - return _productBundleItems; - } - } + } + + public LazyMultimap Attributes + { + get + { + if (_attributes == null) + { + _attributes = new LazyMultimap(keys => _funcAttributes(keys), _productIds); + } + return _attributes; + } + } + + public LazyMultimap AttributeCombinations + { + get + { + if (_attributeCombinations == null) + { + _attributeCombinations = new LazyMultimap(keys => _funcAttributeCombinations(keys), _productIds); + } + return _attributeCombinations; + } + } + + public LazyMultimap TierPrices + { + get + { + if (_tierPrices == null) + { + _tierPrices = new LazyMultimap(keys => _funcTierPrices(keys), _productIdsTierPrices); + } + return _tierPrices; + } + } + + public LazyMultimap ProductCategories + { + get + { + if (_productCategories == null) + { + _productCategories = new LazyMultimap(keys => _funcProductCategories(keys), _productIds); + } + return _productCategories; + } + } + + public LazyMultimap ProductManufacturers + { + get + { + if (_productManufacturers == null) + { + _productManufacturers = new LazyMultimap(keys => _funcProductManufacturers(keys), _productIds); + } + return _productManufacturers; + } + } + + public LazyMultimap AppliedDiscounts + { + get + { + if (_appliedDiscounts == null) + { + _appliedDiscounts = new LazyMultimap(keys => _funcAppliedDiscounts(keys), _productIdsAppliedDiscounts); + } + return _appliedDiscounts; + } + } + + public LazyMultimap ProductBundleItems + { + get + { + if (_productBundleItems == null) + { + _productBundleItems = new LazyMultimap(keys => _funcProductBundleItems(keys), _bundledProductIds); + } + return _productBundleItems; + } + } public LazyMultimap AssociatedProducts { @@ -188,14 +188,14 @@ public LazyMultimap AssociatedProducts } public void Collect(IEnumerable productIds) - { - Attributes.Collect(productIds); - AttributeCombinations.Collect(productIds); - TierPrices.Collect(productIds); - ProductCategories.Collect(productIds); - AppliedDiscounts.Collect(productIds); - ProductBundleItems.Collect(productIds); + { + Attributes.Collect(productIds); + AttributeCombinations.Collect(productIds); + TierPrices.Collect(productIds); + ProductCategories.Collect(productIds); + AppliedDiscounts.Collect(productIds); + ProductBundleItems.Collect(productIds); AssociatedProducts.Collect(productIds); - } - } + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 9897a102b9..e0454c592a 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -23,51 +23,51 @@ public partial class PriceCalculationService : IPriceCalculationService { private readonly IDiscountService _discountService; private readonly ICategoryService _categoryService; - private readonly IManufacturerService _manufacturerService; - private readonly IProductAttributeParser _productAttributeParser; - private readonly IProductService _productService; + private readonly IManufacturerService _manufacturerService; + private readonly IProductAttributeParser _productAttributeParser; + private readonly IProductService _productService; private readonly CatalogSettings _catalogSettings; - private readonly IProductAttributeService _productAttributeService; - private readonly IDownloadService _downloadService; - private readonly ICommonServices _services; - private readonly HttpRequestBase _httpRequestBase; - private readonly ITaxService _taxService; - private readonly TaxSettings _taxSettings; - - public PriceCalculationService( + private readonly IProductAttributeService _productAttributeService; + private readonly IDownloadService _downloadService; + private readonly ICommonServices _services; + private readonly HttpRequestBase _httpRequestBase; + private readonly ITaxService _taxService; + private readonly TaxSettings _taxSettings; + + public PriceCalculationService( IDiscountService discountService, - ICategoryService categoryService, - IManufacturerService manufacturerService, - IProductAttributeParser productAttributeParser, - IProductService productService, + ICategoryService categoryService, + IManufacturerService manufacturerService, + IProductAttributeParser productAttributeParser, + IProductService productService, CatalogSettings catalogSettings, - IProductAttributeService productAttributeService, - IDownloadService downloadService, - ICommonServices services, - HttpRequestBase httpRequestBase, - ITaxService taxService, - TaxSettings taxSettings) + IProductAttributeService productAttributeService, + IDownloadService downloadService, + ICommonServices services, + HttpRequestBase httpRequestBase, + ITaxService taxService, + TaxSettings taxSettings) { _discountService = discountService; _categoryService = categoryService; - _manufacturerService = manufacturerService; + _manufacturerService = manufacturerService; _productAttributeParser = productAttributeParser; - _productService = productService; + _productService = productService; _catalogSettings = catalogSettings; - _productAttributeService = productAttributeService; - _downloadService = downloadService; - _services = services; - _httpRequestBase = httpRequestBase; - _taxService = taxService; - _taxSettings = taxSettings; + _productAttributeService = productAttributeService; + _downloadService = downloadService; + _services = services; + _httpRequestBase = httpRequestBase; + _taxService = taxService; + _taxSettings = taxSettings; } - + #region Utilities /// /// Gets allowed discounts /// - /// Product + /// Product /// Customer /// Discounts protected virtual ICollection GetAllowedDiscounts(Product product, Customer customer, PriceCalculationContext context = null) @@ -76,47 +76,47 @@ protected virtual ICollection GetAllowedDiscounts(Product product, Cus if (_catalogSettings.IgnoreDiscounts) return result; - // check discounts assigned to the product - if (product.HasDiscountsApplied) + // check discounts assigned to the product + if (product.HasDiscountsApplied) { // We use this property ("HasDiscountsApplied") for performance optimziation to avoid unnecessary database calls - IEnumerable appliedDiscounts = null; - - if (context == null) - { - appliedDiscounts = product.AppliedDiscounts; - } - else - { - if (!context.AppliedDiscounts.FullyLoaded) - { - context.AppliedDiscounts.LoadAll(); - } - appliedDiscounts = context.AppliedDiscounts.GetOrLoad(product.Id); - } - - if (appliedDiscounts != null) - { - foreach (var discount in appliedDiscounts) - { - if (discount.DiscountType == DiscountType.AssignedToSkus && !result.Contains(discount) && _discountService.IsDiscountValid(discount, customer)) - { - result.Add(discount); - } - } - } + IEnumerable appliedDiscounts = null; + + if (context == null) + { + appliedDiscounts = product.AppliedDiscounts; + } + else + { + if (!context.AppliedDiscounts.FullyLoaded) + { + context.AppliedDiscounts.LoadAll(); + } + appliedDiscounts = context.AppliedDiscounts.GetOrLoad(product.Id); + } + + if (appliedDiscounts != null) + { + foreach (var discount in appliedDiscounts) + { + if (discount.DiscountType == DiscountType.AssignedToSkus && !result.Contains(discount) && _discountService.IsDiscountValid(discount, customer)) + { + result.Add(discount); + } + } + } } // check discounts assigned to categories var discountsAssignedToCategories = _discountService.GetAllDiscounts(DiscountType.AssignedToCategories); if (discountsAssignedToCategories != null && discountsAssignedToCategories.Any()) { - IEnumerable productCategories = null; - - if (context == null) - productCategories = _categoryService.GetProductCategoriesByProductId(product.Id); - else - productCategories = context.ProductCategories.GetOrLoad(product.Id); + IEnumerable productCategories = null; + + if (context == null) + productCategories = _categoryService.GetProductCategoriesByProductId(product.Id); + else + productCategories = context.ProductCategories.GetOrLoad(product.Id); if (productCategories != null) { @@ -130,10 +130,10 @@ protected virtual ICollection GetAllowedDiscounts(Product product, Cus foreach (var discount in categoryDiscounts) { - if (discount.DiscountType == DiscountType.AssignedToCategories && !result.Contains(discount) && _discountService.IsDiscountValid(discount, customer)) - { - result.Add(discount); - } + if (discount.DiscountType == DiscountType.AssignedToCategories && !result.Contains(discount) && _discountService.IsDiscountValid(discount, customer)) + { + result.Add(discount); + } } } } @@ -144,34 +144,34 @@ protected virtual ICollection GetAllowedDiscounts(Product product, Cus var discountsAssignedToManufacturers = _discountService.GetAllDiscounts(DiscountType.AssignedToManufacturers); if (discountsAssignedToManufacturers != null && discountsAssignedToManufacturers.Any()) - { - IEnumerable productManufacturers = null; - - if (context == null) - productManufacturers = _manufacturerService.GetProductManufacturersByProductId(product.Id); - else - productManufacturers = context.ProductManufacturers.GetOrLoad(product.Id); - - if (productManufacturers != null) - { - foreach (var productManufacturer in productManufacturers) - { - var manu = productManufacturer.Manufacturer; - if (manu.HasDiscountsApplied) - { - var manuDiscounts = manu.AppliedDiscounts; - - foreach (var discount in manuDiscounts) - { - if (discount.DiscountType == DiscountType.AssignedToManufacturers && !result.Contains(discount) && _discountService.IsDiscountValid(discount, customer)) - { - result.Add(discount); - } - } - } - } - } - } + { + IEnumerable productManufacturers = null; + + if (context == null) + productManufacturers = _manufacturerService.GetProductManufacturersByProductId(product.Id); + else + productManufacturers = context.ProductManufacturers.GetOrLoad(product.Id); + + if (productManufacturers != null) + { + foreach (var productManufacturer in productManufacturers) + { + var manu = productManufacturer.Manufacturer; + if (manu.HasDiscountsApplied) + { + var manuDiscounts = manu.AppliedDiscounts; + + foreach (var discount in manuDiscounts) + { + if (discount.DiscountType == DiscountType.AssignedToManufacturers && !result.Contains(discount) && _discountService.IsDiscountValid(discount, customer)) + { + result.Add(discount); + } + } + } + } + } + } return result; } @@ -185,28 +185,28 @@ protected virtual ICollection GetAllowedDiscounts(Product product, Cus /// Price protected virtual decimal? GetMinimumTierPrice(Product product, Customer customer, int quantity, PriceCalculationContext context = null) { - if (!product.HasTierPrices) + if (!product.HasTierPrices) return decimal.Zero; - IEnumerable tierPrices = null; - - if (context == null) - { - tierPrices = product.TierPrices - .OrderBy(tp => tp.Quantity) - .FilterByStore(_services.StoreContext.CurrentStore.Id) - .FilterForCustomer(customer) - .ToList() - .RemoveDuplicatedQuantities(); - } - else - { - tierPrices = context.TierPrices.GetOrLoad(product.Id) - .RemoveDuplicatedQuantities(); - } - - if (tierPrices == null) - return decimal.Zero; + IEnumerable tierPrices = null; + + if (context == null) + { + tierPrices = product.TierPrices + .OrderBy(tp => tp.Quantity) + .FilterByStore(_services.StoreContext.CurrentStore.Id) + .FilterForCustomer(customer) + .ToList() + .RemoveDuplicatedQuantities(); + } + else + { + tierPrices = context.TierPrices.GetOrLoad(product.Id) + .RemoveDuplicatedQuantities(); + } + + if (tierPrices == null) + return decimal.Zero; int previousQty = 1; decimal? previousPrice = null; @@ -232,10 +232,10 @@ protected virtual ICollection GetAllowedDiscounts(Product product, Cus { previousPrice = product.Price - tierPrice.Price; } - + previousQty = tierPrice.Quantity; } - + return previousPrice; } @@ -281,163 +281,163 @@ protected virtual decimal GetTierPriceAttributeAdjustment(Product product, Custo { previousPrice = adjustment - (adjustment / 100 * tierPrice.Price); } - + previousQty = tierPrice.Quantity; } - + return previousPrice; } protected virtual decimal GetPreselectedPrice( Product product, Customer customer, - Currency currency, + Currency currency, PriceCalculationContext context, ProductBundleItemData bundleItem, IEnumerable bundleItems) { var taxRate = decimal.Zero; - var attributesTotalPriceBase = decimal.Zero; - var preSelectedPriceAdjustmentBase = decimal.Zero; - var isBundle = product.ProductType == ProductType.BundledProduct; - var isBundleItemPricing = bundleItem != null && bundleItem.Item.BundleProduct.BundlePerItemPricing; - var isBundlePricing = bundleItem != null && !bundleItem.Item.BundleProduct.BundlePerItemPricing; - var bundleItemId = bundleItem == null ? 0 : bundleItem.Item.Id; - - var query = new ProductVariantQuery(); - var selectedAttributeValues = new List(); - var attributes = context.Attributes.GetOrLoad(product.Id); - - // 1. Fill query with initially selected attributes. - foreach (var attribute in attributes.Where(x => x.ProductVariantAttributeValues.Any() && x.ShouldHaveValues())) - { - int preSelectedValueId = 0; - ProductVariantAttributeValue defaultValue = null; - var selectedValueIds = new List(); - var pvaValues = attribute.ProductVariantAttributeValues; - - foreach (var pvaValue in pvaValues) - { + var attributesTotalPriceBase = decimal.Zero; + var preSelectedPriceAdjustmentBase = decimal.Zero; + var isBundle = product.ProductType == ProductType.BundledProduct; + var isBundleItemPricing = bundleItem != null && bundleItem.Item.BundleProduct.BundlePerItemPricing; + var isBundlePricing = bundleItem != null && !bundleItem.Item.BundleProduct.BundlePerItemPricing; + var bundleItemId = bundleItem == null ? 0 : bundleItem.Item.Id; + + var query = new ProductVariantQuery(); + var selectedAttributeValues = new List(); + var attributes = context.Attributes.GetOrLoad(product.Id); + + // 1. Fill query with initially selected attributes. + foreach (var attribute in attributes.Where(x => x.ProductVariantAttributeValues.Any() && x.ShouldHaveValues())) + { + int preSelectedValueId = 0; + ProductVariantAttributeValue defaultValue = null; + var selectedValueIds = new List(); + var pvaValues = attribute.ProductVariantAttributeValues; + + foreach (var pvaValue in pvaValues) + { ProductBundleItemAttributeFilter attributeFilter = null; if (bundleItem?.Item?.FilterOut(pvaValue, out attributeFilter) ?? false) - continue; - - if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) - preSelectedValueId = attributeFilter.AttributeValueId; - - if (!isBundlePricing && pvaValue.IsPreSelected) - { - var includingTax = _services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; - var attributeValuePriceAdjustment = GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, context, 1); - var priceAdjustmentBase = _taxService.GetProductPrice(product, product.TaxCategoryId, attributeValuePriceAdjustment, - includingTax, customer, currency, _taxSettings.PricesIncludeTax, out taxRate); - - preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); - } - } - - // Value pre-selected by a bundle item filter discards the default pre-selection. - if (preSelectedValueId != 0 && (defaultValue = pvaValues.FirstOrDefault(x => x.Id == preSelectedValueId)) != null) - { - //defaultValue.IsPreSelected = true; - selectedAttributeValues.Add(defaultValue); - query.AddVariant(new ProductVariantQueryItem(defaultValue.Id.ToString()) - { - ProductId = product.Id, - BundleItemId = bundleItemId, - AttributeId = attribute.ProductAttributeId, - VariantAttributeId = attribute.Id, - Alias = attribute.ProductAttribute.Alias, - ValueAlias = defaultValue.Alias - }); - } - else - { - foreach (var value in pvaValues.Where(x => x.IsPreSelected)) - { - selectedAttributeValues.Add(value); - query.AddVariant(new ProductVariantQueryItem(value.Id.ToString()) - { - ProductId = product.Id, - BundleItemId = bundleItemId, - AttributeId = attribute.ProductAttributeId, - VariantAttributeId = attribute.Id, - Alias = attribute.ProductAttribute.Alias, - ValueAlias = value.Alias - }); - } - } - } - - // 2. Find attribute combination for selected attributes and merge it. - if (!isBundle && query.Variants.Any()) - { - var attributeXml = query.CreateSelectedAttributesXml(product.Id, bundleItemId, attributes, _productAttributeParser, _services.Localization, - _downloadService, _catalogSettings, _httpRequestBase, new List()); - - var combinations = context.AttributeCombinations.GetOrLoad(product.Id); - - var selectedCombination = combinations.FirstOrDefault(x => _productAttributeParser.AreProductAttributesEqual(x.AttributesXml, attributeXml)); - - if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) - { - product.MergedDataValues = new Dictionary { { "Price", selectedCombination.Price.Value } }; - - if (selectedCombination.BasePriceAmount.HasValue) - product.MergedDataValues.Add("BasePriceAmount", selectedCombination.BasePriceAmount.Value); - - if (selectedCombination.BasePriceBaseAmount.HasValue) - product.MergedDataValues.Add("BasePriceBaseAmount", selectedCombination.BasePriceBaseAmount.Value); - } - } - - if (_catalogSettings.EnableDynamicPriceUpdate && !isBundlePricing) - { - if (selectedAttributeValues.Count > 0) - { - selectedAttributeValues.Each(x => attributesTotalPriceBase += GetProductVariantAttributeValuePriceAdjustment(x, product, customer, context, 1)); - } - else - { - attributesTotalPriceBase = preSelectedPriceAdjustmentBase; - } - } - - if (bundleItem != null) - { - bundleItem.AdditionalCharge = attributesTotalPriceBase; - } - - var result = GetFinalPrice(product, bundleItems, customer, attributesTotalPriceBase, true, 1, bundleItem, context); - return result; - } - - public virtual PriceCalculationContext CreatePriceCalculationContext( - IEnumerable products = null, - Customer customer = null, - int? storeId = null, + continue; + + if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) + preSelectedValueId = attributeFilter.AttributeValueId; + + if (!isBundlePricing && pvaValue.IsPreSelected) + { + var includingTax = _services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var attributeValuePriceAdjustment = GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, context, 1); + var priceAdjustmentBase = _taxService.GetProductPrice(product, product.TaxCategoryId, attributeValuePriceAdjustment, + includingTax, customer, currency, _taxSettings.PricesIncludeTax, out taxRate); + + preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); + } + } + + // Value pre-selected by a bundle item filter discards the default pre-selection. + if (preSelectedValueId != 0 && (defaultValue = pvaValues.FirstOrDefault(x => x.Id == preSelectedValueId)) != null) + { + //defaultValue.IsPreSelected = true; + selectedAttributeValues.Add(defaultValue); + query.AddVariant(new ProductVariantQueryItem(defaultValue.Id.ToString()) + { + ProductId = product.Id, + BundleItemId = bundleItemId, + AttributeId = attribute.ProductAttributeId, + VariantAttributeId = attribute.Id, + Alias = attribute.ProductAttribute.Alias, + ValueAlias = defaultValue.Alias + }); + } + else + { + foreach (var value in pvaValues.Where(x => x.IsPreSelected)) + { + selectedAttributeValues.Add(value); + query.AddVariant(new ProductVariantQueryItem(value.Id.ToString()) + { + ProductId = product.Id, + BundleItemId = bundleItemId, + AttributeId = attribute.ProductAttributeId, + VariantAttributeId = attribute.Id, + Alias = attribute.ProductAttribute.Alias, + ValueAlias = value.Alias + }); + } + } + } + + // 2. Find attribute combination for selected attributes and merge it. + if (!isBundle && query.Variants.Any()) + { + var attributeXml = query.CreateSelectedAttributesXml(product.Id, bundleItemId, attributes, _productAttributeParser, _services.Localization, + _downloadService, _catalogSettings, _httpRequestBase, new List()); + + var combinations = context.AttributeCombinations.GetOrLoad(product.Id); + + var selectedCombination = combinations.FirstOrDefault(x => _productAttributeParser.AreProductAttributesEqual(x.AttributesXml, attributeXml)); + + if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) + { + product.MergedDataValues = new Dictionary { { "Price", selectedCombination.Price.Value } }; + + if (selectedCombination.BasePriceAmount.HasValue) + product.MergedDataValues.Add("BasePriceAmount", selectedCombination.BasePriceAmount.Value); + + if (selectedCombination.BasePriceBaseAmount.HasValue) + product.MergedDataValues.Add("BasePriceBaseAmount", selectedCombination.BasePriceBaseAmount.Value); + } + } + + if (_catalogSettings.EnableDynamicPriceUpdate && !isBundlePricing) + { + if (selectedAttributeValues.Count > 0) + { + selectedAttributeValues.Each(x => attributesTotalPriceBase += GetProductVariantAttributeValuePriceAdjustment(x, product, customer, context, 1)); + } + else + { + attributesTotalPriceBase = preSelectedPriceAdjustmentBase; + } + } + + if (bundleItem != null) + { + bundleItem.AdditionalCharge = attributesTotalPriceBase; + } + + var result = GetFinalPrice(product, bundleItems, customer, attributesTotalPriceBase, true, 1, bundleItem, context); + return result; + } + + public virtual PriceCalculationContext CreatePriceCalculationContext( + IEnumerable products = null, + Customer customer = null, + int? storeId = null, bool includeHidden = true) - { - if (customer == null) - customer = _services.WorkContext.CurrentCustomer; - - if (!storeId.HasValue) - storeId = _services.StoreContext.CurrentStore.Id; - - var context = new PriceCalculationContext(products, - x => _productAttributeService.GetProductVariantAttributesByProductIds(x, null), - x => _productAttributeService.GetProductVariantAttributeCombinations(x), - x => _productService.GetTierPricesByProductIds(x, customer, storeId.Value), - x => _categoryService.GetProductCategoriesByProductIds(x, includeHidden), - x => _manufacturerService.GetProductManufacturersByProductIds(x, includeHidden), - x => _productService.GetAppliedDiscountsByProductIds(x), - x => _productService.GetBundleItemsByProductIds(x, includeHidden), + { + if (customer == null) + customer = _services.WorkContext.CurrentCustomer; + + if (!storeId.HasValue) + storeId = _services.StoreContext.CurrentStore.Id; + + var context = new PriceCalculationContext(products, + x => _productAttributeService.GetProductVariantAttributesByProductIds(x, null), + x => _productAttributeService.GetProductVariantAttributeCombinations(x), + x => _productService.GetTierPricesByProductIds(x, customer, storeId.Value), + x => _categoryService.GetProductCategoriesByProductIds(x, includeHidden), + x => _manufacturerService.GetProductManufacturersByProductIds(x, includeHidden), + x => _productService.GetAppliedDiscountsByProductIds(x), + x => _productService.GetBundleItemsByProductIds(x, includeHidden), x => _productService.GetAssociatedProductsByProductIds(x, includeHidden) - ); + ); - return context; - } + return context; + } #endregion @@ -450,28 +450,28 @@ public virtual PriceCalculationContext CreatePriceCalculationContext( /// Product special price public virtual decimal? GetSpecialPrice(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + if (product == null) + throw new ArgumentNullException("product"); if (!product.SpecialPrice.HasValue) return null; //check date range DateTime now = DateTime.UtcNow; - if (product.SpecialPriceStartDateTimeUtc.HasValue) + if (product.SpecialPriceStartDateTimeUtc.HasValue) { - DateTime startDate = DateTime.SpecifyKind(product.SpecialPriceStartDateTimeUtc.Value, DateTimeKind.Utc); + DateTime startDate = DateTime.SpecifyKind(product.SpecialPriceStartDateTimeUtc.Value, DateTimeKind.Utc); if (startDate.CompareTo(now) > 0) return null; } - if (product.SpecialPriceEndDateTimeUtc.HasValue) + if (product.SpecialPriceEndDateTimeUtc.HasValue) { - DateTime endDate = DateTime.SpecifyKind(product.SpecialPriceEndDateTimeUtc.Value, DateTimeKind.Utc); + DateTime endDate = DateTime.SpecifyKind(product.SpecialPriceEndDateTimeUtc.Value, DateTimeKind.Utc); if (endDate.CompareTo(now) < 0) return null; } - return product.SpecialPrice.Value; + return product.SpecialPrice.Value; } /// @@ -483,7 +483,7 @@ public virtual PriceCalculationContext CreatePriceCalculationContext( public virtual decimal GetFinalPrice(Product product, bool includeDiscounts) { var customer = _services.WorkContext.CurrentCustomer; - return GetFinalPrice(product, customer, includeDiscounts); + return GetFinalPrice(product, customer, includeDiscounts); } /// @@ -495,7 +495,7 @@ public virtual decimal GetFinalPrice(Product product, bool includeDiscounts) /// Final price public virtual decimal GetFinalPrice(Product product, Customer customer, bool includeDiscounts) { - return GetFinalPrice(product, customer, decimal.Zero, includeDiscounts); + return GetFinalPrice(product, customer, decimal.Zero, includeDiscounts); } /// @@ -506,29 +506,29 @@ public virtual decimal GetFinalPrice(Product product, Customer customer, bool in /// Additional charge /// A value indicating whether include discounts or not for final price computation /// Final price - public virtual decimal GetFinalPrice(Product product, - Customer customer, - decimal additionalCharge, + public virtual decimal GetFinalPrice(Product product, + Customer customer, + decimal additionalCharge, bool includeDiscounts) { return GetFinalPrice(product, customer, additionalCharge, includeDiscounts, 1); } - public virtual decimal GetFinalPrice( - Product product, + public virtual decimal GetFinalPrice( + Product product, Customer customer, - decimal additionalCharge, - bool includeDiscounts, + decimal additionalCharge, + bool includeDiscounts, int quantity, - ProductBundleItemData bundleItem = null, - PriceCalculationContext context = null, - bool isTierPrice = false) - { + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null, + bool isTierPrice = false) + { // Initial price. - var result = product.Price; + var result = product.Price; // Special price. - var specialPrice = GetSpecialPrice(product); + var specialPrice = GetSpecialPrice(product); if (specialPrice.HasValue) { result = specialPrice.Value; @@ -571,7 +571,7 @@ public virtual decimal GetFinalPrice( // Discount + additional charge. if (includeDiscounts) { - var discountAmount = GetDiscountAmount(product, customer, additionalCharge, quantity, out var appliedDiscount, bundleItem, context); + var discountAmount = GetDiscountAmount(product, customer, additionalCharge, quantity, out var appliedDiscount, bundleItem, context); result = result + additionalCharge - discountAmount; } else @@ -582,47 +582,47 @@ public virtual decimal GetFinalPrice( return Math.Max(result, decimal.Zero); } - public virtual decimal GetFinalPrice( - Product product, - IEnumerable bundleItems, - Customer customer, - decimal additionalCharge, - bool includeDiscounts, - int quantity, - ProductBundleItemData bundleItem = null, - PriceCalculationContext context = null) - { - if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing) - { - var result = decimal.Zero; - var items = bundleItems; - - if (items == null) - { - items = context == null - ? _productService.GetBundleItems(product.Id) - : context.ProductBundleItems.GetOrLoad(product.Id).Select(x => new ProductBundleItemData(x)).ToList(); - } - - foreach (var itemData in items.Where(x => x != null && x.Item != null)) - { - var itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context); - - result += decimal.Multiply(itemPrice, itemData.Item.Quantity); - } - - return result < decimal.Zero ? decimal.Zero : result; - } - - return GetFinalPrice(product, customer, additionalCharge, includeDiscounts, quantity, bundleItem, context); - } - - public virtual decimal GetLowestPrice(Product product, Customer customer, PriceCalculationContext context, out bool displayFromMessage) - { + public virtual decimal GetFinalPrice( + Product product, + IEnumerable bundleItems, + Customer customer, + decimal additionalCharge, + bool includeDiscounts, + int quantity, + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null) + { + if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing) + { + var result = decimal.Zero; + var items = bundleItems; + + if (items == null) + { + items = context == null + ? _productService.GetBundleItems(product.Id) + : context.ProductBundleItems.GetOrLoad(product.Id).Select(x => new ProductBundleItemData(x)).ToList(); + } + + foreach (var itemData in items.Where(x => x != null && x.Item != null)) + { + var itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context); + + result += decimal.Multiply(itemPrice, itemData.Item.Quantity); + } + + return result < decimal.Zero ? decimal.Zero : result; + } + + return GetFinalPrice(product, customer, additionalCharge, includeDiscounts, quantity, bundleItem, context); + } + + public virtual decimal GetLowestPrice(Product product, Customer customer, PriceCalculationContext context, out bool displayFromMessage) + { Guard.NotNull(product, nameof(product)); - if (product.ProductType == ProductType.GroupedProduct) - throw Error.InvalidOperation("Choose the other override for products of type grouped product."); + if (product.ProductType == ProductType.GroupedProduct) + throw Error.InvalidOperation("Choose the other override for products of type grouped product."); // Note, attribute price adjustments will not be taken into account here. if (context == null) @@ -630,7 +630,7 @@ public virtual decimal GetLowestPrice(Product product, Customer customer, PriceC context = CreatePriceCalculationContext(customer: customer); } - var isBundlePerItemPricing = product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing; + var isBundlePerItemPricing = product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing; var lowestPrice = GetFinalPrice(product, null, customer, decimal.Zero, true, int.MaxValue, null, context); displayFromMessage = isBundlePerItemPricing; @@ -645,80 +645,80 @@ public virtual decimal GetLowestPrice(Product product, Customer customer, PriceC } if (lowestPrice == decimal.Zero && product.Price == decimal.Zero) - { - lowestPrice = product.LowestAttributeCombinationPrice ?? decimal.Zero; - } - - if (!displayFromMessage && product.ProductType != ProductType.BundledProduct) - { - var attributes = context.Attributes.GetOrLoad(product.Id); - displayFromMessage = attributes.Any(x => x.ProductVariantAttributeValues.Any(y => y.PriceAdjustment != decimal.Zero)); - } - - if (!displayFromMessage && product.HasTierPrices && !isBundlePerItemPricing) - { - var tierPrices = context.TierPrices.GetOrLoad(product.Id) - .RemoveDuplicatedQuantities(); - - displayFromMessage = tierPrices.Count > 0 && !(tierPrices.Count == 1 && tierPrices.First().Quantity <= 1); - } - - return lowestPrice; - } - - public virtual decimal? GetLowestPrice( - Product product, - Customer customer, - PriceCalculationContext context, - IEnumerable associatedProducts, - out Product lowestPriceProduct) - { - if (product == null) - throw new ArgumentNullException("product"); - - if (associatedProducts == null) - throw new ArgumentNullException("associatedProducts"); - - if (product.ProductType != ProductType.GroupedProduct) - throw Error.InvalidOperation("Choose the other override for products not of type grouped product."); - - lowestPriceProduct = null; - decimal? lowestPrice = null; + { + lowestPrice = product.LowestAttributeCombinationPrice ?? decimal.Zero; + } + + if (!displayFromMessage && product.ProductType != ProductType.BundledProduct) + { + var attributes = context.Attributes.GetOrLoad(product.Id); + displayFromMessage = attributes.Any(x => x.ProductVariantAttributeValues.Any(y => y.PriceAdjustment != decimal.Zero)); + } + + if (!displayFromMessage && product.HasTierPrices && !isBundlePerItemPricing) + { + var tierPrices = context.TierPrices.GetOrLoad(product.Id) + .RemoveDuplicatedQuantities(); + + displayFromMessage = tierPrices.Count > 0 && !(tierPrices.Count == 1 && tierPrices.First().Quantity <= 1); + } + + return lowestPrice; + } + + public virtual decimal? GetLowestPrice( + Product product, + Customer customer, + PriceCalculationContext context, + IEnumerable associatedProducts, + out Product lowestPriceProduct) + { + if (product == null) + throw new ArgumentNullException("product"); + + if (associatedProducts == null) + throw new ArgumentNullException("associatedProducts"); + + if (product.ProductType != ProductType.GroupedProduct) + throw Error.InvalidOperation("Choose the other override for products not of type grouped product."); + + lowestPriceProduct = null; + decimal? lowestPrice = null; if (context == null) { context = CreatePriceCalculationContext(customer: customer); } - foreach (var associatedProduct in associatedProducts) - { - var tmpPrice = GetFinalPrice(associatedProduct, customer, decimal.Zero, true, int.MaxValue, null, context); + foreach (var associatedProduct in associatedProducts) + { + var tmpPrice = GetFinalPrice(associatedProduct, customer, decimal.Zero, true, int.MaxValue, null, context); - if (associatedProduct.LowestAttributeCombinationPrice.HasValue && associatedProduct.LowestAttributeCombinationPrice.Value < tmpPrice) - { - tmpPrice = associatedProduct.LowestAttributeCombinationPrice.Value; - } + if (associatedProduct.LowestAttributeCombinationPrice.HasValue && associatedProduct.LowestAttributeCombinationPrice.Value < tmpPrice) + { + tmpPrice = associatedProduct.LowestAttributeCombinationPrice.Value; + } - if (!lowestPrice.HasValue || tmpPrice < lowestPrice.Value) - { - lowestPrice = tmpPrice; - lowestPriceProduct = associatedProduct; - } - } + if (!lowestPrice.HasValue || tmpPrice < lowestPrice.Value) + { + lowestPrice = tmpPrice; + lowestPriceProduct = associatedProduct; + } + } if (lowestPriceProduct == null) { lowestPriceProduct = associatedProducts.FirstOrDefault(); } - return lowestPrice; - } + return lowestPrice; + } - public virtual decimal GetPreselectedPrice(Product product, Customer customer, Currency currency, PriceCalculationContext context) - { + public virtual decimal GetPreselectedPrice(Product product, Customer customer, Currency currency, PriceCalculationContext context) + { Guard.NotNull(product, nameof(product)); - var result = decimal.Zero; + var result = decimal.Zero; if (context == null) { @@ -749,37 +749,37 @@ public virtual decimal GetPreselectedPrice(Product product, Customer customer, C result = GetPreselectedPrice(product, customer, currency, context, null, null); } - return result; - } + return result; + } - /// - /// Gets the product cost - /// - /// Product - /// Shopping cart item attributes in XML - /// Product cost - public virtual decimal GetProductCost(Product product, string attributesXml) - { - if (product == null) - throw new ArgumentNullException("product"); - - decimal result = product.ProductCost; - - _productAttributeParser - .ParseProductVariantAttributeValues(attributesXml) - .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) - .ToList() - .Each(x => - { - var linkedProduct = _productService.GetProductById(x.LinkedProductId); - - if (linkedProduct != null) - result += (linkedProduct.ProductCost * x.Quantity); - }); - - return result; - } + /// + /// Gets the product cost + /// + /// Product + /// Shopping cart item attributes in XML + /// Product cost + public virtual decimal GetProductCost(Product product, string attributesXml) + { + if (product == null) + throw new ArgumentNullException("product"); + + decimal result = product.ProductCost; + + _productAttributeParser + .ParseProductVariantAttributeValues(attributesXml) + .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) + .ToList() + .Each(x => + { + var linkedProduct = _productService.GetProductById(x.LinkedProductId); + + if (linkedProduct != null) + result += (linkedProduct.ProductCost * x.Quantity); + }); + + return result; + } /// /// Gets discount amount @@ -798,7 +798,7 @@ public virtual decimal GetDiscountAmount(Product product) /// Product /// The customer /// Discount amount - public virtual decimal GetDiscountAmount(Product product, + public virtual decimal GetDiscountAmount(Product product, Customer customer) { return GetDiscountAmount(product, customer, decimal.Zero); @@ -811,8 +811,8 @@ public virtual decimal GetDiscountAmount(Product product, /// The customer /// Additional charge /// Discount amount - public virtual decimal GetDiscountAmount(Product product, - Customer customer, + public virtual decimal GetDiscountAmount(Product product, + Customer customer, decimal additionalCharge) { return GetDiscountAmount(product, customer, additionalCharge, out var appliedDiscount); @@ -826,64 +826,64 @@ public virtual decimal GetDiscountAmount(Product product, /// Additional charge /// Applied discount /// Discount amount - public virtual decimal GetDiscountAmount(Product product, + public virtual decimal GetDiscountAmount(Product product, Customer customer, - decimal additionalCharge, + decimal additionalCharge, out Discount appliedDiscount) { return GetDiscountAmount(product, customer, additionalCharge, 1, out appliedDiscount); } public virtual decimal GetDiscountAmount( - Product product, + Product product, Customer customer, decimal additionalCharge, int quantity, out Discount appliedDiscount, - ProductBundleItemData bundleItem = null, - PriceCalculationContext context = null, + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null, decimal? finalPrice = null) { appliedDiscount = null; var appliedDiscountAmount = decimal.Zero; - if (bundleItem != null && bundleItem.Item != null) - { - if (bundleItem.Item.Discount.HasValue && bundleItem.Item.BundleProduct.BundlePerItemPricing) - { - appliedDiscount = new Discount - { - UsePercentage = bundleItem.Item.DiscountPercentage, - DiscountPercentage = bundleItem.Item.Discount.Value, - DiscountAmount = bundleItem.Item.Discount.Value - }; + if (bundleItem != null && bundleItem.Item != null) + { + if (bundleItem.Item.Discount.HasValue && bundleItem.Item.BundleProduct.BundlePerItemPricing) + { + appliedDiscount = new Discount + { + UsePercentage = bundleItem.Item.DiscountPercentage, + DiscountPercentage = bundleItem.Item.Discount.Value, + DiscountAmount = bundleItem.Item.Discount.Value + }; var finalPriceWithoutDiscount = finalPrice ?? GetFinalPrice(product, customer, additionalCharge, false, quantity, bundleItem, context); - appliedDiscountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount); - } - } - else - { - // Don't apply when customer entered price or discounts should be ignored in any case. - if (product.CustomerEntersPrice || _catalogSettings.IgnoreDiscounts) - { - return appliedDiscountAmount; - } - - var allowedDiscounts = GetAllowedDiscounts(product, customer, context); - if (allowedDiscounts.Count == 0) - { - return appliedDiscountAmount; - } + appliedDiscountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount); + } + } + else + { + // Don't apply when customer entered price or discounts should be ignored in any case. + if (product.CustomerEntersPrice || _catalogSettings.IgnoreDiscounts) + { + return appliedDiscountAmount; + } + + var allowedDiscounts = GetAllowedDiscounts(product, customer, context); + if (allowedDiscounts.Count == 0) + { + return appliedDiscountAmount; + } var finalPriceWithoutDiscount = finalPrice ?? GetFinalPrice(product, customer, additionalCharge, false, quantity, bundleItem, context); - appliedDiscount = allowedDiscounts.GetPreferredDiscount(finalPriceWithoutDiscount); + appliedDiscount = allowedDiscounts.GetPreferredDiscount(finalPriceWithoutDiscount); - if (appliedDiscount != null) - { - appliedDiscountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount); - } - } + if (appliedDiscount != null) + { + appliedDiscountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount); + } + } return appliedDiscountAmount; } @@ -908,57 +908,57 @@ public virtual decimal GetSubTotal(OrganizedShoppingCartItem shoppingCartItem, b /// Shopping cart unit price (one item) public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, bool includeDiscounts) { - decimal finalPrice = decimal.Zero; + decimal finalPrice = decimal.Zero; var customer = shoppingCartItem.Item.Customer; - var product = shoppingCartItem.Item.Product; + var product = shoppingCartItem.Item.Product; if (product != null) { - if (product.CustomerEntersPrice) + if (product.CustomerEntersPrice) { finalPrice = shoppingCartItem.Item.CustomerEnteredPrice; } - else if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing) - { - if (shoppingCartItem.ChildItems != null) - { - foreach (var bundleItem in shoppingCartItem.ChildItems) - { - bundleItem.Item.Product.MergeWithCombination(bundleItem.Item.AttributesXml, _productAttributeParser); - } - - var bundleItems = shoppingCartItem.ChildItems + else if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing) + { + if (shoppingCartItem.ChildItems != null) + { + foreach (var bundleItem in shoppingCartItem.ChildItems) + { + bundleItem.Item.Product.MergeWithCombination(bundleItem.Item.AttributesXml, _productAttributeParser); + } + + var bundleItems = shoppingCartItem.ChildItems .Where(x => x.BundleItemData != null && x.BundleItemData.Item != null) .Select(x => x.BundleItemData).ToList(); - finalPrice = GetFinalPrice(product, bundleItems, customer, decimal.Zero, includeDiscounts, shoppingCartItem.Item.Quantity); - } - } + finalPrice = GetFinalPrice(product, bundleItems, customer, decimal.Zero, includeDiscounts, shoppingCartItem.Item.Quantity); + } + } else { - product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); + product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); - var attributesTotalPrice = decimal.Zero; - var pvaValuesEnum = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); + var attributesTotalPrice = decimal.Zero; + var pvaValuesEnum = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); - if (pvaValuesEnum != null) - { - var pvaValues = pvaValuesEnum.ToList(); + if (pvaValuesEnum != null) + { + var pvaValues = pvaValuesEnum.ToList(); - foreach (var pvaValue in pvaValues) - { - attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, shoppingCartItem.Item.Quantity); - } - } + foreach (var pvaValue in pvaValues) + { + attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, shoppingCartItem.Item.Quantity); + } + } - finalPrice = GetFinalPrice(product, customer, attributesTotalPrice, includeDiscounts, shoppingCartItem.Item.Quantity, shoppingCartItem.BundleItemData); + finalPrice = GetFinalPrice(product, customer, attributesTotalPrice, includeDiscounts, shoppingCartItem.Item.Quantity, shoppingCartItem.BundleItemData); } } finalPrice = finalPrice.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); return finalPrice; } - + /// @@ -966,7 +966,7 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, /// /// The shopping cart item /// Discount amount - public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem) + public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem) { return GetDiscountAmount(shoppingCartItem, out var appliedDiscount); } @@ -979,14 +979,14 @@ public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartI /// Discount amount public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem, out Discount appliedDiscount) { - appliedDiscount = null; + appliedDiscount = null; - var customer = shoppingCartItem.Item.Customer; - var totalDiscountAmount = decimal.Zero; - var product = shoppingCartItem.Item.Product; - var quantity = shoppingCartItem.Item.Quantity; + var customer = shoppingCartItem.Item.Customer; + var totalDiscountAmount = decimal.Zero; + var product = shoppingCartItem.Item.Product; + var quantity = shoppingCartItem.Item.Quantity; - if (product != null) + if (product != null) { var attributesTotalPrice = decimal.Zero; var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); @@ -996,26 +996,26 @@ public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartI attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, quantity); } - var productDiscountAmount = GetDiscountAmount(product, customer, attributesTotalPrice, quantity, out appliedDiscount); - totalDiscountAmount = productDiscountAmount * quantity; + var productDiscountAmount = GetDiscountAmount(product, customer, attributesTotalPrice, quantity, out appliedDiscount); + totalDiscountAmount = productDiscountAmount * quantity; } totalDiscountAmount = totalDiscountAmount.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); - return totalDiscountAmount; + return totalDiscountAmount; } - /// - /// Gets the price adjustment of a variant attribute value - /// - /// Product variant attribute value - /// Price adjustment of a variant attribute value - public virtual decimal GetProductVariantAttributeValuePriceAdjustment(ProductVariantAttributeValue attributeValue, + /// + /// Gets the price adjustment of a variant attribute value + /// + /// Product variant attribute value + /// Price adjustment of a variant attribute value + public virtual decimal GetProductVariantAttributeValuePriceAdjustment(ProductVariantAttributeValue attributeValue, Product product, Customer customer, PriceCalculationContext context, int productQuantity = 1) - { + { Guard.NotNull(attributeValue, nameof(attributeValue)); - if (attributeValue.ValueType == ProductVariantAttributeValueType.Simple) + if (attributeValue.ValueType == ProductVariantAttributeValueType.Simple) { if (productQuantity > 1 && attributeValue.PriceAdjustment > 0) { @@ -1025,23 +1025,23 @@ public virtual decimal GetProductVariantAttributeValuePriceAdjustment(ProductVar return tierPriceAttributeAdjustment; } } - + return attributeValue.PriceAdjustment; } - + if (attributeValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) - { - var linkedProduct = _productService.GetProductById(attributeValue.LinkedProductId); - - if (linkedProduct != null) - { - var productPrice = GetFinalPrice(linkedProduct, true) * attributeValue.Quantity; - return productPrice; - } - } - - return decimal.Zero; - } + { + var linkedProduct = _productService.GetProductById(attributeValue.LinkedProductId); + + if (linkedProduct != null) + { + var productPrice = GetFinalPrice(linkedProduct, false) * attributeValue.Quantity; + return productPrice; + } + } + + return decimal.Zero; + } #endregion } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs b/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs index 5ea1ec97ed..2a15907376 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs @@ -56,7 +56,7 @@ public string FormatPrice(decimal price, bool showCurrency, bool showTax) public string FormatPrice(decimal price, bool showCurrency, string currencyCode, bool showTax, Language language) { - var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; + var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; var priceIncludesTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); @@ -64,15 +64,15 @@ public string FormatPrice(decimal price, bool showCurrency, string currencyCode, public string FormatPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax) { - bool showTax = _taxSettings.DisplayTaxSuffix; - return FormatPrice(price, showCurrency, currencyCode, language, priceIncludesTax, showTax); + bool showTax = _taxSettings.DisplayTaxSuffix; + return FormatPrice(price, showCurrency, currencyCode, language, priceIncludesTax, showTax); } - public string FormatPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax) - { - var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; - return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); - } + public string FormatPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax) + { + var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; + return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); + } public string FormatPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax) { @@ -80,29 +80,29 @@ public string FormatPrice(decimal price, bool showCurrency, Currency targetCurre return FormatPrice(price, showCurrency, targetCurrency, language, priceIncludesTax, showTax); } - public string FormatPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax, bool showTax) - { - var formatted = new Money(price, targetCurrency).ToString(showCurrency); + public string FormatPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax, bool showTax) + { + var formatted = new Money(price, targetCurrency).ToString(showCurrency); - if (showTax) - { - // Show tax suffix - var resKey = "Products." + (priceIncludesTax ? "InclTaxSuffix" : "ExclTaxSuffix"); - var taxFormatStr = _localizationService.GetResource(resKey, language.Id, false).NullEmpty() ?? (priceIncludesTax ? "{0} incl. tax" : "{0} excl. tax"); + if (showTax) + { + // Show tax suffix + var resKey = "Products." + (priceIncludesTax ? "InclTaxSuffix" : "ExclTaxSuffix"); + var taxFormatStr = _localizationService.GetResource(resKey, language.Id, false).NullEmpty() ?? (priceIncludesTax ? "{0} incl. tax" : "{0} excl. tax"); - formatted = string.Format(taxFormatStr, formatted); - } + formatted = string.Format(taxFormatStr, formatted); + } - return formatted; - } + return formatted; + } - public string FormatShippingPrice(decimal price, bool showCurrency) + public string FormatShippingPrice(decimal price, bool showCurrency) { var targetCurrency = _workContext.WorkingCurrency; var language = _workContext.WorkingLanguage; - var priceIncludesTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var priceIncludesTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; return FormatShippingPrice(price, showCurrency, targetCurrency, language, priceIncludesTax); } @@ -113,20 +113,20 @@ public string FormatShippingPrice(decimal price, bool showCurrency, Currency tar return FormatShippingPrice(price, showCurrency, targetCurrency, language, priceIncludesTax, showTax); } - public string FormatShippingPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax) - { - var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; - return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); - } + public string FormatShippingPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax) + { + var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; + return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); + } public string FormatShippingPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax, bool showTax) { return FormatPrice(price, showCurrency, targetCurrency, language, priceIncludesTax, showTax); } - + public string FormatShippingPrice(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax) { - var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; + var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; return FormatShippingPrice(price, showCurrency, currency, language, priceIncludesTax); } @@ -136,7 +136,7 @@ public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency) { var targetCurrency = _workContext.WorkingCurrency; var language = _workContext.WorkingLanguage; - var priceIncludesTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var priceIncludesTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; return FormatPaymentMethodAdditionalFee(price, showCurrency, targetCurrency, language, priceIncludesTax); } @@ -147,11 +147,11 @@ public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, return FormatPaymentMethodAdditionalFee(price, showCurrency, targetCurrency, language, priceIncludesTax, showTax); } - public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax) - { - var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; - return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); - } + public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax, bool showTax) + { + var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; + return FormatPrice(price, showCurrency, currency, language, priceIncludesTax, showTax); + } public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax, bool showTax) { @@ -160,7 +160,7 @@ public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, public string FormatPaymentMethodAdditionalFee(decimal price, bool showCurrency, string currencyCode, Language language, bool priceIncludesTax) { - var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; + var currency = _currencyService.GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }; return FormatPaymentMethodAdditionalFee(price, showCurrency, currency, language, priceIncludesTax); } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs index 7ce8def364..b2674af953 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs @@ -5,7 +5,7 @@ namespace SmartStore.Services.Catalog { - public static class ProductAttributeExtensions + public static class ProductAttributeExtensions { /// /// A value indicating whether this product variant attribute should have values @@ -19,73 +19,73 @@ public static bool ShouldHaveValues(this ProductVariantAttribute productVariantA if (productVariantAttribute.AttributeControlType == AttributeControlType.TextBox || productVariantAttribute.AttributeControlType == AttributeControlType.MultilineTextbox || - productVariantAttribute.AttributeControlType == AttributeControlType.Datepicker || + productVariantAttribute.AttributeControlType == AttributeControlType.Datepicker || productVariantAttribute.AttributeControlType == AttributeControlType.FileUpload) return false; // All other attribute control types support values. return true; } - - public static string AddProductAttribute(this ProductVariantAttribute pva, string attributes, string value) - { - string result = string.Empty; - try - { - var xmlDoc = new XmlDocument(); - if (String.IsNullOrEmpty(attributes)) - { - var element1 = xmlDoc.CreateElement("Attributes"); - xmlDoc.AppendChild(element1); - } - else - { - xmlDoc.LoadXml(attributes); - } - var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes"); - XmlElement pvaElement = null; - //find existing - var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductVariantAttribute"); - foreach (XmlNode node1 in nodeList1) - { - if (node1.Attributes != null && node1.Attributes["ID"] != null) - { - string str1 = node1.Attributes["ID"].InnerText.Trim(); - int id = 0; - if (int.TryParse(str1, out id)) - { - if (id == pva.Id) - { - pvaElement = (XmlElement)node1; - break; - } - } - } - } + public static string AddProductAttribute(this ProductVariantAttribute pva, string attributes, string value) + { + string result = string.Empty; + try + { + var xmlDoc = new XmlDocument(); + if (String.IsNullOrEmpty(attributes)) + { + var element1 = xmlDoc.CreateElement("Attributes"); + xmlDoc.AppendChild(element1); + } + else + { + xmlDoc.LoadXml(attributes); + } + var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes"); + + XmlElement pvaElement = null; + //find existing + var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductVariantAttribute"); + foreach (XmlNode node1 in nodeList1) + { + if (node1.Attributes != null && node1.Attributes["ID"] != null) + { + string str1 = node1.Attributes["ID"].InnerText.Trim(); + int id = 0; + if (int.TryParse(str1, out id)) + { + if (id == pva.Id) + { + pvaElement = (XmlElement)node1; + break; + } + } + } + } - //create new one if not found - if (pvaElement == null) - { - pvaElement = xmlDoc.CreateElement("ProductVariantAttribute"); - pvaElement.SetAttribute("ID", pva.Id.ToString()); - rootElement.AppendChild(pvaElement); - } + //create new one if not found + if (pvaElement == null) + { + pvaElement = xmlDoc.CreateElement("ProductVariantAttribute"); + pvaElement.SetAttribute("ID", pva.Id.ToString()); + rootElement.AppendChild(pvaElement); + } - var pvavElement = xmlDoc.CreateElement("ProductVariantAttributeValue"); - pvaElement.AppendChild(pvavElement); + var pvavElement = xmlDoc.CreateElement("ProductVariantAttributeValue"); + pvaElement.AppendChild(pvavElement); - var pvavVElement = xmlDoc.CreateElement("Value"); - pvavVElement.InnerText = value; - pvavElement.AppendChild(pvavVElement); + var pvavVElement = xmlDoc.CreateElement("Value"); + pvavVElement.InnerText = value; + pvavElement.AppendChild(pvavVElement); - result = xmlDoc.OuterXml; - } - catch (Exception exc) - { - Debug.Write(exc.ToString()); - } - return result; - } - } + result = xmlDoc.OuterXml; + } + catch (Exception exc) + { + Debug.Write(exc.ToString()); + } + return result; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs index 5b676afbc4..8e54c8d8a1 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs @@ -21,41 +21,41 @@ public partial class ProductAttributeFormatter : IProductAttributeFormatter private readonly IWorkContext _workContext; private readonly IProductAttributeService _productAttributeService; private readonly IProductAttributeParser _productAttributeParser; - private readonly IPriceCalculationService _priceCalculationService; + private readonly IPriceCalculationService _priceCalculationService; private readonly ICurrencyService _currencyService; private readonly ILocalizationService _localizationService; private readonly ITaxService _taxService; private readonly IPriceFormatter _priceFormatter; private readonly IDownloadService _downloadService; private readonly IWebHelper _webHelper; - private readonly ShoppingCartSettings _shoppingCartSettings; - private readonly CatalogSettings _catalogSettings; + private readonly ShoppingCartSettings _shoppingCartSettings; + private readonly CatalogSettings _catalogSettings; - public ProductAttributeFormatter(IWorkContext workContext, + public ProductAttributeFormatter(IWorkContext workContext, IProductAttributeService productAttributeService, IProductAttributeParser productAttributeParser, - IPriceCalculationService priceCalculationService, + IPriceCalculationService priceCalculationService, ICurrencyService currencyService, ILocalizationService localizationService, ITaxService taxService, IPriceFormatter priceFormatter, IDownloadService downloadService, IWebHelper webHelper, - ShoppingCartSettings shoppingCartSettings, - CatalogSettings catalogSettings) + ShoppingCartSettings shoppingCartSettings, + CatalogSettings catalogSettings) { _workContext = workContext; _productAttributeService = productAttributeService; _productAttributeParser = productAttributeParser; - _priceCalculationService = priceCalculationService; + _priceCalculationService = priceCalculationService; _currencyService = currencyService; _localizationService = localizationService; _taxService = taxService; _priceFormatter = priceFormatter; _downloadService = downloadService; _webHelper = webHelper; - _shoppingCartSettings = shoppingCartSettings; - _catalogSettings = catalogSettings; + _shoppingCartSettings = shoppingCartSettings; + _catalogSettings = catalogSettings; } /// @@ -145,7 +145,7 @@ public string FormatAttributes( if (allowHyperlinks) { - var downloadLink = string.Format("{0}download/getfileupload/?downloadId={1}", _webHelper.GetStoreLocation(false), download.DownloadGuid); + var downloadLink = string.Format("{0}download/getfileupload/?downloadId={1}", _webHelper.GetStoreLocation(), download.DownloadGuid); attributeText = string.Format("{1}", downloadLink, fileName); } else @@ -183,40 +183,40 @@ public string FormatAttributes( if (pvaValue != null) { pvaAttribute = "{0}: {1}".FormatInvariant( - pva.ProductAttribute.GetLocalized(a => a.Name, languageId), - pvaValue.GetLocalized(a => a.Name, languageId)); + pva.ProductAttribute.GetLocalized(a => a.Name, languageId), + pvaValue.GetLocalized(a => a.Name, languageId)); if (renderPrices) { - var attributeValuePriceAdjustment = _priceCalculationService.GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, 1); - var priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, customer, out var taxRate); + var attributeValuePriceAdjustment = _priceCalculationService.GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, 1); + var priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, customer, out var taxRate); var priceAdjustment = _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, _workContext.WorkingCurrency); - if (_shoppingCartSettings.ShowLinkedAttributeValueQuantity && + if (_shoppingCartSettings.ShowLinkedAttributeValueQuantity && pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage && - pvaValue.Quantity > 1) - { - pvaAttribute += string.Format(" {0}", pvaValue.Quantity); - } + pvaValue.Quantity > 1) + { + pvaAttribute += string.Format(" {0}", pvaValue.Quantity); + } - if (_catalogSettings.ShowVariantCombinationPriceAdjustment) - { - if (priceAdjustmentBase > 0) - { - pvaAttribute += " (+{0})".FormatInvariant(_priceFormatter.FormatPrice(priceAdjustment, true, false)); - } - else if (priceAdjustmentBase < decimal.Zero) - { - pvaAttribute += " (-{0})".FormatInvariant(_priceFormatter.FormatPrice(-priceAdjustment, true, false)); - } - } + if (_catalogSettings.ShowVariantCombinationPriceAdjustment) + { + if (priceAdjustmentBase > 0) + { + pvaAttribute += " (+{0})".FormatInvariant(_priceFormatter.FormatPrice(priceAdjustment, true, false)); + } + else if (priceAdjustmentBase < decimal.Zero) + { + pvaAttribute += " (-{0})".FormatInvariant(_priceFormatter.FormatPrice(-priceAdjustment, true, false)); + } + } } } - if (htmlEncode) - { - pvaAttribute = HttpUtility.HtmlEncode(pvaAttribute); - } + if (htmlEncode) + { + pvaAttribute = HttpUtility.HtmlEncode(pvaAttribute); + } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs index 7c6f33ce38..6fb44522a1 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs @@ -1,165 +1,178 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Xml; using System.Xml.Linq; using SmartStore.Collections; +using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; +using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Configuration; using SmartStore.Core.Logging; +using SmartStore.Data.Utilities; +using SmartStore.Utilities.ObjectPools; namespace SmartStore.Services.Catalog { - public partial class ProductAttributeParser : IProductAttributeParser + public partial class ProductAttributeParser : DbSaveHook, IProductAttributeParser { - // 0 = ProductId, 1 = AttributeXml - private const string ATTRIBUTECOMBINATION_BY_IDXML_KEY = "parsedattributecombination.id-{0}-{1}"; + // 0 = ProductId, 1 = AttributeXml + private const string ATTRIBUTECOMBINATION_BY_IDXML_KEY = "parsedattributecombination.id-{0}-{1}"; - // 0 = AttributeXml - private const string ATTRIBUTEVALUES_BY_XML_KEY = "parsedattributevalues-{0}"; + // 0 = AttributeXml + private const string ATTRIBUTEVALUES_BY_XML_KEY = "parsedattributevalues-{0}"; private const string ATTRIBUTEVALUES_PATTERN_KEY = "parsedattributevalues-*"; - private readonly IProductAttributeService _productAttributeService; - private readonly IRepository _pvacRepository; - private readonly IRequestCache _requestCache; + // 0 = ProductId + private const string UNAVAILABLE_COMBINATIONS_KEY = "attributecombination:unavailable-{0}"; + private const string UNAVAILABLE_COMBINATIONS_PATTERN_KEY = "attributecombination:unavailable-*"; - public ProductAttributeParser( - IProductAttributeService productAttributeService, - IRepository pvacRepository, - IRequestCache requestCache) + private readonly IProductAttributeService _productAttributeService; + private readonly IRepository _pvacRepository; + private readonly IRequestCache _requestCache; + private readonly ICacheManager _cache; + private readonly PerformanceSettings _performanceSettings; + + public ProductAttributeParser( + IProductAttributeService productAttributeService, + IRepository pvacRepository, + IRequestCache requestCache, + ICacheManager cache, + PerformanceSettings performanceSettings) { _productAttributeService = productAttributeService; - _pvacRepository = pvacRepository; - _requestCache = requestCache; - - Logger = NullLogger.Instance; - } + _pvacRepository = pvacRepository; + _requestCache = requestCache; + _cache = cache; + _performanceSettings = performanceSettings; + } - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } = NullLogger.Instance; - #region Product attributes + #region Product attributes - public virtual int PrefetchProductVariantAttributes(IEnumerable attributesXml) - { + public virtual int PrefetchProductVariantAttributes(IEnumerable attributesXml) + { if (attributesXml == null || !attributesXml.Any()) { return 0; } - // Determine uncached attributes - var unfetched = attributesXml - .Where(xml => xml.HasValue()) - .Distinct() - .Where(xml => !_requestCache.Contains(ATTRIBUTEVALUES_BY_XML_KEY.FormatInvariant(xml))) - .ToArray(); - - var infos = new List(unfetched.Length); - - foreach (var xml in unfetched) - { - var valueIds = new HashSet(); - var map = DeserializeProductVariantAttributes(xml); - var attributes = _productAttributeService.GetProductVariantAttributesByIds(map.Keys); - - foreach (var attribute in attributes) - { - // Only types that have attribute values! Otherwise entered text is misinterpreted as an attribute value id. - if (!attribute.ShouldHaveValues()) - continue; - - var ids = - from id in map[attribute.Id] - where id.HasValue() - select id.ToInt(); - - valueIds.UnionWith(ids); - } - - var info = new AttributeMapInfo - { - AttributesXml = xml, - DeserializedMap = map, - AllValueIds = valueIds.ToArray() - }; - - infos.Add(info); - } - - // Get all value ids across all maps (each map has many attributes) - var allValueIds = infos.SelectMany(x => x.AllValueIds) - .Distinct() - .ToArray(); - - // Load ALL requested attribute values into a single dictionary in one go (key is Id) - var attributeValues = _productAttributeService.GetProductVariantAttributeValuesByIds(allValueIds).ToDictionarySafe(x => x.Id); - - // Create a single cache entry for each passed xml - foreach (var info in infos) - { - var cachedValues = new List(); - - // Ensure value id order in cached result list is correct - foreach (var id in info.AllValueIds) - { - if (attributeValues.TryGetValue(id, out var value)) - { - cachedValues.Add(value); - } - } - - // Put it in cache - var cacheKey = ATTRIBUTEVALUES_BY_XML_KEY.FormatInvariant(info.AttributesXml); - _requestCache.Put(cacheKey, cachedValues); - } + // Determine uncached attributes + var unfetched = attributesXml + .Where(xml => xml.HasValue()) + .Distinct() + .Where(xml => !_requestCache.Contains(ATTRIBUTEVALUES_BY_XML_KEY.FormatInvariant(xml))) + .ToArray(); + + var infos = new List(unfetched.Length); + + foreach (var xml in unfetched) + { + var valueIds = new HashSet(); + var map = DeserializeProductVariantAttributes(xml); + var attributes = _productAttributeService.GetProductVariantAttributesByIds(map.Keys); + + foreach (var attribute in attributes) + { + // Only types that have attribute values! Otherwise entered text is misinterpreted as an attribute value id. + if (!attribute.ShouldHaveValues()) + continue; + + var ids = + from id in map[attribute.Id] + where id.HasValue() + select id.ToInt(); + + valueIds.UnionWith(ids); + } + + var info = new AttributeMapInfo + { + AttributesXml = xml, + DeserializedMap = map, + AllValueIds = valueIds.ToArray() + }; + + infos.Add(info); + } + + // Get all value ids across all maps (each map has many attributes) + var allValueIds = infos.SelectMany(x => x.AllValueIds) + .Distinct() + .ToArray(); + + // Load ALL requested attribute values into a single dictionary in one go (key is Id) + var attributeValues = _productAttributeService.GetProductVariantAttributeValuesByIds(allValueIds).ToDictionarySafe(x => x.Id); + + // Create a single cache entry for each passed xml + foreach (var info in infos) + { + var cachedValues = new List(); + + // Ensure value id order in cached result list is correct + foreach (var id in info.AllValueIds) + { + if (attributeValues.TryGetValue(id, out var value)) + { + cachedValues.Add(value); + } + } + + // Put it in cache + var cacheKey = ATTRIBUTEVALUES_BY_XML_KEY.FormatInvariant(info.AttributesXml); + _requestCache.Put(cacheKey, cachedValues); + } return unfetched.Length; } - public virtual IList ParseProductVariantAttributes(string attributesXml) - { - var values = ParseProductVariantAttributeValues(attributesXml); - var attrMap = DeserializeProductVariantAttributes(attributesXml); - var attrs = _productAttributeService.GetProductVariantAttributesByIds(attrMap.Keys, values.Select(x => x.ProductVariantAttribute).Distinct().ToList()); + public virtual IList ParseProductVariantAttributes(string attributesXml) + { + var values = ParseProductVariantAttributeValues(attributesXml); + var attrMap = DeserializeProductVariantAttributes(attributesXml); + var attrs = _productAttributeService.GetProductVariantAttributesByIds(attrMap.Keys, values.Select(x => x.ProductVariantAttribute).Distinct().ToList()); - return attrs; - } + return attrs; + } - public virtual IEnumerable ParseProductVariantAttributeValues(string attributeXml) - { - if (attributeXml.IsEmpty()) - return new List(); + public virtual IEnumerable ParseProductVariantAttributeValues(string attributeXml) + { + if (attributeXml.IsEmpty()) + return new List(); - var cacheKey = ATTRIBUTEVALUES_BY_XML_KEY.FormatInvariant(attributeXml); + var cacheKey = ATTRIBUTEVALUES_BY_XML_KEY.FormatInvariant(attributeXml); - var result = _requestCache.Get(cacheKey, () => - { - var allValueIds = new HashSet(); - var attrMap = DeserializeProductVariantAttributes(attributeXml); - var attributes = _productAttributeService.GetProductVariantAttributesByIds(attrMap.Keys); + var result = _requestCache.Get(cacheKey, () => + { + var allValueIds = new HashSet(); + var attrMap = DeserializeProductVariantAttributes(attributeXml); + var attributes = _productAttributeService.GetProductVariantAttributesByIds(attrMap.Keys); - foreach (var attribute in attributes) - { - // Only types that have attribute values! Otherwise entered text is misinterpreted as an attribute value id. - if (!attribute.ShouldHaveValues()) - continue; + foreach (var attribute in attributes) + { + // Only types that have attribute values! Otherwise entered text is misinterpreted as an attribute value id. + if (!attribute.ShouldHaveValues()) + continue; - var ids = - from id in attrMap[attribute.Id] - where id.HasValue() - select id.ToInt(); + var ids = + from id in attrMap[attribute.Id] + where id.HasValue() + select id.ToInt(); - allValueIds.UnionWith(ids); - } + allValueIds.UnionWith(ids); + } - var values = _productAttributeService.GetProductVariantAttributeValuesByIds(allValueIds.ToArray()); + var values = _productAttributeService.GetProductVariantAttributeValuesByIds(allValueIds.ToArray()); - return values; - }); + return values; + }); - return result; - } + return result; + } public virtual void ClearCachedAttributeValues() { @@ -167,77 +180,83 @@ public virtual void ClearCachedAttributeValues() } public virtual Multimap DeserializeProductVariantAttributes(string attributesXml) - { - var attrs = new Multimap(); - if (String.IsNullOrEmpty(attributesXml)) - return attrs; - - try - { - var doc = XDocument.Parse(attributesXml); - - // Attributes/ProductVariantAttribute - foreach (var node1 in doc.Descendants("ProductVariantAttribute")) - { - string sid = node1.Attribute("ID").Value; - if (sid.HasValue()) - { - if (int.TryParse(sid, out var id)) - { - // ProductVariantAttributeValue/Value - foreach (var node2 in node1.Descendants("Value")) - { - attrs.Add(id, node2.Value); - } - } - } - } - } - catch (Exception exception) - { - Logger.Error(exception); - } - - return attrs; - } - - public virtual ICollection ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes) - { - var result = new HashSet(); - - if (attributeCombination == null || !attributeCombination.Any()) - return result; - - var allValueIds = new HashSet(); - - foreach (var pva in attributes.Where(x => x.ShouldHaveValues()).OrderBy(x => x.DisplayOrder).ToArray()) - { - if (attributeCombination.ContainsKey(pva.Id)) - { - var pvaValuesStr = attributeCombination[pva.Id]; - var ids = pvaValuesStr.Where(x => x.HasValue()).Select(x => x.ToInt()); - - allValueIds.UnionWith(ids); - } - } - - foreach (int id in allValueIds) - { - foreach (var attribute in attributes) - { - var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == id); - if (attributeValue != null) - { - result.Add(attributeValue); - break; - } - } - } - - return result; - } - - public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) + { + return DeserializeAttributesXml(attributesXml, null); + } + + private Multimap DeserializeAttributesXml(string attributesXml, int[] includeAttributesIds = null) + { + var attrs = new Multimap(); + if (String.IsNullOrEmpty(attributesXml)) + return attrs; + + try + { + var doc = XDocument.Parse(attributesXml); + + // Attributes/ProductVariantAttribute + foreach (var node1 in doc.Descendants("ProductVariantAttribute")) + { + string sid = node1.Attribute("ID").Value; + if (sid.HasValue()) + { + if (int.TryParse(sid, out var id) && + (includeAttributesIds == null || includeAttributesIds.Contains(id))) + { + // ProductVariantAttributeValue/Value + foreach (var node2 in node1.Descendants("Value")) + { + attrs.Add(id, node2.Value); + } + } + } + } + } + catch (Exception exception) + { + Logger.Error(exception); + } + + return attrs; + } + + public virtual ICollection ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes) + { + var result = new HashSet(); + + if (attributeCombination == null || !attributeCombination.Any()) + return result; + + var allValueIds = new HashSet(); + + foreach (var pva in attributes.Where(x => x.ShouldHaveValues()).OrderBy(x => x.DisplayOrder).ToArray()) + { + if (attributeCombination.ContainsKey(pva.Id)) + { + var pvaValuesStr = attributeCombination[pva.Id]; + var ids = pvaValuesStr.Where(x => x.HasValue()).Select(x => x.ToInt()); + + allValueIds.UnionWith(ids); + } + } + + foreach (int id in allValueIds) + { + foreach (var attribute in attributes) + { + var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == id); + if (attributeValue != null) + { + result.Add(attributeValue); + break; + } + } + } + + return result; + } + + public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) { var selectedProductVariantAttributeValues = new List(); try @@ -268,132 +287,142 @@ public virtual IList ParseValues(string attributesXml, int productVarian } catch (Exception exception) { - Logger.Error(exception); - } + Logger.Error(exception); + } return selectedProductVariantAttributeValues; } public virtual string AddProductAttribute(string attributesXml, ProductVariantAttribute pva, string value) { - return pva.AddProductAttribute(attributesXml, value); + return pva.AddProductAttribute(attributesXml, value); } - public virtual string CreateAttributesXml(Multimap attributes) - { - Guard.NotNull(attributes, nameof(attributes)); + public virtual string CreateAttributesXml(Multimap attributes) + { + Guard.NotNull(attributes, nameof(attributes)); - if (attributes.Count == 0) - return null; + if (attributes.Count == 0) + return null; - var doc = new XmlDocument(); - var root = doc.AppendChild(doc.CreateElement("Attributes")); + var doc = new XmlDocument(); + var root = doc.AppendChild(doc.CreateElement("Attributes")); - foreach (var attr in attributes) - { - var xelAttr = root.AppendChild(doc.CreateElement("ProductVariantAttribute")) as XmlElement; - xelAttr.SetAttribute("ID", attr.Key.ToString()); + foreach (var attr in attributes) + { + var xelAttr = root.AppendChild(doc.CreateElement("ProductVariantAttribute")) as XmlElement; + xelAttr.SetAttribute("ID", attr.Key.ToString()); - foreach (var val in attr.Value) - { - var xelAttrValue = xelAttr.AppendChild(doc.CreateElement("ProductVariantAttributeValue")); - var xelValue = xelAttrValue.AppendChild(doc.CreateElement("Value")); - xelValue.InnerText = val; - } - } + foreach (var val in attr.Value) + { + var xelAttrValue = xelAttr.AppendChild(doc.CreateElement("ProductVariantAttributeValue")); + var xelValue = xelAttrValue.AppendChild(doc.CreateElement("Value")); + xelValue.InnerText = val; + } + } - return doc.OuterXml; - } + return doc.OuterXml; + } - public virtual bool AreProductAttributesEqual(string attributeXml1, string attributeXml2) + public virtual bool AreProductAttributesEqual(string attributeXml1, string attributeXml2) { - if (attributeXml1 == attributeXml2) - return true; + if (attributeXml1 == attributeXml2) + return true; - var attributes1 = DeserializeProductVariantAttributes(attributeXml1); + var attributes1 = DeserializeProductVariantAttributes(attributeXml1); var attributes2 = DeserializeProductVariantAttributes(attributeXml2); - if (attributes1.Count != attributes2.Count) - return false; - - foreach (var kvp in attributes1) - { - if (!attributes2.ContainsKey(kvp.Key)) - { - // the second list does not contain this id: not equal! - return false; - } - - // compare the values - var values1 = kvp.Value; - var values2 = attributes2[kvp.Key]; - - if (values1.Count != values2.Count) - { - // number of values differ: not equal! - return false; - } - - foreach (var value1 in values1) - { - var str1 = value1.TrimSafe(); - - if (!values2.Any(x => x.TrimSafe().IsCaseInsensitiveEqual(str1))) - { - // the second values list for this attribute does not contain this value: not equal! - return false; - } - } - } + return AreProductAttributesEqual(attributes1, attributes2); + } + + private bool AreProductAttributesEqual(Multimap attributes1, Multimap attributes2) + { + if (attributes1.Count != attributes2.Count) + return false; + + foreach (var kvp in attributes1) + { + if (!attributes2.ContainsKey(kvp.Key)) + { + // the second list does not contain this id: not equal! + return false; + } + + // compare the values + var values1 = kvp.Value; + var values2 = attributes2[kvp.Key]; + + if (values1.Count != values2.Count) + { + // number of values differ: not equal! + return false; + } + + foreach (var value1 in values1) + { + var str1 = value1.TrimSafe(); + + if (!values2.Any(x => x.TrimSafe().IsCaseInsensitiveEqual(str1))) + { + // the second values list for this attribute does not contain this value: not equal! + return false; + } + } + } return true; } - public virtual ProductVariantAttributeCombination FindProductVariantAttributeCombination(int productId, string attributesXml) - { - if (attributesXml.IsEmpty()) - return null; + public virtual ProductVariantAttributeCombination FindProductVariantAttributeCombination(int productId, string attributesXml) + { + if (attributesXml.IsEmpty()) + return null; - var cacheKey = ATTRIBUTECOMBINATION_BY_IDXML_KEY.FormatInvariant(productId, attributesXml); + var cacheKey = ATTRIBUTECOMBINATION_BY_IDXML_KEY.FormatInvariant(productId, attributesXml); - var result = _requestCache.Get(cacheKey, () => - { - var query = from x in _pvacRepository.TableUntracked - where x.ProductId == productId - select new - { - x.Id, - x.AttributesXml - }; + var result = _requestCache.Get(cacheKey, () => + { + var query = from x in _pvacRepository.TableUntracked + where x.ProductId == productId + select new + { + x.Id, + x.AttributesXml + }; - var combinations = query.ToList(); - if (combinations.Count == 0) - return null; + var combinations = query.ToList(); + if (combinations.Count == 0) + return null; - foreach (var combination in combinations) - { - bool attributesEqual = AreProductAttributesEqual(combination.AttributesXml, attributesXml); - if (attributesEqual) - return _productAttributeService.GetProductVariantAttributeCombinationById(combination.Id); - } + var listTypeValues = ParseProductVariantAttributeValues(attributesXml); + var listTypeAttributesIds = listTypeValues.Select(x => x.ProductVariantAttributeId).ToArray(); + var listTypeAttributesMap = DeserializeAttributesXml(attributesXml, listTypeAttributesIds); - return null; - }); + foreach (var combination in combinations) + { + var combinationAttributesMap = DeserializeProductVariantAttributes(combination.AttributesXml); + bool attributesEqual = AreProductAttributesEqual(combinationAttributesMap, listTypeAttributesMap); + if (attributesEqual) + return _productAttributeService.GetProductVariantAttributeCombinationById(combination.Id); + } - return result; - } + return null; + }); - #endregion + return result; + } - #region Gift card attributes + #endregion - public string AddGiftCardAttribute( - string attributesXml, - string recipientName, + #region Gift card attributes + + public string AddGiftCardAttribute( + string attributesXml, + string recipientName, string recipientEmail, - string senderName, - string senderEmail, - string giftCardMessage) + string senderName, + string senderEmail, + string giftCardMessage) { var result = string.Empty; @@ -448,19 +477,19 @@ public string AddGiftCardAttribute( } catch (Exception exception) { - Logger.Error(exception); + Logger.Error(exception); } return result; } public void GetGiftCardAttribute( - string attributesXml, - out string recipientName, + string attributesXml, + out string recipientName, out string recipientEmail, - out string senderName, + out string senderName, out string senderEmail, - out string giftCardMessage) + out string giftCardMessage) { recipientName = string.Empty; recipientEmail = string.Empty; @@ -492,17 +521,218 @@ public void GetGiftCardAttribute( } catch (Exception exception) { - Logger.Error(exception); - } + Logger.Error(exception); + } } #endregion - class AttributeMapInfo - { - public string AttributesXml { get; set; } - public Multimap DeserializedMap { get; set; } - public int[] AllValueIds { get; set; } - } + #region Unavailable attribute combinations + + public virtual CombinationAvailabilityInfo IsCombinationAvailable( + Product product, + IEnumerable attributes, + IEnumerable selectedValues, + ProductVariantAttributeValue currentValue) + { + if (product == null || + _performanceSettings.MaxUnavailableAttributeCombinations <= 0 || + !(selectedValues?.Any() ?? false)) + { + return null; + } + + // Get unavailable combinations. + var unavailableCombinations = _cache.Get(UNAVAILABLE_COMBINATIONS_KEY.FormatInvariant(product.Id), () => + { + var data = new Dictionary(); + var query = _pvacRepository.TableUntracked.Where(x => x.ProductId == product.Id); + + if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) + { + query = query.Where(x => !x.IsActive || (x.StockQuantity <= 0 && !x.AllowOutOfStockOrders)); + } + else + { + query = query.Where(x => !x.IsActive); + } + + // Do not proceed if there are too many unavailable combinations. + var unavailableCombinationsCount = query.Count(); + + if (unavailableCombinationsCount <= _performanceSettings.MaxUnavailableAttributeCombinations) + { + var pager = new FastPager(query); + + while (pager.ReadNextPage(out var combinations)) + { + foreach (var combination in combinations) + { + var map = DeserializeProductVariantAttributes(combination.AttributesXml); + if (map.Any()) + { + // :[,...] + var valuesKeys = map + .OrderBy(x => x.Key) + .Select(x => $"{x.Key}:{string.Join(",", x.Value.OrderBy(y => y))}"); + + data[string.Join("-", valuesKeys)] = new CombinationAvailabilityInfo + { + IsActive = combination.IsActive, + IsOutOfStock = combination.StockQuantity <= 0 && !combination.AllowOutOfStockOrders + }; + } + } + } + } + + return data; + }, TimeSpan.FromMinutes(10)); + + if (!unavailableCombinations.Any()) + { + return null; + } + + var builder = PooledStringBuilder.Rent(); + var selectedValuesMap = selectedValues.ToMultimap(x => x.ProductVariantAttributeId, x => x); + + if (attributes == null || currentValue == null) + { + // Create key to test selectedValues. + foreach (var kvp in selectedValuesMap.OrderBy(x => x.Key)) + { + Append(builder, kvp.Key, kvp.Value.Select(x => x.Id).Distinct()); + } + } + else + { + // Create key to test currentValue. + foreach (var attribute in attributes.OrderBy(x => x.Id)) + { + IEnumerable valueIds; + + var selectedIds = selectedValuesMap.ContainsKey(attribute.Id) + ? selectedValuesMap[attribute.Id].Select(x => x.Id) + : null; + + if (attribute.Id == currentValue.ProductVariantAttributeId) + { + // Attribute to be tested. + if (selectedIds != null && attribute.IsMultipleChoice) + { + // Take selected values and append current value. + valueIds = selectedIds.Append(currentValue.Id).Distinct(); + } + else + { + // Single selection attribute -> take current value. + valueIds = new[] { currentValue.Id }; + } + } + else + { + // Other attribute. + if (selectedIds != null) + { + // Take selected value(s). + valueIds = selectedIds; + } + else + { + // No selected value -> no unavailable combination. + return null; + } + } + + Append(builder, attribute.Id, valueIds); + } + } + + var key = builder.ToStringAndReturn(); + //$"{!unavailableCombinations.ContainsKey(key),-5} {currentValue.ProductVariantAttributeId}:{currentValue.Id} -> {key}".Dump(); + + if (unavailableCombinations.TryGetValue(key, out var availability)) + { + return availability; + } + + return null; + + static void Append(PooledStringBuilder psb, int pvaId, IEnumerable pvavIds) + { + var idsStr = string.Join(",", pvavIds.OrderBy(x => x)); + + if (psb.Length > 0) + { + psb.Append("-"); + } + psb.Append($"{pvaId}:{idsStr}"); + } + } + + private static readonly HashSet _combinationsInvalidationTypes = new HashSet(new[] + { + typeof(Setting), + typeof(Product), + typeof(ProductVariantAttributeCombination) + }); + + private void InvalidateUnavailableCombinations(BaseEntity entity, IHookedEntity entry) + { + var type = entry.EntityType; + + if (!_combinationsInvalidationTypes.Contains(type)) + { + throw new NotSupportedException(); + } + + var productId = 0; + + if (type == typeof(Setting)) + { + if (((Setting)entity).Name.IsCaseInsensitiveEqual("PerformanceSettings.MaxUnavailableAttributeCombinations")) + { + _cache.RemoveByPattern(UNAVAILABLE_COMBINATIONS_PATTERN_KEY); + } + } + else if (type == typeof(Product)) + { + productId = entity.Id; + } + else if (type == typeof(ProductVariantAttributeCombination)) + { + productId = ((ProductVariantAttributeCombination)entity).ProductId; + } + + if (productId != 0) + { + _cache.Remove(UNAVAILABLE_COMBINATIONS_KEY.FormatInvariant(productId)); + } + } + + #endregion + + protected override void OnUpdated(BaseEntity entity, IHookedEntity entry) + { + InvalidateUnavailableCombinations(entity, entry); + } + + protected override void OnInserted(BaseEntity entity, IHookedEntity entry) + { + InvalidateUnavailableCombinations(entity, entry); + } + + protected override void OnDeleted(BaseEntity entity, IHookedEntity entry) + { + InvalidateUnavailableCombinations(entity, entry); + } + + class AttributeMapInfo + { + public string AttributesXml { get; set; } + public Multimap DeserializedMap { get; set; } + public int[] AllValueIds { get; set; } + } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs index b307a31348..39b45da0fb 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs @@ -16,75 +16,75 @@ namespace SmartStore.Services.Catalog { public partial class ProductAttributeService : IProductAttributeService { - // 0 = ProductId, 1 = PageIndex, 2 = PageSize - private const string PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY = "SmartStore.productvariantattribute.combinations.id-{0}-{1}-{2}"; - private const string PRODUCTVARIANTATTRIBUTES_PATTERN_KEY = "SmartStore.productvariantattribute.*"; - - // 0 = Attribute value ids, e.g. 16-254-1245 - private const string PRODUCTVARIANTATTRIBUTEVALUES_BY_IDS_KEY = "SmartStore.productvariantattributevalues.ids-{0}"; - private const string PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.productvariantattributevalues*"; - - private readonly IRepository _productAttributeRepository; - private readonly IRepository _productAttributeOptionRepository; - private readonly IRepository _productAttributeOptionsSetRepository; - private readonly IRepository _productVariantAttributeRepository; + // 0 = ProductId, 1 = PageIndex, 2 = PageSize + private const string PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY = "SmartStore.productvariantattribute.combinations.id-{0}-{1}-{2}"; + private const string PRODUCTVARIANTATTRIBUTES_PATTERN_KEY = "SmartStore.productvariantattribute.*"; + + // 0 = Attribute value ids, e.g. 16-254-1245 + private const string PRODUCTVARIANTATTRIBUTEVALUES_BY_IDS_KEY = "SmartStore.productvariantattributevalues.ids-{0}"; + private const string PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.productvariantattributevalues*"; + + private readonly IRepository _productAttributeRepository; + private readonly IRepository _productAttributeOptionRepository; + private readonly IRepository _productAttributeOptionsSetRepository; + private readonly IRepository _productVariantAttributeRepository; private readonly IRepository _pvacRepository; private readonly IRepository _productVariantAttributeValueRepository; - private readonly IRepository _productBundleItemAttributeFilterRepository; - private readonly ILocalizedEntityService _localizedEntityService; + private readonly IRepository _productBundleItemAttributeFilterRepository; + private readonly ILocalizedEntityService _localizedEntityService; private readonly IRequestCache _requestCache; public ProductAttributeService( - IRequestCache requestCache, + IRequestCache requestCache, IRepository productAttributeRepository, - IRepository productAttributeOptionRepository, - IRepository productAttributeOptionsSetRepository, - IRepository productVariantAttributeRepository, + IRepository productAttributeOptionRepository, + IRepository productAttributeOptionsSetRepository, + IRepository productVariantAttributeRepository, IRepository pvacRepository, IRepository productVariantAttributeValueRepository, - IRepository productBundleItemAttributeFilterRepository, - ILocalizedEntityService localizedEntityService) + IRepository productBundleItemAttributeFilterRepository, + ILocalizedEntityService localizedEntityService) { _requestCache = requestCache; _productAttributeRepository = productAttributeRepository; - _productAttributeOptionRepository = productAttributeOptionRepository; - _productAttributeOptionsSetRepository = productAttributeOptionsSetRepository; + _productAttributeOptionRepository = productAttributeOptionRepository; + _productAttributeOptionsSetRepository = productAttributeOptionsSetRepository; _productVariantAttributeRepository = productVariantAttributeRepository; _pvacRepository = pvacRepository; _productVariantAttributeValueRepository = productVariantAttributeValueRepository; - _productBundleItemAttributeFilterRepository = productBundleItemAttributeFilterRepository; - _localizedEntityService = localizedEntityService; - } + _productBundleItemAttributeFilterRepository = productBundleItemAttributeFilterRepository; + _localizedEntityService = localizedEntityService; + } - public Localizer T { get; set; } = NullLocalizer.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; private IList GetSwitchedLoadedAttributeMappings(ICollection productVariantAttributeIds) - { - if (productVariantAttributeIds != null && productVariantAttributeIds.Count > 0) - { - if (productVariantAttributeIds.Count == 1) - { - var pva = GetProductVariantAttributeById(productVariantAttributeIds.First()); - if (pva != null) - { - return new List { pva }; - } - } - else - { - return _productVariantAttributeRepository - .GetMany(productVariantAttributeIds) - .OrderBy(x => x.DisplayOrder) - .ToList(); - } - } - - return new List(); - } - - #region Product attributes - - public virtual void DeleteProductAttribute(ProductAttribute productAttribute) + { + if (productVariantAttributeIds != null && productVariantAttributeIds.Count > 0) + { + if (productVariantAttributeIds.Count == 1) + { + var pva = GetProductVariantAttributeById(productVariantAttributeIds.First()); + if (pva != null) + { + return new List { pva }; + } + } + else + { + return _productVariantAttributeRepository + .GetMany(productVariantAttributeIds) + .OrderBy(x => x.DisplayOrder) + .ToList(); + } + } + + return new List(); + } + + #region Product attributes + + public virtual void DeleteProductAttribute(ProductAttribute productAttribute) { if (productAttribute == null) throw new ArgumentNullException("productAttribute"); @@ -93,7 +93,7 @@ public virtual void DeleteProductAttribute(ProductAttribute productAttribute) //cache _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } public virtual IPagedList GetAllProductAttributes(int pageIndex, int pageSize, bool untracked = true) @@ -111,18 +111,18 @@ public virtual ProductAttribute GetProductAttributeById(int productAttributeId) if (productAttributeId == 0) return null; - return _productAttributeRepository.GetByIdCached(productAttributeId, "db.prodattr.id-" + productAttributeId); - } + return _productAttributeRepository.GetByIdCached(productAttributeId, "db.prodattr.id-" + productAttributeId); + } public virtual void InsertProductAttribute(ProductAttribute productAttribute) { if (productAttribute == null) throw new ArgumentNullException("productAttribute"); - _productAttributeRepository.Insert(productAttribute); - + _productAttributeRepository.Insert(productAttribute); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } public virtual void UpdateProductAttribute(ProductAttribute productAttribute) @@ -130,164 +130,164 @@ public virtual void UpdateProductAttribute(ProductAttribute productAttribute) if (productAttribute == null) throw new ArgumentNullException("productAttribute"); - _productAttributeRepository.Update(productAttribute); + _productAttributeRepository.Update(productAttribute); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } - public virtual Multimap GetExportFieldMappings(string fieldPrefix) - { - Guard.NotEmpty(fieldPrefix, nameof(fieldPrefix)); + public virtual Multimap GetExportFieldMappings(string fieldPrefix) + { + Guard.NotEmpty(fieldPrefix, nameof(fieldPrefix)); - var result = new Multimap(StringComparer.OrdinalIgnoreCase); + var result = new Multimap(StringComparer.OrdinalIgnoreCase); - if (!fieldPrefix.EndsWith(":")) - { - fieldPrefix = fieldPrefix + ":"; - } + if (!fieldPrefix.EndsWith(":")) + { + fieldPrefix = fieldPrefix + ":"; + } - var mappings = _productAttributeRepository.TableUntracked - .Where(x => !string.IsNullOrEmpty(x.ExportMappings)) - .Select(x => new - { - x.Id, - x.ExportMappings - }) - .ToList(); + var mappings = _productAttributeRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.ExportMappings)) + .Select(x => new + { + x.Id, + x.ExportMappings + }) + .ToList(); - foreach (var mapping in mappings) - { - var rows = mapping.ExportMappings.SplitSafe(Environment.NewLine) - .Where(x => x.StartsWith(fieldPrefix, StringComparison.InvariantCultureIgnoreCase)); + foreach (var mapping in mappings) + { + var rows = mapping.ExportMappings.SplitSafe(Environment.NewLine) + .Where(x => x.StartsWith(fieldPrefix, StringComparison.InvariantCultureIgnoreCase)); - foreach (var row in rows) - { - var exportFieldName = row.Substring(fieldPrefix.Length).TrimEnd(); - if (exportFieldName.HasValue()) - { - result.Add(exportFieldName, mapping.Id); - } - } - } + foreach (var row in rows) + { + var exportFieldName = row.Substring(fieldPrefix.Length).TrimEnd(); + if (exportFieldName.HasValue()) + { + result.Add(exportFieldName, mapping.Id); + } + } + } - return result; - } + return result; + } - #endregion + #endregion - #region Product attribute options + #region Product attribute options - public virtual ProductAttributeOption GetProductAttributeOptionById(int id) - { + public virtual ProductAttributeOption GetProductAttributeOptionById(int id) + { if (id == 0) { return null; } - return _productAttributeOptionRepository.GetById(id); - } + return _productAttributeOptionRepository.GetById(id); + } - public virtual IList GetProductAttributeOptionsByOptionsSetId(int optionsSetId) - { - if (optionsSetId == 0) - return new List(); + public virtual IList GetProductAttributeOptionsByOptionsSetId(int optionsSetId) + { + if (optionsSetId == 0) + return new List(); - var entities = _productAttributeOptionRepository.Table - .Where(x => x.ProductAttributeOptionsSetId == optionsSetId) - .OrderBy(x => x.DisplayOrder) - .ThenBy(x => x.Name) - .ToList(); + var entities = _productAttributeOptionRepository.Table + .Where(x => x.ProductAttributeOptionsSetId == optionsSetId) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Name) + .ToList(); - return entities; - } + return entities; + } - public virtual IList GetProductAttributeOptionsByAttributeId(int attributeId) - { - if (attributeId == 0) - return new List(); + public virtual IList GetProductAttributeOptionsByAttributeId(int attributeId) + { + if (attributeId == 0) + return new List(); - var entities = - from o in _productAttributeOptionRepository.Table - join os in _productAttributeOptionsSetRepository.Table on o.ProductAttributeOptionsSetId equals os.Id - where os.ProductAttributeId == attributeId - select o; + var entities = + from o in _productAttributeOptionRepository.Table + join os in _productAttributeOptionsSetRepository.Table on o.ProductAttributeOptionsSetId equals os.Id + where os.ProductAttributeId == attributeId + select o; - return entities.ToList(); - } + return entities.ToList(); + } - public virtual void DeleteProductAttributeOption(ProductAttributeOption productAttributeOption) - { - Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); + public virtual void DeleteProductAttributeOption(ProductAttributeOption productAttributeOption) + { + Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); - _productAttributeOptionRepository.Delete(productAttributeOption); - } + _productAttributeOptionRepository.Delete(productAttributeOption); + } - public virtual void InsertProductAttributeOption(ProductAttributeOption productAttributeOption) - { - Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); + public virtual void InsertProductAttributeOption(ProductAttributeOption productAttributeOption) + { + Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); - _productAttributeOptionRepository.Insert(productAttributeOption); - } + _productAttributeOptionRepository.Insert(productAttributeOption); + } - public virtual void UpdateProductAttributeOption(ProductAttributeOption productAttributeOption) - { - Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); + public virtual void UpdateProductAttributeOption(ProductAttributeOption productAttributeOption) + { + Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); - _productAttributeOptionRepository.Update(productAttributeOption); - } + _productAttributeOptionRepository.Update(productAttributeOption); + } - #endregion + #endregion - #region Product attribute options sets + #region Product attribute options sets - public virtual ProductAttributeOptionsSet GetProductAttributeOptionsSetById(int id) - { - if (id == 0) - return null; + public virtual ProductAttributeOptionsSet GetProductAttributeOptionsSetById(int id) + { + if (id == 0) + return null; - return _productAttributeOptionsSetRepository.GetById(id); - } + return _productAttributeOptionsSetRepository.GetById(id); + } - public virtual IList GetProductAttributeOptionsSetsByAttributeId(int productAttributeId) - { - if (productAttributeId == 0) - return new List(); + public virtual IList GetProductAttributeOptionsSetsByAttributeId(int productAttributeId) + { + if (productAttributeId == 0) + return new List(); - var entities = _productAttributeOptionsSetRepository.Table - .Where(x => x.ProductAttributeId == productAttributeId) - .OrderBy(x => x.Name) - .ToList(); + var entities = _productAttributeOptionsSetRepository.Table + .Where(x => x.ProductAttributeId == productAttributeId) + .OrderBy(x => x.Name) + .ToList(); - return entities; - } + return entities; + } - public virtual void DeleteProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) - { - Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); + public virtual void DeleteProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) + { + Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); - _productAttributeOptionsSetRepository.Delete(productAttributeOptionsSet); - } + _productAttributeOptionsSetRepository.Delete(productAttributeOptionsSet); + } - public virtual void InsertProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) - { - Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); + public virtual void InsertProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) + { + Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); - _productAttributeOptionsSetRepository.Insert(productAttributeOptionsSet); - } + _productAttributeOptionsSetRepository.Insert(productAttributeOptionsSet); + } - public virtual void UpdateProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) - { - Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); + public virtual void UpdateProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) + { + Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); - _productAttributeOptionsSetRepository.Update(productAttributeOptionsSet); - } + _productAttributeOptionsSetRepository.Update(productAttributeOptionsSet); + } - #endregion + #endregion - #region Product variant attributes mappings (ProductVariantAttribute) + #region Product variant attributes mappings (ProductVariantAttribute) - public virtual void DeleteProductVariantAttribute(ProductVariantAttribute productVariantAttribute) + public virtual void DeleteProductVariantAttribute(ProductVariantAttribute productVariantAttribute) { if (productVariantAttribute == null) throw new ArgumentNullException("productVariantAttribute"); @@ -295,89 +295,89 @@ public virtual void DeleteProductVariantAttribute(ProductVariantAttribute produc _productVariantAttributeRepository.Delete(productVariantAttribute); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } - public virtual IList GetProductVariantAttributesByProductId(int productId) + public virtual IList GetProductVariantAttributesByProductId(int productId) { - var query = from pva in _productVariantAttributeRepository.Table.Expand(x => x.ProductAttribute) - orderby pva.DisplayOrder - where pva.ProductId == productId - select pva; + var query = from pva in _productVariantAttributeRepository.Table.Expand(x => x.ProductAttribute) + orderby pva.DisplayOrder + where pva.ProductId == productId + select pva; - var productVariantAttributes = query.ToListCached("db.prodvarattrs.all-" + productId); - return productVariantAttributes; - } + var productVariantAttributes = query.ToListCached("db.prodvarattrs.all-" + productId); + return productVariantAttributes; + } - public virtual Multimap GetProductVariantAttributesByProductIds(int[] productIds, AttributeControlType? controlType) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual Multimap GetProductVariantAttributesByProductIds(int[] productIds, AttributeControlType? controlType) + { + Guard.NotNull(productIds, nameof(productIds)); if (!productIds.Any()) { return new Multimap(); } - var query = - from pva in _productVariantAttributeRepository.TableUntracked.Expand(x => x.ProductAttribute).Expand(x => x.ProductVariantAttributeValues) - where productIds.Contains(pva.ProductId) - select pva; + var query = + from pva in _productVariantAttributeRepository.TableUntracked.Expand(x => x.ProductAttribute).Expand(x => x.ProductVariantAttributeValues) + where productIds.Contains(pva.ProductId) + select pva; - if (controlType.HasValue) - { - query = query.Where(x => x.AttributeControlTypeId == ((int)controlType.Value)); - } + if (controlType.HasValue) + { + query = query.Where(x => x.AttributeControlTypeId == ((int)controlType.Value)); + } - var map = query - .OrderBy(x => x.ProductId) - .ThenBy(x => x.DisplayOrder) - .ToList() - .ToMultimap(x => x.ProductId, x => x); + var map = query + .OrderBy(x => x.ProductId) + .ThenBy(x => x.DisplayOrder) + .ToList() + .ToMultimap(x => x.ProductId, x => x); - return map; - } + return map; + } public virtual ProductVariantAttribute GetProductVariantAttributeById(int productVariantAttributeId) { if (productVariantAttributeId == 0) return null; - return _productVariantAttributeRepository.GetByIdCached(productVariantAttributeId, "db.prodvarattr.id-" + productVariantAttributeId); - } - - public virtual IList GetProductVariantAttributesByIds(IEnumerable productVariantAttributeIds, IEnumerable attributes = null) - { - if (productVariantAttributeIds != null) - { - if (attributes != null) - { - var ids = new List(); - var result = new List(); - - foreach (var id in productVariantAttributeIds) - { - var pva = attributes.FirstOrDefault(x => x.Id == id); - if (pva == null) - ids.Add(id); - else - result.Add(pva); - } - - if (ids.Count > 0) - { - var newLoadedMappings = GetSwitchedLoadedAttributeMappings(ids); - result.AddRange(newLoadedMappings); - } - - // sort by passed identifier sequence - return result.OrderBySequence(productVariantAttributeIds).ToList(); - } - - return GetSwitchedLoadedAttributeMappings(productVariantAttributeIds.ToList()); - } - - return new List(); - } + return _productVariantAttributeRepository.GetByIdCached(productVariantAttributeId, "db.prodvarattr.id-" + productVariantAttributeId); + } + + public virtual IList GetProductVariantAttributesByIds(IEnumerable productVariantAttributeIds, IEnumerable attributes = null) + { + if (productVariantAttributeIds != null) + { + if (attributes != null) + { + var ids = new List(); + var result = new List(); + + foreach (var id in productVariantAttributeIds) + { + var pva = attributes.FirstOrDefault(x => x.Id == id); + if (pva == null) + ids.Add(id); + else + result.Add(pva); + } + + if (ids.Count > 0) + { + var newLoadedMappings = GetSwitchedLoadedAttributeMappings(ids); + result.AddRange(newLoadedMappings); + } + + // sort by passed identifier sequence + return result.OrderBySequence(productVariantAttributeIds).ToList(); + } + + return GetSwitchedLoadedAttributeMappings(productVariantAttributeIds.ToList()); + } + + return new List(); + } public virtual IEnumerable GetProductVariantAttributeValuesByIds(params int[] productVariantAttributeValueIds) { @@ -386,39 +386,39 @@ public virtual IEnumerable GetProductVariantAttrib return Enumerable.Empty(); } - Array.Sort(productVariantAttributeValueIds); + Array.Sort(productVariantAttributeValueIds); - var key = PRODUCTVARIANTATTRIBUTEVALUES_BY_IDS_KEY.FormatInvariant(string.Join("-", productVariantAttributeValueIds)); - return _requestCache.Get(key, () => - { - var validTypeIds = new[] - { - (int)AttributeControlType.DropdownList, - (int)AttributeControlType.RadioList, - (int)AttributeControlType.Checkboxes, - (int)AttributeControlType.Boxes - }; - - var query = from x in _productVariantAttributeValueRepository.Table - let attr = x.ProductVariantAttribute - where productVariantAttributeValueIds.Contains(x.Id) && validTypeIds.Contains(attr.AttributeControlTypeId) - orderby x.ProductVariantAttribute.DisplayOrder, x.DisplayOrder - select x; - - query = query.Include(y => y.ProductVariantAttribute.ProductAttribute); - - return query.ToList(); - }); - } + var key = PRODUCTVARIANTATTRIBUTEVALUES_BY_IDS_KEY.FormatInvariant(string.Join("-", productVariantAttributeValueIds)); + return _requestCache.Get(key, () => + { + var validTypeIds = new[] + { + (int)AttributeControlType.DropdownList, + (int)AttributeControlType.RadioList, + (int)AttributeControlType.Checkboxes, + (int)AttributeControlType.Boxes + }; + + var query = from x in _productVariantAttributeValueRepository.Table + let attr = x.ProductVariantAttribute + where productVariantAttributeValueIds.Contains(x.Id) && validTypeIds.Contains(attr.AttributeControlTypeId) + orderby x.ProductVariantAttribute.DisplayOrder, x.DisplayOrder + select x; + + query = query.Include(y => y.ProductVariantAttribute.ProductAttribute); + + return query.ToList(); + }); + } public virtual void InsertProductVariantAttribute(ProductVariantAttribute productVariantAttribute) { - Guard.NotNull(productVariantAttribute, nameof(productVariantAttribute)); + Guard.NotNull(productVariantAttribute, nameof(productVariantAttribute)); - _productVariantAttributeRepository.Insert(productVariantAttribute); + _productVariantAttributeRepository.Insert(productVariantAttribute); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } public virtual void UpdateProductVariantAttribute(ProductVariantAttribute productVariantAttribute) @@ -426,86 +426,86 @@ public virtual void UpdateProductVariantAttribute(ProductVariantAttribute produc if (productVariantAttribute == null) throw new ArgumentNullException("productVariantAttribute"); - _productVariantAttributeRepository.Update(productVariantAttribute); + _productVariantAttributeRepository.Update(productVariantAttribute); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } - public virtual int CopyAttributeOptions(ProductVariantAttribute productVariantAttribute, int productAttributeOptionsSetId, bool deleteExistingValues) - { - Guard.NotNull(productVariantAttribute, nameof(productVariantAttribute)); - Guard.NotZero(productVariantAttribute.Id, nameof(productVariantAttribute.Id)); - Guard.NotZero(productAttributeOptionsSetId, nameof(productAttributeOptionsSetId)); + public virtual int CopyAttributeOptions(ProductVariantAttribute productVariantAttribute, int productAttributeOptionsSetId, bool deleteExistingValues) + { + Guard.NotNull(productVariantAttribute, nameof(productVariantAttribute)); + Guard.NotZero(productVariantAttribute.Id, nameof(productVariantAttribute.Id)); + Guard.NotZero(productAttributeOptionsSetId, nameof(productAttributeOptionsSetId)); - if (deleteExistingValues) - { - var existingValues = productVariantAttribute.ProductVariantAttributeValues.ToList(); + if (deleteExistingValues) + { + var existingValues = productVariantAttribute.ProductVariantAttributeValues.ToList(); if (!existingValues.Any()) { existingValues = GetProductVariantAttributeValues(productVariantAttribute.Id).ToList(); } - existingValues.Each(x => DeleteProductVariantAttributeValue(x)); - } + existingValues.Each(x => DeleteProductVariantAttributeValue(x)); + } - var result = 0; - var attributeOptions = _productAttributeOptionRepository.TableUntracked - .Where(x => x.ProductAttributeOptionsSetId == productAttributeOptionsSetId) - .ToList(); + var result = 0; + var attributeOptions = _productAttributeOptionRepository.TableUntracked + .Where(x => x.ProductAttributeOptionsSetId == productAttributeOptionsSetId) + .ToList(); if (!attributeOptions.Any()) { return result; } - // Do not insert already existing values (identified by name field). - var existingValueNames = new HashSet(_productVariantAttributeValueRepository.TableUntracked - .Where(x => x.ProductVariantAttributeId == productVariantAttribute.Id) - .Select(x => x.Name) - .ToList()); + // Do not insert already existing values (identified by name field). + var existingValueNames = new HashSet(_productVariantAttributeValueRepository.TableUntracked + .Where(x => x.ProductVariantAttributeId == productVariantAttribute.Id) + .Select(x => x.Name) + .ToList()); - using (_localizedEntityService.BeginScope()) - { - foreach (var option in attributeOptions) - { + using (_localizedEntityService.BeginScope()) + { + foreach (var option in attributeOptions) + { if (existingValueNames.Contains(option.Name)) { continue; } - var pvav = option.Clone(); - pvav.ProductVariantAttributeId = productVariantAttribute.Id; + var pvav = option.Clone(); + pvav.ProductVariantAttributeId = productVariantAttribute.Id; - // No scope commit, we need new entity id. - _productVariantAttributeValueRepository.Insert(pvav); - ++result; + // No scope commit, we need new entity id. + _productVariantAttributeValueRepository.Insert(pvav); + ++result; - // Copy localized properties too. - var optionProperties = _localizedEntityService.GetLocalizedProperties(option.Id, "ProductAttributeOption"); + // Copy localized properties too. + var optionProperties = _localizedEntityService.GetLocalizedProperties(option.Id, "ProductAttributeOption"); - foreach (var property in optionProperties) - { - _localizedEntityService.InsertLocalizedProperty(new LocalizedProperty - { - EntityId = pvav.Id, - LocaleKeyGroup = "ProductVariantAttributeValue", - LocaleKey = property.LocaleKey, - LocaleValue = property.LocaleValue, - LanguageId = property.LanguageId - }); - } - } - } + foreach (var property in optionProperties) + { + _localizedEntityService.InsertLocalizedProperty(new LocalizedProperty + { + EntityId = pvav.Id, + LocaleKeyGroup = "ProductVariantAttributeValue", + LocaleKey = property.LocaleKey, + LocaleValue = property.LocaleValue, + LanguageId = property.LanguageId + }); + } + } + } - return result; - } + return result; + } - #endregion + #endregion - #region Product variant attribute values (ProductVariantAttributeValue) + #region Product variant attribute values (ProductVariantAttributeValue) - public virtual void DeleteProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) + public virtual void DeleteProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) { if (productVariantAttributeValue == null) throw new ArgumentNullException("productVariantAttributeValue"); @@ -513,37 +513,37 @@ public virtual void DeleteProductVariantAttributeValue(ProductVariantAttributeVa _productVariantAttributeValueRepository.Delete(productVariantAttributeValue); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } public virtual IList GetProductVariantAttributeValues(int productVariantAttributeId) { - var query = from pvav in _productVariantAttributeValueRepository.Table - orderby pvav.DisplayOrder - where pvav.ProductVariantAttributeId == productVariantAttributeId - select pvav; + var query = from pvav in _productVariantAttributeValueRepository.Table + orderby pvav.DisplayOrder + where pvav.ProductVariantAttributeId == productVariantAttributeId + select pvav; - var productVariantAttributeValues = query.ToListCached("db.prodvarattrvalues.all-" + productVariantAttributeId); - return productVariantAttributeValues; - } + var productVariantAttributeValues = query.ToListCached("db.prodvarattrvalues.all-" + productVariantAttributeId); + return productVariantAttributeValues; + } public virtual ProductVariantAttributeValue GetProductVariantAttributeValueById(int productVariantAttributeValueId) { if (productVariantAttributeValueId == 0) return null; - return _productVariantAttributeValueRepository.GetByIdCached(productVariantAttributeValueId, "db.prodvarattrval.id-" + productVariantAttributeValueId); - } + return _productVariantAttributeValueRepository.GetByIdCached(productVariantAttributeValueId, "db.prodvarattrval.id-" + productVariantAttributeValueId); + } public virtual void InsertProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) { if (productVariantAttributeValue == null) throw new ArgumentNullException("productVariantAttributeValue"); - _productVariantAttributeValueRepository.Insert(productVariantAttributeValue); + _productVariantAttributeValueRepository.Insert(productVariantAttributeValue); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } public virtual void UpdateProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) @@ -551,31 +551,31 @@ public virtual void UpdateProductVariantAttributeValue(ProductVariantAttributeVa if (productVariantAttributeValue == null) throw new ArgumentNullException("productVariantAttributeValue"); - _productVariantAttributeValueRepository.Update(productVariantAttributeValue); + _productVariantAttributeValueRepository.Update(productVariantAttributeValue); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); - _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); + _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); } #endregion #region Product variant attribute combinations (ProductVariantAttributeCombination) - - private void CombineAll(List> toCombine, List> result, int y, List tmp) - { - var combine = toCombine[y]; - - for (int i = 0; i < combine.Count; ++i) - { - var lst = new List(tmp); - lst.Add(combine[i]); - - if (y == (toCombine.Count - 1)) - result.Add(lst); - else - CombineAll(toCombine, result, y + 1, lst); - } - } + + private void CombineAll(List> toCombine, List> result, int y, List tmp) + { + var combine = toCombine[y]; + + for (int i = 0; i < combine.Count; ++i) + { + var lst = new List(tmp); + lst.Add(combine[i]); + + if (y == (toCombine.Count - 1)) + result.Add(lst); + else + CombineAll(toCombine, result, y + 1, lst); + } + } public virtual void DeleteProductVariantAttributeCombination(ProductVariantAttributeCombination combination) { @@ -585,116 +585,116 @@ public virtual void DeleteProductVariantAttributeCombination(ProductVariantAttri _pvacRepository.Delete(combination); } - public virtual IPagedList GetAllProductVariantAttributeCombinations( - int productId, - int pageIndex, - int pageSize, - bool untracked = true) - { - if (productId == 0) - { - return new PagedList(new List(), pageIndex, pageSize); - } - - string key = string.Format(PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY, productId, 0, int.MaxValue); - return _requestCache.Get(key, () => - { - var query = from pvac in (untracked ? _pvacRepository.TableUntracked : _pvacRepository.Table) - orderby pvac.Id - where pvac.ProductId == productId - select pvac; - - var combinations = new PagedList(query, pageIndex, pageSize); - return combinations; - }); - } - - public virtual IList GetAllProductVariantAttributeCombinationPictureIds(int productId) - { - var pictureIds = new List(); - - if (productId == 0) - return pictureIds; - - var query = from pvac in _pvacRepository.TableUntracked - where - pvac.ProductId == productId - && pvac.IsActive - && !String.IsNullOrEmpty(pvac.AssignedMediaFileIds) - select pvac.AssignedMediaFileIds; - - var data = query.ToList(); - if (data.Any()) - { - int id; - var ids = string.Join(",", data).SplitSafe(",").Distinct(); - - foreach (string str in ids) - { - if (int.TryParse(str, out id) && !pictureIds.Exists(i => i == id)) - pictureIds.Add(id); - } - } - - return pictureIds; - } - - public virtual Multimap GetProductVariantAttributeCombinations(int[] productIds) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual IPagedList GetAllProductVariantAttributeCombinations( + int productId, + int pageIndex, + int pageSize, + bool untracked = true) + { + if (productId == 0) + { + return new PagedList(new List(), pageIndex, pageSize); + } + + string key = string.Format(PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY, productId, 0, int.MaxValue); + return _requestCache.Get(key, () => + { + var query = from pvac in (untracked ? _pvacRepository.TableUntracked : _pvacRepository.Table) + orderby pvac.Id + where pvac.ProductId == productId + select pvac; + + var combinations = new PagedList(query, pageIndex, pageSize); + return combinations; + }); + } + + public virtual IList GetAllProductVariantAttributeCombinationPictureIds(int productId) + { + var pictureIds = new List(); + + if (productId == 0) + return pictureIds; + + var query = from pvac in _pvacRepository.TableUntracked + where + pvac.ProductId == productId + && pvac.IsActive + && !String.IsNullOrEmpty(pvac.AssignedMediaFileIds) + select pvac.AssignedMediaFileIds; + + var data = query.ToList(); + if (data.Any()) + { + int id; + var ids = string.Join(",", data).SplitSafe(",").Distinct(); + + foreach (string str in ids) + { + if (int.TryParse(str, out id) && !pictureIds.Exists(i => i == id)) + pictureIds.Add(id); + } + } + + return pictureIds; + } + + public virtual Multimap GetProductVariantAttributeCombinations(int[] productIds) + { + Guard.NotNull(productIds, nameof(productIds)); if (!productIds.Any()) { return new Multimap(); } - var query = - from pvac in _pvacRepository.TableUntracked - where productIds.Contains(pvac.ProductId) - select pvac; + var query = + from pvac in _pvacRepository.TableUntracked + where productIds.Contains(pvac.ProductId) + select pvac; - var map = query - .OrderBy(x => x.ProductId) - .ToList() - .ToMultimap(x => x.ProductId, x => x); + var map = query + .OrderBy(x => x.ProductId) + .ToList() + .ToMultimap(x => x.ProductId, x => x); - return map; - } + return map; + } - public virtual decimal? GetLowestCombinationPrice(int productId) - { - if (productId == 0) - return null; + public virtual decimal? GetLowestCombinationPrice(int productId) + { + if (productId == 0) + return null; - var query = - from pvac in _pvacRepository.Table - where pvac.ProductId == productId && pvac.Price != null && pvac.IsActive - orderby pvac.Price ascending - select pvac.Price; + var query = + from pvac in _pvacRepository.Table + where pvac.ProductId == productId && pvac.Price != null && pvac.IsActive + orderby pvac.Price ascending + select pvac.Price; - var price = query.FirstOrDefault(); - return price; - } + var price = query.FirstOrDefault(); + return price; + } public virtual ProductVariantAttributeCombination GetProductVariantAttributeCombinationById(int productVariantAttributeCombinationId) { if (productVariantAttributeCombinationId == 0) return null; - + var combination = _pvacRepository.GetById(productVariantAttributeCombinationId); return combination; } - public virtual ProductVariantAttributeCombination GetProductVariantAttributeCombinationBySku(string sku) - { + public virtual ProductVariantAttributeCombination GetProductVariantAttributeCombinationBySku(string sku) + { if (sku.IsEmpty()) { return null; } - var combination = _pvacRepository.Table.FirstOrDefault(x => x.Sku == sku && !x.Product.Deleted); - return combination; - } + var combination = _pvacRepository.Table.FirstOrDefault(x => x.Sku == sku && !x.Product.Deleted); + return combination; + } public virtual ProductVariantAttributeCombination GetAttributeCombinationByGtin(string gtin) { @@ -732,180 +732,180 @@ public virtual void UpdateProductVariantAttributeCombination(ProductVariantAttri if (combination == null) throw new ArgumentNullException("combination"); - //if (combination.IsDefaultCombination) - //{ - // EnsureSingleDefaultVariant(combination); - //} - //else - //{ - // // check if it was default before modification... - // // but make it Type-Safe (resistant to code refactoring ;-)) - // Expression> expr = x => x.IsDefaultCombination; - // string propertyToCheck = expr.ExtractPropertyInfo().Name; - - // object originalValue = null; - // if (_productVariantAttributeCombinationRepository.GetModifiedProperties(combination).TryGetValue(propertyToCheck, out originalValue)) - // { - // bool wasDefault = (bool)originalValue; - // if (wasDefault) - // { - // // we can't uncheck the default variant within a combination list, - // // we would't have a default combination anymore. - // combination.IsDefaultCombination = true; - // } - // } - //} + //if (combination.IsDefaultCombination) + //{ + // EnsureSingleDefaultVariant(combination); + //} + //else + //{ + // // check if it was default before modification... + // // but make it Type-Safe (resistant to code refactoring ;-)) + // Expression> expr = x => x.IsDefaultCombination; + // string propertyToCheck = expr.ExtractPropertyInfo().Name; + + // object originalValue = null; + // if (_productVariantAttributeCombinationRepository.GetModifiedProperties(combination).TryGetValue(propertyToCheck, out originalValue)) + // { + // bool wasDefault = (bool)originalValue; + // if (wasDefault) + // { + // // we can't uncheck the default variant within a combination list, + // // we would't have a default combination anymore. + // combination.IsDefaultCombination = true; + // } + // } + //} _pvacRepository.Update(combination); } - public virtual void CreateAllProductVariantAttributeCombinations(Product product) - { - // Delete all existing combinations. - _pvacRepository.DeleteAll(x => x.ProductId == product.Id); - - var attributes = GetProductVariantAttributesByProductId(product.Id); - if (attributes == null || attributes.Count <= 0) - return; - - var mappedAttributes = attributes - .SelectMany(x => x.ProductVariantAttributeValues) - .ToDictionarySafe(x => x.Id, x => x.ProductVariantAttribute); - - var toCombine = new List>(); - var resultMatrix = new List>(); - var tmp = new List(); - - foreach (var attr in attributes) - { - var attributeValues = attr.ProductVariantAttributeValues.ToList(); - if (attributeValues.Count > 0) - toCombine.Add(attributeValues); - } - - if (toCombine.Count > 0) - { - CombineAll(toCombine, resultMatrix, 0, tmp); - - using (var scope = new DbContextScope(ctx: _pvacRepository.Context, autoCommit: false, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) - { - ProductVariantAttributeCombination combination = null; - var idx = 0; - - foreach (var values in resultMatrix) - { - idx++; - var attributesXml = ""; - - foreach (var value in values) - { - attributesXml = mappedAttributes[value.Id].AddProductAttribute(attributesXml, value.Id.ToString()); - } - - combination = new ProductVariantAttributeCombination - { - ProductId = product.Id, - AttributesXml = attributesXml, - StockQuantity = 10000, - AllowOutOfStockOrders = true, - IsActive = true - }; - - _pvacRepository.Insert(combination); - } - - scope.Commit(); - } - } - - //foreach (var y in resultMatrix) - //{ - // var sb = new System.Text.StringBuilder(); - // foreach (var x in y) - // { - // sb.AppendFormat("{0} ", x.Name); - // } - // sb.ToString().Dump(); - //} - } - - public virtual bool VariantHasAttributeCombinations(int productId) - { - if (productId == 0) - return false; - - var query = - from c in _pvacRepository.Table - where c.ProductId == productId - select c; - - return query.Select(x => x.Id).Any(); - } + public virtual void CreateAllProductVariantAttributeCombinations(Product product) + { + // Delete all existing combinations. + _pvacRepository.DeleteAll(x => x.ProductId == product.Id); + + var attributes = GetProductVariantAttributesByProductId(product.Id); + if (attributes == null || attributes.Count <= 0) + return; + + var mappedAttributes = attributes + .SelectMany(x => x.ProductVariantAttributeValues) + .ToDictionarySafe(x => x.Id, x => x.ProductVariantAttribute); + + var toCombine = new List>(); + var resultMatrix = new List>(); + var tmp = new List(); + + foreach (var attr in attributes) + { + var attributeValues = attr.ProductVariantAttributeValues.ToList(); + if (attributeValues.Count > 0) + toCombine.Add(attributeValues); + } + + if (toCombine.Count > 0) + { + CombineAll(toCombine, resultMatrix, 0, tmp); + + using (var scope = new DbContextScope(ctx: _pvacRepository.Context, autoCommit: false, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) + { + ProductVariantAttributeCombination combination = null; + var idx = 0; + + foreach (var values in resultMatrix) + { + idx++; + var attributesXml = ""; + + foreach (var value in values) + { + attributesXml = mappedAttributes[value.Id].AddProductAttribute(attributesXml, value.Id.ToString()); + } + + combination = new ProductVariantAttributeCombination + { + ProductId = product.Id, + AttributesXml = attributesXml, + StockQuantity = 10000, + AllowOutOfStockOrders = true, + IsActive = true + }; + + _pvacRepository.Insert(combination); + } + + scope.Commit(); + } + } + + //foreach (var y in resultMatrix) + //{ + // var sb = new System.Text.StringBuilder(); + // foreach (var x in y) + // { + // sb.AppendFormat("{0} ", x.Name); + // } + // sb.ToString().Dump(); + //} + } + + public virtual bool VariantHasAttributeCombinations(int productId) + { + if (productId == 0) + return false; + + var query = + from c in _pvacRepository.Table + where c.ProductId == productId + select c; + + return query.Select(x => x.Id).Any(); + } #endregion - #region Product bundle item attribute filter - - public virtual void InsertProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) - { - if (attributeFilter == null) - throw new ArgumentNullException("attributeFilter"); - - if (attributeFilter.AttributeId != 0 && attributeFilter.AttributeValueId != 0) - { - _productBundleItemAttributeFilterRepository.Insert(attributeFilter); - } - } - - public virtual void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) - { - if (attributeFilter == null) - throw new ArgumentNullException("attributeFilter"); - - _productBundleItemAttributeFilterRepository.Update(attributeFilter); - } - - public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) - { - if (attributeFilter == null) - throw new ArgumentNullException("attributeFilter"); - - _productBundleItemAttributeFilterRepository.Delete(attributeFilter); - } - - public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItem bundleItem) - { - if (bundleItem != null && bundleItem.Id != 0) - { - var attributeFilterQuery = - from x in _productBundleItemAttributeFilterRepository.Table - where x.BundleItemId == bundleItem.Id - select x; - - attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); - } - } - - public virtual void DeleteProductBundleItemAttributeFilter(int attributeId, int attributeValueId) - { - var attributeFilterQuery = - from x in _productBundleItemAttributeFilterRepository.Table - where x.AttributeId == attributeId && x.AttributeValueId == attributeValueId - select x; - - attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); - } - - public virtual void DeleteProductBundleItemAttributeFilter(int attributeId) - { - var attributeFilterQuery = - from x in _productBundleItemAttributeFilterRepository.Table - where x.AttributeId == attributeId - select x; - - attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); - } - - #endregion - } + #region Product bundle item attribute filter + + public virtual void InsertProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) + { + if (attributeFilter == null) + throw new ArgumentNullException("attributeFilter"); + + if (attributeFilter.AttributeId != 0 && attributeFilter.AttributeValueId != 0) + { + _productBundleItemAttributeFilterRepository.Insert(attributeFilter); + } + } + + public virtual void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) + { + if (attributeFilter == null) + throw new ArgumentNullException("attributeFilter"); + + _productBundleItemAttributeFilterRepository.Update(attributeFilter); + } + + public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) + { + if (attributeFilter == null) + throw new ArgumentNullException("attributeFilter"); + + _productBundleItemAttributeFilterRepository.Delete(attributeFilter); + } + + public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItem bundleItem) + { + if (bundleItem != null && bundleItem.Id != 0) + { + var attributeFilterQuery = + from x in _productBundleItemAttributeFilterRepository.Table + where x.BundleItemId == bundleItem.Id + select x; + + attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); + } + } + + public virtual void DeleteProductBundleItemAttributeFilter(int attributeId, int attributeValueId) + { + var attributeFilterQuery = + from x in _productBundleItemAttributeFilterRepository.Table + where x.AttributeId == attributeId && x.AttributeValueId == attributeValueId + select x; + + attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); + } + + public virtual void DeleteProductBundleItemAttributeFilter(int attributeId) + { + var attributeFilterQuery = + from x in _productBundleItemAttributeFilterRepository.Table + where x.AttributeId == attributeId + select x; + + attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index f57d755022..8ecf7912c0 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -21,15 +21,15 @@ public static ProductVariantAttributeCombination MergeWithCombination(this Produ return product.MergeWithCombination(selectedAttributes, EngineContext.Current.Resolve()); } - public static ProductVariantAttributeCombination MergeWithCombination(this Product product, string selectedAttributes, IProductAttributeParser productAttributeParser) + public static ProductVariantAttributeCombination MergeWithCombination(this Product product, string selectedAttributes, IProductAttributeParser productAttributeParser) { Guard.NotNull(productAttributeParser, "productAttributeParser"); - if (selectedAttributes.IsEmpty()) - return null; + if (selectedAttributes.IsEmpty()) + return null; - // Let's find appropriate record. - var combination = productAttributeParser.FindProductVariantAttributeCombination(product.Id, selectedAttributes); + // Let's find appropriate record. + var combination = productAttributeParser.FindProductVariantAttributeCombination(product.Id, selectedAttributes); if (combination != null && combination.IsActive) { @@ -40,93 +40,93 @@ public static ProductVariantAttributeCombination MergeWithCombination(this Produ product.MergedDataValues.Clear(); } - return combination; + return combination; } - public static void MergeWithCombination(this Product product, ProductVariantAttributeCombination combination) - { - Guard.NotNull(product, "product"); - - var values = product.MergedDataValues; - - if (values != null) - values.Clear(); - - if (combination == null) - return; - - if (values == null) - product.MergedDataValues = values = new Dictionary(); - - if (ManageInventoryMethod.ManageStockByAttributes == (ManageInventoryMethod)product.ManageInventoryMethodId) - { - values.Add("StockQuantity", combination.StockQuantity); - values.Add("BackorderModeId", combination.AllowOutOfStockOrders ? (int)BackorderMode.AllowQtyBelow0 : (int)BackorderMode.NoBackorders); - } - - if (combination.Sku.HasValue()) - values.Add("Sku", combination.Sku); - if (combination.Gtin.HasValue()) - values.Add("Gtin", combination.Gtin); - if (combination.ManufacturerPartNumber.HasValue()) - values.Add("ManufacturerPartNumber", combination.ManufacturerPartNumber); - - if (combination.Price.HasValue) - values.Add("Price", combination.Price.Value); - - if (combination.DeliveryTimeId.HasValue && combination.DeliveryTimeId.Value > 0) - values.Add("DeliveryTimeId", combination.DeliveryTimeId); - - if (combination.QuantityUnitId.HasValue && combination.QuantityUnitId.Value > 0) - values.Add("QuantityUnitId", combination.QuantityUnitId); - - if (combination.Length.HasValue) - values.Add("Length", combination.Length.Value); - if (combination.Width.HasValue) - values.Add("Width", combination.Width.Value); - if (combination.Height.HasValue) - values.Add("Height", combination.Height.Value); - - if (combination.BasePriceAmount.HasValue) - values.Add("BasePriceAmount", combination.BasePriceAmount); - if (combination.BasePriceBaseAmount.HasValue) - values.Add("BasePriceBaseAmount", combination.BasePriceBaseAmount); - } - - public static IList GetAllCombinationPictureIds(this IEnumerable combinations) - { - var pictureIds = new List(); - - if (combinations != null) - { - var data = combinations - .Where(x => x.IsActive && x.AssignedMediaFileIds != null) - .Select(x => x.AssignedMediaFileIds) - .ToList(); - - if (data.Count > 0) - { - var ids = string.Join(",", data).SplitSafe(",").Distinct(); - - foreach (string str in ids) - { - if (int.TryParse(str, out var id) && !pictureIds.Exists(i => i == id)) - pictureIds.Add(id); - } - } - } - - return pictureIds; - } - - /// - /// Finds a related product item by specified identifiers - /// - /// Source - /// The first product identifier - /// The second product identifier - /// Related product - public static RelatedProduct FindRelatedProduct(this IList source, + public static void MergeWithCombination(this Product product, ProductVariantAttributeCombination combination) + { + Guard.NotNull(product, "product"); + + var values = product.MergedDataValues; + + if (values != null) + values.Clear(); + + if (combination == null) + return; + + if (values == null) + product.MergedDataValues = values = new Dictionary(); + + if (ManageInventoryMethod.ManageStockByAttributes == (ManageInventoryMethod)product.ManageInventoryMethodId) + { + values.Add("StockQuantity", combination.StockQuantity); + values.Add("BackorderModeId", combination.AllowOutOfStockOrders ? (int)BackorderMode.AllowQtyBelow0 : (int)BackorderMode.NoBackorders); + } + + if (combination.Sku.HasValue()) + values.Add("Sku", combination.Sku); + if (combination.Gtin.HasValue()) + values.Add("Gtin", combination.Gtin); + if (combination.ManufacturerPartNumber.HasValue()) + values.Add("ManufacturerPartNumber", combination.ManufacturerPartNumber); + + if (combination.Price.HasValue) + values.Add("Price", combination.Price.Value); + + if (combination.DeliveryTimeId.HasValue && combination.DeliveryTimeId.Value > 0) + values.Add("DeliveryTimeId", combination.DeliveryTimeId); + + if (combination.QuantityUnitId.HasValue && combination.QuantityUnitId.Value > 0) + values.Add("QuantityUnitId", combination.QuantityUnitId); + + if (combination.Length.HasValue) + values.Add("Length", combination.Length.Value); + if (combination.Width.HasValue) + values.Add("Width", combination.Width.Value); + if (combination.Height.HasValue) + values.Add("Height", combination.Height.Value); + + if (combination.BasePriceAmount.HasValue) + values.Add("BasePriceAmount", combination.BasePriceAmount); + if (combination.BasePriceBaseAmount.HasValue) + values.Add("BasePriceBaseAmount", combination.BasePriceBaseAmount); + } + + public static IList GetAllCombinationPictureIds(this IEnumerable combinations) + { + var pictureIds = new List(); + + if (combinations != null) + { + var data = combinations + .Where(x => x.IsActive && x.AssignedMediaFileIds != null) + .Select(x => x.AssignedMediaFileIds) + .ToList(); + + if (data.Count > 0) + { + var ids = string.Join(",", data).SplitSafe(",").Distinct(); + + foreach (string str in ids) + { + if (int.TryParse(str, out var id) && !pictureIds.Exists(i => i == id)) + pictureIds.Add(id); + } + } + } + + return pictureIds; + } + + /// + /// Finds a related product item by specified identifiers + /// + /// Source + /// The first product identifier + /// The second product identifier + /// Related product + public static RelatedProduct FindRelatedProduct(this IList source, int productId1, int productId2) { foreach (RelatedProduct relatedProduct in source) @@ -154,20 +154,20 @@ public static CrossSellProduct FindCrossSellProduct(this IList return null; } - public static bool IsAvailableByStock(this Product product) - { - Guard.NotNull(product, nameof(product)); + public static bool IsAvailableByStock(this Product product) + { + Guard.NotNull(product, nameof(product)); - if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock || product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) - { + if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock || product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) + { if (product.StockQuantity <= 0 && product.BackorderMode == BackorderMode.NoBackorders) { return false; } - } + } - return true; - } + return true; + } /// /// Formats the stock availability/quantity message @@ -177,28 +177,28 @@ public static bool IsAvailableByStock(this Product product) /// The stock message public static string FormatStockMessage(this Product product, ILocalizationService localizationService) { - Guard.NotNull(product, nameof(product)); - Guard.NotNull(localizationService, nameof(localizationService)); + Guard.NotNull(product, nameof(product)); + Guard.NotNull(localizationService, nameof(localizationService)); - string stockMessage = string.Empty; + string stockMessage = string.Empty; if ((product.ManageInventoryMethod == ManageInventoryMethod.ManageStock || product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) && product.DisplayStockAvailability) { - if (product.StockQuantity > 0) - { - if (product.DisplayStockQuantity) - stockMessage = string.Format(localizationService.GetResource("Products.Availability.InStockWithQuantity"), product.StockQuantity); - else - stockMessage = localizationService.GetResource("Products.Availability.InStock"); - } - else - { - if (product.BackorderMode == BackorderMode.NoBackorders || product.BackorderMode == BackorderMode.AllowQtyBelow0) - stockMessage = localizationService.GetResource("Products.Availability.OutOfStock"); - else if (product.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) - stockMessage = localizationService.GetResource("Products.Availability.Backordering"); - } + if (product.StockQuantity > 0) + { + if (product.DisplayStockQuantity) + stockMessage = string.Format(localizationService.GetResource("Products.Availability.InStockWithQuantity"), product.StockQuantity); + else + stockMessage = localizationService.GetResource("Products.Availability.InStock"); + } + else + { + if (product.BackorderMode == BackorderMode.NoBackorders || product.BackorderMode == BackorderMode.AllowQtyBelow0) + stockMessage = localizationService.GetResource("Products.Availability.OutOfStock"); + else if (product.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) + stockMessage = localizationService.GetResource("Products.Availability.Backordering"); + } } return stockMessage; @@ -206,16 +206,16 @@ public static string FormatStockMessage(this Product product, ILocalizationServi public static bool DisplayDeliveryTimeAccordingToStock(this Product product, CatalogSettings catalogSettings) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); Guard.NotNull(catalogSettings, nameof(catalogSettings)); if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock || product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) - { - if (catalogSettings.DeliveryTimeIdForEmptyStock.HasValue && product.StockQuantity <= 0) - return true; + { + if (catalogSettings.DeliveryTimeIdForEmptyStock.HasValue && product.StockQuantity <= 0) + return true; - return (product.StockQuantity > 0); - } + return (product.StockQuantity > 0); + } return true; } @@ -246,21 +246,21 @@ public static bool DisplayDeliveryTimeAccordingToStock(this Product product, Cat /// Catalog settings /// Whether the product is labeled as NEW public static bool IsNew(this Product product, CatalogSettings catalogSettings) - { - if (catalogSettings.LabelAsNewForMaxDays.HasValue) - { - return ((DateTime.UtcNow - product.CreatedOnUtc).Days <= catalogSettings.LabelAsNewForMaxDays.Value); - } + { + if (catalogSettings.LabelAsNewForMaxDays.HasValue) + { + return ((DateTime.UtcNow - product.CreatedOnUtc).Days <= catalogSettings.LabelAsNewForMaxDays.Value); + } - return false; - } + return false; + } - public static bool ProductTagExists(this Product product, int productTagId) + public static bool ProductTagExists(this Product product, int productTagId) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); - var result = product.ProductTags.Any(x => x.Id == productTagId); - return result; + var result = product.ProductTags.Any(x => x.Id == productTagId); + return result; } /// @@ -270,9 +270,9 @@ public static bool ProductTagExists(this Product product, int productTagId) /// Result public static int[] ParseAllowedQuatities(this Product product) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); - var result = new List(); + var result = new List(); if (!String.IsNullOrWhiteSpace(product.AllowedQuantities)) { product @@ -291,25 +291,25 @@ public static int[] ParseAllowedQuatities(this Product product) return result.ToArray(); } - public static int[] ParseRequiredProductIds(this Product product) - { - Guard.NotNull(product, nameof(product)); + public static int[] ParseRequiredProductIds(this Product product) + { + Guard.NotNull(product, nameof(product)); - if (String.IsNullOrEmpty(product.RequiredProductIds)) - return new int[0]; + if (String.IsNullOrEmpty(product.RequiredProductIds)) + return new int[0]; - var ids = new List(); + var ids = new List(); - foreach (var idStr in product.RequiredProductIds - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim())) - { - if (int.TryParse(idStr, out var id)) - ids.Add(id); - } + foreach (var idStr in product.RequiredProductIds + .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim())) + { + if (int.TryParse(idStr, out var id)) + ids.Add(id); + } - return ids.ToArray(); - } + return ids.ToArray(); + } /// /// Gets the base price info @@ -325,21 +325,21 @@ public static int[] ParseRequiredProductIds(this Product product) /// Price adjustment /// The base price info public static string GetBasePriceInfo(this Product product, - ILocalizationService localizationService, - IPriceFormatter priceFormatter, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, ICurrencyService currencyService, - ITaxService taxService, - IPriceCalculationService priceCalculationService, - Customer customer, + ITaxService taxService, + IPriceCalculationService priceCalculationService, + Customer customer, Currency currency, - decimal priceAdjustment = decimal.Zero) + decimal priceAdjustment = decimal.Zero) { - Guard.NotNull(product, nameof(product)); - Guard.NotNull(currencyService, nameof(currencyService)); - Guard.NotNull(taxService, nameof(taxService)); - Guard.NotNull(priceCalculationService, nameof(priceCalculationService)); - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(currency, nameof(currency)); + Guard.NotNull(product, nameof(product)); + Guard.NotNull(currencyService, nameof(currencyService)); + Guard.NotNull(taxService, nameof(taxService)); + Guard.NotNull(priceCalculationService, nameof(priceCalculationService)); + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(currency, nameof(currency)); if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) { @@ -347,67 +347,67 @@ public static string GetBasePriceInfo(this Product product, var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), customer, currency, out var taxrate); price = currencyService.ConvertFromPrimaryStoreCurrency(price, currency); - return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency); - } + return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency); + } return string.Empty; } - /// - /// Gets the base price info - /// - /// Product - /// The calculated product price - /// Localization service - /// Price formatter - /// Target currency - /// The base price info - public static string GetBasePriceInfo(this Product product, - decimal productPrice, - ILocalizationService localizationService, - IPriceFormatter priceFormatter, - Currency currency) - { - Guard.NotNull(product, nameof(product)); - Guard.NotNull(localizationService, nameof(localizationService)); - Guard.NotNull(priceFormatter, nameof(priceFormatter)); - Guard.NotNull(currency, nameof(currency)); - - if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) - { - var value = Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); - var valueFormatted = priceFormatter.FormatPrice(value, true, currency); - var amountFormatted = Math.Round(product.BasePriceAmount.Value, 2).ToString("G29"); - var infoTemplate = localizationService.GetResource("Products.BasePriceInfo"); - - var result = infoTemplate.FormatInvariant( - amountFormatted, - product.BasePriceMeasureUnit, - valueFormatted, - product.BasePriceBaseAmount - ); - - return result; - } - - return string.Empty; - } - - public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) - { - if (product != null && product.ProductType != ProductType.SimpleProduct) - { - var key = "Admin.Catalog.Products.ProductType.{0}.Label".FormatInvariant(product.ProductType.ToString()); - return localizationService.GetResource(key); - } - - return string.Empty; - } - - public static bool CanBeBundleItem(this Product product) - { - return product != null && product.ProductType == ProductType.SimpleProduct && !product.IsRecurring && !product.IsDownload; - } + /// + /// Gets the base price info + /// + /// Product + /// The calculated product price + /// Localization service + /// Price formatter + /// Target currency + /// The base price info + public static string GetBasePriceInfo(this Product product, + decimal productPrice, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, + Currency currency) + { + Guard.NotNull(product, nameof(product)); + Guard.NotNull(localizationService, nameof(localizationService)); + Guard.NotNull(priceFormatter, nameof(priceFormatter)); + Guard.NotNull(currency, nameof(currency)); + + if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) + { + var value = Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); + var valueFormatted = priceFormatter.FormatPrice(value, true, currency); + var amountFormatted = product.BasePriceAmount.Value.ToString("G29"); + var infoTemplate = localizationService.GetResource("Products.BasePriceInfo"); + + var result = infoTemplate.FormatInvariant( + amountFormatted, + product.BasePriceMeasureUnit, + valueFormatted, + product.BasePriceBaseAmount + ); + + return result; + } + + return string.Empty; + } + + public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) + { + if (product != null && product.ProductType != ProductType.SimpleProduct) + { + var key = "Admin.Catalog.Products.ProductType.{0}.Label".FormatInvariant(product.ProductType.ToString()); + return localizationService.GetResource(key); + } + + return string.Empty; + } + + public static bool CanBeBundleItem(this Product product) + { + return product != null && product.ProductType == ProductType.SimpleProduct && !product.IsRecurring && !product.IsDownload; + } public static bool FilterOut(this ProductBundleItem bundleItem, ProductVariantAttributeValue value, out ProductBundleItemAttributeFilter filter) { @@ -422,61 +422,61 @@ public static bool FilterOut(this ProductBundleItem bundleItem, ProductVariantAt } public static string GetLocalizedName(this ProductBundleItem bundleItem) - { - if (bundleItem != null) - { - string name = bundleItem.GetLocalized(x => x.Name); - return (name.HasValue() ? name : bundleItem.Product.GetLocalized(x => x.Name)); - } - return null; - } - public static string GetLocalizedName(this ProductBundleItem bundleItem, int languageId) - { - if (bundleItem != null) - { - string name = bundleItem.GetLocalized(x => x.Name, languageId); - return (name.HasValue() ? name : bundleItem.Product.GetLocalized(x => x.Name, languageId)); - } - return null; - } - - public static ProductBundleItemOrderData ToOrderData(this ProductBundleItemData bundleItemData, decimal priceWithDiscount = decimal.Zero, - string attributesXml = null, string attributesInfo = null) - { + { + if (bundleItem != null) + { + string name = bundleItem.GetLocalized(x => x.Name); + return (name.HasValue() ? name : bundleItem.Product.GetLocalized(x => x.Name)); + } + return null; + } + public static string GetLocalizedName(this ProductBundleItem bundleItem, int languageId) + { + if (bundleItem != null) + { + string name = bundleItem.GetLocalized(x => x.Name, languageId); + return (name.HasValue() ? name : bundleItem.Product.GetLocalized(x => x.Name, languageId)); + } + return null; + } + + public static ProductBundleItemOrderData ToOrderData(this ProductBundleItemData bundleItemData, decimal priceWithDiscount = decimal.Zero, + string attributesXml = null, string attributesInfo = null) + { if (bundleItemData == null || bundleItemData.Item == null) { return null; } - var item = bundleItemData.Item; - string bundleItemName = item.GetLocalized(x => x.Name); - - var bundleData = new ProductBundleItemOrderData() - { - BundleItemId = item.Id, - ProductId = item.ProductId, - Sku = item.Product.Sku, - ProductName = bundleItemName ?? item.Product.GetLocalized(x => x.Name), - ProductSeName = item.Product.GetSeName(), - VisibleIndividually = item.Product.Visibility != ProductVisibility.Hidden, - Quantity = item.Quantity, - DisplayOrder = item.DisplayOrder, - PriceWithDiscount = priceWithDiscount, - AttributesXml = attributesXml, - AttributesInfo = attributesInfo, - PerItemShoppingCart = item.BundleProduct.BundlePerItemShoppingCart - }; - - return bundleData; - } - public static void ToOrderData(this ProductBundleItemData bundleItemData, IList bundleData, decimal priceWithDiscount = decimal.Zero, - string attributesXml = null, string attributesInfo = null) - { - var item = bundleItemData.ToOrderData(priceWithDiscount, attributesXml, attributesInfo); - - if (item != null && item.ProductId != 0 && item.BundleItemId != 0) - bundleData.Add(item); - } + var item = bundleItemData.Item; + string bundleItemName = item.GetLocalized(x => x.Name); + + var bundleData = new ProductBundleItemOrderData() + { + BundleItemId = item.Id, + ProductId = item.ProductId, + Sku = item.Product.Sku, + ProductName = bundleItemName ?? item.Product.GetLocalized(x => x.Name), + ProductSeName = item.Product.GetSeName(), + VisibleIndividually = item.Product.Visibility != ProductVisibility.Hidden, + Quantity = item.Quantity, + DisplayOrder = item.DisplayOrder, + PriceWithDiscount = priceWithDiscount, + AttributesXml = attributesXml, + AttributesInfo = attributesInfo, + PerItemShoppingCart = item.BundleProduct.BundlePerItemShoppingCart + }; + + return bundleData; + } + public static void ToOrderData(this ProductBundleItemData bundleItemData, IList bundleData, decimal priceWithDiscount = decimal.Zero, + string attributesXml = null, string attributesInfo = null) + { + var item = bundleItemData.ToOrderData(priceWithDiscount, attributesXml, attributesInfo); + + if (item != null && item.ProductId != 0 && item.BundleItemId != 0) + bundleData.Add(item); + } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index 4b6c14d9bc..622f9c56c6 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -18,20 +18,20 @@ namespace SmartStore.Services.Catalog { public partial class ProductService : IProductService - { - private readonly IRepository _productRepository; + { + private readonly IRepository _productRepository; private readonly IRepository _relatedProductRepository; private readonly IRepository _crossSellProductRepository; private readonly IRepository _tierPriceRepository; private readonly IRepository _productFileRepository; private readonly IRepository _productVariantAttributeCombinationRepository; - private readonly IRepository _productBundleItemRepository; - private readonly IRepository _shoppingCartItemRepository; - private readonly IProductAttributeService _productAttributeService; + private readonly IRepository _productBundleItemRepository; + private readonly IRepository _shoppingCartItemRepository; + private readonly IProductAttributeService _productAttributeService; private readonly IProductAttributeParser _productAttributeParser; private readonly IDbContext _dbContext; private readonly LocalizationSettings _localizationSettings; - private readonly ICommonServices _services; + private readonly ICommonServices _services; public ProductService( IRepository productRepository, @@ -40,13 +40,13 @@ public ProductService( IRepository tierPriceRepository, IRepository productPictureRepository, IRepository productVariantAttributeCombinationRepository, - IRepository productBundleItemRepository, - IRepository shoppingCartItemRepository, - IProductAttributeService productAttributeService, + IRepository productBundleItemRepository, + IRepository shoppingCartItemRepository, + IProductAttributeService productAttributeService, IProductAttributeParser productAttributeParser, - IDbContext dbContext, + IDbContext dbContext, LocalizationSettings localizationSettings, - ICommonServices services) + ICommonServices services) { _productRepository = productRepository; _relatedProductRepository = relatedProductRepository; @@ -54,155 +54,155 @@ public ProductService( _tierPriceRepository = tierPriceRepository; _productFileRepository = productPictureRepository; _productVariantAttributeCombinationRepository = productVariantAttributeCombinationRepository; - _productBundleItemRepository = productBundleItemRepository; - _shoppingCartItemRepository = shoppingCartItemRepository; + _productBundleItemRepository = productBundleItemRepository; + _shoppingCartItemRepository = shoppingCartItemRepository; _productAttributeService = productAttributeService; _productAttributeParser = productAttributeParser; _dbContext = dbContext; _localizationSettings = localizationSettings; - _services = services; + _services = services; } - #region Utilities - - protected virtual int EnsureMutuallyRelatedProducts(List productIds) - { - int count = 0; - - foreach (int id1 in productIds) - { - var mutualAssociations = ( - from rp in _relatedProductRepository.Table - join p in _productRepository.Table on rp.ProductId2 equals p.Id - where rp.ProductId2 == id1 && !p.Deleted - select rp).ToList(); - - foreach (int id2 in productIds) - { - if (id1 == id2) - continue; - - if (!mutualAssociations.Any(x => x.ProductId1 == id2)) - { - int maxDisplayOrder = _relatedProductRepository.TableUntracked - .Where(x => x.ProductId1 == id2) - .OrderByDescending(x => x.DisplayOrder) - .Select(x => x.DisplayOrder) - .FirstOrDefault(); - - var newRelatedProduct = new RelatedProduct - { - ProductId1 = id2, - ProductId2 = id1, - DisplayOrder = maxDisplayOrder + 1 - }; - - InsertRelatedProduct(newRelatedProduct); - ++count; - } - } - } - - return count; - } - - protected virtual int EnsureMutuallyCrossSellProducts(List productIds) - { - int count = 0; - - foreach (int id1 in productIds) - { - var mutualAssociations = ( - from rp in _crossSellProductRepository.Table - join p in _productRepository.Table on rp.ProductId2 equals p.Id - where rp.ProductId2 == id1 && !p.Deleted - select rp).ToList(); - - foreach (int id2 in productIds) - { - if (id1 == id2) - continue; - - if (!mutualAssociations.Any(x => x.ProductId1 == id2)) - { - var newCrossSellProduct = new CrossSellProduct - { - ProductId1 = id2, - ProductId2 = id1 - }; - - InsertCrossSellProduct(newCrossSellProduct); - ++count; - } - } - } - - return count; - } - - #endregion - - #region Products - - public virtual int CountAllProducts() - { - var query = _productRepository.Table; - query = query.Where(x => !x.Deleted); - - return query.Count(); - - } - - public virtual int CountAllProductVariants() - { - var query = _productVariantAttributeCombinationRepository.Table; - query = query.Where(x => x.IsActive); - - return query.Count(); - } - - public virtual void DeleteProduct(Product product) + #region Utilities + + protected virtual int EnsureMutuallyRelatedProducts(List productIds) + { + int count = 0; + + foreach (int id1 in productIds) + { + var mutualAssociations = ( + from rp in _relatedProductRepository.Table + join p in _productRepository.Table on rp.ProductId2 equals p.Id + where rp.ProductId2 == id1 && !p.Deleted + select rp).ToList(); + + foreach (int id2 in productIds) + { + if (id1 == id2) + continue; + + if (!mutualAssociations.Any(x => x.ProductId1 == id2)) + { + int maxDisplayOrder = _relatedProductRepository.TableUntracked + .Where(x => x.ProductId1 == id2) + .OrderByDescending(x => x.DisplayOrder) + .Select(x => x.DisplayOrder) + .FirstOrDefault(); + + var newRelatedProduct = new RelatedProduct + { + ProductId1 = id2, + ProductId2 = id1, + DisplayOrder = maxDisplayOrder + 1 + }; + + InsertRelatedProduct(newRelatedProduct); + ++count; + } + } + } + + return count; + } + + protected virtual int EnsureMutuallyCrossSellProducts(List productIds) + { + int count = 0; + + foreach (int id1 in productIds) + { + var mutualAssociations = ( + from rp in _crossSellProductRepository.Table + join p in _productRepository.Table on rp.ProductId2 equals p.Id + where rp.ProductId2 == id1 && !p.Deleted + select rp).ToList(); + + foreach (int id2 in productIds) + { + if (id1 == id2) + continue; + + if (!mutualAssociations.Any(x => x.ProductId1 == id2)) + { + var newCrossSellProduct = new CrossSellProduct + { + ProductId1 = id2, + ProductId2 = id1 + }; + + InsertCrossSellProduct(newCrossSellProduct); + ++count; + } + } + } + + return count; + } + + #endregion + + #region Products + + public virtual int CountAllProducts() { - Guard.NotNull(product, nameof(product)); + var query = _productRepository.Table; + query = query.Where(x => !x.Deleted); - product.Deleted = true; - product.DeliveryTimeId = null; - product.QuantityUnitId = null; - product.CountryOfOriginId = null; + return query.Count(); + + } + + public virtual int CountAllProductVariants() + { + var query = _productVariantAttributeCombinationRepository.Table; + query = query.Where(x => x.IsActive); + + return query.Count(); + } + + public virtual void DeleteProduct(Product product) + { + Guard.NotNull(product, nameof(product)); + + product.Deleted = true; + product.DeliveryTimeId = null; + product.QuantityUnitId = null; + product.CountryOfOriginId = null; UpdateProduct(product); - if (product.ProductType == ProductType.GroupedProduct) - { - var associatedProducts = _productRepository.Table - .Where(x => x.ParentGroupedProductId == product.Id) - .ToList(); + if (product.ProductType == ProductType.GroupedProduct) + { + var associatedProducts = _productRepository.Table + .Where(x => x.ParentGroupedProductId == product.Id) + .ToList(); - associatedProducts.ForEach(x => x.ParentGroupedProductId = 0); + associatedProducts.ForEach(x => x.ParentGroupedProductId = 0); - _dbContext.SaveChanges(); - } + _dbContext.SaveChanges(); + } } public virtual IList GetAllProductsDisplayedOnHomePage() { - var query = - from p in _productRepository.Table - orderby p.HomePageDisplayOrder - where p.Published && !p.Deleted && p.ShowOnHomePage - select p; + var query = + from p in _productRepository.Table + orderby p.HomePageDisplayOrder + where p.Published && !p.Deleted && p.ShowOnHomePage + select p; var products = query.ToList(); return products; } - + public virtual Product GetProductById(int productId) { if (productId == 0) return null; - return _productRepository.GetByIdCached(productId, "db.product.id-" + productId); - } + return _productRepository.GetByIdCached(productId, "db.product.id-" + productId); + } public virtual IList GetProductsByIds(int[] productIds, ProductLoadFlags flags = ProductLoadFlags.None) { @@ -215,118 +215,118 @@ public virtual IList GetProductsByIds(int[] productIds, ProductLoadFlag where productIds.Contains(p.Id) select p; - if (flags > ProductLoadFlags.None) - { - query = ApplyLoadFlags(query, flags); - } - - var products = query.ToList(); - - // Sort by passed identifier sequence. - return products.OrderBySequence(productIds).ToList(); - } - - public virtual Product GetProductBySystemName(string systemName) - { - if (systemName.IsEmpty()) - { - return null; - } - - var product = _productRepository.Table.FirstOrDefault(x => x.SystemName == systemName && x.IsSystemProduct); - return product; - } - - private IQueryable ApplyLoadFlags(IQueryable query, ProductLoadFlags flags) - { - if (flags.HasFlag(ProductLoadFlags.WithAttributeCombinations)) - { - query = query.Include(x => x.ProductVariantAttributeCombinations); - } - - if (flags.HasFlag(ProductLoadFlags.WithBundleItems)) - { - query = query.Include(x => x.ProductBundleItems.Select(y => y.Product)); - } - - if (flags.HasFlag(ProductLoadFlags.WithCategories)) - { - query = query.Include(x => x.ProductCategories.Select(y => y.Category)); - } - - if (flags.HasFlag(ProductLoadFlags.WithDiscounts)) - { - query = query.Include(x => x.AppliedDiscounts); - } - - if (flags.HasFlag(ProductLoadFlags.WithManufacturers)) - { - query = query.Include(x => x.ProductManufacturers.Select(y => y.Manufacturer)); - } - - if (flags.HasFlag(ProductLoadFlags.WithPictures)) - { - query = query.Include(x => x.ProductPictures); - } - - if (flags.HasFlag(ProductLoadFlags.WithReviews)) - { - query = query.Include(x => x.ProductReviews); - } - - if (flags.HasFlag(ProductLoadFlags.WithSpecificationAttributes)) - { - query = query.Include(x => x.ProductSpecificationAttributes.Select(y => y.SpecificationAttributeOption)); - } - - if (flags.HasFlag(ProductLoadFlags.WithTags)) - { - query = query.Include(x => x.ProductTags); - } - - if (flags.HasFlag(ProductLoadFlags.WithTierPrices)) - { - query = query.Include(x => x.TierPrices); - } - - if (flags.HasFlag(ProductLoadFlags.WithAttributes)) - { - query = query.Include(x => x.ProductVariantAttributes.Select(y => y.ProductAttribute)); - } - - if (flags.HasFlag(ProductLoadFlags.WithAttributeValues)) - { - query = query.Include(x => x.ProductVariantAttributes.Select(y => y.ProductVariantAttributeValues)); - } - - if (flags.HasFlag(ProductLoadFlags.WithDeliveryTime)) - { - query = query.Include(x => x.DeliveryTime); - } - - return query; - } + if (flags > ProductLoadFlags.None) + { + query = ApplyLoadFlags(query, flags); + } + + var products = query.ToList(); + + // Sort by passed identifier sequence. + return products.OrderBySequence(productIds).ToList(); + } + + public virtual Product GetProductBySystemName(string systemName) + { + if (systemName.IsEmpty()) + { + return null; + } + + var product = _productRepository.Table.FirstOrDefault(x => x.SystemName == systemName && x.IsSystemProduct); + return product; + } + + private IQueryable ApplyLoadFlags(IQueryable query, ProductLoadFlags flags) + { + if (flags.HasFlag(ProductLoadFlags.WithAttributeCombinations)) + { + query = query.Include(x => x.ProductVariantAttributeCombinations); + } + + if (flags.HasFlag(ProductLoadFlags.WithBundleItems)) + { + query = query.Include(x => x.ProductBundleItems.Select(y => y.Product)); + } + + if (flags.HasFlag(ProductLoadFlags.WithCategories)) + { + query = query.Include(x => x.ProductCategories.Select(y => y.Category)); + } + + if (flags.HasFlag(ProductLoadFlags.WithDiscounts)) + { + query = query.Include(x => x.AppliedDiscounts); + } + + if (flags.HasFlag(ProductLoadFlags.WithManufacturers)) + { + query = query.Include(x => x.ProductManufacturers.Select(y => y.Manufacturer)); + } + + if (flags.HasFlag(ProductLoadFlags.WithPictures)) + { + query = query.Include(x => x.ProductPictures); + } + + if (flags.HasFlag(ProductLoadFlags.WithReviews)) + { + query = query.Include(x => x.ProductReviews); + } + + if (flags.HasFlag(ProductLoadFlags.WithSpecificationAttributes)) + { + query = query.Include(x => x.ProductSpecificationAttributes.Select(y => y.SpecificationAttributeOption)); + } + + if (flags.HasFlag(ProductLoadFlags.WithTags)) + { + query = query.Include(x => x.ProductTags); + } + + if (flags.HasFlag(ProductLoadFlags.WithTierPrices)) + { + query = query.Include(x => x.TierPrices); + } + + if (flags.HasFlag(ProductLoadFlags.WithAttributes)) + { + query = query.Include(x => x.ProductVariantAttributes.Select(y => y.ProductAttribute)); + } + + if (flags.HasFlag(ProductLoadFlags.WithAttributeValues)) + { + query = query.Include(x => x.ProductVariantAttributes.Select(y => y.ProductVariantAttributeValues)); + } + + if (flags.HasFlag(ProductLoadFlags.WithDeliveryTime)) + { + query = query.Include(x => x.DeliveryTime); + } + + return query; + } public virtual void InsertProduct(Product product) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); - _productRepository.Insert(product); + _productRepository.Insert(product); } - public virtual void UpdateProduct(Product product) + public virtual void UpdateProduct(Product product) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); _productRepository.Update(product); } public virtual void UpdateProductReviewTotals(Product product) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); - int approvedRatingSum = 0; - int notApprovedRatingSum = 0; + int approvedRatingSum = 0; + int notApprovedRatingSum = 0; int approvedTotalReviews = 0; int notApprovedTotalReviews = 0; var reviews = product.ProductReviews; @@ -335,7 +335,7 @@ public virtual void UpdateProductReviewTotals(Product product) if (pr.IsApproved) { approvedRatingSum += pr.Rating; - approvedTotalReviews ++; + approvedTotalReviews++; } else { @@ -353,51 +353,51 @@ public virtual void UpdateProductReviewTotals(Product product) public virtual IList GetLowStockProducts() { - // Track inventory for product - var query1 = from p in _productRepository.Table - orderby p.MinStockQuantity - where !p.Deleted && - p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock && - p.MinStockQuantity >= p.StockQuantity - select p; - var products1 = query1.ToList(); - - // Track inventory for product by product attributes - var query2 = from p in _productRepository.Table - from pvac in p.ProductVariantAttributeCombinations - where !p.Deleted && - p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes && - pvac.StockQuantity <= 0 - select p; - - // only distinct products (group by ID) - // if we use standard Distinct() method, then all fields will be compared (low performance) - query2 = from p in query2 - group p by p.Id into pGroup - orderby pGroup.Key - select pGroup.FirstOrDefault(); - var products2 = query2.ToList(); - - var result = new List(); - result.AddRange(products1); - result.AddRange(products2); + // Track inventory for product + var query1 = from p in _productRepository.Table + orderby p.MinStockQuantity + where !p.Deleted && + p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock && + p.MinStockQuantity >= p.StockQuantity + select p; + var products1 = query1.ToList(); + + // Track inventory for product by product attributes + var query2 = from p in _productRepository.Table + from pvac in p.ProductVariantAttributeCombinations + where !p.Deleted && + p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes && + pvac.StockQuantity <= 0 + select p; + + // only distinct products (group by ID) + // if we use standard Distinct() method, then all fields will be compared (low performance) + query2 = from p in query2 + group p by p.Id into pGroup + orderby pGroup.Key + select pGroup.FirstOrDefault(); + var products2 = query2.ToList(); + + var result = new List(); + result.AddRange(products1); + result.AddRange(products2); return result; } - public virtual Product GetProductBySku(string sku) - { - if (String.IsNullOrEmpty(sku)) - return null; + public virtual Product GetProductBySku(string sku) + { + if (String.IsNullOrEmpty(sku)) + return null; - sku = sku.Trim(); + sku = sku.Trim(); - var query = from p in _productRepository.Table - orderby p.DisplayOrder, p.Id - where !p.Deleted && p.Sku == sku - select p; - var product = query.FirstOrDefault(); - return product; - } + var query = from p in _productRepository.Table + orderby p.DisplayOrder, p.Id + where !p.Deleted && p.Sku == sku + select p; + var product = query.FirstOrDefault(); + return product; + } public virtual Product GetProductByGtin(string gtin) { @@ -414,134 +414,180 @@ orderby p.Id return product; } - public virtual Product GetProductByManufacturerPartNumber(string manufacturerPartNumber) - { - if (manufacturerPartNumber.IsEmpty()) - return null; - - manufacturerPartNumber = manufacturerPartNumber.Trim(); - - var product = _productRepository.Table - .Where(x => !x.Deleted && x.ManufacturerPartNumber == manufacturerPartNumber) - .OrderBy(x => x.Id) - .FirstOrDefault(); - - return product; - } - - public virtual Product GetProductByName(string name) - { - if (name.IsEmpty()) - return null; - - name = name.Trim(); - - var product = _productRepository.Table - .Where(x => !x.Deleted && x.Name == name) - .OrderBy(x => x.Id) - .FirstOrDefault(); - - return product; - } - - public virtual AdjustInventoryResult AdjustInventory(OrganizedShoppingCartItem sci, bool decrease) - { - if (sci == null) - throw new ArgumentNullException("cartItem"); - - if (sci.Item.Product.ProductType == ProductType.BundledProduct && sci.Item.Product.BundlePerItemShoppingCart) - { - if (sci.ChildItems != null) - { - foreach (var child in sci.ChildItems.Where(x => x.Item.Id != sci.Item.Id)) - { - AdjustInventory(child.Item.Product, decrease, sci.Item.Quantity * child.Item.Quantity, child.Item.AttributesXml); - } - } - return new AdjustInventoryResult(); - } - else - { - return AdjustInventory(sci.Item.Product, decrease, sci.Item.Quantity, sci.Item.AttributesXml); - } - } - - public virtual AdjustInventoryResult AdjustInventory(OrderItem orderItem, bool decrease, int quantity) - { - Guard.NotNull(orderItem, nameof(orderItem)); - - if (orderItem.Product.ProductType == ProductType.BundledProduct && orderItem.Product.BundlePerItemShoppingCart) - { - if (orderItem.BundleData.HasValue()) - { - var bundleData = orderItem.GetBundleData(); - if (bundleData.Count > 0) - { - var products = GetProductsByIds(bundleData.Select(x => x.ProductId).ToArray()); - - foreach (var item in bundleData) - { - var product = products.FirstOrDefault(x => x.Id == item.ProductId); - if (product != null) - AdjustInventory(product, decrease, quantity * item.Quantity, item.AttributesXml); - } - } - } - return new AdjustInventoryResult(); - } - else - { - return AdjustInventory(orderItem.Product, decrease, quantity, orderItem.AttributesXml); - } - } - - public virtual AdjustInventoryResult AdjustInventory(Product product, bool decrease, int quantity, string attributesXml) + public virtual Product GetProductByManufacturerPartNumber(string manufacturerPartNumber) + { + if (manufacturerPartNumber.IsEmpty()) + return null; + + manufacturerPartNumber = manufacturerPartNumber.Trim(); + + var product = _productRepository.Table + .Where(x => !x.Deleted && x.ManufacturerPartNumber == manufacturerPartNumber) + .OrderBy(x => x.Id) + .FirstOrDefault(); + + return product; + } + + public virtual Product GetProductByIdentificationNumber( + string identificationNumber, + out ProductVariantAttributeCombination attributeCombination, + bool includeHidden = false, + bool untracked = true) + { + attributeCombination = null; + + if (string.IsNullOrWhiteSpace(identificationNumber)) + { + return null; + } + + identificationNumber = identificationNumber.Trim(); + + var pq = + from x in untracked ? _productRepository.TableUntracked : _productRepository.Table + where !x.Deleted && (x.Sku == identificationNumber || x.ManufacturerPartNumber == identificationNumber || x.Gtin == identificationNumber) + select x; + + if (!includeHidden) + { + pq = pq.Where(x => x.Visibility <= ProductVisibility.SearchResults && x.Published); + } + + var product = pq.FirstOrDefault(); + if (product != null) + { + return product; + } + + var pvaq = + from x in untracked ? _productVariantAttributeCombinationRepository.TableUntracked : _productVariantAttributeCombinationRepository.Table + where !x.Product.Deleted && (x.Sku == identificationNumber || x.ManufacturerPartNumber == identificationNumber || x.Gtin == identificationNumber) + select x; + + if (!includeHidden) + { + pvaq = pvaq.Where(x => x.Product.Visibility <= ProductVisibility.SearchResults && x.Product.Published && x.IsActive); + } + + attributeCombination = pvaq.FirstOrDefault(); + + return attributeCombination?.Product; + } + + public virtual Product GetProductByName(string name) + { + if (name.IsEmpty()) + return null; + + name = name.Trim(); + + var product = _productRepository.Table + .Where(x => !x.Deleted && x.Name == name) + .OrderBy(x => x.Id) + .FirstOrDefault(); + + return product; + } + + public virtual AdjustInventoryResult AdjustInventory(OrganizedShoppingCartItem sci, bool decrease) + { + if (sci == null) + throw new ArgumentNullException("cartItem"); + + if (sci.Item.Product.ProductType == ProductType.BundledProduct && sci.Item.Product.BundlePerItemShoppingCart) + { + if (sci.ChildItems != null) + { + foreach (var child in sci.ChildItems.Where(x => x.Item.Id != sci.Item.Id)) + { + AdjustInventory(child.Item.Product, decrease, sci.Item.Quantity * child.Item.Quantity, child.Item.AttributesXml); + } + } + return new AdjustInventoryResult(); + } + else + { + return AdjustInventory(sci.Item.Product, decrease, sci.Item.Quantity, sci.Item.AttributesXml); + } + } + + public virtual AdjustInventoryResult AdjustInventory(OrderItem orderItem, bool decrease, int quantity) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(orderItem, nameof(orderItem)); - var result = new AdjustInventoryResult(); + if (orderItem.Product.ProductType == ProductType.BundledProduct && orderItem.Product.BundlePerItemShoppingCart) + { + if (orderItem.BundleData.HasValue()) + { + var bundleData = orderItem.GetBundleData(); + if (bundleData.Count > 0) + { + var products = GetProductsByIds(bundleData.Select(x => x.ProductId).ToArray()); - switch (product.ManageInventoryMethod) + foreach (var item in bundleData) + { + var product = products.FirstOrDefault(x => x.Id == item.ProductId); + if (product != null) + AdjustInventory(product, decrease, quantity * item.Quantity, item.AttributesXml); + } + } + } + return new AdjustInventoryResult(); + } + else + { + return AdjustInventory(orderItem.Product, decrease, quantity, orderItem.AttributesXml); + } + } + + public virtual AdjustInventoryResult AdjustInventory(Product product, bool decrease, int quantity, string attributesXml) + { + Guard.NotNull(product, nameof(product)); + + var result = new AdjustInventoryResult(); + + switch (product.ManageInventoryMethod) { case ManageInventoryMethod.DontManageStock: { //do nothing } - break; + break; case ManageInventoryMethod.ManageStock: { - result.StockQuantityOld = product.StockQuantity; + result.StockQuantityOld = product.StockQuantity; if (decrease) - result.StockQuantityNew = product.StockQuantity - quantity; - else - result.StockQuantityNew = product.StockQuantity + quantity; + result.StockQuantityNew = product.StockQuantity - quantity; + else + result.StockQuantityNew = product.StockQuantity + quantity; - bool newPublished = product.Published; - bool newDisableBuyButton = product.DisableBuyButton; - bool newDisableWishlistButton = product.DisableWishlistButton; + bool newPublished = product.Published; + bool newDisableBuyButton = product.DisableBuyButton; + bool newDisableWishlistButton = product.DisableWishlistButton; //check if minimum quantity is reached switch (product.LowStockActivity) { case LowStockActivity.DisableBuyButton: - newDisableBuyButton = product.MinStockQuantity >= result.StockQuantityNew; - newDisableWishlistButton = product.MinStockQuantity >= result.StockQuantityNew; + newDisableBuyButton = product.MinStockQuantity >= result.StockQuantityNew; + newDisableWishlistButton = product.MinStockQuantity >= result.StockQuantityNew; break; case LowStockActivity.Unpublish: - newPublished = product.MinStockQuantity <= result.StockQuantityNew; + newPublished = product.MinStockQuantity <= result.StockQuantityNew; break; } - product.StockQuantity = result.StockQuantityNew; - product.DisableBuyButton = newDisableBuyButton; - product.DisableWishlistButton = newDisableWishlistButton; - product.Published = newPublished; + product.StockQuantity = result.StockQuantityNew; + product.DisableBuyButton = newDisableBuyButton; + product.DisableWishlistButton = newDisableWishlistButton; + product.Published = newPublished; + + UpdateProduct(product); - UpdateProduct(product); - //send email notification - if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew) - _services.MessageFactory.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); + if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew) + _services.MessageFactory.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); } break; case ManageInventoryMethod.ManageStockByAttributes: @@ -549,13 +595,13 @@ public virtual AdjustInventoryResult AdjustInventory(Product product, bool decre var combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, attributesXml); if (combination != null) { - result.StockQuantityOld = combination.StockQuantity; + result.StockQuantityOld = combination.StockQuantity; if (decrease) - result.StockQuantityNew = combination.StockQuantity - quantity; + result.StockQuantityNew = combination.StockQuantity - quantity; else - result.StockQuantityNew = combination.StockQuantity + quantity; + result.StockQuantityNew = combination.StockQuantity + quantity; - combination.StockQuantity = result.StockQuantityNew; + combination.StockQuantity = result.StockQuantityNew; _productAttributeService.UpdateProductVariantAttributeCombination(combination); } } @@ -564,56 +610,56 @@ public virtual AdjustInventoryResult AdjustInventory(Product product, bool decre break; } - var attributeValues = _productAttributeParser.ParseProductVariantAttributeValues(attributesXml); + var attributeValues = _productAttributeParser.ParseProductVariantAttributeValues(attributesXml); - attributeValues - .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) - .ToList() - .Each(x => - { - var linkedProduct = GetProductById(x.LinkedProductId); - if (linkedProduct != null) - AdjustInventory(linkedProduct, decrease, quantity * x.Quantity, ""); - }); + attributeValues + .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) + .ToList() + .Each(x => + { + var linkedProduct = GetProductById(x.LinkedProductId); + if (linkedProduct != null) + AdjustInventory(linkedProduct, decrease, quantity * x.Quantity, ""); + }); - return result; + return result; } - - public virtual void UpdateHasTierPricesProperty(Product product) + + public virtual void UpdateHasTierPricesProperty(Product product) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); - var prevValue = product.HasTierPrices; - product.HasTierPrices = product.TierPrices.Count > 0; - if (prevValue != product.HasTierPrices) - UpdateProduct(product); + var prevValue = product.HasTierPrices; + product.HasTierPrices = product.TierPrices.Count > 0; + if (prevValue != product.HasTierPrices) + UpdateProduct(product); } - public virtual void UpdateLowestAttributeCombinationPriceProperty(Product product) - { - Guard.NotNull(product, nameof(product)); + public virtual void UpdateLowestAttributeCombinationPriceProperty(Product product) + { + Guard.NotNull(product, nameof(product)); - var prevValue = product.LowestAttributeCombinationPrice; + var prevValue = product.LowestAttributeCombinationPrice; - product.LowestAttributeCombinationPrice = _productAttributeService.GetLowestCombinationPrice(product.Id); + product.LowestAttributeCombinationPrice = _productAttributeService.GetLowestCombinationPrice(product.Id); - if (prevValue != product.LowestAttributeCombinationPrice) - UpdateProduct(product); - } + if (prevValue != product.LowestAttributeCombinationPrice) + UpdateProduct(product); + } - public virtual void UpdateHasDiscountsApplied(Product product) + public virtual void UpdateHasDiscountsApplied(Product product) { - Guard.NotNull(product, nameof(product)); + Guard.NotNull(product, nameof(product)); - var prevValue = product.HasDiscountsApplied; - product.HasDiscountsApplied = product.AppliedDiscounts.Count > 0; - if (prevValue != product.HasDiscountsApplied) - UpdateProduct(product); + var prevValue = product.HasDiscountsApplied; + product.HasDiscountsApplied = product.AppliedDiscounts.Count > 0; + if (prevValue != product.HasDiscountsApplied) + UpdateProduct(product); } - public virtual Multimap GetProductTagsByProductIds(int[] productIds, bool includeHidden = false) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual Multimap GetProductTagsByProductIds(int[] productIds, bool includeHidden = false) + { + Guard.NotNull(productIds, nameof(productIds)); var map = new Multimap(); if (!productIds.Any()) @@ -639,16 +685,16 @@ public virtual Multimap GetProductTagsByProductIds(int[] produc }) .ToList(); - foreach (var item in items) - { + foreach (var item in items) + { foreach (var tag in item.Tags) { map.Add(item.ProductId, tag); } - } + } - return map; - } + return map; + } public virtual Multimap GetAssociatedProductsByProductIds(int[] productIds, bool showHidden = false) { @@ -674,8 +720,8 @@ public virtual Multimap GetAssociatedProductsByProductIds(int[] pr } public virtual Multimap GetAppliedDiscountsByProductIds(int[] productIds) - { - Guard.NotNull(productIds, nameof(productIds)); + { + Guard.NotNull(productIds, nameof(productIds)); var map = new Multimap(); if (!productIds.Any()) @@ -685,20 +731,20 @@ public virtual Multimap GetAppliedDiscountsByProductIds(int[] pro var query = _productRepository.Table // .TableUntracked does not seem to eager load .Expand(x => x.AppliedDiscounts.Select(y => y.RuleSets)) - .Where(x => productIds.Contains(x.Id)) - .Select(x => new - { - ProductId = x.Id, - Discounts = x.AppliedDiscounts - }); + .Where(x => productIds.Contains(x.Id)) + .Select(x => new + { + ProductId = x.Id, + Discounts = x.AppliedDiscounts + }); - foreach (var item in query.ToList()) - { - map.AddRange(item.ProductId, item.Discounts); - } + foreach (var item in query.ToList()) + { + map.AddRange(item.ProductId, item.Discounts); + } - return map; - } + return map; + } #endregion @@ -706,9 +752,9 @@ public virtual Multimap GetAppliedDiscountsByProductIds(int[] pro public virtual void DeleteRelatedProduct(RelatedProduct relatedProduct) { - Guard.NotNull(relatedProduct, nameof(relatedProduct)); + Guard.NotNull(relatedProduct, nameof(relatedProduct)); - _relatedProductRepository.Delete(relatedProduct); + _relatedProductRepository.Delete(relatedProduct); } public virtual IList GetRelatedProductsByProductId1(int productId1, bool showHidden = false) @@ -727,36 +773,36 @@ public virtual RelatedProduct GetRelatedProductById(int relatedProductId) { if (relatedProductId == 0) return null; - + var relatedProduct = _relatedProductRepository.GetById(relatedProductId); return relatedProduct; } public virtual void InsertRelatedProduct(RelatedProduct relatedProduct) { - Guard.NotNull(relatedProduct, nameof(relatedProduct)); + Guard.NotNull(relatedProduct, nameof(relatedProduct)); - _relatedProductRepository.Insert(relatedProduct); + _relatedProductRepository.Insert(relatedProduct); } public virtual void UpdateRelatedProduct(RelatedProduct relatedProduct) { - Guard.NotNull(relatedProduct, nameof(relatedProduct)); + Guard.NotNull(relatedProduct, nameof(relatedProduct)); - _relatedProductRepository.Update(relatedProduct); + _relatedProductRepository.Update(relatedProduct); } - public virtual int EnsureMutuallyRelatedProducts(int productId1) - { - var relatedProducts = GetRelatedProductsByProductId1(productId1, true); - var productIds = relatedProducts.Select(x => x.ProductId2).ToList(); + public virtual int EnsureMutuallyRelatedProducts(int productId1) + { + var relatedProducts = GetRelatedProductsByProductId1(productId1, true); + var productIds = relatedProducts.Select(x => x.ProductId2).ToList(); - if (productIds.Count > 0 && !productIds.Any(x => x == productId1)) - productIds.Add(productId1); + if (productIds.Count > 0 && !productIds.Any(x => x == productId1)) + productIds.Add(productId1); - int count = EnsureMutuallyRelatedProducts(productIds); - return count; - } + int count = EnsureMutuallyRelatedProducts(productIds); + return count; + } #endregion @@ -764,9 +810,9 @@ public virtual int EnsureMutuallyRelatedProducts(int productId1) public virtual void DeleteCrossSellProduct(CrossSellProduct crossSellProduct) { - Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); + Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); - _crossSellProductRepository.Delete(crossSellProduct); + _crossSellProductRepository.Delete(crossSellProduct); } public virtual IList GetCrossSellProductsByProductId1(int productId1, bool showHidden = false) @@ -781,21 +827,21 @@ orderby csp.Id return crossSellProducts; } - public virtual IList GetCrossSellProductsByProductIds(IEnumerable productIds, bool showHidden = false) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual IList GetCrossSellProductsByProductIds(IEnumerable productIds, bool showHidden = false) + { + Guard.NotNull(productIds, nameof(productIds)); - var query = from csp in _crossSellProductRepository.Table - join p in _productRepository.Table on csp.ProductId2 equals p.Id - where productIds.Contains(csp.ProductId1) && !p.Deleted && (showHidden || p.Published) - orderby csp.Id - select csp; + var query = from csp in _crossSellProductRepository.Table + join p in _productRepository.Table on csp.ProductId2 equals p.Id + where productIds.Contains(csp.ProductId1) && !p.Deleted && (showHidden || p.Published) + orderby csp.Id + select csp; - var crossSellProducts = query.ToList(); - return crossSellProducts; - } + var crossSellProducts = query.ToList(); + return crossSellProducts; + } - public virtual CrossSellProduct GetCrossSellProductById(int crossSellProductId) + public virtual CrossSellProduct GetCrossSellProductById(int crossSellProductId) { if (crossSellProductId == 0) return null; @@ -806,114 +852,115 @@ public virtual CrossSellProduct GetCrossSellProductById(int crossSellProductId) public virtual void InsertCrossSellProduct(CrossSellProduct crossSellProduct) { - Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); + Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); - _crossSellProductRepository.Insert(crossSellProduct); + _crossSellProductRepository.Insert(crossSellProduct); } public virtual void UpdateCrossSellProduct(CrossSellProduct crossSellProduct) { - Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); + Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); - _crossSellProductRepository.Update(crossSellProduct); + _crossSellProductRepository.Update(crossSellProduct); } - public virtual IList GetCrosssellProductsByShoppingCart(IList cart, int numberOfProducts) - { - var result = new List(); + public virtual IList GetCrosssellProductsByShoppingCart(IList cart, int numberOfProducts) + { + var result = new List(); - if (numberOfProducts == 0) - return result; + if (numberOfProducts == 0) + return result; - if (cart == null || cart.Count == 0) - return result; + if (cart == null || cart.Count == 0) + return result; - var cartProductIds = new HashSet(cart.Select(x => x.Item.ProductId)); - var csItems = GetCrossSellProductsByProductIds(cartProductIds); - var productIdsToLoad = new HashSet(csItems.Select(x => x.ProductId2).Except(cartProductIds)); + var cartProductIds = new HashSet(cart.Select(x => x.Item.ProductId)); + var csItems = GetCrossSellProductsByProductIds(cartProductIds); + var productIdsToLoad = new HashSet(csItems.Select(x => x.ProductId2).Except(cartProductIds)); - if (productIdsToLoad.Count > 0) - { - result.AddRange(GetProductsByIds(productIdsToLoad.Take(numberOfProducts).ToArray())); - } + if (productIdsToLoad.Count > 0) + { + result.AddRange(GetProductsByIds(productIdsToLoad.Take(numberOfProducts).ToArray())); + } - return result; - } + return result; + } - public virtual int EnsureMutuallyCrossSellProducts(int productId1) - { - var crossSellProducts = GetCrossSellProductsByProductId1(productId1, true); - var productIds = crossSellProducts.Select(x => x.ProductId2).ToList(); + public virtual int EnsureMutuallyCrossSellProducts(int productId1) + { + var crossSellProducts = GetCrossSellProductsByProductId1(productId1, true); + var productIds = crossSellProducts.Select(x => x.ProductId2).ToList(); - if (productIds.Count > 0 && !productIds.Any(x => x == productId1)) - productIds.Add(productId1); + if (productIds.Count > 0 && !productIds.Any(x => x == productId1)) + productIds.Add(productId1); - int count = EnsureMutuallyCrossSellProducts(productIds); - return count; - } + int count = EnsureMutuallyCrossSellProducts(productIds); + return count; + } #endregion - + #region Tier prices - + public virtual void DeleteTierPrice(TierPrice tierPrice) { - Guard.NotNull(tierPrice, nameof(tierPrice)); + Guard.NotNull(tierPrice, nameof(tierPrice)); - _tierPriceRepository.Delete(tierPrice); + _tierPriceRepository.Delete(tierPrice); } public virtual TierPrice GetTierPriceById(int tierPriceId) { if (tierPriceId == 0) return null; - + var tierPrice = _tierPriceRepository.GetById(tierPriceId); return tierPrice; } - public virtual Multimap GetTierPricesByProductIds(int[] productIds, Customer customer = null, int storeId = 0) - { - Guard.NotNull(productIds, nameof(productIds)); + public virtual Multimap GetTierPricesByProductIds(int[] productIds, Customer customer = null, int storeId = 0) + { + Guard.NotNull(productIds, nameof(productIds)); if (!productIds.Any()) { return new Multimap(); } - var query = - from x in _tierPriceRepository.TableUntracked - where productIds.Contains(x.ProductId) - select x; - - if (storeId != 0) - query = query.Where(x => x.StoreId == 0 || x.StoreId == storeId); + var query = _tierPriceRepository.TableUntracked + .Include(x => x.CustomerRole) + .Where(x => productIds.Contains(x.ProductId)); - query = query.OrderBy(x => x.ProductId).ThenBy(x => x.Quantity); + if (storeId != 0) + query = query.Where(x => x.StoreId == 0 || x.StoreId == storeId); - var list = query.ToList(); + var list = query + .ToList() + // Sorting locally is most likely faster + .OrderBy(x => x.ProductId) + .ThenBy(x => x.Quantity) + .AsEnumerable(); - if (customer != null) - list = list.FilterForCustomer(customer).ToList(); + if (customer != null) + list = list.FilterForCustomer(customer).ToList(); - var map = list - .ToMultimap(x => x.ProductId, x => x); + var map = list.ToMultimap(x => x.ProductId, x => x); - return map; - } + return map; + } public virtual void InsertTierPrice(TierPrice tierPrice) { - Guard.NotNull(tierPrice, nameof(tierPrice)); + Guard.NotNull(tierPrice, nameof(tierPrice)); - _tierPriceRepository.Insert(tierPrice); + _tierPriceRepository.Insert(tierPrice); } public virtual void UpdateTierPrice(TierPrice tierPrice) { - Guard.NotNull(tierPrice, nameof(tierPrice)); + Guard.NotNull(tierPrice, nameof(tierPrice)); - _tierPriceRepository.Update(tierPrice); + _tierPriceRepository.Update(tierPrice); } #endregion @@ -922,35 +969,35 @@ public virtual void UpdateTierPrice(TierPrice tierPrice) public virtual void DeleteProductPicture(ProductMediaFile productPicture) { - Guard.NotNull(productPicture, nameof(productPicture)); + Guard.NotNull(productPicture, nameof(productPicture)); - UnassignDeletedPictureFromVariantCombinations(productPicture); + UnassignDeletedPictureFromVariantCombinations(productPicture); _productFileRepository.Delete(productPicture); } private void UnassignDeletedPictureFromVariantCombinations(ProductMediaFile productPicture) { - var picId = productPicture.Id; + var picId = productPicture.MediaFileId; bool touched = false; - var combinations = - from c in this._productVariantAttributeCombinationRepository.Table - where c.ProductId == productPicture.Product.Id && !String.IsNullOrEmpty(c.AssignedMediaFileIds) - select c; - - foreach (var c in combinations) - { - var ids = c.GetAssignedMediaIds().ToList(); - if (ids.Contains(picId)) - { - ids.Remove(picId); - //c.AssignedPictureIds = ids.Count > 0 ? String.Join(",", ids) : null; - c.SetAssignedMediaIds(ids.ToArray()); - touched = true; - // we will save after we're done. It's faster. - } - } + var combinations = + from c in _productVariantAttributeCombinationRepository.Table + where c.ProductId == productPicture.Product.Id && !String.IsNullOrEmpty(c.AssignedMediaFileIds) + select c; + + foreach (var c in combinations) + { + var ids = c.GetAssignedMediaIds().ToList(); + if (ids.Contains(picId)) + { + ids.Remove(picId); + //c.AssignedPictureIds = ids.Count > 0 ? String.Join(",", ids) : null; + c.SetAssignedMediaIds(ids.ToArray()); + touched = true; + // we will save after we're done. It's faster. + } + } // save in one shot! if (touched) @@ -985,11 +1032,11 @@ public virtual IList GetProductPicturesByProductId(int product return productFiles; } - public virtual Multimap GetProductPicturesByProductIds( + public virtual Multimap GetProductPicturesByProductIds( int[] productIds, int? maxPicturesPerProduct = null, MediaLoadFlags flags = MediaLoadFlags.None) - { + { if (!(productIds?.Any() ?? false)) { return new Multimap(); @@ -1045,9 +1092,9 @@ public virtual ProductMediaFile GetProductPictureById(int productPictureId) public virtual void InsertProductPicture(ProductMediaFile productPicture) { - Guard.NotNull(productPicture, nameof(productPicture)); + Guard.NotNull(productPicture, nameof(productPicture)); - _productFileRepository.Insert(productPicture); + _productFileRepository.Insert(productPicture); } /// @@ -1056,120 +1103,120 @@ public virtual void InsertProductPicture(ProductMediaFile productPicture) /// Product picture public virtual void UpdateProductPicture(ProductMediaFile productPicture) { - Guard.NotNull(productPicture, nameof(productPicture)); + Guard.NotNull(productPicture, nameof(productPicture)); - _productFileRepository.Update(productPicture); + _productFileRepository.Update(productPicture); } #endregion - #region Bundled products + #region Bundled products - public virtual void InsertBundleItem(ProductBundleItem bundleItem) - { - Guard.NotNull(bundleItem, nameof(bundleItem)); + public virtual void InsertBundleItem(ProductBundleItem bundleItem) + { + Guard.NotNull(bundleItem, nameof(bundleItem)); - if (bundleItem.BundleProductId == 0) - throw new SmartException("BundleProductId of a bundle item cannot be 0."); + if (bundleItem.BundleProductId == 0) + throw new SmartException("BundleProductId of a bundle item cannot be 0."); - if (bundleItem.ProductId == 0) - throw new SmartException("ProductId of a bundle item cannot be 0."); + if (bundleItem.ProductId == 0) + throw new SmartException("ProductId of a bundle item cannot be 0."); - if (bundleItem.ProductId == bundleItem.BundleProductId) - throw new SmartException("A bundle item cannot be an element of itself."); + if (bundleItem.ProductId == bundleItem.BundleProductId) + throw new SmartException("A bundle item cannot be an element of itself."); - _productBundleItemRepository.Insert(bundleItem); - } + _productBundleItemRepository.Insert(bundleItem); + } - public virtual void UpdateBundleItem(ProductBundleItem bundleItem) - { - Guard.NotNull(bundleItem, nameof(bundleItem)); + public virtual void UpdateBundleItem(ProductBundleItem bundleItem) + { + Guard.NotNull(bundleItem, nameof(bundleItem)); - _productBundleItemRepository.Update(bundleItem); - } + _productBundleItemRepository.Update(bundleItem); + } - public virtual void DeleteBundleItem(ProductBundleItem bundleItem) - { - Guard.NotNull(bundleItem, nameof(bundleItem)); + public virtual void DeleteBundleItem(ProductBundleItem bundleItem) + { + Guard.NotNull(bundleItem, nameof(bundleItem)); - // remove bundles from shopping carts (otherwise bundle item cannot be deleted) - var parentCartItemIds = _shoppingCartItemRepository.TableUntracked - .Where(x => x.BundleItemId == bundleItem.Id && x.ParentItemId != null) - .Select(x => x.ParentItemId) - .ToList(); + // remove bundles from shopping carts (otherwise bundle item cannot be deleted) + var parentCartItemIds = _shoppingCartItemRepository.TableUntracked + .Where(x => x.BundleItemId == bundleItem.Id && x.ParentItemId != null) + .Select(x => x.ParentItemId) + .ToList(); - if (parentCartItemIds.Any()) - { - var cartItems = _shoppingCartItemRepository.Table - .Where(x => parentCartItemIds.Contains(x.Id)) - .ToList(); + if (parentCartItemIds.Any()) + { + var cartItems = _shoppingCartItemRepository.Table + .Where(x => parentCartItemIds.Contains(x.Id)) + .ToList(); - foreach (var parentItem in cartItems) - { - var childItems = _shoppingCartItemRepository.Table - .Where(x => x.ParentItemId != null && x.ParentItemId.Value == parentItem.Id && x.Id != parentItem.Id) - .ToList(); + foreach (var parentItem in cartItems) + { + var childItems = _shoppingCartItemRepository.Table + .Where(x => x.ParentItemId != null && x.ParentItemId.Value == parentItem.Id && x.Id != parentItem.Id) + .ToList(); - childItems.Each(x => _shoppingCartItemRepository.Delete(x)); + childItems.Each(x => _shoppingCartItemRepository.Delete(x)); - _shoppingCartItemRepository.Delete(parentItem); - } - } + _shoppingCartItemRepository.Delete(parentItem); + } + } - // delete bundle item - _productBundleItemRepository.Delete(bundleItem); - } + // delete bundle item + _productBundleItemRepository.Delete(bundleItem); + } - public virtual ProductBundleItem GetBundleItemById(int bundleItemId) - { - if (bundleItemId == 0) - return null; + public virtual ProductBundleItem GetBundleItemById(int bundleItemId) + { + if (bundleItemId == 0) + return null; - return _productBundleItemRepository.GetById(bundleItemId); - } + return _productBundleItemRepository.GetById(bundleItemId); + } public virtual IList GetBundleItems(int bundleProductId, bool showHidden = false) - { - var query = - from pbi in _productBundleItemRepository.Table - join p in _productRepository.Table on pbi.ProductId equals p.Id - where pbi.BundleProductId == bundleProductId && !p.Deleted && (showHidden || (pbi.Published && p.Published)) - orderby pbi.DisplayOrder - select pbi; + { + var query = + from pbi in _productBundleItemRepository.Table + join p in _productRepository.Table on pbi.ProductId equals p.Id + where pbi.BundleProductId == bundleProductId && !p.Deleted && (showHidden || (pbi.Published && p.Published)) + orderby pbi.DisplayOrder + select pbi; - query = query.Include(x => x.Product); + query = query.Include(x => x.Product); - var bundleItemData = new List(); + var bundleItemData = new List(); - query.ToList().Each(x => bundleItemData.Add(new ProductBundleItemData(x))); + query.ToList().Each(x => bundleItemData.Add(new ProductBundleItemData(x))); - return bundleItemData; - } + return bundleItemData; + } public virtual Multimap GetBundleItemsByProductIds(int[] productIds, bool showHidden = false) - { - Guard.NotNull(productIds, nameof(productIds)); + { + Guard.NotNull(productIds, nameof(productIds)); if (!productIds.Any()) { return new Multimap(); } - var query = - from pbi in _productBundleItemRepository.TableUntracked - join p in _productRepository.TableUntracked on pbi.ProductId equals p.Id - where productIds.Contains(pbi.BundleProductId) && !p.Deleted && (showHidden || (pbi.Published && p.Published)) - orderby pbi.DisplayOrder - select pbi; + var query = + from pbi in _productBundleItemRepository.TableUntracked + join p in _productRepository.TableUntracked on pbi.ProductId equals p.Id + where productIds.Contains(pbi.BundleProductId) && !p.Deleted && (showHidden || (pbi.Published && p.Published)) + orderby pbi.DisplayOrder + select pbi; var map = query .Include(x => x.Product) .Include(x => x.BundleProduct) .ToList() - .ToMultimap(x => x.BundleProductId, x => x); + .ToMultimap(x => x.BundleProductId, x => x); - return map; - } + return map; + } public virtual bool IsBundleItem(int productId) { @@ -1188,6 +1235,6 @@ join bundle in _productRepository.TableUntracked on pbi.BundleProductId equals b return result; } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs index bd3cdcb10a..0a58afc7b0 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs @@ -21,39 +21,39 @@ public partial class ProductTagService : ScopedServiceBase, IProductTagService /// private const string PRODUCTTAG_COUNT_KEY = "producttag:count-{0}-{1}"; - /// - /// Key pattern to clear cache - /// - private const string PRODUCTTAG_PATTERN_KEY = "producttag:*"; + /// + /// Key pattern to clear cache + /// + private const string PRODUCTTAG_PATTERN_KEY = "producttag:*"; private readonly IRepository _productTagRepository; - private readonly IRepository _storeMappingRepository; - private readonly IDataProvider _dataProvider; - private readonly IDbContext _dbContext; - private readonly CommonSettings _commonSettings; - private readonly ICacheManager _cacheManager; + private readonly IRepository _storeMappingRepository; + private readonly IDataProvider _dataProvider; + private readonly IDbContext _dbContext; + private readonly CommonSettings _commonSettings; + private readonly ICacheManager _cacheManager; public ProductTagService( - IRepository productTagRepository, - IRepository storeMappingRepository, - IDataProvider dataProvider, - IDbContext dbContext, - CommonSettings commonSettings, - ICacheManager cacheManager) + IRepository productTagRepository, + IRepository storeMappingRepository, + IDataProvider dataProvider, + IDbContext dbContext, + CommonSettings commonSettings, + ICacheManager cacheManager) { _productTagRepository = productTagRepository; - _storeMappingRepository = storeMappingRepository; - _dataProvider = dataProvider; - _dbContext = dbContext; - _commonSettings = commonSettings; - _cacheManager = cacheManager; + _storeMappingRepository = storeMappingRepository; + _dataProvider = dataProvider; + _dbContext = dbContext; + _commonSettings = commonSettings; + _cacheManager = cacheManager; - QuerySettings = DbQuerySettings.Default; - } + QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } - public virtual IList GetAllProductTags(bool includeHidden = false) + public virtual IList GetAllProductTags(bool includeHidden = false) { var query = _productTagRepository.Table; @@ -233,16 +233,16 @@ public virtual void UpdateProductTags(Product product, string[] tagNames) } public virtual int GetProductCount(int productTagId, int storeId, bool includeHidden = false) - { - var dictionary = GetProductCount(storeId, includeHidden); + { + var dictionary = GetProductCount(storeId, includeHidden); if (dictionary.TryGetValue(productTagId, out var count)) { return count; } - return 0; - } + return 0; + } protected virtual Dictionary GetProductCount(int storeId, bool includeHidden) { diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs index fbf625225e..4ac7112d9b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs @@ -20,7 +20,7 @@ public partial class ProductTemplateService : IProductTemplateService private readonly IEventPublisher _eventPublisher; #endregion - + #region Ctor /// @@ -64,7 +64,7 @@ orderby pt.DisplayOrder var templates = query.ToList(); return templates; } - + /// /// Gets a product template /// @@ -101,7 +101,7 @@ public virtual void UpdateProductTemplate(ProductTemplate productTemplate) _productTemplateRepository.Update(productTemplate); } - + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs b/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs index 8d46161252..e7c26224f7 100644 --- a/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs @@ -3,27 +3,29 @@ using System.Linq; using System.Web; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; using SmartStore.Services.Security; namespace SmartStore.Services.Catalog { - /// - /// Recently viewed products service - /// - public partial class RecentlyViewedProductsService : IRecentlyViewedProductsService + /// + /// Recently viewed products service + /// + public partial class RecentlyViewedProductsService : IRecentlyViewedProductsService { #region Fields private readonly ICommonServices _services; private readonly HttpContextBase _httpContext; private readonly IProductService _productService; - private readonly IAclService _aclService; - private readonly CatalogSettings _catalogSettings; + private readonly IAclService _aclService; + private readonly CatalogSettings _catalogSettings; + private readonly PrivacySettings _privacySettings; #endregion #region Ctor - + /// /// Ctor /// @@ -33,15 +35,17 @@ public partial class RecentlyViewedProductsService : IRecentlyViewedProductsServ public RecentlyViewedProductsService( ICommonServices services, HttpContextBase httpContext, - IProductService productService, - IAclService aclService, - CatalogSettings catalogSettings) + IProductService productService, + IAclService aclService, + CatalogSettings catalogSettings, + PrivacySettings privacySettings) { _services = services; _httpContext = httpContext; _productService = productService; - _aclService = aclService; + _aclService = aclService; _catalogSettings = catalogSettings; + _privacySettings = privacySettings; } #endregion @@ -90,10 +94,10 @@ protected IList GetRecentlyViewedProductsIds(int number) public virtual IList GetRecentlyViewedProducts(int number) { var productIds = GetRecentlyViewedProductsIds(number); - var recentlyViewedProducts = _productService - .GetProductsByIds(productIds.ToArray()) - .Where(x => x.Published && !x.Deleted && !x.IsSystemProduct && _aclService.Authorize(x)) - .ToList(); + var recentlyViewedProducts = _productService + .GetProductsByIds(productIds.ToArray()) + .Where(x => x.Published && !x.Deleted && !x.IsSystemProduct && _aclService.Authorize(x)) + .ToList(); return recentlyViewedProducts; } @@ -105,42 +109,38 @@ public virtual IList GetRecentlyViewedProducts(int number) public virtual void AddProductToRecentlyViewedList(int productId) { if (!_catalogSettings.RecentlyViewedProductsEnabled) + { return; + } var oldProductIds = GetRecentlyViewedProductsIds(); var newProductIds = new List(oldProductIds); - if (!newProductIds.Contains(productId)) - { - newProductIds.Add(productId); - } + newProductIds.Remove(productId); + newProductIds.Insert(0, productId); - var recentlyViewedCookie = _httpContext.Request.Cookies.Get("SmartStore.RecentlyViewedProducts"); - if (recentlyViewedCookie == null) - { - recentlyViewedCookie = new HttpCookie("SmartStore.RecentlyViewedProducts"); - recentlyViewedCookie.HttpOnly = true; - } + var recentlyViewedCookie = _httpContext.Request.Cookies.Get("SmartStore.RecentlyViewedProducts") ?? new HttpCookie("SmartStore.RecentlyViewedProducts"); recentlyViewedCookie.Values.Clear(); - int maxProducts = _catalogSettings.RecentlyViewedProductsNumber; + var maxProducts = _catalogSettings.RecentlyViewedProductsNumber; if (maxProducts <= 0) - maxProducts = 10; + { + maxProducts = 8; + } - int skip = Math.Max(0, newProductIds.Count - maxProducts); - newProductIds.Skip(skip).Take(maxProducts).Each(x => { + newProductIds.Take(maxProducts).Each(x => + { recentlyViewedCookie.Values.Add("RecentlyViewedProductIds", x.ToString()); }); - if (_services.WebHelper.IsCurrentConnectionSecured()) - { - recentlyViewedCookie.Secure = true; - } - recentlyViewedCookie.Expires = DateTime.Now.AddDays(10.0); + recentlyViewedCookie.HttpOnly = true; + recentlyViewedCookie.Secure = _services.WebHelper.IsCurrentConnectionSecured(); + recentlyViewedCookie.SameSite = recentlyViewedCookie.Secure ? (SameSiteMode)_privacySettings.SameSiteMode : SameSiteMode.Lax; + _httpContext.Response.Cookies.Set(recentlyViewedCookie); } - + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleEvaluatorTask.cs b/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleEvaluatorTask.cs index 2c4e367b5b..71056d3463 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleEvaluatorTask.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleEvaluatorTask.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; +using SmartStore.Data.Utilities; using SmartStore.Rules; using SmartStore.Services.Catalog.Rules; using SmartStore.Services.Tasks; @@ -38,31 +39,57 @@ public override async Task ExecuteAsync(TaskExecutionContext ctx) var count = 0; var numDeleted = 0; var numAdded = 0; + var numCategories = 0; var pageSize = 500; - var categoryQuery = _categoryRepository.TableUntracked.Expand(x => x.RuleSets); + var pageIndex = -1; - if (ctx.Parameters.ContainsKey("CategoryIds")) - { - var categoryIds = ctx.Parameters["CategoryIds"].ToIntArray(); - categoryQuery = categoryQuery.Where(x => categoryIds.Contains(x.Id)); - - numDeleted = _productCategoryRepository.Context.ExecuteSqlCommand( - "Delete From [dbo].[Product_Category_Mapping] Where [CategoryId] In ({0}) And [IsSystemMapping] = 1", - false, - null, - string.Join(",", categoryIds)); - } - else + var categoryIds = ctx.Parameters.ContainsKey("CategoryIds") + ? ctx.Parameters["CategoryIds"].ToIntArray() + : null; + + // Hooks are enabled because search index needs to be updated. + using (var scope = new DbContextScope(ctx: _productCategoryRepository.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: true, autoCommit: false)) { - numDeleted = _productCategoryRepository.Context.ExecuteSqlCommand("Delete From [dbo].[Product_Category_Mapping] Where [IsSystemMapping] = 1"); - } + // Delete existing system mappings. + var deleteQuery = _productCategoryRepository.Table.Where(x => x.IsSystemMapping); + if (categoryIds != null) + { + deleteQuery = deleteQuery.Where(x => categoryIds.Contains(x.CategoryId)); + } - var categories = await categoryQuery - .Where(x => x.Published && !x.Deleted && x.RuleSets.Any(y => y.IsActive)) - .ToListAsync(); + var pager = new FastPager(deleteQuery, pageSize); + + while (pager.ReadNextPage(out var mappings)) + { + if (ctx.CancellationToken.IsCancellationRequested) + return; + + if (mappings.Any()) + { + _productCategoryRepository.DeleteRange(mappings); + numDeleted += await scope.CommitAsync(); + } + } + + try + { + _productCategoryRepository.Context.DetachEntities(); + } + catch { } + + // Insert new product category mappings. + var categoryQuery = _categoryRepository.TableUntracked.Expand(x => x.RuleSets); + if (categoryIds != null) + { + categoryQuery = categoryQuery.Where(x => categoryIds.Contains(x.Id)); + } + + var categories = await categoryQuery + .Where(x => x.Published && !x.Deleted && x.RuleSets.Any(y => y.IsActive)) + .ToListAsync(); + + numCategories = categories.Count; - using (var scope = new DbContextScope(ctx: _productCategoryRepository.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false)) - { foreach (var category in categories) { var ruleSetProductIds = new HashSet(); @@ -77,7 +104,7 @@ public override async Task ExecuteAsync(TaskExecutionContext ctx) if (_ruleFactory.CreateExpressionGroup(ruleSet, _productRuleProvider) is SearchFilterExpression expression) { - var pageIndex = -1; + pageIndex = -1; while (true) { // Do not touch searchResult.Hits. We only need the product identifiers. @@ -124,7 +151,7 @@ public override async Task ExecuteAsync(TaskExecutionContext ctx) } } - Debug.WriteLineIf(numDeleted > 0 || numAdded > 0, $"Deleted {numDeleted} and added {numAdded} product mappings for {categories.Count} categories."); + Debug.WriteLineIf(numDeleted > 0 || numAdded > 0, $"Deleted {numDeleted} and added {numAdded} product mappings for {numCategories} categories."); } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleProvider.cs b/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleProvider.cs index 59c53078e7..1db01b8256 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleProvider.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Rules/ProductRuleProvider.cs @@ -422,7 +422,9 @@ CatalogSearchQuery createdFilter(SearchFilterContext ctx, DateTime x) if (_pluginFinder.GetPluginDescriptorBySystemName("SmartStore.MegaSearchPlus") != null) { ISearchFilter[] filters(string fieldName, int parentId, int[] valueIds) - => valueIds.Select(id => SearchFilter.ByField(fieldName, id).ExactMatch().NotAnalyzed().HasParent(parentId)).ToArray(); + { + return valueIds.Select(id => SearchFilter.ByField(fieldName, id).ExactMatch().NotAnalyzed().HasParent(parentId)).ToArray(); + } var pageIndex = -1; var variantQuery = _productAttributeRepository.Value.TableUntracked diff --git a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs index d6ad0e1af0..1ad17f078e 100644 --- a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs @@ -10,114 +10,114 @@ namespace SmartStore.Services.Catalog { - public partial class SpecificationAttributeService : ISpecificationAttributeService - { - private readonly IRepository _specificationAttributeRepository; - private readonly IRepository _specificationAttributeOptionRepository; - private readonly IRepository _productSpecificationAttributeRepository; - private readonly IEventPublisher _eventPublisher; - - public SpecificationAttributeService( - IRepository specificationAttributeRepository, - IRepository specificationAttributeOptionRepository, - IRepository productSpecificationAttributeRepository, - IEventPublisher eventPublisher) - { - _specificationAttributeRepository = specificationAttributeRepository; - _specificationAttributeOptionRepository = specificationAttributeOptionRepository; - _productSpecificationAttributeRepository = productSpecificationAttributeRepository; - _eventPublisher = eventPublisher; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - #region Specification attribute - - public virtual SpecificationAttribute GetSpecificationAttributeById(int specificationAttributeId) - { - if (specificationAttributeId == 0) - return null; - - return _specificationAttributeRepository.GetById(specificationAttributeId); - } - - public virtual IQueryable GetSpecificationAttributes() - { - var query = - from sa in _specificationAttributeRepository.Table - orderby sa.DisplayOrder, sa.Name - select sa; - - return query; - } - - public virtual IQueryable GetSpecificationAttributesByIds(int[] ids) - { - if (ids == null || ids.Length == 0) - return null; - - var query = - from sa in _specificationAttributeRepository.Table - where ids.Contains(sa.Id) - orderby sa.DisplayOrder, sa.Name - select sa; - - return query; - } - - public virtual void DeleteSpecificationAttribute(SpecificationAttribute specificationAttribute) - { - if (specificationAttribute == null) - throw new ArgumentNullException("specificationAttribute"); - - _specificationAttributeRepository.Delete(specificationAttribute); - } - - public virtual void InsertSpecificationAttribute(SpecificationAttribute specificationAttribute) - { - if (specificationAttribute == null) - throw new ArgumentNullException("specificationAttribute"); - - _specificationAttributeRepository.Insert(specificationAttribute); - } - - public virtual void UpdateSpecificationAttribute(SpecificationAttribute specificationAttribute) - { - if (specificationAttribute == null) - throw new ArgumentNullException("specificationAttribute"); - - _specificationAttributeRepository.Update(specificationAttribute); - } - - #endregion - - #region Specification attribute option - - public virtual SpecificationAttributeOption GetSpecificationAttributeOptionById(int specificationAttributeOptionId) - { - if (specificationAttributeOptionId == 0) - return null; - - return _specificationAttributeOptionRepository.GetById(specificationAttributeOptionId); - } + public partial class SpecificationAttributeService : ISpecificationAttributeService + { + private readonly IRepository _specificationAttributeRepository; + private readonly IRepository _specificationAttributeOptionRepository; + private readonly IRepository _productSpecificationAttributeRepository; + private readonly IEventPublisher _eventPublisher; + + public SpecificationAttributeService( + IRepository specificationAttributeRepository, + IRepository specificationAttributeOptionRepository, + IRepository productSpecificationAttributeRepository, + IEventPublisher eventPublisher) + { + _specificationAttributeRepository = specificationAttributeRepository; + _specificationAttributeOptionRepository = specificationAttributeOptionRepository; + _productSpecificationAttributeRepository = productSpecificationAttributeRepository; + _eventPublisher = eventPublisher; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #region Specification attribute + + public virtual SpecificationAttribute GetSpecificationAttributeById(int specificationAttributeId) + { + if (specificationAttributeId == 0) + return null; + + return _specificationAttributeRepository.GetById(specificationAttributeId); + } + + public virtual IQueryable GetSpecificationAttributes() + { + var query = + from sa in _specificationAttributeRepository.Table + orderby sa.DisplayOrder, sa.Name + select sa; + + return query; + } + + public virtual IQueryable GetSpecificationAttributesByIds(int[] ids) + { + if (ids == null || ids.Length == 0) + return null; + + var query = + from sa in _specificationAttributeRepository.Table + where ids.Contains(sa.Id) + orderby sa.DisplayOrder, sa.Name + select sa; + + return query; + } + + public virtual void DeleteSpecificationAttribute(SpecificationAttribute specificationAttribute) + { + if (specificationAttribute == null) + throw new ArgumentNullException("specificationAttribute"); + + _specificationAttributeRepository.Delete(specificationAttribute); + } + + public virtual void InsertSpecificationAttribute(SpecificationAttribute specificationAttribute) + { + if (specificationAttribute == null) + throw new ArgumentNullException("specificationAttribute"); + + _specificationAttributeRepository.Insert(specificationAttribute); + } + + public virtual void UpdateSpecificationAttribute(SpecificationAttribute specificationAttribute) + { + if (specificationAttribute == null) + throw new ArgumentNullException("specificationAttribute"); + + _specificationAttributeRepository.Update(specificationAttribute); + } + + #endregion + + #region Specification attribute option + + public virtual SpecificationAttributeOption GetSpecificationAttributeOptionById(int specificationAttributeOptionId) + { + if (specificationAttributeOptionId == 0) + return null; + + return _specificationAttributeOptionRepository.GetById(specificationAttributeOptionId); + } public virtual IList GetSpecificationAttributeOptionsBySpecificationAttribute(int specificationAttributeId) - { + { if (specificationAttributeId == 0) { return new List(); } - var query = from sao in _specificationAttributeOptionRepository.Table - orderby sao.DisplayOrder - where sao.SpecificationAttributeId == specificationAttributeId - select sao; + var query = from sao in _specificationAttributeOptionRepository.Table + orderby sao.DisplayOrder + where sao.SpecificationAttributeId == specificationAttributeId + select sao; - var specificationAttributeOptions = query.ToList(); - return specificationAttributeOptions; - } + var specificationAttributeOptions = query.ToList(); + return specificationAttributeOptions; + } public virtual Multimap GetSpecificationAttributeOptionsBySpecificationAttributeIds(int[] specificationAttributeIds) { @@ -133,94 +133,94 @@ public virtual Multimap GetSpecificationAttri } public virtual void DeleteSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) - { - if (specificationAttributeOption == null) - throw new ArgumentNullException("specificationAttributeOption"); - - _specificationAttributeOptionRepository.Delete(specificationAttributeOption); - } - - public virtual void InsertSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) - { - if (specificationAttributeOption == null) - throw new ArgumentNullException("specificationAttributeOption"); - - _specificationAttributeOptionRepository.Insert(specificationAttributeOption); - } - - public virtual void UpdateSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) - { - if (specificationAttributeOption == null) - throw new ArgumentNullException("specificationAttributeOption"); - - _specificationAttributeOptionRepository.Update(specificationAttributeOption); - } - - #endregion - - #region Product specification attribute - - public virtual void DeleteProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) - { - if (productSpecificationAttribute == null) - throw new ArgumentNullException("productSpecificationAttribute"); - - _productSpecificationAttributeRepository.Delete(productSpecificationAttribute); - } - - public virtual IList GetProductSpecificationAttributesByProductId(int productId) - { - return GetProductSpecificationAttributesByProductId(productId, null, null); - } - - public virtual IList GetProductSpecificationAttributesByProductId(int productId, bool? allowFiltering, bool? showOnProductPage) - { - if (productId == 0) - { - return new List(); - } - - if (allowFiltering.HasValue || showOnProductPage.HasValue) - { - // Note: Join or Expand of SpecificationAttribute, both provides the same SQL. - var joinedQuery = - from psa in _productSpecificationAttributeRepository.Table - join sao in _specificationAttributeOptionRepository.Table on psa.SpecificationAttributeOptionId equals sao.Id - where psa.ProductId == productId - select new - { - ProductAttribute = psa, - Attribute = sao.SpecificationAttribute - }; - - if (allowFiltering.HasValue) - { - joinedQuery = joinedQuery.Where(x => - (x.ProductAttribute.AllowFiltering == null && x.Attribute.AllowFiltering == allowFiltering.Value) || - (x.ProductAttribute.AllowFiltering != null && x.ProductAttribute.AllowFiltering == allowFiltering.Value) - ); - } - - if (showOnProductPage.HasValue) - { - joinedQuery = joinedQuery.Where(x => - (x.ProductAttribute.ShowOnProductPage == null && x.Attribute.ShowOnProductPage == showOnProductPage.Value) || - (x.ProductAttribute.ShowOnProductPage != null && x.ProductAttribute.ShowOnProductPage == showOnProductPage.Value) - ); - } - - var query = joinedQuery.Select(x => x.ProductAttribute).OrderBy(x => x.DisplayOrder); - return query.ToListCached(); - } - else - { - var query = _productSpecificationAttributeRepository.Table - .Where(x => x.ProductId == productId) - .OrderBy(x => x.DisplayOrder); - - return query.ToListCached(); - } - } + { + if (specificationAttributeOption == null) + throw new ArgumentNullException("specificationAttributeOption"); + + _specificationAttributeOptionRepository.Delete(specificationAttributeOption); + } + + public virtual void InsertSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) + { + if (specificationAttributeOption == null) + throw new ArgumentNullException("specificationAttributeOption"); + + _specificationAttributeOptionRepository.Insert(specificationAttributeOption); + } + + public virtual void UpdateSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) + { + if (specificationAttributeOption == null) + throw new ArgumentNullException("specificationAttributeOption"); + + _specificationAttributeOptionRepository.Update(specificationAttributeOption); + } + + #endregion + + #region Product specification attribute + + public virtual void DeleteProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) + { + if (productSpecificationAttribute == null) + throw new ArgumentNullException("productSpecificationAttribute"); + + _productSpecificationAttributeRepository.Delete(productSpecificationAttribute); + } + + public virtual IList GetProductSpecificationAttributesByProductId(int productId) + { + return GetProductSpecificationAttributesByProductId(productId, null, null); + } + + public virtual IList GetProductSpecificationAttributesByProductId(int productId, bool? allowFiltering, bool? showOnProductPage) + { + if (productId == 0) + { + return new List(); + } + + if (allowFiltering.HasValue || showOnProductPage.HasValue) + { + // Note: Join or Expand of SpecificationAttribute, both provides the same SQL. + var joinedQuery = + from psa in _productSpecificationAttributeRepository.Table + join sao in _specificationAttributeOptionRepository.Table on psa.SpecificationAttributeOptionId equals sao.Id + where psa.ProductId == productId + select new + { + ProductAttribute = psa, + Attribute = sao.SpecificationAttribute + }; + + if (allowFiltering.HasValue) + { + joinedQuery = joinedQuery.Where(x => + (x.ProductAttribute.AllowFiltering == null && x.Attribute.AllowFiltering == allowFiltering.Value) || + (x.ProductAttribute.AllowFiltering != null && x.ProductAttribute.AllowFiltering == allowFiltering.Value) + ); + } + + if (showOnProductPage.HasValue) + { + joinedQuery = joinedQuery.Where(x => + (x.ProductAttribute.ShowOnProductPage == null && x.Attribute.ShowOnProductPage == showOnProductPage.Value) || + (x.ProductAttribute.ShowOnProductPage != null && x.ProductAttribute.ShowOnProductPage == showOnProductPage.Value) + ); + } + + var query = joinedQuery.Select(x => x.ProductAttribute).OrderBy(x => x.DisplayOrder); + return query.ToListCached(); + } + else + { + var query = _productSpecificationAttributeRepository.Table + .Where(x => x.ProductId == productId) + .OrderBy(x => x.DisplayOrder); + + return query.ToListCached(); + } + } public virtual Multimap GetProductSpecificationAttributesByProductIds(int[] productIds) { @@ -244,30 +244,30 @@ public virtual Multimap GetProductSpecificat } public virtual ProductSpecificationAttribute GetProductSpecificationAttributeById(int productSpecificationAttributeId) - { - if (productSpecificationAttributeId == 0) - return null; + { + if (productSpecificationAttributeId == 0) + return null; - var productSpecificationAttribute = _productSpecificationAttributeRepository.GetById(productSpecificationAttributeId); - return productSpecificationAttribute; - } + var productSpecificationAttribute = _productSpecificationAttributeRepository.GetById(productSpecificationAttributeId); + return productSpecificationAttribute; + } - public virtual void InsertProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) - { - if (productSpecificationAttribute == null) - throw new ArgumentNullException("productSpecificationAttribute"); + public virtual void InsertProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) + { + if (productSpecificationAttribute == null) + throw new ArgumentNullException("productSpecificationAttribute"); - _productSpecificationAttributeRepository.Insert(productSpecificationAttribute); - } + _productSpecificationAttributeRepository.Insert(productSpecificationAttribute); + } - public virtual void UpdateProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) - { - if (productSpecificationAttribute == null) - throw new ArgumentNullException("productSpecificationAttribute"); + public virtual void UpdateProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) + { + if (productSpecificationAttribute == null) + throw new ArgumentNullException("productSpecificationAttribute"); - _productSpecificationAttributeRepository.Update(productSpecificationAttribute); - } + _productSpecificationAttributeRepository.Update(productSpecificationAttribute); + } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/TierPriceExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/TierPriceExtensions.cs index 4849eed4d2..5e6ff6108f 100644 --- a/src/Libraries/SmartStore.Services/Catalog/TierPriceExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/TierPriceExtensions.cs @@ -11,19 +11,19 @@ namespace SmartStore.Services.Catalog /// public static class TierPriceExtensions { - /// - /// Filter tier prices by a store - /// - /// Tier prices - /// Store identifier - /// Filtered tier prices + /// + /// Filter tier prices by a store + /// + /// Tier prices + /// Store identifier + /// Filtered tier prices public static IEnumerable FilterByStore(this IEnumerable source, int storeId) - { - if (source == null) - throw new ArgumentNullException("source"); + { + if (source == null) + throw new ArgumentNullException("source"); return source.Where(x => x.StoreId == 0 || x.StoreId == storeId); - } + } /// /// Filter tier prices for a customer @@ -33,8 +33,7 @@ public static IEnumerable FilterByStore(this IEnumerable s /// Filtered tier prices public static IEnumerable FilterForCustomer(this IEnumerable source, Customer customer) { - if (source == null) - throw new ArgumentNullException("source"); + Guard.NotNull(source, nameof(source)); foreach (var tierPrice in source) { @@ -57,12 +56,10 @@ public static IEnumerable FilterForCustomer(this IEnumerable @@ -74,7 +71,7 @@ public static ICollection RemoveDuplicatedQuantities(this ICollection { if (source == null) throw new ArgumentNullException("source"); - + // Find duplicates var query = from tierPrice in source group tierPrice by tierPrice.Quantity into g @@ -86,7 +83,7 @@ where g.Count() > 1 var minTierPrice = item.TierPrices.Aggregate((tp1, tp2) => (tp1.Price < tp2.Price ? tp1 : tp2)); // Remove all other records item.TierPrices.Remove(minTierPrice); - item.TierPrices.ForEach(x=> source.Remove(x)); + item.TierPrices.ForEach(x => source.Remove(x)); } return source; diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs index 1d1335946d..7344a6b0c6 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs @@ -16,32 +16,32 @@ namespace SmartStore.Services.Cms.Blocks { - public abstract class BlockHandlerBase : IBlockHandler where T : IBlock - { - public ICommonServices Services { get; set; } + public abstract class BlockHandlerBase : IBlockHandler where T : IBlock + { + public ICommonServices Services { get; set; } public ILogger Logger { get; set; } - public ILocalizedEntityService LocalizedEntityService { get; set; } + public ILocalizedEntityService LocalizedEntityService { get; set; } - public virtual T Create(IBlockEntity entity) - { - return Activator.CreateInstance(); - } + public virtual T Create(IBlockEntity entity) + { + return Activator.CreateInstance(); + } - public virtual T Load(IBlockEntity entity, StoryViewMode viewMode) - { - Guard.NotNull(entity, nameof(entity)); + public virtual T Load(IBlockEntity entity, StoryViewMode viewMode) + { + Guard.NotNull(entity, nameof(entity)); - var block = Create(entity); - var json = entity.Model; + var block = Create(entity); + var json = entity.Model; - if (json.IsEmpty()) - { - return block; - } + if (json.IsEmpty()) + { + return block; + } - JsonConvert.PopulateObject(json, block); + JsonConvert.PopulateObject(json, block); if (block is IBindableBlock bindableBlock) { @@ -50,32 +50,32 @@ public virtual T Load(IBlockEntity entity, StoryViewMode viewMode) } return block; - } + } - public virtual bool IsValid(T block) - { - return true; - } + public virtual bool IsValid(T block) + { + return true; + } - public virtual void Save(T block, IBlockEntity entity) - { - Guard.NotNull(entity, nameof(entity)); + public virtual void Save(T block, IBlockEntity entity) + { + Guard.NotNull(entity, nameof(entity)); - if (block == null) - { - return; - } + if (block == null) + { + return; + } - var settings = new JsonSerializerSettings - { + var settings = new JsonSerializerSettings + { ContractResolver = SmartContractResolver.Instance, TypeNameHandling = TypeNameHandling.Objects, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - ObjectCreationHandling = ObjectCreationHandling.Replace, - NullValueHandling = NullValueHandling.Ignore - }; + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + ObjectCreationHandling = ObjectCreationHandling.Replace, + NullValueHandling = NullValueHandling.Ignore + }; - entity.Model = JsonConvert.SerializeObject(block, Formatting.None, settings); + entity.Model = JsonConvert.SerializeObject(block, Formatting.None, settings); // save BindEntintyName & BindEntintyId if (block is IBindableBlock bindableBlock) @@ -90,55 +90,60 @@ public virtual void AfterSave(IBlockContainer container, IBlockEntity entity) // Default impl does nothing. } + public virtual void BeforeRender(IBlockContainer container, StoryViewMode viewMode, IBlockHtmlParts htmlParts) + { + // Default impl does nothing. + } + public virtual string Clone(IBlockEntity sourceEntity, IBlockEntity clonedEntity) - { - return sourceEntity.Model; - } - - public void Render(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper) - { - RenderCore(element, templates, htmlHelper, htmlHelper.ViewContext.Writer); - } - - public IHtmlString ToHtmlString(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper) - { - using (var writer = new StringWriter(CultureInfo.CurrentCulture)) - { - RenderCore(element, templates, htmlHelper, writer); - return MvcHtmlString.Create(writer.ToString()); - } - } - - protected virtual void RenderCore(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) - { - RenderByView(element, templates, htmlHelper, textWriter); - } - - protected void RenderByView(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) - { - Guard.NotNull(element, nameof(element)); - Guard.NotNull(templates, nameof(templates)); - Guard.NotNull(htmlHelper, nameof(htmlHelper)); - Guard.NotNull(textWriter, nameof(textWriter)); - - var viewContext = htmlHelper.ViewContext; - - if (!element.Metadata.IsInbuilt) - { - // Change "area" token in RouteData in order to begin search in the plugin's view folder. - var originalRouteData = htmlHelper.ViewContext.RouteData; - var routeData = new RouteData(originalRouteData.Route, originalRouteData.RouteHandler); - routeData.Values.Merge(originalRouteData.Values); - routeData.DataTokens["area"] = element.Metadata.AreaName; - - viewContext = new ViewContext - { - RouteData = routeData, - RequestContext = htmlHelper.ViewContext.RequestContext - }; - } - - var viewResult = FindFirstView(element.Metadata, templates, viewContext, out var searchedLocations); + { + return sourceEntity.Model; + } + + public void Render(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper) + { + RenderCore(element, templates, htmlHelper, htmlHelper.ViewContext.Writer); + } + + public IHtmlString ToHtmlString(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper) + { + using (var writer = new StringWriter(CultureInfo.CurrentCulture)) + { + RenderCore(element, templates, htmlHelper, writer); + return MvcHtmlString.Create(writer.ToString()); + } + } + + protected virtual void RenderCore(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) + { + RenderByView(element, templates, htmlHelper, textWriter); + } + + protected void RenderByView(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) + { + Guard.NotNull(element, nameof(element)); + Guard.NotNull(templates, nameof(templates)); + Guard.NotNull(htmlHelper, nameof(htmlHelper)); + Guard.NotNull(textWriter, nameof(textWriter)); + + var viewContext = htmlHelper.ViewContext; + + if (!element.Metadata.IsInbuilt) + { + // Change "area" token in RouteData in order to begin search in the plugin's view folder. + var originalRouteData = htmlHelper.ViewContext.RouteData; + var routeData = new RouteData(originalRouteData.Route, originalRouteData.RouteHandler); + routeData.Values.Merge(originalRouteData.Values); + routeData.DataTokens["area"] = element.Metadata.AreaName; + + viewContext = new ViewContext + { + RouteData = routeData, + RequestContext = htmlHelper.ViewContext.RequestContext + }; + } + + var viewResult = FindFirstView(element.Metadata, templates, viewContext, out var searchedLocations); if (viewResult == null) { @@ -147,36 +152,36 @@ protected void RenderByView(IBlockContainer element, IEnumerable templat throw new FileNotFoundException(msg); } - var viewData = new ViewDataDictionary(element.Block); - viewData.TemplateInfo.HtmlFieldPrefix = "Block"; - - viewContext = new ViewContext( - htmlHelper.ViewContext, - viewResult.View, - viewData, - htmlHelper.ViewContext.TempData, - textWriter); - - viewResult.View.Render(viewContext, textWriter); - } - - private ViewEngineResult FindFirstView(IBlockMetadata blockMetadata, IEnumerable templates, ViewContext viewContext, out ICollection searchedLocations) - { - searchedLocations = new List(); - - foreach (var template in templates) - { - var viewName = string.Concat("BlockTemplates/", blockMetadata.SystemName, "/", template); - var viewResult = ViewEngines.Engines.FindPartialView(viewContext, viewName); - searchedLocations.AddRange(viewResult.SearchedLocations); - if (viewResult.View != null) - { - return viewResult; - } - } - - return null; - } + var viewData = new ViewDataDictionary(element.Block); + viewData.TemplateInfo.HtmlFieldPrefix = "Block"; + + viewContext = new ViewContext( + htmlHelper.ViewContext, + viewResult.View, + viewData, + htmlHelper.ViewContext.TempData, + textWriter); + + viewResult.View.Render(viewContext, textWriter); + } + + private ViewEngineResult FindFirstView(IBlockMetadata blockMetadata, IEnumerable templates, ViewContext viewContext, out ICollection searchedLocations) + { + searchedLocations = new List(); + + foreach (var template in templates) + { + var viewName = string.Concat("BlockTemplates/", blockMetadata.SystemName, "/", template); + var viewResult = ViewEngines.Engines.FindPartialView(viewContext, viewName); + searchedLocations.AddRange(viewResult.SearchedLocations); + if (viewResult.View != null) + { + return viewResult; + } + } + + return null; + } protected void RenderByChildAction(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) { @@ -268,9 +273,10 @@ void endRender() #endregion - protected virtual RouteInfo GetRoute(IBlockContainer element, string template) - => throw new NotImplementedException(); - + protected virtual RouteInfo GetRoute(IBlockContainer element, string template) + { + throw new NotImplementedException(); + } class ActionInvokerWithResultValidator : AsyncControllerActionInvoker { diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs index ee65bc8ff5..3d62bf97ba 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs @@ -3,77 +3,77 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// Applies metadata to concrete block types which implement . - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class BlockAttribute : Attribute - { - public BlockAttribute(string systemName) - { - Guard.NotNull(systemName, nameof(systemName)); + /// + /// Applies metadata to concrete block types which implement . + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class BlockAttribute : Attribute + { + public BlockAttribute(string systemName) + { + Guard.NotNull(systemName, nameof(systemName)); - SystemName = systemName; - } + SystemName = systemName; + } - /// - /// The block system name, e.g. 'html', 'picture' etc. - /// - public string SystemName { get; set; } + /// + /// The block system name, e.g. 'html', 'picture' etc. + /// + public string SystemName { get; set; } - /// - /// The english friendly name of the block - /// - public string FriendlyName { get; set; } + /// + /// The english friendly name of the block + /// + public string FriendlyName { get; set; } - /// - /// The icon class name of the block, e.g. 'fa fa-sitemap' - /// - public string Icon { get; set; } + /// + /// The icon class name of the block, e.g. 'fa fa-sitemap' + /// + public string Icon { get; set; } - /// - /// The order of display - /// - public int DisplayOrder { get; set; } + /// + /// The order of display + /// + public int DisplayOrder { get; set; } - public bool IsInternal { get; set; } - } + public bool IsInternal { get; set; } + } - /// - /// Represents block registration metadata - /// - public interface IBlockMetadata : IProviderMetadata - { - string AreaName { get; } - string Icon { get; } - bool IsInternal { get; } - bool IsInbuilt { get; } - Type BlockClrType { get; } - Type BlockHandlerClrType { get; } - } + /// + /// Represents block registration metadata + /// + public interface IBlockMetadata : IProviderMetadata + { + string AreaName { get; } + string Icon { get; } + bool IsInternal { get; } + bool IsInbuilt { get; } + Type BlockClrType { get; } + Type BlockHandlerClrType { get; } + } - public class BlockMetadata : IBlockMetadata, ICloneable - { - public string AreaName { get; set; } - public string SystemName { get; set; } - public string FriendlyName { get; set; } - public string Description { get; set; } - public string ResourceKeyPattern { get; set; } - public string Icon { get; set; } - public int DisplayOrder { get; set; } - public bool IsInternal { get; set; } - public bool IsInbuilt { get; set; } - public Type BlockClrType { get; set; } - public Type BlockHandlerClrType { get; set; } + public class BlockMetadata : IBlockMetadata, ICloneable + { + public string AreaName { get; set; } + public string SystemName { get; set; } + public string FriendlyName { get; set; } + public string Description { get; set; } + public string ResourceKeyPattern { get; set; } + public string Icon { get; set; } + public int DisplayOrder { get; set; } + public bool IsInternal { get; set; } + public bool IsInbuilt { get; set; } + public Type BlockClrType { get; set; } + public Type BlockHandlerClrType { get; set; } - public BlockMetadata Clone() - { - return (BlockMetadata)this.MemberwiseClone(); - } + public BlockMetadata Clone() + { + return (BlockMetadata)this.MemberwiseClone(); + } - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } - } + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlock.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlock.cs index 3638b5e31c..94e3ffb4e3 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlock.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlock.cs @@ -3,15 +3,15 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// When implemented on types, makes the block bindable to - /// product, category and manufacturer entities. The UI will display a 'Data binding' - /// section which allows selection of an entity. - /// - /// - /// The handler for a bindable block type MUST implement . - /// - public interface IBindableBlock : IBlock + /// + /// When implemented on types, makes the block bindable to + /// product, category and manufacturer entities. The UI will display a 'Data binding' + /// section which allows selection of an entity. + /// + /// + /// The handler for a bindable block type MUST implement . + /// + public interface IBindableBlock : IBlock { /// /// The name of the bound entity, e.g. 'product'. @@ -32,22 +32,22 @@ public interface IBindableBlock : IBlock /// bool CanBind { get; } - /// - /// Returns a value to indicate whether the binding source () has been loaded from the store already. - /// - bool IsLoaded { get; } + /// + /// Returns a value to indicate whether the binding source () has been loaded from the store already. + /// + bool IsLoaded { get; } /// /// The data item of the bound entity. /// IDictionary DataItem { get; set; } - /// - /// Returns a value to indicate whether the block has been bound already. - /// A block is considered bound if the binding source dictionary has been - /// applied to the block instance's bindable properties. - /// - bool IsBound { get; set; } + /// + /// Returns a value to indicate whether the block has been bound already. + /// A block is considered bound if the binding source dictionary has been + /// applied to the block instance's bindable properties. + /// + bool IsBound { get; set; } /// /// Resets the data item of the bound entity. After calling this method, @@ -62,19 +62,10 @@ public abstract class BindableBlockBase : IBindableBlock public virtual int? BindEntityId { get; set; } [JsonIgnore] - public bool CanBind - { - get - { - return BindEntityName.HasValue() && BindEntityId.HasValue; - } - } + public bool CanBind => BindEntityName.HasValue() && BindEntityId.HasValue; [JsonIgnore] - public bool IsLoaded - { - get { return DataItem != null; } - } + public bool IsLoaded => DataItem != null; [JsonIgnore] public IDictionary DataItem { get; set; } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs index 0658902f37..0e15cb8771 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs @@ -5,51 +5,51 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// Retrieves information about UI mapping between template tokens and form controls. - /// - /// - /// only works in conjunction with . - /// Make sure that the handled block type implements . - /// - public interface IBindableBlockHandler : IBlockHandler + /// + /// Retrieves information about UI mapping between template tokens and form controls. + /// + /// + /// only works in conjunction with . + /// Make sure that the handled block type implements . + /// + public interface IBindableBlockHandler : IBlockHandler { - /// - /// Build and returns the template mapping configuration. - /// - /// - TemplateMappingConfiguration GetMappingConfiguration(); - } + /// + /// Build and returns the template mapping configuration. + /// + /// + TemplateMappingConfiguration GetMappingConfiguration(); + } - /// - /// Contans information about UI mapping between template tokens and form controls. - /// - public class TemplateMappingConfiguration + /// + /// Contans information about UI mapping between template tokens and form controls. + /// + public class TemplateMappingConfiguration { - /// - /// A list of template tokens and their corresponding form controls. - /// - public IList Map { get; set; } + /// + /// A list of template tokens and their corresponding form controls. + /// + public IList Map { get; set; } - /// - /// The complete list of field names which can be bound to entity fields. - /// + /// + /// The complete list of field names which can be bound to entity fields. + /// public string[] BindableFields { get; set; } } - /// - /// Block data binding mapping definition for a single field. - /// + /// + /// Block data binding mapping definition for a single field. + /// public class TemplateMapping { - /// - /// Name of the bound field, e.g. 'Title', 'Description', 'Price' etc. - /// - public string FieldName { get; set; } + /// + /// Name of the bound field, e.g. 'Title', 'Description', 'Price' etc. + /// + public string FieldName { get; set; } - /// - /// The 'Liquid' template - /// + /// + /// The 'Liquid' template + /// public string Template { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlock.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlock.cs index 5f08fb785d..4560ff9cff 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlock.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlock.cs @@ -3,11 +3,11 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// Marker interface for a PageBuilder block implementation. - /// Blocks are handled by corresponding implementations. - /// - public interface IBlock - { - } + /// + /// Marker interface for a PageBuilder block implementation. + /// Blocks are handled by corresponding implementations. + /// + public interface IBlock + { + } } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockContainer.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockContainer.cs index 9f5e50c931..a88a386a85 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockContainer.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockContainer.cs @@ -2,50 +2,50 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// Contains base data for a block instance. - /// - public interface IBlockContainer - { - /// - /// The type of the block, e.g. 'html'. - /// - string BlockType { get; } - - /// - /// The actual block instance. - /// - IBlock Block { get; } - - /// - /// The block metadata. - /// - IBlockMetadata Metadata { get; } - - /// - /// The block title / display name. - /// - string Title { get; } - - /// - /// Whether the block's text content color should be inversed so - /// that it gets readable on dark backgrounds. - /// - bool IsInversed { get; } - - /// - /// The block html id. - /// - string HtmlId { get; } - - /// - /// The block css class name(s). - /// - string CssClass { get; } - - /// - /// The block css style expression. - /// - string CssStyle { get; } - } + /// + /// Contains base data for a block instance. + /// + public interface IBlockContainer + { + /// + /// The type of the block, e.g. 'html'. + /// + string BlockType { get; } + + /// + /// The actual block instance. + /// + IBlock Block { get; } + + /// + /// The block metadata. + /// + IBlockMetadata Metadata { get; } + + /// + /// The block title / display name. + /// + string Title { get; } + + /// + /// Whether the block's text content color should be inversed so + /// that it gets readable on dark backgrounds. + /// + bool IsInversed { get; } + + /// + /// The block html id. + /// + string HtmlId { get; } + + /// + /// The block css class name(s). + /// + string CssClass { get; } + + /// + /// The block css style expression. + /// + string CssStyle { get; } + } } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs index 100f78e138..63dfdf82b3 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs @@ -2,20 +2,20 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// Represents a block instance's storage data. - /// - public interface IBlockEntity - { - int Id { get; set; } - int StoryId { get; } - string BlockType { get; set; } - string Model { get; set; } - string TagLine { get; set; } - string Title { get; set; } - string SubTitle { get; set; } - string Body { get; set; } - string Template { get; set; } + /// + /// Represents a block instance's storage data. + /// + public interface IBlockEntity + { + int Id { get; set; } + int StoryId { get; } + string BlockType { get; set; } + string Model { get; set; } + string TagLine { get; set; } + string Title { get; set; } + string SubTitle { get; set; } + string Body { get; set; } + string Template { get; set; } string Custom1 { get; set; } string Custom2 { get; set; } string Custom3 { get; set; } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs index 1bd024bef3..58d74e18ec 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs @@ -5,82 +5,55 @@ namespace SmartStore.Services.Cms.Blocks { - /// - /// Indicates the view mode of a PageBuilder block. - /// - public enum StoryViewMode - { - /// - /// Final rendering result in public frontend - /// - Public, - /// - /// Preview mode in backend - /// - Preview, - /// - /// Visual block editing in backend's story editor - /// - GridEdit, - /// - /// Property dialog in backend - /// - Edit - } - - /// - /// Handles rendering of corresponding implementations. - /// - public interface IBlockHandler - { - /// - /// Renders the block result. - /// - /// The block element to render. - /// A list of template names. The first valid template will be used for rendering. - /// Html helper instance. - void Render(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHeper); - - /// - /// returns the block's render result as string. - /// - /// The block element to render. - /// A list of valid template names. The first valid template will be used for rendering. - /// Html helper instance. - IHtmlString ToHtmlString(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper); - } - - /// - /// Handles loading, saving and rendering of corresponding implementations. - /// - public interface IBlockHandler : IBlockHandler where T : IBlock - { - /// - /// Creates the implementation instance for the passed block entity. - /// - /// The serialized entity to create a block instance for. - /// A concrete block implementation instance. - T Create(IBlockEntity entity); + /// + /// Indicates the view mode of a PageBuilder block. + /// + public enum StoryViewMode + { + /// + /// Final rendering result in public frontend + /// + Public, + /// + /// Preview mode in backend + /// + Preview, + /// + /// Visual block editing in backend's story editor + /// + GridEdit, + /// + /// Property dialog in backend + /// + Edit + } - /// - /// Creates and loads the implementation instance for the passed block entity and view mode. - /// - /// The serialized entity to load a block instance for. - /// Depending on view mode, some data may be loaded or not. - /// A concrete block implementation instance. - T Load(IBlockEntity entity, StoryViewMode viewMode); + public interface IBlockHtmlParts + { + void AddCssClass(string value); + void AddHtmlAttribute(string name, object value); + } - bool IsValid(T block); + /// + /// Handles rendering of corresponding implementations. + /// + public interface IBlockHandler + { + /// + /// Called when the block is about to be rendered. Gives you the chance to add css classes or html attributes to the root element. + /// + /// The block instance wrapper / model. + /// The stories current view mode. + /// Allows adding classes or attributes. + void BeforeRender(IBlockContainer container, StoryViewMode viewMode, IBlockHtmlParts htmlParts); /// - /// Serializes the passed block instance and saves the result in the property. + /// Renders the block result. /// - /// The block instance to save. - /// - /// The corresponding entity record for the block. In most cases will - /// be converted to JSON and assigned to property, which then will be saved in the data storage. - /// - void Save(T block, IBlockEntity entity); + /// The block element to render. + /// A list of template names. The first valid template will be used for rendering. + /// Html helper instance. + void Render(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHeper); /// /// Called after the entity has been saved to the database to perform operations which require an entity id (e.g. localization stuff). @@ -99,5 +72,46 @@ public interface IBlockHandler : IBlockHandler where T : IBlock /// The target entity clone which is about to be saved in the data storage. /// The serialized block clone. string Clone(IBlockEntity sourceEntity, IBlockEntity clonedEntity); - } + + /// + /// returns the block's render result as string. + /// + /// The block element to render. + /// A list of valid template names. The first valid template will be used for rendering. + /// Html helper instance. + IHtmlString ToHtmlString(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper); + } + + /// + /// Handles loading, saving and rendering of corresponding implementations. + /// + public interface IBlockHandler : IBlockHandler where T : IBlock + { + /// + /// Creates the implementation instance for the passed block entity. + /// + /// The serialized entity to create a block instance for. + /// A concrete block implementation instance. + T Create(IBlockEntity entity); + + /// + /// Creates and loads the implementation instance for the passed block entity and view mode. + /// + /// The serialized entity to load a block instance for. + /// Depending on view mode, some data may be loaded or not. + /// A concrete block implementation instance. + T Load(IBlockEntity entity, StoryViewMode viewMode); + + bool IsValid(T block); + + /// + /// Serializes the passed block instance and saves the result in the property. + /// + /// The block instance to save. + /// + /// The corresponding entity record for the block. In most cases will + /// be converted to JSON and assigned to property, which then will be saved in the data storage. + /// + void Save(T block, IBlockEntity entity); + } } diff --git a/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs b/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs index 0a98489d7e..e3b14ebbae 100644 --- a/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs +++ b/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs @@ -8,18 +8,18 @@ namespace SmartStore.Services.Cms /// public partial interface ILinkResolver { - /// - /// Resolves a link expression. - /// - /// - /// Link expression, e.g. product:123, category:234, topic:2, topic:aboutus etc. - /// Supported entities are product, category, manufacturer and topic. Value of topic can - /// either be the topic id or system name. - /// - /// Customer roles to check access for. null to use current customer's roles. - /// Language identifier. 0 to use current working language. - /// Store identifier. 0 to use current store. - /// LinkResolverResult - LinkResolverResult Resolve(string linkExpression, IEnumerable roles = null, int languageId = 0, int storeId = 0); + /// + /// Resolves a link expression. + /// + /// + /// Link expression, e.g. product:123, category:234, topic:2, topic:aboutus etc. + /// Supported entities are product, category, manufacturer and topic. Value of topic can + /// either be the topic id or system name. + /// + /// Customer roles to check access for. null to use current customer's roles. + /// Language identifier. 0 to use current working language. + /// Store identifier. 0 to use current store. + /// LinkResolverResult + LinkResolverResult Resolve(string linkExpression, IEnumerable roles = null, int languageId = 0, int storeId = 0); } } diff --git a/src/Libraries/SmartStore.Services/Cms/IWidgetService.cs b/src/Libraries/SmartStore.Services/Cms/IWidgetService.cs index 8e4faf8bb5..d23e345b7b 100644 --- a/src/Libraries/SmartStore.Services/Cms/IWidgetService.cs +++ b/src/Libraries/SmartStore.Services/Cms/IWidgetService.cs @@ -15,14 +15,14 @@ public partial interface IWidgetService /// Widgets IEnumerable> LoadActiveWidgets(int storeId = 0); - + /// /// Load active widgets /// /// Widget zone - /// Load records allows only in specified store; pass 0 to load all records + /// Load records allows only in specified store; pass 0 to load all records /// Widgets - IEnumerable> LoadActiveWidgetsByWidgetZone(string widgetZone, int storeId = 0); + IEnumerable> LoadActiveWidgetsByWidgetZone(string widgetZone, int storeId = 0); /// /// Load widget by system name diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs index 852e79282c..ae89d1d75b 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs @@ -7,8 +7,10 @@ using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Topics; using SmartStore.Services.Localization; using SmartStore.Services.Security; @@ -20,18 +22,18 @@ namespace SmartStore.Services.Cms { public partial class LinkResolver : ILinkResolver { - /// - /// {0} : Expression w/o q - /// {1} : LanguageId - /// {2} : Store - /// {3} : RolesIdent - /// - internal const string LINKRESOLVER_KEY = "linkresolver:{0}-{1}-{2}-{3}"; - - // 0: Expression - internal const string LINKRESOLVER_PATTERN_KEY = "linkresolver:{0}-*"; - - protected readonly ICommonServices _services; + /// + /// {0} : Expression w/o q + /// {1} : LanguageId + /// {2} : Store + /// {3} : RolesIdent + /// + internal const string LINKRESOLVER_KEY = "linkresolver:{0}-{1}-{2}-{3}"; + + // 0: Expression + internal const string LINKRESOLVER_PATTERN_KEY = "linkresolver:{0}-*"; + + protected readonly ICommonServices _services; protected readonly IUrlRecordService _urlRecordService; protected readonly ILocalizedEntityService _localizedEntityService; protected readonly IAclService _aclService; @@ -55,20 +57,18 @@ public LinkResolver( _storeMappingService = storeMappingService; _topicService = topicService; _urlHelper = urlHelper; - - QuerySettings = DbQuerySettings.Default; } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } = DbQuerySettings.Default; public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable roles = null, int languageId = 0, int storeId = 0) { - if (linkExpression.IsEmpty()) - { - return new LinkResolverResult { Type = LinkType.Url, Status = LinkStatus.NotFound }; - } + if (linkExpression.IsEmpty()) + { + return new LinkResolverResult { Type = LinkType.Url, Status = LinkStatus.NotFound }; + } - if (roles == null) + if (roles == null) { roles = _services.WorkContext.CurrentCustomer.CustomerRoleMappings.Select(x => x.CustomerRole); } @@ -83,78 +83,96 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable x.Active).Select(x => x.Id))); - - d = _services.Cache.Get(cacheKey, () => - { - var d2 = d.Clone(); - - switch (d2.Type) - { - case LinkType.Product: - GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary - { - Name = x.Name, - Published = x.Published, - Deleted = x.Deleted, - SubjectToAcl = x.SubjectToAcl, - LimitedToStores = x.LimitedToStores, + if (d.Type == LinkType.Url) + { + var url = d.Value.ToString(); + if (url.EmptyNull().StartsWith("~")) + { + url = VirtualPathUtility.ToAbsolute(url); + } + d.Link = d.Label = url; + } + else if (d.Type == LinkType.File) + { + d.Link = d.Label = d.Value.ToString(); + } + else + { + var cacheKey = LINKRESOLVER_KEY.FormatInvariant( + d.Expression.EmptyNull().ToLower(), + languageId, + storeId, + string.Join(",", roles.Where(x => x.Active).Select(x => x.Id))); + + d = _services.Cache.Get(cacheKey, () => + { + var d2 = d.Clone(); + + switch (d2.Type) + { + case LinkType.Product: + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary + { + Name = x.Name, + Published = x.Published, + Deleted = x.Deleted, + SubjectToAcl = x.SubjectToAcl, + LimitedToStores = x.LimitedToStores, PictureId = x.MainPictureId - }); - break; - case LinkType.Category: - GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary - { - Name = x.Name, + }); + break; + case LinkType.Category: + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary + { + Name = x.Name, Published = x.Published, - Deleted = x.Deleted, - SubjectToAcl = x.SubjectToAcl, - LimitedToStores = x.LimitedToStores, + Deleted = x.Deleted, + SubjectToAcl = x.SubjectToAcl, + LimitedToStores = x.LimitedToStores, + PictureId = x.MediaFileId + }); + break; + case LinkType.Manufacturer: + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary + { + Name = x.Name, + Published = x.Published, + Deleted = x.Deleted, + LimitedToStores = x.LimitedToStores, + PictureId = x.MediaFileId + }); + break; + case LinkType.Topic: + GetEntityData(d2, storeId, languageId, x => null); + break; + case LinkType.BlogPost: + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary + { + Name = x.Title, + Published = x.IsPublished, + LimitedToStores = x.LimitedToStores, PictureId = x.MediaFileId - }); - break; - case LinkType.Manufacturer: - GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary - { - Name = x.Name, + }); + break; + case LinkType.NewsItem: + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary + { + Name = x.Title, Published = x.Published, - Deleted = x.Deleted, - LimitedToStores = x.LimitedToStores, + LimitedToStores = x.LimitedToStores, PictureId = x.MediaFileId - }); - break; - case LinkType.Topic: - GetEntityData(d2, storeId, languageId, x => null); - break; - default: - throw new SmartException("Unknown link builder type."); - } - - return d2; - }); - } + }); + break; + default: + throw new SmartException("Unknown link builder type."); + } + + return d2; + }); + } var result = new LinkResolverResult { @@ -175,19 +193,21 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable -1) - { - type = expression.Substring(0, colonIndex); - if (type.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - type = null; - colonIndex = -1; - } - } - - path = expression.Substring(colonIndex + 1); - - var qmIndex = path.IndexOf('?'); - if (qmIndex > -1) - { - query = path.Substring(qmIndex + 1); - path = path.Substring(0, qmIndex); - } - - return true; - } + { + type = null; + path = null; + query = null; + + if (string.IsNullOrWhiteSpace(expression)) + { + return false; + } + + var colonIndex = expression.IndexOf(':'); + if (colonIndex > -1) + { + type = expression.Substring(0, colonIndex); + if (type.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + type = null; + colonIndex = -1; + } + } + + path = expression.Substring(colonIndex + 1); + + var qmIndex = path.IndexOf('?'); + if (qmIndex > -1) + { + query = path.Substring(qmIndex + 1); + path = path.Substring(0, qmIndex); + } + + return true; + } protected virtual string GetLocalized(int entityId, string localeKeyGroup, string localeKey, int languageId, string defaultValue) { @@ -240,46 +260,48 @@ protected virtual string GetLocalized(int entityId, string localeKeyGroup, strin protected virtual LinkResolverData Parse(string linkExpression) { - if (TokenizeExpression(linkExpression, out var type, out var path, out var query)) - { - if (!string.IsNullOrWhiteSpace(type) && Enum.TryParse(type, true, out LinkType linkType)) - { - var result = new LinkResolverData { Type = linkType, Expression = string.Concat(type, ":", path) }; - - switch (linkType) - { - case LinkType.Product: - case LinkType.Category: - case LinkType.Manufacturer: - case LinkType.Topic: - if (int.TryParse(path, out var id)) - { - // Reduce thrown exceptions in console - result.Value = id; - } - else - { - result.Value = path; - } - - result.QueryString = query; - break; - case LinkType.Url: - result.Value = path + (query.HasValue() ? "?" + query : ""); - break; - case LinkType.File: - result.Value = path; - result.QueryString = query; - break; - default: - throw new SmartException("Unknown link builder type."); - } - - return result; - } - } - - return new LinkResolverData { Type = LinkType.Url, Value = linkExpression.EmptyNull() }; + if (TokenizeExpression(linkExpression, out var type, out var path, out var query)) + { + if (!string.IsNullOrWhiteSpace(type) && Enum.TryParse(type, true, out LinkType linkType)) + { + var result = new LinkResolverData { Type = linkType, Expression = string.Concat(type, ":", path) }; + + switch (linkType) + { + case LinkType.Product: + case LinkType.Category: + case LinkType.Manufacturer: + case LinkType.Topic: + case LinkType.BlogPost: + case LinkType.NewsItem: + if (int.TryParse(path, out var id)) + { + // Reduce thrown exceptions in console + result.Value = id; + } + else + { + result.Value = path; + } + + result.QueryString = query; + break; + case LinkType.Url: + result.Value = path + (query.HasValue() ? "?" + query : ""); + break; + case LinkType.File: + result.Value = path; + result.QueryString = query; + break; + default: + throw new SmartException("Unknown link builder type."); + } + + return result; + } + } + + return new LinkResolverData { Type = LinkType.Url, Value = linkExpression.EmptyNull() }; } internal void GetEntityData( @@ -351,18 +373,23 @@ internal void GetEntityData( data.Status = summary.Deleted ? LinkStatus.NotFound : summary.Published ? LinkStatus.Ok : LinkStatus.Hidden; - - if (data.Type == LinkType.Topic) - { - data.Label = GetLocalized(data.Id, entityName, nameof(Topic.ShortTitle), languageId, null) - ?? GetLocalized(data.Id, entityName, "Title", languageId, null) - ?? summary.ShortTitle.NullEmpty() - ?? summary.Title.NullEmpty() - ?? summary.Name; - } - else + + switch (data.Type) { - data.Label = GetLocalized(data.Id, entityName, "Name", languageId, summary.Name); + case LinkType.Topic: + data.Label = GetLocalized(data.Id, entityName, nameof(Topic.ShortTitle), languageId, null) + ?? GetLocalized(data.Id, entityName, "Title", languageId, null) + ?? summary.ShortTitle.NullEmpty() + ?? summary.Title.NullEmpty() + ?? summary.Name; + break; + case LinkType.BlogPost: + case LinkType.NewsItem: + data.Label = GetLocalized(data.Id, entityName, "Title", languageId, summary.Name); + break; + default: + data.Label = GetLocalized(data.Id, entityName, "Name", languageId, summary.Name); + break; } data.Slug = _urlRecordService.GetActiveSlug(data.Id, entityName, languageId).NullEmpty() ?? _urlRecordService.GetActiveSlug(data.Id, entityName, 0); diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs index 2eab564051..aba228ccff 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs @@ -12,18 +12,18 @@ namespace SmartStore.Services.Cms { - public partial class LinkResolverInvalidator : DbSaveHook - { - private readonly ICommonServices _services; + public partial class LinkResolverInvalidator : DbSaveHook + { + private readonly ICommonServices _services; - private static readonly HashSet _toxicProps = new HashSet - { - nameof(Topic.SystemName), - nameof(Topic.IsPublished), - nameof(Topic.SubjectToAcl), - nameof(Topic.LimitedToStores), - nameof(Topic.Title), - nameof(Topic.ShortTitle), + private static readonly HashSet _toxicProps = new HashSet + { + nameof(Topic.SystemName), + nameof(Topic.IsPublished), + nameof(Topic.SubjectToAcl), + nameof(Topic.LimitedToStores), + nameof(Topic.Title), + nameof(Topic.ShortTitle), nameof(Topic.Intro), nameof(Product.Name), nameof(Product.Deleted), @@ -31,65 +31,68 @@ public partial class LinkResolverInvalidator : DbSaveHook nameof(Category.Published), nameof(Category.MediaFileId), nameof(StoreMapping.StoreId), - nameof(UrlRecord.IsActive), - nameof(UrlRecord.Slug) - }; + nameof(UrlRecord.IsActive), + nameof(UrlRecord.Slug) + }; - public LinkResolverInvalidator(ICommonServices services) - { - _services = services; - } + public LinkResolverInvalidator(ICommonServices services) + { + _services = services; + } - public override void OnBeforeSave(IHookedEntity entry) - { - var cache = _services.Cache; - var e = entry.Entity; + public override void OnBeforeSave(IHookedEntity entry) + { + var cache = _services.Cache; + var e = entry.Entity; - var evict = e is Topic || e is Product || e is Category || e is Manufacturer || e is StoreMapping || e is UrlRecord; - if (evict && entry.InitialState == EntityState.Modified) - { - var modProps = entry.Entry.GetModifiedProperties(_services.DbContext); - evict = modProps.Keys.Any(x => _toxicProps.Contains(x)); - } + var evict = e is Topic || e is Product || e is Category || e is Manufacturer || e is StoreMapping || e is UrlRecord; + if (!evict) + throw new NotSupportedException(); // Perf - if (!evict) - return; + if (evict && entry.InitialState == EntityState.Modified) + { + var modProps = entry.Entry.GetModifiedProperties(_services.DbContext); + evict = modProps.Keys.Any(x => _toxicProps.Contains(x)); + } - int? evictTopicId = null; + if (!evict) + return; - if (e is Topic t) - { - cache.RemoveByPattern(BuildPatternKey("topic", t.Id)); - cache.RemoveByPattern(BuildPatternKey("topic", t.SystemName)); - } - else if (e is Category || e is Product || e is Manufacturer) - { - cache.RemoveByPattern(BuildPatternKey(e.GetEntityName().ToLowerInvariant(), e.Id)); - } - else if (e is UrlRecord ur) - { - cache.RemoveByPattern(BuildPatternKey(ur.EntityName.ToLowerInvariant(), ur.EntityId)); - evictTopicId = ur.EntityId; - } - else if (e is StoreMapping sm) - { - cache.RemoveByPattern(BuildPatternKey(sm.EntityName.ToLowerInvariant(), sm.EntityId)); - evictTopicId = sm.EntityId; - } + int? evictTopicId = null; - if (evictTopicId.HasValue) - { - var systemName = _services.DbContext.Set().Where(x => x.Id == evictTopicId.Value).Select(x => x.SystemName).FirstOrDefault(); - if (systemName.HasValue()) - { - cache.RemoveByPattern(BuildPatternKey("topic", systemName)); - } - } - } + if (e is Topic t) + { + cache.RemoveByPattern(BuildPatternKey("topic", t.Id)); + cache.RemoveByPattern(BuildPatternKey("topic", t.SystemName)); + } + else if (e is Category || e is Product || e is Manufacturer) + { + cache.RemoveByPattern(BuildPatternKey(e.GetEntityName().ToLowerInvariant(), e.Id)); + } + else if (e is UrlRecord ur) + { + cache.RemoveByPattern(BuildPatternKey(ur.EntityName.ToLowerInvariant(), ur.EntityId)); + evictTopicId = ur.EntityId; + } + else if (e is StoreMapping sm) + { + cache.RemoveByPattern(BuildPatternKey(sm.EntityName.ToLowerInvariant(), sm.EntityId)); + evictTopicId = sm.EntityId; + } - private string BuildPatternKey(string entityName, object ident) - { - return LinkResolver.LINKRESOLVER_PATTERN_KEY.FormatInvariant(String.Concat(entityName, ":", ident)); - } - } + if (evictTopicId.HasValue) + { + var systemName = _services.DbContext.Set().Where(x => x.Id == evictTopicId.Value).Select(x => x.SystemName).FirstOrDefault(); + if (systemName.HasValue()) + { + cache.RemoveByPattern(BuildPatternKey("topic", systemName)); + } + } + } + + private string BuildPatternKey(string entityName, object ident) + { + return LinkResolver.LINKRESOLVER_PATTERN_KEY.FormatInvariant(String.Concat(entityName, ":", ident)); + } + } } diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs index 71002103a9..ac97fd982d 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs @@ -8,6 +8,8 @@ public enum LinkType Category, Manufacturer, Topic, + BlogPost, + NewsItem, Url = 20, File = 30 } @@ -15,8 +17,8 @@ public enum LinkType public enum LinkStatus { Ok, - Forbidden, - NotFound, + Forbidden, + NotFound, Hidden } @@ -24,17 +26,17 @@ public class LinkResolverResult { private string _link; - /// - /// The raw expression without query string. - /// - public string Expression { get; set; } + /// + /// The raw expression without query string. + /// + public string Expression { get; set; } - /// - /// The query string part. - /// - public string QueryString { get; set; } + /// + /// The query string part. + /// + public string QueryString { get; set; } - public LinkType Type { get; set; } + public LinkType Type { get; set; } public object Value { get; set; } public LinkStatus Status { get; set; } @@ -71,10 +73,10 @@ public string Link } public override string ToString() - { - return this.Link.EmptyNull(); - } - } + { + return this.Link.EmptyNull(); + } + } [Serializable] public partial class LinkResolverData : LinkResolverResult, ICloneable @@ -84,15 +86,15 @@ public partial class LinkResolverData : LinkResolverResult, ICloneableMenu entities. IPagedList GetAllMenus( string systemName = null, - int storeId = 0, + int storeId = 0, bool includeHidden = false, int pageIndex = 0, int pageSize = int.MaxValue); diff --git a/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs b/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs index 93bc1a9c84..748535a4f4 100644 --- a/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs +++ b/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs @@ -190,7 +190,7 @@ public virtual bool MenuExists(string systemName) return false; } - return GetMenuSystemNames(true).Contains(systemName); + return GetMenuSystemNames(true).Contains(systemName); } #region Menu items @@ -207,8 +207,8 @@ public virtual void InsertMenuItem(MenuItemRecord item) _menuItemRepository.Insert(item); - _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); - } + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } public virtual void UpdateMenuItem(MenuItemRecord item) { @@ -222,8 +222,8 @@ public virtual void UpdateMenuItem(MenuItemRecord item) _menuItemRepository.Update(item); - _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); - } + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } public virtual void DeleteMenuItem(MenuItemRecord item, bool deleteChilds = true) { @@ -236,21 +236,21 @@ public virtual void DeleteMenuItem(MenuItemRecord item, bool deleteChilds = true { _menuItemRepository.Delete(item); } - else - { - var ids = new HashSet { item.Id }; - GetChildIds(item.Id, ids); + else + { + var ids = new HashSet { item.Id }; + GetChildIds(item.Id, ids); - foreach (var chunk in ids.Slice(200)) - { - var items = _menuItemRepository.Table.Where(x => chunk.Contains(x.Id)).ToList(); - _menuItemRepository.DeleteRange(items); - } - } + foreach (var chunk in ids.Slice(200)) + { + var items = _menuItemRepository.Table.Where(x => chunk.Contains(x.Id)).ToList(); + _menuItemRepository.DeleteRange(items); + } + } - _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); - void GetChildIds(int parentId, HashSet ids) + void GetChildIds(int parentId, HashSet ids) { var childIds = _menuItemRepository.TableUntracked .Where(x => x.ParentItemId == parentId) @@ -302,22 +302,22 @@ public virtual IList GetMenuItems(string systemName, int storeId #region Utilities private ISet GetMenuSystemNames(bool ensureCreated) - { - if (ensureCreated || _services.Cache.Contains(MENU_ALLSYSTEMNAMES_CACHE_KEY)) - { - return _services.Cache.GetHashSet(MENU_ALLSYSTEMNAMES_CACHE_KEY, () => - { - return _menuRepository.TableUntracked - .Where(x => x.Published) + { + if (ensureCreated || _services.Cache.Contains(MENU_ALLSYSTEMNAMES_CACHE_KEY)) + { + return _services.Cache.GetHashSet(MENU_ALLSYSTEMNAMES_CACHE_KEY, () => + { + return _menuRepository.TableUntracked + .Where(x => x.Published) .OrderByDescending(x => x.IsSystemMenu) .ThenBy(x => x.Id) - .Select(x => x.SystemName) - .ToArray(); - }); - } + .Select(x => x.SystemName) + .ToArray(); + }); + } - return null; - } + return null; + } protected virtual IQueryable BuildMenuQuery( int id, @@ -352,7 +352,7 @@ protected virtual IQueryable BuildMenuQuery( if (storeId > 0 && !QuerySettings.IgnoreMultiStore) { - query = + query = from x in query join m in _storeMappingRepository.Table on new { x1 = x.Id, x2 = entityName } equals new { x1 = m.EntityId, x2 = m.EntityName } into sm @@ -367,7 +367,7 @@ from m in sm.DefaultIfEmpty() { var allowedRoleIds = _services.WorkContext.CurrentCustomer.GetRoleIds(); - query = + query = from x in query join a in _aclRepository.Table on new { x1 = x.Id, x2 = entityName } equals new { x1 = a.EntityId, x2 = a.EntityName } into ac @@ -380,7 +380,7 @@ from a in ac.DefaultIfEmpty() if (applied && groupBy) { - query = + query = from x in query group x by x.Id into grp orderby grp.Key diff --git a/src/Libraries/SmartStore.Services/Cms/WidgetExtensions.cs b/src/Libraries/SmartStore.Services/Cms/WidgetExtensions.cs index 9e435b04d4..d5f860f1a2 100644 --- a/src/Libraries/SmartStore.Services/Cms/WidgetExtensions.cs +++ b/src/Libraries/SmartStore.Services/Cms/WidgetExtensions.cs @@ -3,28 +3,28 @@ using SmartStore.Core.Domain.Cms; using SmartStore.Core.Plugins; -namespace SmartStore.Services.Cms +namespace SmartStore.Services.Cms { public static class WidgetExtensions { public static bool IsWidgetActive(this Provider widget, WidgetSettings widgetSettings) { - Guard.NotNull(widget, nameof(widget)); + Guard.NotNull(widget, nameof(widget)); - return widget.ToLazy().IsWidgetActive(widgetSettings); + return widget.ToLazy().IsWidgetActive(widgetSettings); } - public static bool IsWidgetActive(this Lazy widget, WidgetSettings widgetSettings) - { - Guard.NotNull(widget, nameof(widget)); - Guard.NotNull(widgetSettings, nameof(widgetSettings)); + public static bool IsWidgetActive(this Lazy widget, WidgetSettings widgetSettings) + { + Guard.NotNull(widget, nameof(widget)); + Guard.NotNull(widgetSettings, nameof(widgetSettings)); - if (widgetSettings.ActiveWidgetSystemNames == null) - { - return false; - } + if (widgetSettings.ActiveWidgetSystemNames == null) + { + return false; + } - return widgetSettings.ActiveWidgetSystemNames.Contains(widget.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); - } + return widgetSettings.ActiveWidgetSystemNames.Contains(widget.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Cms/WidgetService.cs b/src/Libraries/SmartStore.Services/Cms/WidgetService.cs index a3d6a8b135..6f2acd907d 100644 --- a/src/Libraries/SmartStore.Services/Cms/WidgetService.cs +++ b/src/Libraries/SmartStore.Services/Cms/WidgetService.cs @@ -17,17 +17,17 @@ public partial class WidgetService : IWidgetService #region Fields & Consts - private const string WIDGETS_ACTIVE_KEY = "SmartStore.widgets.active-{0}"; - private const string WIDGETS_ZONEMAPPED_KEY = "SmartStore.widgets.zonemapped-{0}"; + private const string WIDGETS_ACTIVE_KEY = "SmartStore.widgets.active-{0}"; + private const string WIDGETS_ZONEMAPPED_KEY = "SmartStore.widgets.zonemapped-{0}"; private readonly IPluginFinder _pluginFinder; private readonly WidgetSettings _widgetSettings; - private readonly ISettingService _settingService; - private readonly IProviderManager _providerManager; - private readonly IRequestCache _requestCache; + private readonly ISettingService _settingService; + private readonly IProviderManager _providerManager; + private readonly IRequestCache _requestCache; #endregion - + #region Ctor /// @@ -35,34 +35,34 @@ public partial class WidgetService : IWidgetService /// /// Plugin finder /// Widget settings - /// Plugin service - public WidgetService( - IPluginFinder pluginFinder, - WidgetSettings widgetSettings, - ISettingService settingService, - IProviderManager providerManager, - IRequestCache requestCache) + /// Plugin service + public WidgetService( + IPluginFinder pluginFinder, + WidgetSettings widgetSettings, + ISettingService settingService, + IProviderManager providerManager, + IRequestCache requestCache) { this._pluginFinder = pluginFinder; this._widgetSettings = widgetSettings; - this._settingService = settingService; - this._providerManager = providerManager; - this._requestCache = requestCache; + this._settingService = settingService; + this._providerManager = providerManager; + this._requestCache = requestCache; } #endregion #region Methods - /// - /// Load all widgets - /// - /// Load records allows only in specified store; pass 0 to load all records - /// Widgets - public virtual IEnumerable> LoadAllWidgets(int storeId = 0) - { - return _providerManager.GetAllProviders(storeId); - } + /// + /// Load all widgets + /// + /// Load records allows only in specified store; pass 0 to load all records + /// Widgets + public virtual IEnumerable> LoadAllWidgets(int storeId = 0) + { + return _providerManager.GetAllProviders(storeId); + } /// /// Load active widgets @@ -71,12 +71,13 @@ public virtual IEnumerable> LoadAllWidgets(int storeId = 0) /// Widgets public virtual IEnumerable> LoadActiveWidgets(int storeId = 0) { - var activeWidgets = _requestCache.Get(WIDGETS_ACTIVE_KEY.FormatInvariant(storeId), () => { - var allWigets = LoadAllWidgets(storeId); - return allWigets.Where(p => _widgetSettings.ActiveWidgetSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)).ToList(); - }); + var activeWidgets = _requestCache.Get(WIDGETS_ACTIVE_KEY.FormatInvariant(storeId), () => + { + var allWigets = LoadAllWidgets(storeId); + return allWigets.Where(p => _widgetSettings.ActiveWidgetSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)).ToList(); + }); - return activeWidgets; + return activeWidgets; } /// @@ -88,44 +89,45 @@ public virtual IEnumerable> LoadActiveWidgets(int storeId = 0) public virtual IEnumerable> LoadActiveWidgetsByWidgetZone(string widgetZone, int storeId = 0) { if (widgetZone.IsEmpty()) - return Enumerable.Empty>(); - - var mappedWidgets = _requestCache.Get(WIDGETS_ZONEMAPPED_KEY.FormatInvariant(storeId), () => { - var activeWidgets = LoadActiveWidgets(storeId); - var map = new Multimap>(); - - foreach (var widget in activeWidgets) - { - var zones = widget.Value.GetWidgetZones(); - if (zones != null && zones.Any()) - { - foreach (var zone in zones.Select(x => x.ToLower())) - { - map.Add(zone, widget); - } - } - } - - return map; - }); - - widgetZone = widgetZone.ToLower(); - if (mappedWidgets.ContainsKey(widgetZone)) - { - return mappedWidgets[widgetZone]; - } - - return Enumerable.Empty>(); + return Enumerable.Empty>(); + + var mappedWidgets = _requestCache.Get(WIDGETS_ZONEMAPPED_KEY.FormatInvariant(storeId), () => + { + var activeWidgets = LoadActiveWidgets(storeId); + var map = new Multimap>(); + + foreach (var widget in activeWidgets) + { + var zones = widget.Value.GetWidgetZones(); + if (zones != null && zones.Any()) + { + foreach (var zone in zones.Select(x => x.ToLower())) + { + map.Add(zone, widget); + } + } + } + + return map; + }); + + widgetZone = widgetZone.ToLower(); + if (mappedWidgets.ContainsKey(widgetZone)) + { + return mappedWidgets[widgetZone]; + } + + return Enumerable.Empty>(); } /// /// Load widget by system name /// - /// System name + /// System name /// Found widget - public virtual Provider LoadWidgetBySystemName(string systemName) + public virtual Provider LoadWidgetBySystemName(string systemName) { - return _providerManager.GetProvider(systemName); + return _providerManager.GetProvider(systemName); } #endregion diff --git a/src/Libraries/SmartStore.Services/Common/AddressExtensions.cs b/src/Libraries/SmartStore.Services/Common/AddressExtensions.cs index bd749898d4..4a3da77c14 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressExtensions.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressExtensions.cs @@ -8,135 +8,135 @@ namespace SmartStore.Services.Common { public static class AddressExtensions { - /// - /// Find first occurrence of an address - /// - /// Addresses to be searched - /// Address to find - /// First occurrence of address - public static Address FindAddress(this ICollection
source, Address address) - { - return source.FindAddress( - address.FirstName, - address.LastName, - address.PhoneNumber, - address.Email, - address.FaxNumber, - address.Company, - address.Address1, - address.Address2, - address.City, - address.StateProvinceId, - address.ZipPostalCode, - address.CountryId - ); - } + /// + /// Find first occurrence of an address + /// + /// Addresses to be searched + /// Address to find + /// First occurrence of address + public static Address FindAddress(this ICollection
source, Address address) + { + return source.FindAddress( + address.FirstName, + address.LastName, + address.PhoneNumber, + address.Email, + address.FaxNumber, + address.Company, + address.Address1, + address.Address2, + address.City, + address.StateProvinceId, + address.ZipPostalCode, + address.CountryId + ); + } - /// - /// Find an address - /// - /// Source - /// First name - /// Last name - /// Phone number - /// Email - /// Fax number - /// Company - /// Address 1 - /// Address 2 - /// City - /// State/province identifier - /// Zip postal code - /// Country identifier - /// Address - public static Address FindAddress( - this ICollection
source, - string firstName, - string lastName, - string phoneNumber, - string email, - string faxNumber, - string company, - string address1, - string address2, - string city, - int? stateProvinceId, - string zipPostalCode, - int? countryId) + /// + /// Find an address + /// + /// Source + /// First name + /// Last name + /// Phone number + /// Email + /// Fax number + /// Company + /// Address 1 + /// Address 2 + /// City + /// State/province identifier + /// Zip postal code + /// Country identifier + /// Address + public static Address FindAddress( + this ICollection
source, + string firstName, + string lastName, + string phoneNumber, + string email, + string faxNumber, + string company, + string address1, + string address2, + string city, + int? stateProvinceId, + string zipPostalCode, + int? countryId) { - Func addressMatcher = (x) => - { - return x.Email.IsCaseInsensitiveEqual(email) - && x.LastName.IsCaseInsensitiveEqual(lastName) - && x.FirstName.IsCaseInsensitiveEqual(firstName) - && x.Address1.IsCaseInsensitiveEqual(address1) - && x.Address2.IsCaseInsensitiveEqual(address2) - && x.Company.IsCaseInsensitiveEqual(company) - && x.ZipPostalCode.IsCaseInsensitiveEqual(zipPostalCode) - && x.City.IsCaseInsensitiveEqual(city) - && x.PhoneNumber.IsCaseInsensitiveEqual(phoneNumber) - && x.FaxNumber.IsCaseInsensitiveEqual(faxNumber) - && x.StateProvinceId == stateProvinceId - && x.CountryId == countryId; - }; + Func addressMatcher = (x) => + { + return x.Email.IsCaseInsensitiveEqual(email) + && x.LastName.IsCaseInsensitiveEqual(lastName) + && x.FirstName.IsCaseInsensitiveEqual(firstName) + && x.Address1.IsCaseInsensitiveEqual(address1) + && x.Address2.IsCaseInsensitiveEqual(address2) + && x.Company.IsCaseInsensitiveEqual(company) + && x.ZipPostalCode.IsCaseInsensitiveEqual(zipPostalCode) + && x.City.IsCaseInsensitiveEqual(city) + && x.PhoneNumber.IsCaseInsensitiveEqual(phoneNumber) + && x.FaxNumber.IsCaseInsensitiveEqual(faxNumber) + && x.StateProvinceId == stateProvinceId + && x.CountryId == countryId; + }; - return source.FirstOrDefault(addressMatcher); + return source.FirstOrDefault(addressMatcher); } - /// - /// Returns the full name of the address. - /// - public static string GetFullName(this Address address, bool withCompanyName = true) - { - if (address == null) - return null; + /// + /// Returns the full name of the address. + /// + public static string GetFullName(this Address address, bool withCompanyName = true) + { + if (address == null) + return null; - string result = string.Empty; - if (address.FirstName.HasValue() || address.LastName.HasValue()) - { - result = string.Format("{0} {1}", address.FirstName, address.LastName).Trim(); - } + string result = string.Empty; + if (address.FirstName.HasValue() || address.LastName.HasValue()) + { + result = string.Format("{0} {1}", address.FirstName, address.LastName).Trim(); + } - if (withCompanyName && address.Company.HasValue()) - { - result = string.Concat(result, result.HasValue() ? ", " : "", address.Company); - } + if (withCompanyName && address.Company.HasValue()) + { + result = string.Concat(result, result.HasValue() ? ", " : "", address.Company); + } - return result; - } + return result; + } - public static string GetFullSalutaion(this Address address) - { - if (address == null) - return null; + public static string GetFullSalutaion(this Address address) + { + if (address == null) + return null; - return string.Format("{0}{1}", - address.Salutation.EmptyNull(), - address.Title.HasValue() ? " " + address.Title : ""); - } + return string.Format("{0}{1}", + address.Salutation.EmptyNull(), + address.Title.HasValue() ? " " + address.Title : ""); + } - /// - /// Checks whether the postal data of two addresses are equal. - /// - public static bool IsPostalDataEqual(this Address address, Address other) - { - if (address != null && other != null) - { - if (address.FirstName.IsCaseInsensitiveEqual(other.FirstName) && - address.LastName.IsCaseInsensitiveEqual(other.LastName) && - address.Company.IsCaseInsensitiveEqual(other.Company) && - address.Address1.IsCaseInsensitiveEqual(other.Address1) && - address.Address2.IsCaseInsensitiveEqual(other.Address2) && - address.ZipPostalCode.IsCaseInsensitiveEqual(other.ZipPostalCode) && - address.City.IsCaseInsensitiveEqual(other.City) && - address.StateProvinceId == other.StateProvinceId && - address.CountryId == other.CountryId) - { - return true; - } - } + /// + /// Checks whether the postal data of two addresses are equal. + /// + public static bool IsPostalDataEqual(this Address address, Address other) + { + if (address != null && other != null) + { + if (address.FirstName.IsCaseInsensitiveEqual(other.FirstName) && + address.LastName.IsCaseInsensitiveEqual(other.LastName) && + address.Company.IsCaseInsensitiveEqual(other.Company) && + address.Address1.IsCaseInsensitiveEqual(other.Address1) && + address.Address2.IsCaseInsensitiveEqual(other.Address2) && + address.ZipPostalCode.IsCaseInsensitiveEqual(other.ZipPostalCode) && + address.City.IsCaseInsensitiveEqual(other.City) && + address.StateProvinceId == other.StateProvinceId && + address.CountryId == other.CountryId) + { + return true; + } + } - return false; - } + return false; + } } } diff --git a/src/Libraries/SmartStore.Services/Common/AddressService.cs b/src/Libraries/SmartStore.Services/Common/AddressService.cs index de52593d01..3953689090 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressService.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressService.cs @@ -18,42 +18,42 @@ public partial class AddressService : IAddressService private readonly IRepository
_addressRepository; private readonly ICountryService _countryService; private readonly IStateProvinceService _stateProvinceService; - private readonly ICommonServices _services; + private readonly ICommonServices _services; private readonly AddressSettings _addressSettings; - private readonly ITemplateEngine _templateEngine; - private readonly IMessageModelProvider _messageModelProvider; - - public AddressService( - IRepository
addressRepository, - ICountryService countryService, - IStateProvinceService stateProvinceService, - ICommonServices services, - AddressSettings addressSettings, - ITemplateEngine templateEngine, - IMessageModelProvider messageModelProvider) + private readonly ITemplateEngine _templateEngine; + private readonly IMessageModelProvider _messageModelProvider; + + public AddressService( + IRepository
addressRepository, + ICountryService countryService, + IStateProvinceService stateProvinceService, + ICommonServices services, + AddressSettings addressSettings, + ITemplateEngine templateEngine, + IMessageModelProvider messageModelProvider) { _addressRepository = addressRepository; _countryService = countryService; _stateProvinceService = stateProvinceService; _services = services; _addressSettings = addressSettings; - _templateEngine = templateEngine; - _messageModelProvider = messageModelProvider; + _templateEngine = templateEngine; + _messageModelProvider = messageModelProvider; } public virtual void DeleteAddress(Address address) { - Guard.NotNull(address, nameof(address)); + Guard.NotNull(address, nameof(address)); _addressRepository.Delete(address); } - public virtual void DeleteAddress(int id) - { - var address = GetAddressById(id); - if (address != null) - DeleteAddress(address); - } + public virtual void DeleteAddress(int id) + { + var address = GetAddressById(id); + if (address != null) + DeleteAddress(address); + } public virtual int GetAddressTotalByCountryId(int countryId) { @@ -86,23 +86,23 @@ public virtual Address GetAddressById(int addressId) return address; } - public virtual IList
GetAddressByIds(int[] addressIds) - { - Guard.NotNull(addressIds, nameof(addressIds)); + public virtual IList
GetAddressByIds(int[] addressIds) + { + Guard.NotNull(addressIds, nameof(addressIds)); - var query = - from x in _addressRepository.TableUntracked.Expand(x => x.Country).Expand(x => x.StateProvince) - where addressIds.Contains(x.Id) - select x; + var query = + from x in _addressRepository.TableUntracked.Expand(x => x.Country).Expand(x => x.StateProvince) + where addressIds.Contains(x.Id) + select x; - return query.ToList(); - } + return query.ToList(); + } public virtual void InsertAddress(Address address) { - Guard.NotNull(address, nameof(address)); + Guard.NotNull(address, nameof(address)); - address.CreatedOnUtc = DateTime.UtcNow; + address.CreatedOnUtc = DateTime.UtcNow; //some validation if (address.CountryId == 0) @@ -115,10 +115,10 @@ public virtual void InsertAddress(Address address) public virtual void UpdateAddress(Address address) { - Guard.NotNull(address, nameof(address)); + Guard.NotNull(address, nameof(address)); - //some validation - if (address.CountryId == 0) + //some validation + if (address.CountryId == 0) address.CountryId = null; if (address.StateProvinceId == 0) address.StateProvinceId = null; @@ -128,9 +128,9 @@ public virtual void UpdateAddress(Address address) public virtual bool IsAddressValid(Address address) { - Guard.NotNull(address, nameof(address)); + Guard.NotNull(address, nameof(address)); - if (String.IsNullOrWhiteSpace(address.FirstName)) + if (String.IsNullOrWhiteSpace(address.FirstName)) return false; if (String.IsNullOrWhiteSpace(address.LastName)) @@ -184,10 +184,10 @@ public virtual bool IsAddressValid(Address address) } } - if (_addressSettings.CityEnabled && - _addressSettings.CityRequired && - String.IsNullOrWhiteSpace(address.City)) - return false; + if (_addressSettings.CityEnabled && + _addressSettings.CityRequired && + String.IsNullOrWhiteSpace(address.City)) + return false; if (_addressSettings.PhoneEnabled && _addressSettings.PhoneRequired && @@ -202,63 +202,63 @@ public virtual bool IsAddressValid(Address address) return true; } - public virtual string FormatAddress(CompanyInformationSettings settings, bool newLineToBr = false) - { - Guard.NotNull(settings, nameof(settings)); - - var address = new Address - { - Address1 = settings.Street, - Address2 = settings.Street2, - City = settings.City, - Company = settings.CompanyName, - FirstName = settings.Firstname, - LastName = settings.Lastname, - Salutation = settings.Salutation, - Title = settings.Title, - ZipPostalCode = settings.ZipCode, - CountryId = settings.CountryId, - Country = _countryService.GetCountryById(settings.CountryId) - }; - - return FormatAddress(address, newLineToBr); - } - - public virtual string FormatAddress(Address address, bool newLineToBr = false) - { - Guard.NotNull(address, nameof(address)); - - var messageContext = new MessageContext - { - Language = _services.WorkContext.WorkingLanguage, - Store = _services.StoreContext.CurrentStore, - Model = new TemplateModel() - }; - - _messageModelProvider.AddModelPart(address, messageContext, "Address"); - var model = messageContext.Model["Address"]; - - var result = FormatAddress(model, address?.Country?.AddressFormat, messageContext.FormatProvider); - - if (newLineToBr) - { - result = HtmlUtils.ConvertPlainTextToHtml(result); - } - - return result; - } - - public virtual string FormatAddress(object address, string template = null, IFormatProvider formatProvider = null) - { - Guard.NotNull(address, nameof(address)); - - template = template.NullEmpty() ?? Address.DefaultAddressFormat; - - var result = _templateEngine - .Render(template, address, formatProvider ?? CultureInfo.CurrentCulture) - .Compact(true); - - return result; - } - } + public virtual string FormatAddress(CompanyInformationSettings settings, bool newLineToBr = false) + { + Guard.NotNull(settings, nameof(settings)); + + var address = new Address + { + Address1 = settings.Street, + Address2 = settings.Street2, + City = settings.City, + Company = settings.CompanyName, + FirstName = settings.Firstname, + LastName = settings.Lastname, + Salutation = settings.Salutation, + Title = settings.Title, + ZipPostalCode = settings.ZipCode, + CountryId = settings.CountryId, + Country = _countryService.GetCountryById(settings.CountryId) + }; + + return FormatAddress(address, newLineToBr); + } + + public virtual string FormatAddress(Address address, bool newLineToBr = false) + { + Guard.NotNull(address, nameof(address)); + + var messageContext = new MessageContext + { + Language = _services.WorkContext.WorkingLanguage, + Store = _services.StoreContext.CurrentStore, + Model = new TemplateModel() + }; + + _messageModelProvider.AddModelPart(address, messageContext, "Address"); + var model = messageContext.Model["Address"]; + + var result = FormatAddress(model, address?.Country?.AddressFormat, messageContext.FormatProvider); + + if (newLineToBr) + { + result = HtmlUtils.ConvertPlainTextToHtml(result); + } + + return result; + } + + public virtual string FormatAddress(object address, string template = null, IFormatProvider formatProvider = null) + { + Guard.NotNull(address, nameof(address)); + + template = template.NullEmpty() ?? Address.DefaultAddressFormat; + + var result = _templateEngine + .Render(template, address, formatProvider ?? CultureInfo.CurrentCulture) + .Compact(true); + + return result; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeExtensions.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeExtensions.cs index 6313febf45..ad5307de84 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeExtensions.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeExtensions.cs @@ -20,32 +20,32 @@ public static class GenericAttributeExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TProp GetAttribute(this BaseEntity entity, string key, int storeId = 0) { - return GetAttribute( - entity, - key, - EngineContext.Current.Resolve(), - storeId); + return GetAttribute( + entity, + key, + EngineContext.Current.Resolve(), + storeId); } - /// - /// Gets an entity generic attribute value - /// - /// Property type - /// Entity - /// Key - /// GenericAttributeService - /// Load a value specific for a certain store; pass 0 to load a value shared for all stores - /// Attribute value - public static TProp GetAttribute(this BaseEntity entity, string key, IGenericAttributeService genericAttributeService, int storeId = 0) + /// + /// Gets an entity generic attribute value + /// + /// Property type + /// Entity + /// Key + /// GenericAttributeService + /// Load a value specific for a certain store; pass 0 to load a value shared for all stores + /// Attribute value + public static TProp GetAttribute(this BaseEntity entity, string key, IGenericAttributeService genericAttributeService, int storeId = 0) { - if (entity == null) - throw new ArgumentNullException(nameof(entity)); + if (entity == null) + throw new ArgumentNullException(nameof(entity)); - return (genericAttributeService ?? EngineContext.Current.Resolve()).GetAttribute( - entity.GetUnproxiedType().Name, - entity.Id, - key, - storeId); - } + return (genericAttributeService ?? EngineContext.Current.Resolve()).GetAttribute( + entity.GetUnproxiedType().Name, + entity.Id, + key, + storeId); + } } } diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs index 3fb471d1d0..6fca33e692 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs @@ -14,36 +14,36 @@ namespace SmartStore.Services.Common { public partial class GenericAttributeService : IGenericAttributeService { - private readonly IRepository _genericAttributeRepository; + private readonly IRepository _genericAttributeRepository; private readonly IEventPublisher _eventPublisher; - private readonly IRepository _orderRepository; - private readonly IDictionary> _valueCache; + private readonly IRepository _orderRepository; + private readonly IDictionary> _valueCache; - public GenericAttributeService( + public GenericAttributeService( IRepository genericAttributeRepository, IEventPublisher eventPublisher, - IRepository orderRepository) + IRepository orderRepository) { _genericAttributeRepository = genericAttributeRepository; _eventPublisher = eventPublisher; - _orderRepository = orderRepository; - _valueCache = new Dictionary>(); - } + _orderRepository = orderRepository; + _valueCache = new Dictionary>(); + } public virtual void DeleteAttribute(GenericAttribute attribute) { - Guard.NotNull(attribute, nameof(attribute)); + Guard.NotNull(attribute, nameof(attribute)); - int entityId = attribute.EntityId; - string keyGroup = attribute.KeyGroup; + int entityId = attribute.EntityId; + string keyGroup = attribute.KeyGroup; _genericAttributeRepository.Delete(attribute); - if (keyGroup.IsCaseInsensitiveEqual("Order") && entityId != 0) - { - var order = _orderRepository.GetById(entityId); - _eventPublisher.PublishOrderUpdated(order); - } + if (keyGroup.IsCaseInsensitiveEqual("Order") && entityId != 0) + { + var order = _orderRepository.GetById(entityId); + _eventPublisher.PublishOrderUpdated(order); + } } public virtual GenericAttribute GetAttributeById(int attributeId) @@ -57,171 +57,171 @@ public virtual GenericAttribute GetAttributeById(int attributeId) public virtual void InsertAttribute(GenericAttribute attribute) { - Guard.NotNull(attribute, nameof(attribute)); + Guard.NotNull(attribute, nameof(attribute)); - _genericAttributeRepository.Insert(attribute); + _genericAttributeRepository.Insert(attribute); - if (attribute.KeyGroup.IsCaseInsensitiveEqual("Order") && attribute.EntityId != 0) - { - var order = _orderRepository.GetById(attribute.EntityId); - _eventPublisher.PublishOrderUpdated(order); - } + if (attribute.KeyGroup.IsCaseInsensitiveEqual("Order") && attribute.EntityId != 0) + { + var order = _orderRepository.GetById(attribute.EntityId); + _eventPublisher.PublishOrderUpdated(order); + } } public virtual void UpdateAttribute(GenericAttribute attribute) { - Guard.NotNull(attribute, nameof(attribute)); + Guard.NotNull(attribute, nameof(attribute)); - _genericAttributeRepository.Update(attribute); + _genericAttributeRepository.Update(attribute); - if (attribute.KeyGroup.IsCaseInsensitiveEqual("Order") && attribute.EntityId != 0) - { - var order = _orderRepository.GetById(attribute.EntityId); - _eventPublisher.PublishOrderUpdated(order); - } + if (attribute.KeyGroup.IsCaseInsensitiveEqual("Order") && attribute.EntityId != 0) + { + var order = _orderRepository.GetById(attribute.EntityId); + _eventPublisher.PublishOrderUpdated(order); + } } - public virtual IList GetAttributesForEntity(int entityId, string entityName) + public virtual IList GetAttributesForEntity(int entityId, string entityName) { - var query = from ga in _genericAttributeRepository.Table - where ga.EntityId == entityId && ga.KeyGroup == entityName - select ga; - - return query.ToListCached("db.ga.{0}-{1}".FormatInvariant(entityId, entityName)); - } - - public virtual Multimap GetAttributesForEntity(int[] entityIds, string entityName) - { - Guard.NotNull(entityIds, nameof(entityIds)); - - var query = _genericAttributeRepository.TableUntracked - .Where(x => entityIds.Contains(x.EntityId) && x.KeyGroup == entityName); - - var map = query - .ToList() - .ToMultimap(x => x.EntityId, x => x); - - return map; - } - - public virtual TProp GetAttribute(string entityName, int entityId, string key, int storeId = 0) - { - if (entityName == null) - throw new ArgumentNullException(nameof(entityName)); - - if (key == null) - throw new ArgumentNullException(nameof(key)); - - var entityKey = entityName.ToLowerInvariant() + entityId; - if (!_valueCache.TryGetValue(entityKey, out var attrs)) - { - var list = GetAttributesForEntity(entityId, entityName); - if (list.Count > 0) - { - attrs = list.ToDictionarySafe(k => k.Key.ToLowerInvariant() + k.StoreId, v => (object)v.Value); - } - _valueCache[entityKey] = attrs; - } - - TProp result = default; - - if (attrs != null) - { - var entryKey = key.ToLowerInvariant() + storeId; - if (attrs.TryGetValue(entryKey, out var rawValue)) - { - if (rawValue.GetType() == typeof(TProp)) - { - result = (TProp)rawValue; - } - else - { - result = rawValue.Convert(); - // to skip repeated value type conversion - attrs[entryKey] = result; - } - } - } - - return result; - } - - public virtual IQueryable GetAttributes(string key, string entityName) - { - var query = - from ga in _genericAttributeRepository.Table - where ga.Key == key && ga.KeyGroup == entityName - select ga; - - return query; - } - - public virtual void SaveAttribute(BaseEntity entity, string key, TProp value, int storeId = 0) + var query = from ga in _genericAttributeRepository.Table + where ga.EntityId == entityId && ga.KeyGroup == entityName + select ga; + + return query.ToListCached("db.ga.{0}-{1}".FormatInvariant(entityId, entityName)); + } + + public virtual Multimap GetAttributesForEntity(int[] entityIds, string entityName) + { + Guard.NotNull(entityIds, nameof(entityIds)); + + var query = _genericAttributeRepository.TableUntracked + .Where(x => entityIds.Contains(x.EntityId) && x.KeyGroup == entityName); + + var map = query + .ToList() + .ToMultimap(x => x.EntityId, x => x); + + return map; + } + + public virtual TProp GetAttribute(string entityName, int entityId, string key, int storeId = 0) + { + if (entityName == null) + throw new ArgumentNullException(nameof(entityName)); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + var entityKey = entityName.ToLowerInvariant() + entityId; + if (!_valueCache.TryGetValue(entityKey, out var attrs)) + { + var list = GetAttributesForEntity(entityId, entityName); + if (list.Count > 0) + { + attrs = list.ToDictionarySafe(k => k.Key.ToLowerInvariant() + k.StoreId, v => (object)v.Value); + } + _valueCache[entityKey] = attrs; + } + + TProp result = default; + + if (attrs != null) + { + var entryKey = key.ToLowerInvariant() + storeId; + if (attrs.TryGetValue(entryKey, out var rawValue)) + { + if (rawValue.GetType() == typeof(TProp)) + { + result = (TProp)rawValue; + } + else + { + result = rawValue.Convert(); + // to skip repeated value type conversion + attrs[entryKey] = result; + } + } + } + + return result; + } + + public virtual IQueryable GetAttributes(string key, string entityName) { - Guard.NotNull(entity, nameof(entity)); + var query = + from ga in _genericAttributeRepository.Table + where ga.Key == key && ga.KeyGroup == entityName + select ga; - SaveAttribute(entity.Id, key, entity.GetUnproxiedType().Name, value, storeId); + return query; } - public virtual void SaveAttribute(int entityId, string key, string entityName, TProp value, int storeId = 0) - { - Guard.NotZero(entityId, nameof(entityId)); - - var valueStr = value.Convert(); - var props = GetAttributesForEntity(entityId, entityName); - - // should be culture invariant - var prop = props.FirstOrDefault(ga => ga.StoreId == storeId && ga.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); - - if (prop != null) - { - if (string.IsNullOrWhiteSpace(valueStr)) - { - // delete - DeleteAttribute(prop); - CacheTryRemove(entityName, entityId, prop); - } - else - { - // update - prop.Value = valueStr; - UpdateAttribute(prop); - CacheTryAddOrUpdate(entityName, entityId, prop); - } - } - else - { - if (!string.IsNullOrWhiteSpace(valueStr)) - { - // insert - prop = new GenericAttribute - { - EntityId = entityId, - Key = key, - KeyGroup = entityName, - Value = valueStr, - StoreId = storeId - }; - - InsertAttribute(prop); - - CacheTryAddOrUpdate(entityName, entityId, prop); - } - } - } - - private void CacheTryRemove(string entityName, int entityId, GenericAttribute attr) - { - var entityKey = entityName.ToLowerInvariant() + entityId; - var entryKey = attr.Key.ToLowerInvariant() + attr.StoreId; - _valueCache.Get(entityKey).TryRemove(entryKey, out _); - } - - private void CacheTryAddOrUpdate(string entityName, int entityId, GenericAttribute attr) - { - var entityKey = entityName.ToLowerInvariant() + entityId; - var entryKey = attr.Key.ToLowerInvariant() + attr.StoreId; - _valueCache.Get(entityKey).TryAdd(entryKey, attr.Value, true); - } - } + public virtual void SaveAttribute(BaseEntity entity, string key, TProp value, int storeId = 0) + { + Guard.NotNull(entity, nameof(entity)); + + SaveAttribute(entity.Id, key, entity.GetUnproxiedType().Name, value, storeId); + } + + public virtual void SaveAttribute(int entityId, string key, string entityName, TProp value, int storeId = 0) + { + Guard.NotZero(entityId, nameof(entityId)); + + var valueStr = value.Convert(); + var props = GetAttributesForEntity(entityId, entityName); + + // should be culture invariant + var prop = props.FirstOrDefault(ga => ga.StoreId == storeId && ga.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); + + if (prop != null) + { + if (string.IsNullOrWhiteSpace(valueStr)) + { + // delete + DeleteAttribute(prop); + CacheTryRemove(entityName, entityId, prop); + } + else + { + // update + prop.Value = valueStr; + UpdateAttribute(prop); + CacheTryAddOrUpdate(entityName, entityId, prop); + } + } + else + { + if (!string.IsNullOrWhiteSpace(valueStr)) + { + // insert + prop = new GenericAttribute + { + EntityId = entityId, + Key = key, + KeyGroup = entityName, + Value = valueStr, + StoreId = storeId + }; + + InsertAttribute(prop); + + CacheTryAddOrUpdate(entityName, entityId, prop); + } + } + } + + private void CacheTryRemove(string entityName, int entityId, GenericAttribute attr) + { + var entityKey = entityName.ToLowerInvariant() + entityId; + var entryKey = attr.Key.ToLowerInvariant() + attr.StoreId; + _valueCache.Get(entityKey).TryRemove(entryKey, out _); + } + + private void CacheTryAddOrUpdate(string entityName, int entityId, GenericAttribute attr) + { + var entityKey = entityName.ToLowerInvariant() + entityId; + var entryKey = attr.Key.ToLowerInvariant() + attr.StoreId; + _valueCache.Get(entityKey).TryAdd(entryKey, attr.Value, true); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IAddressService.cs b/src/Libraries/SmartStore.Services/Common/IAddressService.cs index 5ece5f704d..90b9bfb9de 100644 --- a/src/Libraries/SmartStore.Services/Common/IAddressService.cs +++ b/src/Libraries/SmartStore.Services/Common/IAddressService.cs @@ -16,7 +16,7 @@ public partial interface IAddressService /// Address void DeleteAddress(Address address); - void DeleteAddress(int id); + void DeleteAddress(int id); /// /// Gets total number of addresses by country identifier @@ -39,12 +39,12 @@ public partial interface IAddressService /// Address Address GetAddressById(int addressId); - /// - /// Gets addresses by address identifiers - /// - /// Address identifiers - /// Addresses - IList
GetAddressByIds(int[] addressIds); + /// + /// Gets addresses by address identifiers + /// + /// Address identifiers + /// Addresses + IList
GetAddressByIds(int[] addressIds); /// /// Inserts an address @@ -65,28 +65,28 @@ public partial interface IAddressService /// Result bool IsAddressValid(Address address); - /// - /// Formats the address according to the countries address formatting template - /// - /// Address to format - /// Whether new lines should be replaced with html BR tags - /// The formatted address - string FormatAddress(CompanyInformationSettings settings, bool newLineToBr = false); + /// + /// Formats the address according to the countries address formatting template + /// + /// Address to format + /// Whether new lines should be replaced with html BR tags + /// The formatted address + string FormatAddress(CompanyInformationSettings settings, bool newLineToBr = false); - /// - /// Formats the address according to the countries address formatting template - /// - /// Address to format - /// Whether new lines should be replaced with html BR tags - /// The formatted address - string FormatAddress(Address address, bool newLineToBr = false); + /// + /// Formats the address according to the countries address formatting template + /// + /// Address to format + /// Whether new lines should be replaced with html BR tags + /// The formatted address + string FormatAddress(Address address, bool newLineToBr = false); - /// - /// Formats the address according to the countries address formatting template - /// - /// Address to format. Usually passed by the template engine as a dictionary - /// The (liquid) formatting template. If null, the system global template will be used. - /// The formatted address - string FormatAddress(object address, string template = null, IFormatProvider formatProvider = null); - } + /// + /// Formats the address according to the countries address formatting template + /// + /// Address to format. Usually passed by the template engine as a dictionary + /// The (liquid) formatting template. If null, the system global template will be used. + /// The formatted address + string FormatAddress(object address, string template = null, IFormatProvider formatProvider = null); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs index dffd998afe..68653cfec1 100644 --- a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs @@ -44,51 +44,51 @@ public partial interface IGenericAttributeService /// Generic attributes IList GetAttributesForEntity(int entityId, string entityName); - /// - /// Get attributes - /// - /// Entity identifiers - /// Key group - /// Generic attributes - Multimap GetAttributesForEntity(int[] entityIds, string entityName); + /// + /// Get attributes + /// + /// Entity identifiers + /// Key group + /// Generic attributes + Multimap GetAttributesForEntity(int[] entityIds, string entityName); - /// - /// Get attribute value - /// - /// Key group - /// Entity identifier - /// Key - /// Store identifier; pass 0 if this attribute will be available for all stores - /// Converted generic attribute value - TProp GetAttribute(string entityName, int entityId, string key, int storeId = 0); + /// + /// Get attribute value + /// + /// Key group + /// Entity identifier + /// Key + /// Store identifier; pass 0 if this attribute will be available for all stores + /// Converted generic attribute value + TProp GetAttribute(string entityName, int entityId, string key, int storeId = 0); - /// - /// Get queryable attributes - /// - /// The key - /// The key group - /// Queryable attributes - IQueryable GetAttributes(string key, string entityName); + /// + /// Get queryable attributes + /// + /// The key + /// The key group + /// Queryable attributes + IQueryable GetAttributes(string key, string entityName); - /// + /// /// Save attribute value /// /// Property type /// Entity /// Key /// Value - /// Store identifier; pass 0 if this attribute will be available for all stores - void SaveAttribute(BaseEntity entity, string key, TProp value, int storeId = 0); + /// Store identifier; pass 0 if this attribute will be available for all stores + void SaveAttribute(BaseEntity entity, string key, TProp value, int storeId = 0); - /// - /// Save attribute value - /// - /// Property type - /// Entity identifier - /// The key - /// The key group - /// Property type - /// Store identifier; pass 0 if this attribute will be available for all stores - void SaveAttribute(int entityId, string key, string keyGroup, TProp value, int storeId = 0); - } + /// + /// Save attribute value + /// + /// Property type + /// Entity identifier + /// The key + /// The key group + /// Property type + /// Store identifier; pass 0 if this attribute will be available for all stores + void SaveAttribute(int entityId, string key, string keyGroup, TProp value, int storeId = 0); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IUserAgent.cs b/src/Libraries/SmartStore.Services/Common/IUserAgent.cs index d61f9444c2..5a3c5db1cb 100644 --- a/src/Libraries/SmartStore.Services/Common/IUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/IUserAgent.cs @@ -5,168 +5,168 @@ using System.Threading.Tasks; namespace SmartStore.Services.Common -{ - public interface IUserAgent - { - string RawValue { get; set; } - - UserAgentInfo UserAgent { get; } - DeviceInfo Device { get; } - OSInfo OS { get; } +{ + public interface IUserAgent + { + string RawValue { get; set; } - bool IsBot { get; } - bool IsMobileDevice { get; } - bool IsTablet { get; } - bool IsPdfConverter { get; } - } + UserAgentInfo UserAgent { get; } + DeviceInfo Device { get; } + OSInfo OS { get; } - public sealed class DeviceInfo - { - public DeviceInfo(string family, bool isBot) - { - this.Family = family; - this.IsBot = isBot; - } - public override string ToString() - { - return this.Family; - } - public string Family { get; private set; } - public bool IsBot { get; private set; } - } + bool IsBot { get; } + bool IsMobileDevice { get; } + bool IsTablet { get; } + bool IsPdfConverter { get; } + } - public sealed class OSInfo - { - public OSInfo(string family, string major, string minor, string patch, string patchMinor) - { - this.Family = family; - this.Major = major; - this.Minor = minor; - this.Patch = patch; - this.PatchMinor = patchMinor; - } - public override string ToString() - { - var str = VersionString.Format(Major, Minor, Patch, PatchMinor); - return (this.Family + (!string.IsNullOrEmpty(str) ? (" " + str) : null)); - } - public string Family { get; private set; } - public string Major { get; private set; } - public string Minor { get; private set; } - public string Patch { get; private set; } - public string PatchMinor { get; private set; } + public sealed class DeviceInfo + { + public DeviceInfo(string family, bool isBot) + { + this.Family = family; + this.IsBot = isBot; + } + public override string ToString() + { + return this.Family; + } + public string Family { get; private set; } + public bool IsBot { get; private set; } + } - private static string FormatVersionString(params string[] parts) - { - return string.Join(".", (from v in parts - where !string.IsNullOrEmpty(v) - select v).ToArray()); - } - } + public sealed class OSInfo + { + public OSInfo(string family, string major, string minor, string patch, string patchMinor) + { + this.Family = family; + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.PatchMinor = patchMinor; + } + public override string ToString() + { + var str = VersionString.Format(Major, Minor, Patch, PatchMinor); + return (this.Family + (!string.IsNullOrEmpty(str) ? (" " + str) : null)); + } + public string Family { get; private set; } + public string Major { get; private set; } + public string Minor { get; private set; } + public string Patch { get; private set; } + public string PatchMinor { get; private set; } - public sealed class UserAgentInfo - { - private static readonly HashSet s_Bots = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - "BingPreview" - }; + private static string FormatVersionString(params string[] parts) + { + return string.Join(".", (from v in parts + where !string.IsNullOrEmpty(v) + select v).ToArray()); + } + } - private bool? _isBot; - private bool? _supportsWebP; + public sealed class UserAgentInfo + { + private static readonly HashSet s_Bots = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "BingPreview" + }; - public UserAgentInfo(string family, string major, string minor, string patch) - { - this.Family = family; - this.Major = major; - this.Minor = minor; - this.Patch = patch; - } - public override string ToString() - { - var str = VersionString.Format(Major, Minor, Patch); - return (this.Family + (!string.IsNullOrEmpty(str) ? (" " + str) : null)); - } - public string Family { get; private set; } - public string Major { get; private set; } - public string Minor { get; private set; } - public string Patch { get; private set; } - public bool IsBot - { - get - { - if (!_isBot.HasValue) - { - _isBot = s_Bots.Contains(Family); - } - return _isBot.Value; - } - } - public bool SupportsWebP - { - get - { - if (_supportsWebP == null) - { - if (Family == "Chrome") - { - _supportsWebP = Major.ToInt() >= 32; - } - else if (Family.StartsWith("Chrome Mobile")) - { - _supportsWebP = Major.ToInt() >= 79; - } - else if (Family == "Firefox") - { - _supportsWebP = Major.ToInt() >= 65; - } - else if (Family.StartsWith("Firefox Mobile")) - { - _supportsWebP = Major.ToInt() >= 68; - } - else if (Family == "Edge") - { - _supportsWebP = Major.ToInt() >= 18; - } - else if (Family == "Opera") - { - _supportsWebP = Major.ToInt() >= 19; - } - else if (Family == "Opera Mini") - { - _supportsWebP = true; - } - else if (Family.StartsWith("Android")) - { - _supportsWebP = Major.ToInt() >= 5 || (Major.ToInt() == 4 && Minor.ToInt() >= 2); - } - else if (Family.StartsWith("Samsung Internet")) - { - _supportsWebP = Major.ToInt() >= 4; - } - else if (Family.StartsWith("Baidu")) - { - _supportsWebP = Major.ToInt() >= 8 || (Major.ToInt() == 7 && Minor.ToInt() >= 12); - } - else - { - _supportsWebP = false; - } - } + private bool? _isBot; + private bool? _supportsWebP; - return _supportsWebP.Value; - } - } - } + public UserAgentInfo(string family, string major, string minor, string patch) + { + this.Family = family; + this.Major = major; + this.Minor = minor; + this.Patch = patch; + } + public override string ToString() + { + var str = VersionString.Format(Major, Minor, Patch); + return (this.Family + (!string.IsNullOrEmpty(str) ? (" " + str) : null)); + } + public string Family { get; private set; } + public string Major { get; private set; } + public string Minor { get; private set; } + public string Patch { get; private set; } + public bool IsBot + { + get + { + if (!_isBot.HasValue) + { + _isBot = s_Bots.Contains(Family); + } + return _isBot.Value; + } + } + public bool SupportsWebP + { + get + { + if (_supportsWebP == null) + { + if (Family == "Chrome") + { + _supportsWebP = Major.ToInt() >= 32; + } + else if (Family.StartsWith("Chrome Mobile")) + { + _supportsWebP = Major.ToInt() >= 79; + } + else if (Family == "Firefox") + { + _supportsWebP = Major.ToInt() >= 65; + } + else if (Family.StartsWith("Firefox Mobile")) + { + _supportsWebP = Major.ToInt() >= 68; + } + else if (Family == "Edge") + { + _supportsWebP = Major.ToInt() >= 18; + } + else if (Family == "Opera") + { + _supportsWebP = Major.ToInt() >= 19; + } + else if (Family == "Opera Mini") + { + _supportsWebP = true; + } + else if (Family.StartsWith("Android")) + { + _supportsWebP = Major.ToInt() >= 5 || (Major.ToInt() == 4 && Minor.ToInt() >= 2); + } + else if (Family.StartsWith("Samsung Internet")) + { + _supportsWebP = Major.ToInt() >= 4; + } + else if (Family.StartsWith("Baidu")) + { + _supportsWebP = Major.ToInt() >= 8 || (Major.ToInt() == 7 && Minor.ToInt() >= 12); + } + else + { + _supportsWebP = false; + } + } - internal static class VersionString - { - public static string Format(params string[] parts) - { - return string.Join(".", (from v in parts - where !string.IsNullOrEmpty(v) - select v).ToArray()); - } - } + return _supportsWebP.Value; + } + } + } + + internal static class VersionString + { + public static string Format(params string[] parts) + { + return string.Join(".", (from v in parts + where !string.IsNullOrEmpty(v) + select v).ToArray()); + } + } } diff --git a/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs b/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs index d5b3c8d746..f6a6aae462 100644 --- a/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs +++ b/src/Libraries/SmartStore.Services/Common/MaintenanceService.cs @@ -44,7 +44,7 @@ public MaintenanceService(IDataProvider dataProvider, IDbContext dbContext, /// /// Entity /// Integer ident; null if cannot get the result - public virtual int? GetTableIdent() where T: BaseEntity + public virtual int? GetTableIdent() where T : BaseEntity { if (_commonSettings.UseStoredProceduresIfSupported && _dataProvider.StoredProceduresSupported) { diff --git a/src/Libraries/SmartStore.Services/Common/MobileDeviceHelper.cs b/src/Libraries/SmartStore.Services/Common/MobileDeviceHelper.cs index 0586d08a98..b37a4d13fa 100644 --- a/src/Libraries/SmartStore.Services/Common/MobileDeviceHelper.cs +++ b/src/Libraries/SmartStore.Services/Common/MobileDeviceHelper.cs @@ -5,16 +5,16 @@ namespace SmartStore.Services.Common { public partial class MobileDeviceHelper : IMobileDeviceHelper { - private readonly IUserAgent _userAgent; + private readonly IUserAgent _userAgent; public MobileDeviceHelper(IUserAgent userAgent) { - _userAgent = userAgent; + _userAgent = userAgent; } public virtual bool IsMobileDevice() { - return _userAgent.IsMobileDevice && !_userAgent.IsTablet; + return _userAgent.IsMobileDevice && !_userAgent.IsTablet; } } diff --git a/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs b/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs index e02429fba3..4ef87fa3d7 100644 --- a/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs +++ b/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs @@ -3,14 +3,14 @@ namespace SmartStore.Services.Common { - /// - /// Task to cleanup temporary files - /// - public partial class TempFileCleanupTask : ITask - { - public void Execute(TaskExecutionContext ctx) - { - FileSystemHelper.ClearTempDirectories(); - } - } + /// + /// Task to cleanup temporary files + /// + public partial class TempFileCleanupTask : ITask + { + public void Execute(TaskExecutionContext ctx) + { + FileSystemHelper.ClearTempDirectories(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs index 03f767cc9d..9f1705d9a3 100644 --- a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text.RegularExpressions; using System.Web; using SmartStore.Utilities; @@ -7,258 +8,260 @@ namespace SmartStore.Services.Common { - public class UAParserUserAgent : IUserAgent - { - private readonly static uap.Parser s_uap; - private static readonly Regex s_pdfConverterPattern = new Regex(@"wkhtmltopdf", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + public class UAParserUserAgent : IUserAgent + { + private readonly static uap.Parser s_uap; + private static readonly Regex s_pdfConverterPattern = new Regex(@"wkhtmltopdf", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - # region Mobile UAs, OS & Devices + #region Mobile UAs, OS & Devices - private static readonly HashSet s_MobileOS = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - "Android", - "iOS", - "Windows Mobile", - "Windows Phone", - "Windows CE", - "Symbian OS", - "BlackBerry OS", - "BlackBerry Tablet OS", - "Firefox OS", - "Brew MP", - "webOS", - "Bada", - "Kindle", - "Maemo" - }; + private static readonly HashSet s_MobileOS = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "Android", + "iOS", + "Windows Mobile", + "Windows Phone", + "Windows CE", + "Symbian OS", + "BlackBerry OS", + "BlackBerry Tablet OS", + "Firefox OS", + "Brew MP", + "webOS", + "Bada", + "Kindle", + "Maemo" + }; - private static readonly HashSet s_MobileBrowsers = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - "Googlebot-Mobile", - "Baiduspider-mobile", - "Android", - "Firefox Mobile", - "Opera Mobile", - "Opera Mini", - "Mobile Safari", - "Amazon Silk", - "webOS Browser", - "MicroB", - "Ovi Browser", - "NetFront", - "NetFront NX", - "Chrome Mobile", - "Chrome Mobile iOS", - "UC Browser", - "Tizen Browser", - "Baidu Explorer", - "QQ Browser Mini", - "QQ Browser Mobile", - "IE Mobile", - "Polaris", - "ONE Browser", - "iBrowser Mini", - "Nokia Services (WAP) Browser", - "Nokia Browser", - "Nokia OSS Browser", - "BlackBerry WebKit", - "BlackBerry", "Palm", - "Palm Blazer", - "Palm Pre", - "Teleca Browser", - "SEMC-Browser", - "PlayStation Portable", - "Nokia", - "Maemo Browser", - "Obigo", - "Bolt", - "Iris", - "UP.Browser", - "Minimo", - "Bunjaloo", - "Jasmine", - "Dolfin", - "Polaris", - "Skyfire" - }; + private static readonly HashSet s_MobileBrowsers = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "Googlebot-Mobile", + "Baiduspider-mobile", + "Android", + "Firefox Mobile", + "Opera Mobile", + "Opera Mini", + "Mobile Safari", + "Amazon Silk", + "webOS Browser", + "MicroB", + "Ovi Browser", + "NetFront", + "NetFront NX", + "Chrome Mobile", + "Chrome Mobile iOS", + "UC Browser", + "Tizen Browser", + "Baidu Explorer", + "QQ Browser Mini", + "QQ Browser Mobile", + "IE Mobile", + "Polaris", + "ONE Browser", + "iBrowser Mini", + "Nokia Services (WAP) Browser", + "Nokia Browser", + "Nokia OSS Browser", + "BlackBerry WebKit", + "BlackBerry", "Palm", + "Palm Blazer", + "Palm Pre", + "Teleca Browser", + "SEMC-Browser", + "PlayStation Portable", + "Nokia", + "Maemo Browser", + "Obigo", + "Bolt", + "Iris", + "UP.Browser", + "Minimo", + "Bunjaloo", + "Jasmine", + "Dolfin", + "Polaris", + "Skyfire" + }; - private static readonly HashSet s_MobileDevices = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - "BlackBerry", - "MI PAD", - "iPhone", - "iPad", - "iPod", - "Kindle", - "Kindle Fire", - "Nokia", - "Lumia", - "Palm", - "DoCoMo", - "HP TouchPad", - "Xoom", - "Motorola", - "Generic Feature Phone", - "Generic Smartphone" - }; + private static readonly HashSet s_MobileDevices = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "BlackBerry", + "MI PAD", + "iPhone", + "iPad", + "iPod", + "Kindle", + "Kindle Fire", + "Nokia", + "Lumia", + "Palm", + "DoCoMo", + "HP TouchPad", + "Xoom", + "Motorola", + "Generic Feature Phone", + "Generic Smartphone" + }; - #endregion + #endregion - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; - private string _rawValue; - private UserAgentInfo _userAgent; - private DeviceInfo _device; - private OSInfo _os; + private string _rawValue; + private UserAgentInfo _userAgent; + private DeviceInfo _device; + private OSInfo _os; - private bool? _isBot; - private bool? _isMobileDevice; - private bool? _isTablet; - private bool? _isPdfConverter; + private bool? _isBot; + private bool? _isMobileDevice; + private bool? _isTablet; + private bool? _isPdfConverter; - static UAParserUserAgent() - { - //s_uap = uap.Parser.GetDefault(); - s_uap = uap.Parser.FromYamlFile(CommonHelper.MapPath("~/App_Data/ua-parser.regexes.yaml")); - } + static UAParserUserAgent() + { + var path = CommonHelper.MapPath("~/App_Data/UAParser.regexes.yaml"); + s_uap = File.Exists(path) + ? uap.Parser.FromYaml(path) + : uap.Parser.GetDefault(); + } - public UAParserUserAgent(HttpContextBase httpContext) - { - this._httpContext = httpContext; - } + public UAParserUserAgent(HttpContextBase httpContext) + { + this._httpContext = httpContext; + } - public string RawValue - { - get - { - if (_rawValue == null) - { - if (_httpContext.Request != null) - { - _rawValue = _httpContext.Request.UserAgent.EmptyNull(); - } - else - { - _rawValue = ""; - } - } + public string RawValue + { + get + { + if (_rawValue == null) + { + if (_httpContext.Request != null) + { + _rawValue = _httpContext.Request.UserAgent.EmptyNull(); + } + else + { + _rawValue = ""; + } + } - return _rawValue; - } - // for (unit) test purpose - set - { - _rawValue = value; - _userAgent = null; - _device = null; - _os = null; - _isBot = null; - _isMobileDevice = null; - _isTablet = null; - _isPdfConverter = null; - } - } + return _rawValue; + } + // for (unit) test purpose + set + { + _rawValue = value; + _userAgent = null; + _device = null; + _os = null; + _isBot = null; + _isMobileDevice = null; + _isTablet = null; + _isPdfConverter = null; + } + } - public virtual UserAgentInfo UserAgent - { - get - { - if (_userAgent == null) - { - var tmp = s_uap.ParseUserAgent(this.RawValue); - _userAgent = new UserAgentInfo(tmp.Family, tmp.Major, tmp.Minor, tmp.Patch); - } - return _userAgent; - } - } + public virtual UserAgentInfo UserAgent + { + get + { + if (_userAgent == null) + { + var tmp = s_uap.ParseUserAgent(this.RawValue); + _userAgent = new UserAgentInfo(tmp.Family, tmp.Major, tmp.Minor, tmp.Patch); + } + return _userAgent; + } + } - public virtual DeviceInfo Device - { - get - { - if (_device == null) - { - var tmp = s_uap.ParseDevice(this.RawValue); - _device = new DeviceInfo(tmp.Family, tmp.IsSpider()); - } - return _device; - } - } + public virtual DeviceInfo Device + { + get + { + if (_device == null) + { + var tmp = s_uap.ParseDevice(this.RawValue); + _device = new DeviceInfo(tmp.Family, tmp.IsSpider()); + } + return _device; + } + } - public virtual OSInfo OS - { - get - { - if (_os == null) - { - var tmp = s_uap.ParseOS(this.RawValue); - _os = new OSInfo(tmp.Family, tmp.Major, tmp.Minor, tmp.Patch, tmp.PatchMinor); - } - return _os; - } - } + public virtual OSInfo OS + { + get + { + if (_os == null) + { + var tmp = s_uap.ParseOS(this.RawValue); + _os = new OSInfo(tmp.Family, tmp.Major, tmp.Minor, tmp.Patch, tmp.PatchMinor); + } + return _os; + } + } - public virtual bool IsBot - { - get - { - if (!_isBot.HasValue) - { - // empty useragent > bad bot! - _isBot = this.RawValue.IsEmpty() || _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; - } - return _isBot.Value; - } - } + public virtual bool IsBot + { + get + { + if (!_isBot.HasValue) + { + // empty useragent > bad bot! + _isBot = this.RawValue.IsEmpty() || _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; + } + return _isBot.Value; + } + } - public virtual bool IsMobileDevice - { - get - { - if (!_isMobileDevice.HasValue) - { - _isMobileDevice = - s_MobileOS.Contains(this.OS.Family) || - s_MobileBrowsers.Contains(this.UserAgent.Family) || - s_MobileDevices.Contains(this.Device.Family); - } - return _isMobileDevice.Value; - } - } + public virtual bool IsMobileDevice + { + get + { + if (!_isMobileDevice.HasValue) + { + _isMobileDevice = + s_MobileOS.Contains(this.OS.Family) || + s_MobileBrowsers.Contains(this.UserAgent.Family) || + s_MobileDevices.Contains(this.Device.Family); + } + return _isMobileDevice.Value; + } + } - public virtual bool IsTablet - { - get - { - if (!_isTablet.HasValue) - { - _isTablet = - Regex.IsMatch(this.Device.Family, "iPad|Kindle Fire|Nexus 10|Xoom|Transformer|MI PAD|IdeaTab", RegexOptions.CultureInvariant) || - this.OS.Family == "BlackBerry Tablet OS"; - } - return _isTablet.Value; - } - } + public virtual bool IsTablet + { + get + { + if (!_isTablet.HasValue) + { + _isTablet = + Regex.IsMatch(this.Device.Family, "iPad|Kindle Fire|Nexus 10|Xoom|Transformer|MI PAD|IdeaTab", RegexOptions.CultureInvariant) || + this.OS.Family == "BlackBerry Tablet OS"; + } + return _isTablet.Value; + } + } - public virtual bool IsPdfConverter - { - get - { - if (!_isPdfConverter.HasValue) - { - _isPdfConverter = s_pdfConverterPattern.IsMatch(this.RawValue); - } - return _isPdfConverter.Value; - } - } - } + public virtual bool IsPdfConverter + { + get + { + if (!_isPdfConverter.HasValue) + { + _isPdfConverter = s_pdfConverterPattern.IsMatch(this.RawValue); + } + return _isPdfConverter.Value; + } + } + } - internal static class DeviceExtensions - { - internal static bool IsSpider(this UAParser.Device device) - { - return device.Family.Equals("Spider", StringComparison.InvariantCultureIgnoreCase); - } - } + internal static class DeviceExtensions + { + internal static bool IsSpider(this UAParser.Device device) + { + return device.Family.Equals("Spider", StringComparison.InvariantCultureIgnoreCase); + } + } } diff --git a/src/Libraries/SmartStore.Services/CommonServices.cs b/src/Libraries/SmartStore.Services/CommonServices.cs index d2d7228c91..2d95688a0f 100644 --- a/src/Libraries/SmartStore.Services/CommonServices.cs +++ b/src/Libraries/SmartStore.Services/CommonServices.cs @@ -16,91 +16,91 @@ namespace SmartStore.Services { public class CommonServices : ICommonServices - { - private readonly IComponentContext _container; - private readonly Lazy _env; - private readonly Lazy _cacheManager; - private readonly Lazy _requestCache; - private readonly Lazy _dbContext; - private readonly Lazy _storeContext; - private readonly Lazy _webHelper; - private readonly Lazy _workContext; - private readonly Lazy _eventPublisher; - private readonly Lazy _localization; - private readonly Lazy _customerActivity; - private readonly Lazy _mediaService; - private readonly Lazy _notifier; + { + private readonly IComponentContext _container; + private readonly Lazy _env; + private readonly Lazy _cacheManager; + private readonly Lazy _requestCache; + private readonly Lazy _dbContext; + private readonly Lazy _storeContext; + private readonly Lazy _webHelper; + private readonly Lazy _workContext; + private readonly Lazy _eventPublisher; + private readonly Lazy _localization; + private readonly Lazy _customerActivity; + private readonly Lazy _mediaService; + private readonly Lazy _notifier; private readonly Lazy _permissions; private readonly Lazy _settings; - private readonly Lazy _storeService; - private readonly Lazy _dateTimeHelper; - private readonly Lazy _displayControl; - private readonly Lazy _chronometer; - private readonly Lazy _messageFactory; + private readonly Lazy _storeService; + private readonly Lazy _dateTimeHelper; + private readonly Lazy _displayControl; + private readonly Lazy _chronometer; + private readonly Lazy _messageFactory; - public CommonServices( - IComponentContext container, - Lazy env, - Lazy cacheManager, - Lazy requestCache, - Lazy dbContext, - Lazy storeContext, - Lazy webHelper, - Lazy workContext, - Lazy eventPublisher, - Lazy localization, - Lazy customerActivity, - Lazy mediaService, - Lazy notifier, + public CommonServices( + IComponentContext container, + Lazy env, + Lazy cacheManager, + Lazy requestCache, + Lazy dbContext, + Lazy storeContext, + Lazy webHelper, + Lazy workContext, + Lazy eventPublisher, + Lazy localization, + Lazy customerActivity, + Lazy mediaService, + Lazy notifier, Lazy permissions, Lazy settings, - Lazy storeService, - Lazy dateTimeHelper, - Lazy displayControl, - Lazy chronometer, - Lazy messageFactory) - { - _container = container; - _env = env; - _cacheManager = cacheManager; - _requestCache = requestCache; - _dbContext = dbContext; - _storeContext = storeContext; - _webHelper = webHelper; - _workContext = workContext; - _eventPublisher = eventPublisher; - _localization = localization; - _customerActivity = customerActivity; - _mediaService = mediaService; - _notifier = notifier; + Lazy storeService, + Lazy dateTimeHelper, + Lazy displayControl, + Lazy chronometer, + Lazy messageFactory) + { + _container = container; + _env = env; + _cacheManager = cacheManager; + _requestCache = requestCache; + _dbContext = dbContext; + _storeContext = storeContext; + _webHelper = webHelper; + _workContext = workContext; + _eventPublisher = eventPublisher; + _localization = localization; + _customerActivity = customerActivity; + _mediaService = mediaService; + _notifier = notifier; _permissions = permissions; - _settings = settings; - _storeService = storeService; - _dateTimeHelper = dateTimeHelper; - _displayControl = displayControl; - _chronometer = chronometer; - _messageFactory = messageFactory; - } + _settings = settings; + _storeService = storeService; + _dateTimeHelper = dateTimeHelper; + _displayControl = displayControl; + _chronometer = chronometer; + _messageFactory = messageFactory; + } - public IComponentContext Container => _container; - public IApplicationEnvironment ApplicationEnvironment => _env.Value; - public ICacheManager Cache => _cacheManager.Value; - public IRequestCache RequestCache => _requestCache.Value; - public IDbContext DbContext => _dbContext.Value; - public IStoreContext StoreContext => _storeContext.Value; - public IWebHelper WebHelper => _webHelper.Value; - public IWorkContext WorkContext => _workContext.Value; - public IEventPublisher EventPublisher => _eventPublisher.Value; - public ILocalizationService Localization => _localization.Value; - public ICustomerActivityService CustomerActivity => _customerActivity.Value; - public IMediaService MediaService => _mediaService.Value; - public INotifier Notifier => _notifier.Value; + public IComponentContext Container => _container; + public IApplicationEnvironment ApplicationEnvironment => _env.Value; + public ICacheManager Cache => _cacheManager.Value; + public IRequestCache RequestCache => _requestCache.Value; + public IDbContext DbContext => _dbContext.Value; + public IStoreContext StoreContext => _storeContext.Value; + public IWebHelper WebHelper => _webHelper.Value; + public IWorkContext WorkContext => _workContext.Value; + public IEventPublisher EventPublisher => _eventPublisher.Value; + public ILocalizationService Localization => _localization.Value; + public ICustomerActivityService CustomerActivity => _customerActivity.Value; + public IMediaService MediaService => _mediaService.Value; + public INotifier Notifier => _notifier.Value; public IPermissionService Permissions => _permissions.Value; public ISettingService Settings => _settings.Value; - public IStoreService StoreService => _storeService.Value; - public IDateTimeHelper DateTimeHelper => _dateTimeHelper.Value; - public IDisplayControl DisplayControl => _displayControl.Value; - public IChronometer Chronometer => _chronometer.Value; - public IMessageFactory MessageFactory => _messageFactory.Value; - } + public IStoreService StoreService => _storeService.Value; + public IDateTimeHelper DateTimeHelper => _dateTimeHelper.Value; + public IDisplayControl DisplayControl => _displayControl.Value; + public IChronometer Chronometer => _chronometer.Value; + public IMessageFactory MessageFactory => _messageFactory.Value; + } } diff --git a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs index 34680e47df..df20288ba2 100644 --- a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs +++ b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs @@ -16,129 +16,129 @@ namespace SmartStore.Services.Configuration { - public partial class SettingService : ScopedServiceBase, ISettingService + public partial class SettingService : ScopedServiceBase, ISettingService { - private const string SETTINGS_ALL_KEY = "setting:all"; + private const string SETTINGS_ALL_KEY = "setting:all"; private readonly IRepository _settingRepository; private readonly ICacheManager _cacheManager; - public SettingService(ICacheManager cacheManager, IRepository settingRepository) + public SettingService(ICacheManager cacheManager, IRepository settingRepository) { _cacheManager = cacheManager; _settingRepository = settingRepository; } - public ILogger Logger { get; set; } = NullLogger.Instance; - - protected virtual IDictionary GetAllCachedSettings() - { - return _cacheManager.Get(SETTINGS_ALL_KEY, () => - { - var query = from s in _settingRepository.TableUntracked - orderby s.Name, s.StoreId - select s; - - var settings = query.ToList(); - var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var s in settings) - { - var settingKey = CreateCacheKey(s.Name, s.StoreId); - - var cachedSetting = new CachedSetting - { - Id = s.Id, - Name = s.Name, - Value = s.Value, - StoreId = s.StoreId - }; - - dictionary[settingKey] = cachedSetting; - } - - return dictionary; - }, independent: true, allowRecursion: true); - } - - protected virtual PropertyInfo GetPropertyInfo(Expression> keySelector) - { - var member = keySelector.Body as MemberExpression; - if (member == null) - { - throw new ArgumentException($"Expression '{keySelector}' refers to a method, not a property."); - } - - var propInfo = member.Member as PropertyInfo; - if (propInfo == null) - { - throw new ArgumentException($"Expression '{keySelector}' refers to a field, not a property."); - } - - return propInfo; - } - - public virtual void InsertSetting(Setting setting, bool clearCache = true) + public ILogger Logger { get; set; } = NullLogger.Instance; + + protected virtual IDictionary GetAllCachedSettings() + { + return _cacheManager.Get(SETTINGS_ALL_KEY, () => + { + var query = from s in _settingRepository.TableUntracked + orderby s.Name, s.StoreId + select s; + + var settings = query.ToList(); + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var s in settings) + { + var settingKey = CreateCacheKey(s.Name, s.StoreId); + + var cachedSetting = new CachedSetting + { + Id = s.Id, + Name = s.Name, + Value = s.Value, + StoreId = s.StoreId + }; + + dictionary[settingKey] = cachedSetting; + } + + return dictionary; + }, independent: true, allowRecursion: true); + } + + protected virtual PropertyInfo GetPropertyInfo(Expression> keySelector) + { + var member = keySelector.Body as MemberExpression; + if (member == null) + { + throw new ArgumentException($"Expression '{keySelector}' refers to a method, not a property."); + } + + var propInfo = member.Member as PropertyInfo; + if (propInfo == null) + { + throw new ArgumentException($"Expression '{keySelector}' refers to a field, not a property."); + } + + return propInfo; + } + + public virtual void InsertSetting(Setting setting, bool clearCache = true) { - Guard.NotNull(setting, nameof(setting)); + Guard.NotNull(setting, nameof(setting)); - _settingRepository.Insert(setting); + _settingRepository.Insert(setting); - HasChanges = true; + HasChanges = true; - if (clearCache) - ClearCache(); + if (clearCache) + ClearCache(); } public virtual void UpdateSetting(Setting setting, bool clearCache = true) { - Guard.NotNull(setting, nameof(setting)); + Guard.NotNull(setting, nameof(setting)); _settingRepository.Update(setting); - HasChanges = true; + HasChanges = true; - if (clearCache) - ClearCache(); + if (clearCache) + ClearCache(); } - private ISettings LoadSettingsJson(Type settingType, int storeId = 0) - { - string key = settingType.Namespace + "." + settingType.Name; + private ISettings LoadSettingsJson(Type settingType, int storeId = 0) + { + string key = settingType.Namespace + "." + settingType.Name; - var settings = (ISettings)Activator.CreateInstance(settingType); + var settings = (ISettings)Activator.CreateInstance(settingType); - var rawSetting = GetSettingByKey(key, storeId: storeId, loadSharedValueIfNotFound: true); - if (rawSetting.HasValue()) - { - JsonConvert.PopulateObject(rawSetting, settings); - } + var rawSetting = GetSettingByKey(key, storeId: storeId, loadSharedValueIfNotFound: true); + if (rawSetting.HasValue()) + { + JsonConvert.PopulateObject(rawSetting, settings); + } - return settings; - } + return settings; + } - private void SaveSettingsJson(ISettings settings) - { - Type t = settings.GetType(); - string key = t.Namespace + "." + t.Name; - var storeId = 0; + private void SaveSettingsJson(ISettings settings) + { + Type t = settings.GetType(); + string key = t.Namespace + "." + t.Name; + var storeId = 0; - var rawSettings = JsonConvert.SerializeObject(settings); - SetSetting(key, rawSettings, storeId, true); - } + var rawSettings = JsonConvert.SerializeObject(settings); + SetSetting(key, rawSettings, storeId, true); + } - private void DeleteSettingsJson() - { - Type t = typeof(T); - string key = t.Namespace + "." + t.Name; + private void DeleteSettingsJson() + { + Type t = typeof(T); + string key = t.Namespace + "." + t.Name; - var setting = GetAllSettings() - .FirstOrDefault(x => x.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase)); + var setting = GetAllSettings() + .FirstOrDefault(x => x.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase)); - if (setting != null) - { - DeleteSetting(setting); - } - } + if (setting != null) + { + DeleteSetting(setting); + } + } public virtual Setting GetSettingById(int settingId) @@ -150,405 +150,385 @@ public virtual Setting GetSettingById(int settingId) return setting; } - public virtual Setting GetSettingEntityByKey(string key, int storeId = 0) - { - Guard.NotEmpty(key, nameof(key)); - - var query = _settingRepository.Table.Where(x => x.Name == key); - - if (storeId > 0) - { - query = query.Where(x => x.StoreId == storeId || x.StoreId == 0).OrderByDescending(x => x.StoreId); - } - else - { - query = query.Where(x => x.StoreId == 0); - } - - return query.FirstOrDefault(); - } - - public virtual T GetSettingByKey( - string key, - T defaultValue = default, - int storeId = 0, - bool loadSharedValueIfNotFound = false) + public virtual Setting GetSettingEntityByKey(string key, int storeId = 0) + { + Guard.NotEmpty(key, nameof(key)); + + var query = _settingRepository.Table.Where(x => x.Name == key); + + if (storeId > 0) + { + query = query.Where(x => x.StoreId == storeId || x.StoreId == 0).OrderByDescending(x => x.StoreId); + } + else + { + query = query.Where(x => x.StoreId == 0); + } + + return query.FirstOrDefault(); + } + + public virtual T GetSettingByKey( + string key, + T defaultValue = default, + int storeId = 0, + bool loadSharedValueIfNotFound = false) { - Guard.NotEmpty(key, nameof(key)); + Guard.NotEmpty(key, nameof(key)); - var settings = GetAllCachedSettings(); + var settings = GetAllCachedSettings(); - var cacheKey = CreateCacheKey(key, storeId); + var cacheKey = CreateCacheKey(key, storeId); - if (settings.TryGetValue(cacheKey, out CachedSetting cachedSetting)) - { - return cachedSetting.Value.Convert(); - } + if (settings.TryGetValue(cacheKey, out CachedSetting cachedSetting)) + { + return cachedSetting.Value.Convert(); + } - // fallback to shared (storeId = 0) if desired - if (storeId > 0 && loadSharedValueIfNotFound) - { - cacheKey = CreateCacheKey(key, 0); - if (settings.TryGetValue(cacheKey, out cachedSetting)) - { - return cachedSetting.Value.Convert(); - } - } + // fallback to shared (storeId = 0) if desired + if (storeId > 0 && loadSharedValueIfNotFound) + { + cacheKey = CreateCacheKey(key, 0); + if (settings.TryGetValue(cacheKey, out cachedSetting)) + { + return cachedSetting.Value.Convert(); + } + } return defaultValue; } - public virtual IList GetAllSettings() - { - var query = from s in _settingRepository.Table - orderby s.Name, s.StoreId - select s; - var settings = query.ToList(); - return settings; - } - - public virtual bool SettingExists( - T settings, - Expression> keySelector, - int storeId = 0) - where T : ISettings, new() - { - var propInfo = GetPropertyInfo(keySelector); - var key = string.Concat(typeof(T).Name, ".", propInfo.Name); - - string setting = GetSettingByKey(key, storeId: storeId); - return setting != null; - } + public virtual IList GetAllSettings() + { + var query = from s in _settingRepository.Table + orderby s.Name, s.StoreId + select s; + var settings = query.ToList(); + return settings; + } + + public virtual bool SettingExists( + T settings, + Expression> keySelector, + int storeId = 0) + where T : ISettings, new() + { + var propInfo = GetPropertyInfo(keySelector); + var key = string.Concat(typeof(T).Name, ".", propInfo.Name); + + string setting = GetSettingByKey(key, storeId: storeId); + return setting != null; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T LoadSetting(int storeId = 0) where T : ISettings, new() - { - return (T)LoadSettingCore(typeof(T), storeId); - } - - public ISettings LoadSetting(Type settingType, int storeId = 0) - { - Guard.NotNull(settingType, nameof(settingType)); - Guard.HasDefaultConstructor(settingType); - - if (!typeof(ISettings).IsAssignableFrom(settingType)) - { - throw new ArgumentException($"The type to load settings for must be a subclass of the '{typeof(ISettings).FullName}' interface", nameof(settingType)); - } - - return LoadSettingCore(settingType, storeId); - } - - protected virtual ISettings LoadSettingCore(Type settingType, int storeId = 0) - { - var allSettings = GetAllCachedSettings(); - var instance = (ISettings)Activator.CreateInstance(settingType); - var prefix = settingType.Name; - - foreach (var fastProp in FastProperty.GetProperties(settingType).Values) - { - var prop = fastProp.Property; - - // Get properties we can read and write to - if (!prop.CanWrite) - continue; - - string key = prefix + "." + prop.Name; - - if (!allSettings.TryGetValue(CreateCacheKey(key, storeId), out var cachedSetting) && storeId > 0) - { - // Fallback to shared (storeId = 0) - allSettings.TryGetValue(CreateCacheKey(key, 0), out cachedSetting); - } - - string setting = cachedSetting?.Value; - - if (setting == null) - { - if (fastProp.IsSequenceType) - { - if ((fastProp.GetValue(instance) as IEnumerable) != null) - { - // Instance of IEnumerable<> was already created, most likely in the constructor of the settings concrete class. - // In this case we shouldn't let the EnumerableConverter create a new instance but keep this one. - continue; - } - } - else - { - #region Obsolete ('EnumerableConverter' can handle this case now) - //if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) - //{ - // // convenience: don't return null for simple list types - // var listArg = prop.PropertyType.GetGenericArguments()[0]; - // object list = null; - - // if (listArg == typeof(int)) - // list = new List(); - // else if (listArg == typeof(decimal)) - // list = new List(); - // else if (listArg == typeof(string)) - // list = new List(); - - // if (list != null) - // { - // fastProp.SetValue(settings, list); - // } - //} - #endregion - - continue; - } - - } - - var converter = TypeConverterFactory.GetConverter(prop.PropertyType); - - if (converter == null || !converter.CanConvertFrom(typeof(string))) - continue; - - try - { - object value = converter.ConvertFrom(setting); - - // Set property - fastProp.SetValue(instance, value); - } - catch (Exception ex) - { - var msg = "Could not convert setting '{0}' to type '{1}'".FormatInvariant(key, prop.PropertyType.Name); - Logger.Error(ex, msg); - } - } - - return instance; - } - - public virtual SaveSettingResult SetSetting(string key, T value, int storeId = 0, bool clearCache = true) + { + return (T)LoadSettingCore(typeof(T), storeId); + } + + public ISettings LoadSetting(Type settingType, int storeId = 0) + { + Guard.NotNull(settingType, nameof(settingType)); + Guard.HasDefaultConstructor(settingType); + + if (!typeof(ISettings).IsAssignableFrom(settingType)) + { + throw new ArgumentException($"The type to load settings for must be a subclass of the '{typeof(ISettings).FullName}' interface", nameof(settingType)); + } + + return LoadSettingCore(settingType, storeId); + } + + protected virtual ISettings LoadSettingCore(Type settingType, int storeId = 0) + { + var allSettings = GetAllCachedSettings(); + var instance = (ISettings)Activator.CreateInstance(settingType); + var prefix = settingType.Name; + + foreach (var fastProp in FastProperty.GetProperties(settingType).Values) + { + var prop = fastProp.Property; + + // Get properties we can read and write to + if (!prop.CanWrite) + continue; + + string key = prefix + "." + prop.Name; + + if (!allSettings.TryGetValue(CreateCacheKey(key, storeId), out var cachedSetting) && storeId > 0) + { + // Fallback to shared (storeId = 0) + allSettings.TryGetValue(CreateCacheKey(key, 0), out cachedSetting); + } + + string setting = cachedSetting?.Value; + + if (setting == null) + { + if (fastProp.IsSequenceType) + { + if ((fastProp.GetValue(instance) as IEnumerable) != null) + { + // Instance of IEnumerable<> was already created, most likely in the constructor of the settings concrete class. + // In this case we shouldn't let the EnumerableConverter create a new instance but keep this one. + continue; + } + } + else + { + continue; + } + + } + + var converter = TypeConverterFactory.GetConverter(prop.PropertyType); + + if (converter == null || !converter.CanConvertFrom(typeof(string))) + continue; + + try + { + object value = converter.ConvertFrom(setting); + + // Set property + fastProp.SetValue(instance, value); + } + catch (Exception ex) + { + var msg = "Could not convert setting '{0}' to type '{1}'".FormatInvariant(key, prop.PropertyType.Name); + Logger.Error(ex, msg); + } + } + + return instance; + } + + public virtual SaveSettingResult SetSetting(string key, T value, int storeId = 0, bool clearCache = true) { Guard.NotEmpty(key, nameof(key)); - var str = value.Convert(); - var allSettings = GetAllCachedSettings(); - var cacheKey = CreateCacheKey(key, storeId); - var insert = false; - - if (allSettings.TryGetValue(cacheKey, out CachedSetting cachedSetting)) - { - var setting = GetSettingById(cachedSetting.Id); - if (setting != null) - { - // Update - if (setting.Value != str) - { - setting.Value = str; - UpdateSetting(setting, clearCache); - return SaveSettingResult.Modified; - } - } - else - { - insert = true; - } - } - else - { - insert = true; - } - - if (insert) - { - // Insert - var setting = new Setting - { - Name = key.ToLowerInvariant(), - Value = str, - StoreId = storeId - }; - InsertSetting(setting, clearCache); - return SaveSettingResult.Inserted; - } - - return SaveSettingResult.Unchanged; + var str = value.Convert(); + var allSettings = GetAllCachedSettings(); + var cacheKey = CreateCacheKey(key, storeId); + var insert = false; + + if (allSettings.TryGetValue(cacheKey, out CachedSetting cachedSetting)) + { + var setting = GetSettingById(cachedSetting.Id); + if (setting != null) + { + // Update + if (setting.Value != str) + { + setting.Value = str; + UpdateSetting(setting, clearCache); + return SaveSettingResult.Modified; + } + } + else + { + insert = true; + } + } + else + { + insert = true; + } + + if (insert) + { + // Insert + var setting = new Setting + { + Name = key.ToLowerInvariant(), + Value = str, + StoreId = storeId + }; + InsertSetting(setting, clearCache); + return SaveSettingResult.Inserted; + } + + return SaveSettingResult.Unchanged; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool SaveSetting(T settings, int storeId = 0) where T : ISettings, new() { - Guard.NotNull(settings, nameof(settings)); + Guard.NotNull(settings, nameof(settings)); - using (BeginScope(clearCache: true)) - { - var hasChanges = false; - var modifiedProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); - var settingType = typeof(T); - var prefix = settingType.Name; + using (BeginScope(clearCache: true)) + { + var hasChanges = false; + var modifiedProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + var settingType = typeof(T); + var prefix = settingType.Name; - /* We do not clear cache after each setting update. + /* We do not clear cache after each setting update. * This behavior can increase performance because cached settings will not be cleared * and loaded from database after each update */ - foreach (var prop in FastProperty.GetProperties(settingType).Values) - { - // get properties we can read and write to - if (!prop.IsPublicSettable) - continue; - - var converter = TypeConverterFactory.GetConverter(prop.Property.PropertyType); - if (converter == null || !converter.CanConvertFrom(typeof(string))) - continue; - - string key = prefix + "." + prop.Name; - // Duck typing is not supported in C#. That's why we're using dynamic type - dynamic currentValue = prop.GetValue(settings); - - if (SetSetting(key, currentValue ?? "", storeId, false) > SaveSettingResult.Unchanged) - { - hasChanges = true; - } - } - - return hasChanges; - } - } - - public virtual SaveSettingResult SaveSetting( - T settings, - Expression> keySelector, - int storeId = 0, - bool clearCache = true) where T : ISettings, new() - { - var propInfo = GetPropertyInfo(keySelector); - var key = string.Concat(typeof(T).Name, ".", propInfo.Name); - - // Duck typing is not supported in C#. That's why we're using dynamic type. - var fastProp = FastProperty.GetProperty(propInfo, PropertyCachingStrategy.EagerCached); - dynamic currentValue = fastProp.GetValue(settings); - - return SetSetting(key, currentValue ?? "", storeId, clearCache); - } - - public virtual SaveSettingResult UpdateSetting( - T settings, - Expression> keySelector, - bool overrideForStore, - int storeId = 0) where T : ISettings, new() - { - if (overrideForStore || storeId == 0) - { - return SaveSetting(settings, keySelector, storeId, false); - } - else if (storeId > 0 && DeleteSetting(settings, keySelector, storeId)) - { - return SaveSettingResult.Deleted; - } - - return SaveSettingResult.Unchanged; - } - - public virtual void DeleteSetting(Setting setting) - { - if (setting == null) - throw new ArgumentNullException("setting"); - - _settingRepository.Delete(setting); - - HasChanges = true; - - ClearCache(); - } + foreach (var prop in FastProperty.GetProperties(settingType).Values) + { + // get properties we can read and write to + if (!prop.IsPublicSettable) + continue; + + var converter = TypeConverterFactory.GetConverter(prop.Property.PropertyType); + if (converter == null || !converter.CanConvertFrom(typeof(string))) + continue; + + string key = prefix + "." + prop.Name; + // Duck typing is not supported in C#. That's why we're using dynamic type + dynamic currentValue = prop.GetValue(settings); + + if (SetSetting(key, currentValue ?? "", storeId, false) > SaveSettingResult.Unchanged) + { + hasChanges = true; + } + } + + return hasChanges; + } + } + + public virtual SaveSettingResult SaveSetting( + T settings, + Expression> keySelector, + int storeId = 0, + bool clearCache = true) where T : ISettings, new() + { + var propInfo = GetPropertyInfo(keySelector); + var key = string.Concat(typeof(T).Name, ".", propInfo.Name); + + // Duck typing is not supported in C#. That's why we're using dynamic type. + var fastProp = FastProperty.GetProperty(propInfo, PropertyCachingStrategy.EagerCached); + dynamic currentValue = fastProp.GetValue(settings); + + return SetSetting(key, currentValue ?? "", storeId, clearCache); + } + + public virtual SaveSettingResult UpdateSetting( + T settings, + Expression> keySelector, + bool overrideForStore, + int storeId = 0) where T : ISettings, new() + { + if (overrideForStore || storeId == 0) + { + return SaveSetting(settings, keySelector, storeId, false); + } + else if (storeId > 0 && DeleteSetting(settings, keySelector, storeId)) + { + return SaveSettingResult.Deleted; + } + + return SaveSettingResult.Unchanged; + } + + public virtual void DeleteSetting(Setting setting) + { + if (setting == null) + throw new ArgumentNullException("setting"); + + _settingRepository.Delete(setting); + + HasChanges = true; + + ClearCache(); + } public virtual void DeleteSetting() where T : ISettings, new() { - using (BeginScope()) - { - var settingsToDelete = new List(); - var allSettings = GetAllSettings(); - foreach (var prop in typeof(T).GetProperties()) - { - string key = typeof(T).Name + "." + prop.Name; - settingsToDelete.AddRange(allSettings.Where(x => x.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase))); - } - - foreach (var setting in settingsToDelete) - { - DeleteSetting(setting); - } - } + using (BeginScope()) + { + var settingsToDelete = new List(); + var allSettings = GetAllSettings(); + foreach (var prop in typeof(T).GetProperties()) + { + string key = typeof(T).Name + "." + prop.Name; + settingsToDelete.AddRange(allSettings.Where(x => x.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase))); + } + + foreach (var setting in settingsToDelete) + { + DeleteSetting(setting); + } + } + } + + public virtual bool DeleteSetting( + T settings, + Expression> keySelector, + int storeId = 0) where T : ISettings, new() + { + var propInfo = GetPropertyInfo(keySelector); + var key = string.Concat(typeof(T).Name, ".", propInfo.Name); + + return DeleteSetting(key, storeId); } - public virtual bool DeleteSetting( - T settings, - Expression> keySelector, - int storeId = 0) where T : ISettings, new() - { - var propInfo = GetPropertyInfo(keySelector); - var key = string.Concat(typeof(T).Name, ".", propInfo.Name); - - return DeleteSetting(key, storeId); - } - - public virtual bool DeleteSetting(string key, int storeId = 0) - { - if (key.HasValue()) - { - key = key.Trim().ToLowerInvariant(); - - var setting = ( - from s in _settingRepository.Table - where s.StoreId == storeId && s.Name == key - select s).FirstOrDefault(); - - if (setting != null) - { - DeleteSetting(setting); - return true; - } - } - - return false; - } - - public virtual int DeleteSettings(string rootKey) { - int result = 0; - - if (rootKey.IsEmpty()) - return 0; - - try - { - string sqlDelete = "DELETE FROM [Setting] WHERE [Name] LIKE '{0}%'".FormatWith(rootKey.EndsWith(".") ? rootKey : rootKey + "."); - result = _settingRepository.Context.ExecuteSqlCommand(sqlDelete); - - if (result > 0) - HasChanges = true; - - ClearCache(); - } - catch (Exception exc) - { - exc.Dump(); - } - - return result; - } - - protected override void OnClearCache() - { - _cacheManager.Remove(SETTINGS_ALL_KEY); - } - - protected string CreateCacheKey(string name, int storeId) - { - return name.Trim().ToLowerInvariant() + "/" + storeId.ToString(); - } + public virtual bool DeleteSetting(string key, int storeId = 0) + { + if (key.HasValue()) + { + key = key.Trim().ToLowerInvariant(); + + var setting = ( + from s in _settingRepository.Table + where s.StoreId == storeId && s.Name == key + select s).FirstOrDefault(); + + if (setting != null) + { + DeleteSetting(setting); + return true; + } + } + + return false; + } + + public virtual int DeleteSettings(string rootKey) + { + int result = 0; + + if (rootKey.IsEmpty()) + return 0; + + try + { + string sqlDelete = "DELETE FROM [Setting] WHERE [Name] LIKE '{0}%'".FormatWith(rootKey.EndsWith(".") ? rootKey : rootKey + "."); + result = _settingRepository.Context.ExecuteSqlCommand(sqlDelete); + + if (result > 0) + HasChanges = true; + + ClearCache(); + } + catch (Exception exc) + { + exc.Dump(); + } + + return result; + } + + protected override void OnClearCache() + { + _cacheManager.Remove(SETTINGS_ALL_KEY); + } + + protected string CreateCacheKey(string name, int storeId) + { + return name.Trim().ToLowerInvariant() + "/" + storeId.ToString(); + } } - [Serializable] - public class CachedSetting - { - public int Id { get; set; } - public string Name { get; set; } - public string Value { get; set; } - public int StoreId { get; set; } - } + [Serializable] + public class CachedSetting + { + public int Id { get; set; } + public string Name { get; set; } + public string Value { get; set; } + public int StoreId { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Customers/ChangePasswordRequest.cs b/src/Libraries/SmartStore.Services/Customers/ChangePasswordRequest.cs index 54b0e26ef4..cb8e9a5306 100644 --- a/src/Libraries/SmartStore.Services/Customers/ChangePasswordRequest.cs +++ b/src/Libraries/SmartStore.Services/Customers/ChangePasswordRequest.cs @@ -10,7 +10,7 @@ public class ChangePasswordRequest public string NewPassword { get; set; } public string OldPassword { get; set; } - public ChangePasswordRequest(string email, bool validateRequest, + public ChangePasswordRequest(string email, bool validateRequest, PasswordFormat newPasswordFormat, string newPassword, string oldPassword = "") { this.Email = email; diff --git a/src/Libraries/SmartStore.Services/Customers/CookieManager.cs b/src/Libraries/SmartStore.Services/Customers/CookieManager.cs index 5c997c622c..486f1a42b1 100644 --- a/src/Libraries/SmartStore.Services/Customers/CookieManager.cs +++ b/src/Libraries/SmartStore.Services/Customers/CookieManager.cs @@ -1,8 +1,10 @@ using Newtonsoft.Json; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; +using SmartStore.Services.Localization; using System; using System.Collections.Generic; using System.Linq; @@ -12,138 +14,166 @@ namespace SmartStore.Services.Customers { - /// - /// Class that will be serialized and stored as string in a cookie. - /// - public class ConsentCookie - { - public bool AllowAnalytics { get; set; } - public bool AllowThirdParty { get; set; } - } - - public partial class CookieManager : ICookieManager - { - private readonly ICommonServices _services; - private readonly ITypeFinder _typeFinder; - private readonly HttpContextBase _httpContext; - - public const string ConsentCookieName = "CookieConsent"; - - private readonly static object _lock = new object(); - private static IList _cookiePublisherTypes = null; - - public CookieManager( - ICommonServices services, - ITypeFinder typeFinder, - HttpContextBase httpContext) - { - _services = services; - _typeFinder = typeFinder; - _httpContext = httpContext; - } - - public LocalizerEx T { get; set; } = NullLocalizer.InstanceEx; - - public ILogger Logger { get; set; } = NullLogger.Instance; - - protected virtual IEnumerable GetAllCookiePublishers() - { - if (_cookiePublisherTypes == null) - { - lock (_lock) - { - _cookiePublisherTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true).ToList(); - } - } - - var cookiePublishers = _cookiePublisherTypes - .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as ICookiePublisher) - .ToArray(); - - return cookiePublishers; - } - - public virtual List GetAllCookieInfos() - { - var cookieInfos = new List(); - var plugins = GetAllCookiePublishers(); - - foreach(var plugin in plugins) - { - var typedInstance = plugin as ICookiePublisher; - cookieInfos.Add(typedInstance.GetCookieInfo()); - } - - return cookieInfos; - } - - public virtual bool IsCookieAllowed(ControllerContext context, CookieType cookieType) - { - // Ask whether cookie type is allowed. - var cookie = context.HttpContext.Request.Cookies[ConsentCookieName]; - if (cookie != null) - { - try - { - var cookieData = JsonConvert.DeserializeObject(cookie.Value); - if ((cookieData.AllowAnalytics && cookieType == CookieType.Analytics) || (cookieData.AllowThirdParty && cookieType == CookieType.ThirdParty)) - { - return true; - } - } - catch { - // Lets be tolerant in case of error. - return true; - } - } - - // If no cookie was set return false - return false; - } - - public virtual void SetConsentCookie(HttpResponseBase response, bool allowAnalytics = false, bool allowThirdParty = false) - { - var expiry = TimeSpan.FromDays(365); - - var cookieData = new ConsentCookie - { - AllowAnalytics = allowAnalytics, - AllowThirdParty = allowThirdParty - }; - - var consentCookie = new HttpCookie(ConsentCookieName) - { - // Store JSON serialized object which contains current allowed types (Analytics & ThirdParty) in cookie. - Value = JsonConvert.SerializeObject(cookieData), - Expires = DateTime.UtcNow + expiry - }; - - response.Cookies.Set(consentCookie); - } - - public virtual ConsentCookie GetCookieData(ControllerContext context) - { - var httpContext = context.HttpContext; - - if (httpContext.Items.Contains(ConsentCookieName)) - { - return httpContext.Items[ConsentCookieName] as ConsentCookie; - } - - var cookie = httpContext.Request.Cookies[ConsentCookieName]; - - if (cookie != null) - { - try - { - var cookieData = JsonConvert.DeserializeObject(cookie.Value); - httpContext.Items[ConsentCookieName] = cookieData; - return cookieData; - } - catch { } - } - - return null; - } - } + /// + /// Class that will be serialized and stored as string in a cookie. + /// + public class ConsentCookie + { + public bool AllowAnalytics { get; set; } + public bool AllowThirdParty { get; set; } + } + + public partial class CookieManager : ICookieManager + { + private readonly ICommonServices _services; + private readonly ITypeFinder _typeFinder; + private readonly HttpContextBase _httpContext; + private readonly PrivacySettings _privacySettings; + + public const string ConsentCookieName = "CookieConsent"; + + private readonly static object _lock = new object(); + private static IList _cookiePublisherTypes = null; + + public CookieManager( + ICommonServices services, + ITypeFinder typeFinder, + HttpContextBase httpContext, + PrivacySettings privacySettings) + { + _services = services; + _typeFinder = typeFinder; + _httpContext = httpContext; + _privacySettings = privacySettings; + } + + public LocalizerEx T { get; set; } = NullLocalizer.InstanceEx; + + public ILogger Logger { get; set; } = NullLogger.Instance; + + protected virtual IEnumerable GetAllCookiePublishers() + { + if (_cookiePublisherTypes == null) + { + lock (_lock) + { + if (_cookiePublisherTypes == null) + { + _cookiePublisherTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true).ToList(); + } + } + } + + var cookiePublishers = _cookiePublisherTypes + .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as ICookiePublisher) + .ToArray(); + + return cookiePublishers; + } + + public virtual List GetAllCookieInfos(bool addSettingCookies = false) + { + var cookieInfos = new List(); + var plugins = GetAllCookiePublishers(); + + foreach (var plugin in plugins) + { + var typedInstance = plugin as ICookiePublisher; + var cookieInfo = typedInstance.GetCookieInfo(); + if (cookieInfo != null) + cookieInfos.AddRange(cookieInfo); + } + + // Add user defined cookies from privacy settings. + if (addSettingCookies && _privacySettings.CookieInfos.HasValue()) + { + var ciList = JsonConvert.DeserializeObject>(_privacySettings.CookieInfos); + + if (ciList != null && ciList.Count > 0) + { + foreach (var ci in ciList) + { + ci.Name = ci.GetLocalized(x => x.Name); + ci.Description = ci.GetLocalized(x => x.Description); + cookieInfos.Add(ci); + } + } + } + + return cookieInfos; + } + + public virtual bool IsCookieAllowed(ControllerContext context, CookieType cookieType) + { + // Ask whether cookie type is allowed. + var cookie = context.HttpContext.Request.Cookies[ConsentCookieName]; + if (cookie != null) + { + try + { + var cookieData = JsonConvert.DeserializeObject(cookie.Value); + if ((cookieData.AllowAnalytics && cookieType == CookieType.Analytics) || (cookieData.AllowThirdParty && cookieType == CookieType.ThirdParty) || cookieType == CookieType.Required) + { + return true; + } + } + catch + { + // Lets be tolerant in case of error. + return true; + } + } + + // If no cookie was set return false + return false; + } + + public virtual void SetConsentCookie(HttpResponseBase response, bool allowAnalytics = false, bool allowThirdParty = false) + { + var expiry = TimeSpan.FromDays(365); + + var cookieData = new ConsentCookie + { + AllowAnalytics = allowAnalytics, + AllowThirdParty = allowThirdParty + }; + + var secure = _services.WebHelper.IsCurrentConnectionSecured(); + var consentCookie = new HttpCookie(ConsentCookieName) + { + // Store JSON serialized object which contains current allowed types (Analytics & ThirdParty) in cookie. + Value = JsonConvert.SerializeObject(cookieData), + Expires = DateTime.UtcNow + expiry, + Secure = secure, + SameSite = secure ? (SameSiteMode)_privacySettings.SameSiteMode : SameSiteMode.Lax + }; + + response.Cookies.Set(consentCookie); + } + + public virtual ConsentCookie GetCookieData(ControllerContext context) + { + var httpContext = context.HttpContext; + + if (httpContext.Items.Contains(ConsentCookieName)) + { + return httpContext.Items[ConsentCookieName] as ConsentCookie; + } + + var cookie = httpContext.Request.Cookies[ConsentCookieName]; + + if (cookie != null) + { + try + { + var cookieData = JsonConvert.DeserializeObject(cookie.Value); + httpContext.Items[ConsentCookieName] = cookieData; + return cookieData; + } + catch { } + } + + return null; + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs index 8f83fb33a8..564247e889 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs @@ -1,43 +1,51 @@ using System; -using System.Collections.Generic; using System.Linq; +using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Events; namespace SmartStore.Services.Customers { public partial class CustomerContentService : ICustomerContentService { private readonly IRepository _contentRepository; - private readonly IEventPublisher _eventPublisher; - public CustomerContentService(IRepository contentRepository, IEventPublisher eventPublisher) + public CustomerContentService(IRepository contentRepository) { _contentRepository = contentRepository; - _eventPublisher = eventPublisher; } public virtual void DeleteCustomerContent(CustomerContent content) { - Guard.NotNull(content, nameof(content)); + Guard.NotNull(content, nameof(content)); _contentRepository.Delete(content); } - public virtual IList GetAllCustomerContent(int customerId, bool? approved) + public virtual IPagedList GetAllCustomerContent( + int customerId, + bool? approved, + int pageIndex = 0, + int pageSize = int.MaxValue) { - var query = from c in _contentRepository.Table - orderby c.CreatedOnUtc descending - where !approved.HasValue || c.IsApproved == approved && - (customerId == 0 || c.CustomerId == customerId) - select c; + var query = _contentRepository.Table; - var content = query.ToList(); + if (approved.HasValue) + query = query.Where(c => c.IsApproved == approved); + if (customerId > 0) + query = query.Where(c => c.CustomerId == customerId); + + var content = new PagedList(query, pageIndex, pageSize); return content; } - public virtual IList GetAllCustomerContent(int customerId, bool? approved, DateTime? fromUtc = null, DateTime? toUtc = null) where T : CustomerContent + public virtual IPagedList GetAllCustomerContent( + int customerId, + bool? approved, + DateTime? fromUtc = null, + DateTime? toUtc = null, + int pageIndex = 0, + int pageSize = int.MaxValue) where T : CustomerContent { var query = _contentRepository.Table; @@ -52,7 +60,7 @@ public virtual IList GetAllCustomerContent(int customerId, bool? approved, query = query.OrderByDescending(c => c.CreatedOnUtc); - var content = query.OfType().ToList(); + var content = new PagedList(query.OfType(), pageIndex, pageSize); return content; } @@ -62,21 +70,21 @@ public virtual CustomerContent GetCustomerContentById(int contentId) return null; return _contentRepository.GetById(contentId); - + } public virtual void InsertCustomerContent(CustomerContent content) { - Guard.NotNull(content, nameof(content)); + Guard.NotNull(content, nameof(content)); - _contentRepository.Insert(content); + _contentRepository.Insert(content); } public virtual void UpdateCustomerContent(CustomerContent content) { - Guard.NotNull(content, nameof(content)); + Guard.NotNull(content, nameof(content)); - _contentRepository.Update(content); + _contentRepository.Update(content); } } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs index f1a0d1eb1b..cb0880844c 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs @@ -17,7 +17,7 @@ namespace SmartStore.Services.Customers { - public static class CustomerExtentions + public static class CustomerExtentions { private static readonly string[] _systemColors = new string[] { "primary", "secondary", "success", "info", "warning", "danger", "light", "dark" }; @@ -30,10 +30,10 @@ public static class CustomerExtentions /// Result public static bool IsInCustomerRole(this Customer customer, string customerRoleSystemName, bool onlyActiveCustomerRoles = true) { - Guard.NotNull(customer, nameof(customer)); - Guard.NotEmpty(customerRoleSystemName, nameof(customerRoleSystemName)); + Guard.NotNull(customer, nameof(customer)); + Guard.NotEmpty(customerRoleSystemName, nameof(customerRoleSystemName)); - var result = customer.CustomerRoleMappings + var result = customer.CustomerRoleMappings .Where(rm => !onlyActiveCustomerRoles || rm.CustomerRole.Active) .Where(rm => rm.CustomerRole.SystemName == customerRoleSystemName) .FirstOrDefault() != null; @@ -48,10 +48,10 @@ public static bool IsInCustomerRole(this Customer customer, string customerRoleS /// Result public static bool IsBackgroundTaskAccount(this Customer customer) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); - if (!customer.IsSystemAccount || customer.SystemName.IsEmpty()) - return false; + if (!customer.IsSystemAccount || customer.SystemName.IsEmpty()) + return false; var result = customer.SystemName.Equals(SystemCustomerNames.BackgroundTask, StringComparison.InvariantCultureIgnoreCase); return result; @@ -64,30 +64,30 @@ public static bool IsBackgroundTaskAccount(this Customer customer) /// Result public static bool IsSearchEngineAccount(this Customer customer) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); - if (!customer.IsSystemAccount || customer.SystemName.IsEmpty()) - return false; + if (!customer.IsSystemAccount || customer.SystemName.IsEmpty()) + return false; var result = customer.SystemName.Equals(SystemCustomerNames.SearchEngine, StringComparison.InvariantCultureIgnoreCase); return result; } - /// - /// Gets a value indicating whether customer is the pdf converter - /// - /// Customer - /// Result - public static bool IsPdfConverter(this Customer customer) - { - Guard.NotNull(customer, nameof(customer)); + /// + /// Gets a value indicating whether customer is the pdf converter + /// + /// Customer + /// Result + public static bool IsPdfConverter(this Customer customer) + { + Guard.NotNull(customer, nameof(customer)); - if (!customer.IsSystemAccount || customer.SystemName.IsEmpty()) - return false; + if (!customer.IsSystemAccount || customer.SystemName.IsEmpty()) + return false; - var result = customer.SystemName.Equals(SystemCustomerNames.PdfConverter, StringComparison.InvariantCultureIgnoreCase); - return result; - } + var result = customer.SystemName.Equals(SystemCustomerNames.PdfConverter, StringComparison.InvariantCultureIgnoreCase); + return result; + } /// /// Gets a value indicating whether customer is administrator @@ -108,9 +108,10 @@ public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles /// A value indicating whether we should look only in active customer roles /// Result [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSuperAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) { - return IsInCustomerRole(customer, SystemCustomerRoleNames.SuperAdministrators, onlyActiveCustomerRoles); - } + public static bool IsSuperAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) + { + return IsInCustomerRole(customer, SystemCustomerRoleNames.SuperAdministrators, onlyActiveCustomerRoles); + } /// /// Gets a value indicating whether customer is a forum moderator @@ -147,29 +148,52 @@ public static bool IsGuest(this Customer customer, bool onlyActiveCustomerRoles { return IsInCustomerRole(customer, SystemCustomerRoleNames.Guests, onlyActiveCustomerRoles); } - + + /// + /// Gets the customer's full name. + /// + /// Customer. + /// Customer full name. public static string GetFullName(this Customer customer) { - if (customer == null) - return string.Empty; - - if (customer.FullName.HasValue()) - { - return customer.FullName; - } - - string name = customer.BillingAddress?.GetFullName(); - if (name.IsEmpty()) - { - name = customer.ShippingAddress?.GetFullName(); - } - if (name.IsEmpty()) - { - name = customer.Addresses.FirstOrDefault()?.GetFullName(); - } - - return name.TrimSafe(); - } + if (customer == null) + return string.Empty; + + if (customer.FullName.HasValue()) + { + return customer.FullName; + } + + string name = customer.BillingAddress?.GetFullName(); + if (name.IsEmpty()) + { + name = customer.ShippingAddress?.GetFullName(); + } + if (name.IsEmpty()) + { + name = customer.Addresses.FirstOrDefault()?.GetFullName(); + } + + return name.TrimSafe(); + } + + /// + /// Gets the display name of a customer (full name, user name or email). + /// + /// Customer. + /// Localizer. + /// Display name of a customer. + public static string GetDisplayName(this Customer customer, Localizer T) + { + if (customer != null) + { + return customer.IsGuest() + ? T("Customer.Guest").Text + : customer.GetFullName().NullEmpty() ?? customer.Username ?? customer.FindEmail(); + } + + return null; + } /// /// Formats the customer name. @@ -289,31 +313,31 @@ public static string FormatUserName( /// Find any email address of customer /// public static string FindEmail(this Customer customer) - { - if (customer != null) - { - return customer.Email.NullEmpty() - ?? customer.BillingAddress?.Email?.NullEmpty() - ?? customer.ShippingAddress?.Email?.NullEmpty(); - } - - return null; - } - - public static Language GetLanguage(this Customer customer) - { - if (customer == null) - return null; + { + if (customer != null) + { + return customer.Email.NullEmpty() + ?? customer.BillingAddress?.Email?.NullEmpty() + ?? customer.ShippingAddress?.Email?.NullEmpty(); + } + + return null; + } + + public static Language GetLanguage(this Customer customer) + { + if (customer == null) + return null; var language = EngineContext.Current.Resolve().GetLanguageById(customer.GetAttribute(SystemCustomerAttributeNames.LanguageId)); if (language == null || !language.Published) - { - language = EngineContext.Current.Resolve().WorkingLanguage; - } + { + language = EngineContext.Current.Resolve().WorkingLanguage; + } - return language; - } + return language; + } /// /// Returns a random system color suitable as avatar background color. @@ -342,160 +366,160 @@ public static string GetAvatarColor(this Customer customer, IGenericAttributeSer return color; } - #region Shopping cart - - public static int CountProductsInCart(this Customer customer, ShoppingCartType cartType, int? storeId = null) - { - if (customer != null) - { - var cartService = EngineContext.Current.Resolve(); - var count = cartService.CountItems(customer, cartType, storeId); - - return count; - } - - return 0; - } - - public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null) - { - if (customer != null) - { - var cartService = EngineContext.Current.Resolve(); - var items = cartService.GetCartItems(customer, cartType, storeId); - - return items; - } - - return new List(); - } - - #endregion - - #region Gift cards - - /// - /// Gets coupon codes - /// - /// Customer - /// Coupon codes - public static string[] ParseAppliedGiftCardCouponCodes(this Customer customer) - { - var genericAttributeService = EngineContext.Current.Resolve(); - string existingGiftCartCouponCodes = customer.GetAttribute(SystemCustomerAttributeNames.GiftCardCouponCodes, - genericAttributeService); - - var couponCodes = new List(); - if (String.IsNullOrEmpty(existingGiftCartCouponCodes)) - return couponCodes.ToArray(); - - try - { - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(existingGiftCartCouponCodes); - - var nodeList1 = xmlDoc.SelectNodes(@"//GiftCardCouponCodes/CouponCode"); - foreach (XmlNode node1 in nodeList1) - { - if (node1.Attributes != null && node1.Attributes["Code"] != null) - { - string code = node1.Attributes["Code"].InnerText.Trim(); - couponCodes.Add(code); - } - } - } - catch (Exception exc) - { - Debug.Write(exc.ToString()); - } - return couponCodes.ToArray(); - } - - /// - /// Adds a coupon code - /// - /// Customer - /// Coupon code - /// New coupon codes document - public static void ApplyGiftCardCouponCode(this Customer customer, string couponCode) - { - var genericAttributeService = EngineContext.Current.Resolve(); - string result = string.Empty; - try - { - string existingGiftCartCouponCodes = customer.GetAttribute(SystemCustomerAttributeNames.GiftCardCouponCodes, - genericAttributeService); - - couponCode = couponCode.Trim().ToLower(); - - var xmlDoc = new XmlDocument(); - if (String.IsNullOrEmpty(existingGiftCartCouponCodes)) - { - var element1 = xmlDoc.CreateElement("GiftCardCouponCodes"); - xmlDoc.AppendChild(element1); - } - else - { - xmlDoc.LoadXml(existingGiftCartCouponCodes); - } - var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//GiftCardCouponCodes"); - - XmlElement gcElement = null; - //find existing - var nodeList1 = xmlDoc.SelectNodes(@"//GiftCardCouponCodes/CouponCode"); - foreach (XmlNode node1 in nodeList1) - { - if (node1.Attributes != null && node1.Attributes["Code"] != null) - { - string couponCodeAttribute = node1.Attributes["Code"].InnerText.Trim(); - if (couponCodeAttribute.ToLower() == couponCode.ToLower()) - { - gcElement = (XmlElement)node1; - break; - } - } - } - - //create new one if not found - if (gcElement == null) - { - gcElement = xmlDoc.CreateElement("CouponCode"); - gcElement.SetAttribute("Code", couponCode); - rootElement.AppendChild(gcElement); - } - - result = xmlDoc.OuterXml; - } - catch (Exception exc) - { - Debug.Write(exc.ToString()); - } - - //apply new value - genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.GiftCardCouponCodes, result); - } - - /// - /// Removes a coupon code - /// - /// Customer - /// Coupon code to remove - /// New coupon codes document - public static void RemoveGiftCardCouponCode(this Customer customer, string couponCode) - { - //get applied coupon codes - var existingCouponCodes = customer.ParseAppliedGiftCardCouponCodes(); - - //clear them - var genericAttributeService = EngineContext.Current.Resolve(); - genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.GiftCardCouponCodes, null); - - //save again except removed one - foreach (string existingCouponCode in existingCouponCodes) - if (!existingCouponCode.Equals(couponCode, StringComparison.InvariantCultureIgnoreCase)) - customer.ApplyGiftCardCouponCode(existingCouponCode); - } - - #endregion - } + #region Shopping cart + + public static int CountProductsInCart(this Customer customer, ShoppingCartType cartType, int? storeId = null) + { + if (customer != null) + { + var cartService = EngineContext.Current.Resolve(); + var count = cartService.CountItems(customer, cartType, storeId); + + return count; + } + + return 0; + } + + public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null) + { + if (customer != null) + { + var cartService = EngineContext.Current.Resolve(); + var items = cartService.GetCartItems(customer, cartType, storeId); + + return items; + } + + return new List(); + } + + #endregion + + #region Gift cards + + /// + /// Gets coupon codes + /// + /// Customer + /// Coupon codes + public static string[] ParseAppliedGiftCardCouponCodes(this Customer customer) + { + var genericAttributeService = EngineContext.Current.Resolve(); + string existingGiftCartCouponCodes = customer.GetAttribute(SystemCustomerAttributeNames.GiftCardCouponCodes, + genericAttributeService); + + var couponCodes = new List(); + if (String.IsNullOrEmpty(existingGiftCartCouponCodes)) + return couponCodes.ToArray(); + + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(existingGiftCartCouponCodes); + + var nodeList1 = xmlDoc.SelectNodes(@"//GiftCardCouponCodes/CouponCode"); + foreach (XmlNode node1 in nodeList1) + { + if (node1.Attributes != null && node1.Attributes["Code"] != null) + { + string code = node1.Attributes["Code"].InnerText.Trim(); + couponCodes.Add(code); + } + } + } + catch (Exception exc) + { + Debug.Write(exc.ToString()); + } + return couponCodes.ToArray(); + } + + /// + /// Adds a coupon code + /// + /// Customer + /// Coupon code + /// New coupon codes document + public static void ApplyGiftCardCouponCode(this Customer customer, string couponCode) + { + var genericAttributeService = EngineContext.Current.Resolve(); + string result = string.Empty; + try + { + string existingGiftCartCouponCodes = customer.GetAttribute(SystemCustomerAttributeNames.GiftCardCouponCodes, + genericAttributeService); + + couponCode = couponCode.Trim().ToLower(); + + var xmlDoc = new XmlDocument(); + if (String.IsNullOrEmpty(existingGiftCartCouponCodes)) + { + var element1 = xmlDoc.CreateElement("GiftCardCouponCodes"); + xmlDoc.AppendChild(element1); + } + else + { + xmlDoc.LoadXml(existingGiftCartCouponCodes); + } + var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//GiftCardCouponCodes"); + + XmlElement gcElement = null; + //find existing + var nodeList1 = xmlDoc.SelectNodes(@"//GiftCardCouponCodes/CouponCode"); + foreach (XmlNode node1 in nodeList1) + { + if (node1.Attributes != null && node1.Attributes["Code"] != null) + { + string couponCodeAttribute = node1.Attributes["Code"].InnerText.Trim(); + if (couponCodeAttribute.ToLower() == couponCode.ToLower()) + { + gcElement = (XmlElement)node1; + break; + } + } + } + + //create new one if not found + if (gcElement == null) + { + gcElement = xmlDoc.CreateElement("CouponCode"); + gcElement.SetAttribute("Code", couponCode); + rootElement.AppendChild(gcElement); + } + + result = xmlDoc.OuterXml; + } + catch (Exception exc) + { + Debug.Write(exc.ToString()); + } + + //apply new value + genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.GiftCardCouponCodes, result); + } + + /// + /// Removes a coupon code + /// + /// Customer + /// Coupon code to remove + /// New coupon codes document + public static void RemoveGiftCardCouponCode(this Customer customer, string couponCode) + { + //get applied coupon codes + var existingCouponCodes = customer.ParseAppliedGiftCardCouponCodes(); + + //clear them + var genericAttributeService = EngineContext.Current.Resolve(); + genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.GiftCardCouponCodes, null); + + //save again except removed one + foreach (string existingCouponCode in existingCouponCodes) + if (!existingCouponCode.Equals(couponCode, StringComparison.InvariantCultureIgnoreCase)) + customer.ApplyGiftCardCouponCode(existingCouponCode); + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs index 398e45d2a8..af006f35e5 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs @@ -5,76 +5,76 @@ namespace SmartStore.Services.Customers { - public static class CustomerMessageFactoryExtensions - { - /// - /// Sends 'New customer' notification message to a store owner - /// - public static CreateMessageResult SendCustomerRegisteredNotificationMessage(this IMessageFactory factory, Customer customer, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerRegistered, languageId, customer: customer), true); - } + public static class CustomerMessageFactoryExtensions + { + /// + /// Sends 'New customer' notification message to a store owner + /// + public static CreateMessageResult SendCustomerRegisteredNotificationMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerRegistered, languageId, customer: customer), true); + } - /// - /// Sends a welcome message to a customer - /// - public static CreateMessageResult SendCustomerWelcomeMessage(this IMessageFactory factory, Customer customer, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerWelcome, languageId, customer: customer), true); - } + /// + /// Sends a welcome message to a customer + /// + public static CreateMessageResult SendCustomerWelcomeMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerWelcome, languageId, customer: customer), true); + } - /// - /// Sends an email validation message to a customer - /// - public static CreateMessageResult SendCustomerEmailValidationMessage(this IMessageFactory factory, Customer customer, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerEmailValidation, languageId, customer: customer), true); - } + /// + /// Sends an email validation message to a customer + /// + public static CreateMessageResult SendCustomerEmailValidationMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerEmailValidation, languageId, customer: customer), true); + } - /// - /// Sends password recovery message to a customer - /// - public static CreateMessageResult SendCustomerPasswordRecoveryMessage(this IMessageFactory factory, Customer customer, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerPasswordRecovery, languageId, customer: customer), true); - } + /// + /// Sends password recovery message to a customer + /// + public static CreateMessageResult SendCustomerPasswordRecoveryMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerPasswordRecovery, languageId, customer: customer), true); + } - /// - /// Sends wishlist "email a friend" message - /// - public static CreateMessageResult SendShareWishlistMessage(this IMessageFactory factory, Customer customer, - string fromEmail, string toEmail, string personalMessage, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); + /// + /// Sends wishlist "email a friend" message + /// + public static CreateMessageResult SendShareWishlistMessage(this IMessageFactory factory, Customer customer, + string fromEmail, string toEmail, string personalMessage, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); - var model = new NamedModelPart("Wishlist") - { - ["PersonalMessage"] = personalMessage, - ["From"] = fromEmail, - ["To"] = toEmail - }; + var model = new NamedModelPart("Wishlist") + { + ["PersonalMessage"] = personalMessage, + ["From"] = fromEmail, + ["To"] = toEmail + }; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShareWishlist, languageId, customer: customer), true, model); - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShareWishlist, languageId, customer: customer), true, model); + } - /// - /// Sends a "new VAT sumitted" notification to a store owner - /// - public static CreateMessageResult SendNewVatSubmittedStoreOwnerNotification(this IMessageFactory factory, Customer customer, string vatName, string vatAddress, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); + /// + /// Sends a "new VAT sumitted" notification to a store owner + /// + public static CreateMessageResult SendNewVatSubmittedStoreOwnerNotification(this IMessageFactory factory, Customer customer, string vatName, string vatAddress, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); - var model = new NamedModelPart("VatValidationResult") - { - ["Name"] = vatName, - ["Address"] = vatAddress - }; + var model = new NamedModelPart("VatValidationResult") + { + ["Name"] = vatName, + ["Address"] = vatAddress + }; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewVatSubmittedStoreOwner, languageId, customer: customer), true, model); - } - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewVatSubmittedStoreOwner, languageId, customer: customer), true, model); + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationRequest.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationRequest.cs index a0804fed2b..d4f93db0cf 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationRequest.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationRequest.cs @@ -12,7 +12,7 @@ public class CustomerRegistrationRequest public bool IsApproved { get; set; } public CustomerRegistrationRequest(Customer customer, string email, string username, - string password, + string password, PasswordFormat passwordFormat, bool isApproved = true) { diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationResult.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationResult.cs index fc0dad7d05..12a94af9ae 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationResult.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationResult.cs @@ -1,22 +1,19 @@ using System.Collections.Generic; -namespace SmartStore.Services.Customers +namespace SmartStore.Services.Customers { - public class CustomerRegistrationResult + public class CustomerRegistrationResult { public IList Errors { get; set; } - public CustomerRegistrationResult() + public CustomerRegistrationResult() { this.Errors = new List(); } - public bool Success - { - get { return this.Errors.Count == 0; } - } + public bool Success => this.Errors.Count == 0; - public void AddError(string error) + public void AddError(string error) { this.Errors.Add(error); } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs index 074dab7ad3..4cfc490e35 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs @@ -9,18 +9,18 @@ namespace SmartStore.Services.Customers { - public partial class CustomerRegistrationService : ICustomerRegistrationService + public partial class CustomerRegistrationService : ICustomerRegistrationService { private readonly ICustomerService _customerService; private readonly IEncryptionService _encryptionService; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; private readonly RewardPointsSettings _rewardPointsSettings; private readonly CustomerSettings _customerSettings; - private readonly IStoreContext _storeContext; + private readonly IStoreContext _storeContext; private readonly IEventPublisher _eventPublisher; - public CustomerRegistrationService(ICustomerService customerService, - IEncryptionService encryptionService, + public CustomerRegistrationService(ICustomerService customerService, + IEncryptionService encryptionService, INewsLetterSubscriptionService newsLetterSubscriptionService, RewardPointsSettings rewardPointsSettings, CustomerSettings customerSettings, IStoreContext storeContext, IEventPublisher eventPublisher) @@ -30,15 +30,15 @@ public CustomerRegistrationService(ICustomerService customerService, _newsLetterSubscriptionService = newsLetterSubscriptionService; _rewardPointsSettings = rewardPointsSettings; _customerSettings = customerSettings; - _storeContext = storeContext; + _storeContext = storeContext; _eventPublisher = eventPublisher; - T = NullLocalizer.Instance; - } + T = NullLocalizer.Instance; + } - public Localizer T { get; set; } + public Localizer T { get; set; } - public virtual bool ValidateCustomer(string usernameOrEmail, string password) + public virtual bool ValidateCustomer(string usernameOrEmail, string password) { Customer customer = null; @@ -95,8 +95,8 @@ public virtual bool ValidateCustomer(string usernameOrEmail, string password) public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationRequest request) { - Guard.NotNull(request, nameof(request)); - Guard.NotNull(request.Customer, nameof(request.Customer)); + Guard.NotNull(request, nameof(request)); + Guard.NotNull(request.Customer, nameof(request.Customer)); var result = new CustomerRegistrationResult(); @@ -124,7 +124,7 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR return result; } - if (!request.Email.IsEmail()) + if (!request.Email.IsEmail()) { result.AddError(T("Common.WrongEmail")); return result; @@ -162,17 +162,17 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR switch (request.PasswordFormat) { - case PasswordFormat.Clear: - request.Customer.Password = request.Password; - break; - case PasswordFormat.Encrypted: - request.Customer.Password = _encryptionService.EncryptText(request.Password); - break; - case PasswordFormat.Hashed: - string saltKey = _encryptionService.CreateSaltKey(5); - request.Customer.PasswordSalt = saltKey; - request.Customer.Password = _encryptionService.CreatePasswordHash(request.Password, saltKey, _customerSettings.HashedPasswordFormat); - break; + case PasswordFormat.Clear: + request.Customer.Password = request.Password; + break; + case PasswordFormat.Encrypted: + request.Customer.Password = _encryptionService.EncryptText(request.Password); + break; + case PasswordFormat.Hashed: + string saltKey = _encryptionService.CreateSaltKey(5); + request.Customer.PasswordSalt = saltKey; + request.Customer.Password = _encryptionService.CreatePasswordHash(request.Password, saltKey, _customerSettings.HashedPasswordFormat); + break; } request.Customer.Active = request.IsApproved; @@ -184,38 +184,38 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR } if (_customerSettings.RegisterCustomerRoleId != 0) - { - var customerRole = _customerService.GetCustomerRoleById(_customerSettings.RegisterCustomerRoleId); + { + var customerRole = _customerService.GetCustomerRoleById(_customerSettings.RegisterCustomerRoleId); if (customerRole != null && customerRole.Id != registeredRole.Id) { _customerService.InsertCustomerRoleMapping(new CustomerRoleMapping { CustomerId = request.Customer.Id, CustomerRoleId = customerRole.Id }); } - } + } - // Add to 'Registered' role. + // Add to 'Registered' role. _customerService.InsertCustomerRoleMapping(new CustomerRoleMapping { CustomerId = request.Customer.Id, CustomerRoleId = registeredRole.Id }); // Remove from 'Guests' role. var mappings = request.Customer.CustomerRoleMappings.Where(x => !x.IsSystemMapping && x.CustomerRole.SystemName == SystemCustomerRoleNames.Guests).ToList(); mappings.Each(x => _customerService.DeleteCustomerRoleMapping(x)); - // Add reward points for customer registration (if enabled) - if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForRegistration > 0) - { - request.Customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForRegistration, T("RewardPoints.Message.RegisteredAsCustomer")); - } + // Add reward points for customer registration (if enabled) + if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForRegistration > 0) + { + request.Customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForRegistration, T("RewardPoints.Message.RegisteredAsCustomer")); + } _customerService.UpdateCustomer(request.Customer); _eventPublisher.Publish(new CustomerRegisteredEvent { Customer = request.Customer }); return result; } - + public virtual PasswordChangeResult ChangePassword(ChangePasswordRequest request) { - Guard.NotNull(request, nameof(request)); + Guard.NotNull(request, nameof(request)); - var result = new PasswordChangeResult(); + var result = new PasswordChangeResult(); if (!request.Email.HasValue()) { result.AddError(T("Account.ChangePassword.Errors.EmailIsNotProvided")); @@ -297,12 +297,12 @@ public virtual PasswordChangeResult ChangePassword(ChangePasswordRequest request public virtual void SetEmail(Customer customer, string newEmail) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); newEmail = newEmail.Trim(); string oldEmail = customer.Email; - if (!newEmail.IsEmail()) + if (!newEmail.IsEmail()) throw new SmartException(T("Account.EmailUsernameErrors.NewEmailIsNotValid")); if (newEmail.Length > 100) @@ -329,9 +329,9 @@ public virtual void SetEmail(Customer customer, string newEmail) public virtual void SetUsername(Customer customer, string newUsername) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); - if (_customerSettings.CustomerLoginType == CustomerLoginType.Email) + if (_customerSettings.CustomerLoginType == CustomerLoginType.Email) throw new SmartException("Usernames are disabled"); if (!_customerSettings.AllowUsersToChangeUsernames) diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerReportService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerReportService.cs index 4631a6f7ef..4aff7b3b81 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerReportService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerReportService.cs @@ -2,143 +2,96 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; +using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Localization; -using SmartStore.Services.Catalog; using SmartStore.Services.Helpers; +using SmartStore.Services.Orders; namespace SmartStore.Services.Customers { - /// - /// Customer report service - /// public partial class CustomerReportService : ICustomerReportService { - #region Fields - private readonly IRepository _customerRepository; private readonly IRepository _orderRepository; private readonly ICustomerService _customerService; private readonly IDateTimeHelper _dateTimeHelper; - private readonly IPriceFormatter _priceFormatter; - - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Customer repository - /// Order repository - /// Customer service - /// Date time helper - public CustomerReportService(IRepository customerRepository, - IRepository orderRepository, ICustomerService customerService, - IDateTimeHelper dateTimeHelper, IPriceFormatter priceFormatter) + + public CustomerReportService( + IRepository customerRepository, + IRepository orderRepository, + ICustomerService customerService, + IDateTimeHelper dateTimeHelper) { _customerRepository = customerRepository; _orderRepository = orderRepository; _customerService = customerService; _dateTimeHelper = dateTimeHelper; - _priceFormatter = priceFormatter; } - #endregion - - public Localizer T + public Localizer T { get; set; } = NullLocalizer.Instance; + + public virtual IPagedList GetTopCustomersReport( + DateTime? startTime, + DateTime? endTime, + OrderStatus? orderStatus, + PaymentStatus? paymentStatus, + ShippingStatus? shippingStatus, + ReportSorting sorting, + int pageIndex = 0, + int pageSize = int.MaxValue) { - get; - set; - } = NullLocalizer.Instance; - - - #region Methods - - /// - /// Get best customers - /// - /// Order start time; null to load all - /// Order end time; null to load all - /// Order status; null to load all records - /// Order payment status; null to load all records - /// Order shippment status; null to load all records - /// 1 - order by order total, 2 - order by number of orders - /// Report - public virtual IList GetTopCustomersReport(DateTime? startTime, - DateTime? endTime, OrderStatus? os, PaymentStatus? ps, ShippingStatus? ss, int orderBy, int count) - { - int? orderStatusId = null; - if (os.HasValue) - orderStatusId = (int)os.Value; - - int? paymentStatusId = null; - if (ps.HasValue) - paymentStatusId = (int)ps.Value; - - int? shippingStatusId = null; - if (ss.HasValue) - shippingStatusId = (int)ss.Value; - var query1 = from c in _customerRepository.Table - join o in _orderRepository.Table on c.Id equals o.CustomerId - where (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && - (!endTime.HasValue || endTime.Value >= o.CreatedOnUtc) && - (!orderStatusId.HasValue || orderStatusId == o.OrderStatusId) && - (!paymentStatusId.HasValue || paymentStatusId == o.PaymentStatusId) && - (!shippingStatusId.HasValue || shippingStatusId == o.ShippingStatusId) && - (!o.Deleted) && - (!c.Deleted) - select new { c, o }; - - var query2 = from co in query1 - group co by co.c.Id into g - select new - { - CustomerId = g.Key, - OrderTotal = g.Sum(x => x.o.OrderTotal), - OrderCount = g.Count() - }; - switch (orderBy) + var orderStatusId = orderStatus.HasValue ? (int)orderStatus.Value : (int?)null; + var paymentStatusId = paymentStatus.HasValue ? (int)paymentStatus.Value : (int?)null; + var shippingStatusId = shippingStatus.HasValue ? (int)shippingStatus.Value : (int?)null; + + var query = + from c in _customerRepository.TableUntracked + join o in _orderRepository.TableUntracked on c.Id equals o.CustomerId + where (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && + (!endTime.HasValue || endTime.Value >= o.CreatedOnUtc) && + (!orderStatusId.HasValue || orderStatusId == o.OrderStatusId) && + (!paymentStatusId.HasValue || paymentStatusId == o.PaymentStatusId) && + (!shippingStatusId.HasValue || shippingStatusId == o.ShippingStatusId) && + (!o.Deleted) && + (!c.Deleted) + select new { c, o }; + + var groupedQuery = + from co in query + group co by co.c.Id into g + select new TopCustomerReportLine + { + CustomerId = g.Key, + OrderTotal = g.Sum(x => x.o.OrderTotal), + OrderCount = g.Count() + }; + + switch (sorting) { - case 1: - { - query2 = query2.OrderByDescending(x => x.OrderTotal); - } + case ReportSorting.ByAmountAsc: + groupedQuery = groupedQuery.OrderBy(x => x.OrderTotal); break; - case 2: - { - query2 = query2.OrderByDescending(x => x.OrderCount).ThenByDescending(x => x.OrderTotal); - } + case ReportSorting.ByAmountDesc: + groupedQuery = groupedQuery.OrderByDescending(x => x.OrderTotal); break; + case ReportSorting.ByQuantityAsc: + groupedQuery = groupedQuery.OrderBy(x => x.OrderCount).ThenByDescending(x => x.OrderTotal); + break; + case ReportSorting.ByQuantityDesc: default: - throw new ArgumentException("Wrong orderBy parameter", "orderBy"); + groupedQuery = groupedQuery.OrderByDescending(x => x.OrderCount).ThenByDescending(x => x.OrderTotal); + break; } - query2 = query2.Take(() => count); - - var result = query2.ToList().Select(x => - { - return new TopCustomerReportLine() - { - CustomerId = x.CustomerId, - OrderTotal = x.OrderTotal, - OrderCount = x.OrderCount, - }; - - }).ToList(); - + var result = new PagedList(groupedQuery, pageIndex, pageSize); return result; } - /// - /// Gets a report of customers registered in the last days - /// - /// Customers registered in the last days - /// Number of registered customers public virtual int GetRegisteredCustomersReport(int days) { DateTime date = _dateTimeHelper.ConvertToUserTime(DateTime.Now).AddDays(-days); @@ -159,14 +112,16 @@ from rm in c.CustomerRoleMappings return count; } - public List GetRegisteredCustomersDate() + public virtual List GetRegisteredCustomersDate() { - //Get registred customers in last 365 days, group by day and count them + // Get registred customers in last 365 days, group by day and count them. var date = _dateTimeHelper.ConvertToUserTime(DateTime.Now).AddDays(-365); var registeredCustomerRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); if (registeredCustomerRole == null) + { return new List(); + } var query = from c in _customerRepository.Table from rm in c.CustomerRoleMappings @@ -178,6 +133,7 @@ group c.CreatedOnUtc by DbFunctions.TruncateTime(c.CreatedOnUtc) into dg var data = new List(); data.Add(new RegistredCustomersDate(DateTime.Now.AddDays(-760), 0)); + foreach (var item in query.ToList()) { data.Add(new RegistredCustomersDate(item.Date, item.Count)); @@ -189,7 +145,7 @@ group c.CreatedOnUtc by DbFunctions.TruncateTime(c.CreatedOnUtc) into dg public virtual int GetCustomerRegistrations(DateTime? startTimeUtc, DateTime? endTimeUtc) { var registeredCustomerRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); - if(registeredCustomerRole == null) + if (registeredCustomerRole == null) { return 0; } @@ -221,7 +177,5 @@ public RegistredCustomersDate(DateTime date, int count) Count = count; } } - - #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs b/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs index e0737ac075..1faec0c439 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerSearchQuery.cs @@ -4,87 +4,87 @@ namespace SmartStore.Services.Customers { - public class CustomerSearchQuery : ICloneable - { - /// - /// Customer registration from; null to load all customers. Default: null. - /// - public DateTime? RegistrationFromUtc { get; set; } - - /// - /// Customer registration to; null to load all customers. Default: null. - /// - public DateTime? RegistrationToUtc { get; set; } - - /// - /// Customer last activity date (from)s. Default: null. - /// - public DateTime? LastActivityFromUtc { get; set; } - - /// - /// A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers. Default: null. - /// - public int[] CustomerRoleIds { get; set; } - - /// - /// Affiliate identifie. Default: null. - /// - public int? AffiliateId { get; set; } - - /// - /// Email. Default: null. - /// - public string Email { get; set; } - - /// - /// UserName. Default: null. - /// - public string Username { get; set; } - - /// - /// Phone. Default: null. - /// - public string Phone { get; set; } - - /// - /// ZipPostalCode. Default: null. - /// - public string ZipPostalCode { get; set; } - - /// - /// CustomerNumber. Default: null. - /// - public string CustomerNumber { get; set; } - - /// - /// Day of birth. Default: null. - /// - public int? DayOfBirth { get; set; } - - /// - /// Month of birth. Default: null. - /// - public int? MonthOfBirth { get; set; } - - /// - /// Password format. Default: null. - /// - public PasswordFormat? PasswordFormat { get; set; } - - /// - /// Whether to only load customers with shopping cart. Default: false (meaning: no matter) - /// - public bool OnlyWithCart { get; set; } - - /// - /// What shopping cart type to filter; used when 'HasCart' param is 'true'. Default: null. - /// - public ShoppingCartType? CartType { get; set; } - - /// - /// Whether only (soft)-deleted records should be loaded. Default: false (meaning: only undeleted) - /// - public bool? Deleted { get; set; } = false; + public class CustomerSearchQuery : ICloneable + { + /// + /// Customer registration from; null to load all customers. Default: null. + /// + public DateTime? RegistrationFromUtc { get; set; } + + /// + /// Customer registration to; null to load all customers. Default: null. + /// + public DateTime? RegistrationToUtc { get; set; } + + /// + /// Customer last activity date (from)s. Default: null. + /// + public DateTime? LastActivityFromUtc { get; set; } + + /// + /// A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers. Default: null. + /// + public int[] CustomerRoleIds { get; set; } + + /// + /// Affiliate identifie. Default: null. + /// + public int? AffiliateId { get; set; } + + /// + /// Email. Default: null. + /// + public string Email { get; set; } + + /// + /// UserName. Default: null. + /// + public string Username { get; set; } + + /// + /// Phone. Default: null. + /// + public string Phone { get; set; } + + /// + /// ZipPostalCode. Default: null. + /// + public string ZipPostalCode { get; set; } + + /// + /// CustomerNumber. Default: null. + /// + public string CustomerNumber { get; set; } + + /// + /// Day of birth. Default: null. + /// + public int? DayOfBirth { get; set; } + + /// + /// Month of birth. Default: null. + /// + public int? MonthOfBirth { get; set; } + + /// + /// Password format. Default: null. + /// + public PasswordFormat? PasswordFormat { get; set; } + + /// + /// Whether to only load customers with shopping cart. Default: false (meaning: no matter) + /// + public bool OnlyWithCart { get; set; } + + /// + /// What shopping cart type to filter; used when 'HasCart' param is 'true'. Default: null. + /// + public ShoppingCartType? CartType { get; set; } + + /// + /// Whether only (soft)-deleted records should be loaded. Default: false (meaning: only undeleted) + /// + public bool? Deleted { get; set; } = false; /// /// Whether only active customers should be loaded. @@ -96,29 +96,29 @@ public class CustomerSearchQuery : ICloneable /// public bool? IsSystemAccount { get; set; } = false; - /// - /// Searches in FullName (FirstName + LastName) and Company fields. Default: null. - /// - public string SearchTerm { get; set; } - - /// - /// Page index. Default: 0. - /// - public int PageIndex { get; set; } - - /// - /// Page index. Default: 50. - /// - public int PageSize { get; set; } = 50; - - public CustomerSearchQuery Clone() - { - return (CustomerSearchQuery)this.MemberwiseClone(); - } - - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } - } + /// + /// Searches in FullName (FirstName + LastName) and Company fields. Default: null. + /// + public string SearchTerm { get; set; } + + /// + /// Page index. Default: 0. + /// + public int PageIndex { get; set; } + + /// + /// Page index. Default: 50. + /// + public int PageSize { get; set; } = 50; + + public CustomerSearchQuery Clone() + { + return (CustomerSearchQuery)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index 4402ae33f9..10c842935b 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; +using System.Data.Entity; using System.Diagnostics; using System.Linq; -using System.Linq.Expressions; +using System.Text; using System.Web; -using System.Data.Entity; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Forums; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Fakes; @@ -20,7 +19,6 @@ using SmartStore.Data.Caching; using SmartStore.Services.Common; using SmartStore.Services.Localization; -using System.Threading.Tasks; using SmartStore.Services.Security; namespace SmartStore.Services.Customers @@ -31,58 +29,58 @@ public partial class CustomerService : ICustomerService private readonly IRepository _customerRoleRepository; private readonly IRepository _customerRoleMappingRepository; private readonly IRepository _gaRepository; - private readonly IRepository _rewardPointsHistoryRepository; + private readonly IRepository _rewardPointsHistoryRepository; private readonly IRepository _shoppingCartItemRepository; private readonly IGenericAttributeService _genericAttributeService; - private readonly RewardPointsSettings _rewardPointsSettings; - private readonly ICommonServices _services; - private readonly HttpContextBase _httpContext; - private readonly IUserAgent _userAgent; - private readonly CustomerSettings _customerSettings; - private readonly Lazy _gdprTool; - - public CustomerService( + private readonly Lazy _rewardPointsSettings; + private readonly ICommonServices _services; + private readonly HttpContextBase _httpContext; + private readonly IUserAgent _userAgent; + private readonly CustomerSettings _customerSettings; + private readonly Lazy _gdprTool; + + public CustomerService( IRepository customerRepository, IRepository customerRoleRepository, IRepository customerRoleMappingRepository, IRepository gaRepository, - IRepository rewardPointsHistoryRepository, + IRepository rewardPointsHistoryRepository, IRepository shoppingCartItemRepository, IGenericAttributeService genericAttributeService, - RewardPointsSettings rewardPointsSettings, - ICommonServices services, - HttpContextBase httpContext, - IUserAgent userAgent, - CustomerSettings customerSettings, - Lazy gdprTool) + Lazy rewardPointsSettings, + ICommonServices services, + HttpContextBase httpContext, + IUserAgent userAgent, + CustomerSettings customerSettings, + Lazy gdprTool) { _customerRepository = customerRepository; _customerRoleRepository = customerRoleRepository; _customerRoleMappingRepository = customerRoleMappingRepository; _gaRepository = gaRepository; - _rewardPointsHistoryRepository = rewardPointsHistoryRepository; + _rewardPointsHistoryRepository = rewardPointsHistoryRepository; _shoppingCartItemRepository = shoppingCartItemRepository; _genericAttributeService = genericAttributeService; - _rewardPointsSettings = rewardPointsSettings; - _services = services; - _httpContext = httpContext; - _userAgent = userAgent; - _customerSettings = customerSettings; - _gdprTool = gdprTool; - - T = NullLocalizer.Instance; - Logger = NullLogger.Instance; + _rewardPointsSettings = rewardPointsSettings; + _services = services; + _httpContext = httpContext; + _userAgent = userAgent; + _customerSettings = customerSettings; + _gdprTool = gdprTool; + + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; } - public Localizer T { get; set; } + public Localizer T { get; set; } - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } #region Customers - public virtual IPagedList SearchCustomers(CustomerSearchQuery q) - { - Guard.NotNull(q, nameof(q)); + public virtual IPagedList SearchCustomers(CustomerSearchQuery q) + { + Guard.NotNull(q, nameof(q)); var isOrdered = false; IQueryable query = null; @@ -120,105 +118,105 @@ select grp query = _customerRepository.Table; } - if (q.Email.HasValue()) - { - query = query.Where(c => c.Email.Contains(q.Email)); - } - - if (q.Username.HasValue()) - { - query = query.Where(c => c.Username.Contains(q.Username)); - } - - if (q.CustomerNumber.HasValue()) - { - query = query.Where(c => c.CustomerNumber.Contains(q.CustomerNumber)); - } - - if (q.AffiliateId.GetValueOrDefault() > 0) - { - query = query.Where(c => c.AffiliateId == q.AffiliateId.Value); - } - - if (q.SearchTerm.HasValue()) - { - if (_customerSettings.CompanyEnabled) - { - query = query.Where(c => c.FullName.Contains(q.SearchTerm) || c.Company.Contains(q.SearchTerm)); - } - else - { - query = query.Where(c => c.FullName.Contains(q.SearchTerm)); - } - } - - if (q.DayOfBirth.GetValueOrDefault() > 0) - { - query = query.Where(c => c.BirthDate.Value.Day == q.DayOfBirth.Value); - } - - if (q.MonthOfBirth.GetValueOrDefault() > 0) - { - query = query.Where(c => c.BirthDate.Value.Month == q.MonthOfBirth.Value); - } - - if (q.RegistrationFromUtc.HasValue) - { - query = query.Where(c => q.RegistrationFromUtc.Value <= c.CreatedOnUtc); - } - - if (q.RegistrationToUtc.HasValue) - { - query = query.Where(c => q.RegistrationToUtc.Value >= c.CreatedOnUtc); - } - - if (q.LastActivityFromUtc.HasValue) - { - query = query.Where(c => q.LastActivityFromUtc.Value <= c.LastActivityDateUtc); - } - - if (q.CustomerRoleIds != null && q.CustomerRoleIds.Length > 0) - { - query = query.Where(c => c.CustomerRoleMappings.Select(rm => rm.CustomerRoleId).Intersect(q.CustomerRoleIds).Count() > 0); - } - - if (q.Deleted.HasValue) - { - query = query.Where(c => c.Deleted == q.Deleted.Value); - } + if (q.Email.HasValue()) + { + query = query.Where(c => c.Email.Contains(q.Email)); + } + + if (q.Username.HasValue()) + { + query = query.Where(c => c.Username.Contains(q.Username)); + } + + if (q.CustomerNumber.HasValue()) + { + query = query.Where(c => c.CustomerNumber.Contains(q.CustomerNumber)); + } + + if (q.AffiliateId.GetValueOrDefault() > 0) + { + query = query.Where(c => c.AffiliateId == q.AffiliateId.Value); + } + + if (q.SearchTerm.HasValue()) + { + if (_customerSettings.CompanyEnabled) + { + query = query.Where(c => c.FullName.Contains(q.SearchTerm) || c.Company.Contains(q.SearchTerm)); + } + else + { + query = query.Where(c => c.FullName.Contains(q.SearchTerm)); + } + } + + if (q.DayOfBirth.GetValueOrDefault() > 0) + { + query = query.Where(c => c.BirthDate.Value.Day == q.DayOfBirth.Value); + } + + if (q.MonthOfBirth.GetValueOrDefault() > 0) + { + query = query.Where(c => c.BirthDate.Value.Month == q.MonthOfBirth.Value); + } + + if (q.RegistrationFromUtc.HasValue) + { + query = query.Where(c => q.RegistrationFromUtc.Value <= c.CreatedOnUtc); + } + + if (q.RegistrationToUtc.HasValue) + { + query = query.Where(c => q.RegistrationToUtc.Value >= c.CreatedOnUtc); + } + + if (q.LastActivityFromUtc.HasValue) + { + query = query.Where(c => q.LastActivityFromUtc.Value <= c.LastActivityDateUtc); + } + + if (q.CustomerRoleIds != null && q.CustomerRoleIds.Length > 0) + { + query = query.Where(c => c.CustomerRoleMappings.Select(rm => rm.CustomerRoleId).Intersect(q.CustomerRoleIds).Count() > 0); + } + + if (q.Deleted.HasValue) + { + query = query.Where(c => c.Deleted == q.Deleted.Value); + } if (q.Active.HasValue) { query = query.Where(c => c.Active == q.Active.Value); } - if (q.IsSystemAccount.HasValue) - { - query = q.IsSystemAccount.Value == true - ? query.Where(c => !string.IsNullOrEmpty(c.SystemName)) - : query.Where(c => string.IsNullOrEmpty(c.SystemName)); - } - - if (q.PasswordFormat.HasValue) - { - int passwordFormatId = (int)q.PasswordFormat.Value; - query = query.Where(c => c.PasswordFormatId == passwordFormatId); - } - - // Search by phone - if (q.Phone.HasValue()) - { - query = query - .Join(_gaRepository.Table, x => x.Id, y => y.EntityId, (x, y) => new { Customer = x, Attribute = y }) - .Where(z => z.Attribute.KeyGroup == "Customer" && - z.Attribute.Key == SystemCustomerAttributeNames.Phone && - z.Attribute.Value.Contains(q.Phone)) - .Select(z => z.Customer); - } - - // Search by zip - if (q.ZipPostalCode.HasValue()) - { + if (q.IsSystemAccount.HasValue) + { + query = q.IsSystemAccount.Value == true + ? query.Where(c => !string.IsNullOrEmpty(c.SystemName)) + : query.Where(c => string.IsNullOrEmpty(c.SystemName)); + } + + if (q.PasswordFormat.HasValue) + { + int passwordFormatId = (int)q.PasswordFormat.Value; + query = query.Where(c => c.PasswordFormatId == passwordFormatId); + } + + // Search by phone + if (q.Phone.HasValue()) + { + query = query + .Join(_gaRepository.Table, x => x.Id, y => y.EntityId, (x, y) => new { Customer = x, Attribute = y }) + .Where(z => z.Attribute.KeyGroup == "Customer" && + z.Attribute.Key == SystemCustomerAttributeNames.Phone && + z.Attribute.Value.Contains(q.Phone)) + .Select(z => z.Customer); + } + + // Search by zip + if (q.ZipPostalCode.HasValue()) + { query = query .Join(_gaRepository.Table, x => x.Id, y => y.EntityId, (x, y) => new { Customer = x, Attribute = y }) .Where(z => z.Attribute.KeyGroup == "Customer" && @@ -232,87 +230,91 @@ select grp query = query.OrderByDescending(c => c.CreatedOnUtc); } - var customers = new PagedList(query, q.PageIndex, q.PageSize); - return customers; - } + var customers = new PagedList(query, q.PageIndex, q.PageSize); + return customers; + } - public virtual IPagedList GetAllCustomersByPasswordFormat(PasswordFormat passwordFormat) + public virtual IPagedList GetAllCustomersByPasswordFormat(PasswordFormat passwordFormat) { - var q = new CustomerSearchQuery - { - PasswordFormat = passwordFormat, - PageIndex = 0, - PageSize = 500 - }; + var q = new CustomerSearchQuery + { + PasswordFormat = passwordFormat, + PageIndex = 0, + PageSize = 500 + }; - var customers = SearchCustomers(q); - return customers; + var customers = SearchCustomers(q); + return customers; } public virtual IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, int[] customerRoleIds, int pageIndex, int pageSize) { - var q = new CustomerSearchQuery - { - LastActivityFromUtc = lastActivityFromUtc, - CustomerRoleIds = customerRoleIds, - IsSystemAccount = false, - PageIndex = pageIndex, - PageSize = pageSize - }; + var q = new CustomerSearchQuery + { + LastActivityFromUtc = lastActivityFromUtc, + CustomerRoleIds = customerRoleIds, + IsSystemAccount = false, + PageIndex = pageIndex, + PageSize = pageSize + }; var customers = SearchCustomers(q); - customers.AlterQuery(x => x.OrderByDescending(c => c.LastActivityDateUtc)); + customers.AlterQuery(x => x.OrderByDescending(c => c.LastActivityDateUtc)); return customers; } public virtual void DeleteCustomer(Customer customer) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); if (customer.IsSystemAccount) throw new SmartException(string.Format("System customer account ({0}) cannot not be deleted", customer.SystemName)); - // Soft delete + // Soft delete customer.Deleted = true; - // Anonymize IP addresses - var language = customer.GetLanguage(); + // Anonymize IP addresses + var language = customer.GetLanguage(); - _gdprTool.Value.AnonymizeData(customer, x => x.LastIpAddress, IdentifierDataType.IpAddress, language); + _gdprTool.Value.AnonymizeData(customer, x => x.LastIpAddress, IdentifierDataType.IpAddress, language); - foreach (var post in customer.ForumPosts) - { - _gdprTool.Value.AnonymizeData(post, x => x.IPAddress, IdentifierDataType.IpAddress, language); - } + foreach (var post in customer.ForumPosts) + { + _gdprTool.Value.AnonymizeData(post, x => x.IPAddress, IdentifierDataType.IpAddress, language); + } - // Customer Content - foreach (var item in customer.CustomerContent) - { - _gdprTool.Value.AnonymizeData(item, x => x.IpAddress, IdentifierDataType.IpAddress, language); - } + // Customer Content + foreach (var item in customer.CustomerContent) + { + _gdprTool.Value.AnonymizeData(item, x => x.IpAddress, IdentifierDataType.IpAddress, language); + } - UpdateCustomer(customer); + UpdateCustomer(customer); } public virtual Customer GetCustomerById(int customerId) { if (customerId == 0) return null; - + // var customer = _customerRepository.GetById(customerId); - var customer = IncludeShoppingCart(_customerRepository.Table).SingleOrDefault(x => x.Id == customerId); + var customer = IncludeRoles(_customerRepository.Table).SingleOrDefault(x => x.Id == customerId); - return customer; + return customer; } - private IQueryable IncludeShoppingCart(IQueryable query) - { - return query - .Expand(x => x.ShoppingCartItems.Select(y => y.BundleItem)) - .Expand(x => x.ShoppingCartItems.Select(y => y.Product.AppliedDiscounts.Select(z => z.RuleSets))); - } + private IQueryable IncludeRoles(IQueryable query) + { + return query + .Expand(x => x.CustomerRoleMappings.Select(y => y.CustomerRole)); + + /// The generated SQL is way too heavy (!). Discard later (??) + //return query + // .Expand(x => x.ShoppingCartItems.Select(y => y.BundleItem)) + // .Expand(x => x.ShoppingCartItems.Select(y => y.Product.AppliedDiscounts.Select(z => z.RuleSets))); + } public virtual IList GetCustomersByIds(int[] customerIds) { @@ -325,22 +327,22 @@ where customerIds.Contains(c.Id) var customers = query.ToList(); - // sort by passed identifier sequence - return customers.OrderBySequence(customerIds).ToList(); + // sort by passed identifier sequence + return customers.OrderBySequence(customerIds).ToList(); } - public virtual IList GetSystemAccountCustomers() - { - return _customerRepository.Table.Where(x => x.IsSystemAccount).ToList(); - } + public virtual IList GetSystemAccountCustomers() + { + return _customerRepository.Table.Where(x => x.IsSystemAccount).ToList(); + } public virtual Customer GetCustomerByGuid(Guid customerGuid) { if (customerGuid == Guid.Empty) return null; - var query = from c in IncludeShoppingCart(_customerRepository.Table) - where c.CustomerGuid == customerGuid + var query = from c in IncludeRoles(_customerRepository.Table) + where c.CustomerGuid == customerGuid orderby c.Id select c; @@ -353,8 +355,8 @@ public virtual Customer GetCustomerByEmail(string email) if (string.IsNullOrWhiteSpace(email)) return null; - var query = from c in IncludeShoppingCart(_customerRepository.Table) - orderby c.Id + var query = from c in IncludeRoles(_customerRepository.Table) + orderby c.Id where c.Email == email select c; @@ -367,13 +369,13 @@ public virtual Customer GetCustomerBySystemName(string systemName) if (string.IsNullOrWhiteSpace(systemName)) return null; - var query = from c in _customerRepository.Table - orderby c.Id - where c.SystemName == systemName - select c; + var query = from c in _customerRepository.Table + orderby c.Id + where c.SystemName == systemName + select c; - var customer = query.FirstOrDefault(); - return customer; + var customer = query.FirstOrDefault(); + return customer; } public virtual Customer GetCustomerByUsername(string username) @@ -381,8 +383,8 @@ public virtual Customer GetCustomerByUsername(string username) if (string.IsNullOrWhiteSpace(username)) return null; - var query = from c in IncludeShoppingCart(_customerRepository.Table) - orderby c.Id + var query = from c in IncludeRoles(_customerRepository.Table) + orderby c.Id where c.Username == username select c; @@ -392,14 +394,14 @@ orderby c.Id public virtual Customer InsertGuestCustomer(Guid? customerGuid = null) { - var customer = new Customer + var customer = new Customer { CustomerGuid = customerGuid ?? Guid.NewGuid(), Active = true, CreatedOnUtc = DateTime.UtcNow, LastActivityDateUtc = DateTime.UtcNow, }; - + // Add to 'Guests' role var guestRole = GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); if (guestRole == null) @@ -407,268 +409,249 @@ public virtual Customer InsertGuestCustomer(Guid? customerGuid = null) throw new SmartException("'Guests' role could not be loaded"); } - using (new DbContextScope(autoCommit: true)) - { + using (new DbContextScope(autoCommit: true)) + { // Ensure that entities are saved to db in any case customer.CustomerRoleMappings.Add(new CustomerRoleMapping { CustomerId = customer.Id, CustomerRoleId = guestRole.Id }); - _customerRepository.Insert(customer); - - var clientIdent = _services.WebHelper.GetClientIdent(); - if (clientIdent.HasValue()) - { - _genericAttributeService.SaveAttribute(customer, "ClientIdent", clientIdent); - } - } - - //Logger.DebugFormat("Guest account created for anonymous visitor. Id: {0}, ClientIdent: {1}", customer.CustomerGuid, clientIdent ?? "n/a"); - - return customer; - } - - public virtual Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60) - { - if (_httpContext.IsFakeContext() || _userAgent.IsBot || _userAgent.IsPdfConverter) - { - return null; - } - - using (_services.Chronometer.Step("FindGuestCustomerByClientIdent")) - { - clientIdent = clientIdent.NullEmpty() ?? _services.WebHelper.GetClientIdent(); - if (clientIdent.IsEmpty()) - { - return null; - } - - var dateFrom = DateTime.UtcNow.AddSeconds(maxAgeSeconds * -1); - - IQueryable query; - if (DataSettings.Current.IsSqlServer) - { - query = from a in _gaRepository.TableUntracked - join c in _customerRepository.Table on a.EntityId equals c.Id into Customers - from c in Customers.DefaultIfEmpty() - where c.LastActivityDateUtc >= dateFrom - && c.Username == null - && c.Email == null - && a.KeyGroup == "Customer" - && a.Key == "ClientIdent" - && a.Value == clientIdent - select c; - } - else - { - query = from a in _gaRepository.TableUntracked - join c in _customerRepository.Table on a.EntityId equals c.Id into Customers - from c in Customers.DefaultIfEmpty() - where c.LastActivityDateUtc >= dateFrom - && c.Username == null - && c.Email == null - && a.KeyGroup == "Customer" - && a.Key == "ClientIdent" - && a.Value.Contains(clientIdent) // SQLCE doesn't like ntext in WHERE clauses - select c; - } - - return query.FirstOrDefault(); - } - } - - public virtual void InsertCustomer(Customer customer) - { - Guard.NotNull(customer, nameof(customer)); - - _customerRepository.Insert(customer); - } - + _customerRepository.Insert(customer); + + var clientIdent = _services.WebHelper.GetClientIdent(); + if (clientIdent.HasValue()) + { + _genericAttributeService.SaveAttribute(customer, "ClientIdent", clientIdent); + } + } + + //Logger.DebugFormat("Guest account created for anonymous visitor. Id: {0}, ClientIdent: {1}", customer.CustomerGuid, clientIdent ?? "n/a"); + + return customer; + } + + public virtual Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60) + { + if (_httpContext.IsFakeContext() || _userAgent.IsBot || _userAgent.IsPdfConverter) + { + return null; + } + + using (_services.Chronometer.Step("FindGuestCustomerByClientIdent")) + { + clientIdent = clientIdent.NullEmpty() ?? _services.WebHelper.GetClientIdent(); + if (clientIdent.IsEmpty()) + { + return null; + } + + var dateFrom = DateTime.UtcNow.AddSeconds(maxAgeSeconds * -1); + + IQueryable query; + if (DataSettings.Current.IsSqlServer) + { + query = from a in _gaRepository.Table + join c in _customerRepository.Table on a.EntityId equals c.Id into Customers + from c in Customers.DefaultIfEmpty() + where c.LastActivityDateUtc >= dateFrom + && c.Username == null + && c.Email == null + && a.KeyGroup == "Customer" + && a.Key == "ClientIdent" + && a.Value == clientIdent + select c; + } + else + { + query = from a in _gaRepository.Table + join c in _customerRepository.Table on a.EntityId equals c.Id into Customers + from c in Customers.DefaultIfEmpty() + where c.LastActivityDateUtc >= dateFrom + && c.Username == null + && c.Email == null + && a.KeyGroup == "Customer" + && a.Key == "ClientIdent" + && a.Value.Contains(clientIdent) // SQLCE doesn't like ntext in WHERE clauses + select c; + } + + return query.FirstOrDefault(); + } + } + + public virtual void InsertCustomer(Customer customer) + { + Guard.NotNull(customer, nameof(customer)); + + /// Validate unique user. + if (customer.Email.HasValue() && GetCustomerByEmail(customer.Email) != null) + { + throw new SmartException(T("Account.Register.Errors.EmailAlreadyExists")); + } + + if (customer.Username.HasValue() && + _customerSettings.CustomerLoginType != CustomerLoginType.Email && + GetCustomerByUsername(customer.Username) != null) + { + throw new SmartException(T("Account.Register.Errors.UsernameAlreadyExists")); + } + + _customerRepository.Insert(customer); + } + public virtual void UpdateCustomer(Customer customer) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); - _customerRepository.Update(customer); + _customerRepository.Update(customer); } - public virtual void ResetCheckoutData( - Customer customer, - int storeId, - bool clearCouponCodes = false, - bool clearCheckoutAttributes = false, - bool clearRewardPoints = false, - bool clearShippingMethod = true, + public virtual void ResetCheckoutData( + Customer customer, + int storeId, + bool clearCouponCodes = false, + bool clearCheckoutAttributes = false, + bool clearRewardPoints = false, + bool clearShippingMethod = true, bool clearPaymentMethod = true, - bool clearCreditBalance = false) + bool clearCreditBalance = false) { - Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(customer, nameof(customer)); - if (clearCouponCodes) + if (clearCouponCodes) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.DiscountCouponCode, null); - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.GiftCardCouponCodes, null); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.DiscountCouponCode, null); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.GiftCardCouponCodes, null); } if (clearCheckoutAttributes) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.CheckoutAttributes, null); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.CheckoutAttributes, null); } if (clearRewardPoints) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.UseRewardPointsDuringCheckout, false, storeId); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.UseRewardPointsDuringCheckout, false, storeId); } - if (clearCreditBalance) - { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.UseCreditBalanceDuringCheckout, decimal.Zero, storeId); - } + if (clearCreditBalance) + { + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.UseCreditBalanceDuringCheckout, decimal.Zero, storeId); + } if (clearShippingMethod) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedShippingOption, null, storeId); - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.OfferedShippingOptions, null, storeId); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedShippingOption, null, storeId); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.OfferedShippingOptions, null, storeId); } if (clearPaymentMethod) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, null, storeId); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, null, storeId); } UpdateCustomer(customer); } - - public virtual async Task DeleteGuestCustomersAsync( - DateTime? registrationFrom, - DateTime? registrationTo, - bool onlyWithoutShoppingCart, - int maxItemsToDelete = 5000) - { - var ctx = _customerRepository.Context; - - using (var scope = new DbContextScope(ctx: ctx, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true, autoCommit: false)) - { - var guestRole = GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); - if (guestRole == null) - throw new SmartException("'Guests' role could not be loaded"); - - var query = _customerRepository.Table; - - if (registrationFrom.HasValue) - query = query.Where(c => registrationFrom.Value <= c.CreatedOnUtc); - if (registrationTo.HasValue) - query = query.Where(c => registrationTo.Value >= c.CreatedOnUtc); - - query = query.Where(c => c.CustomerRoleMappings.Select(rm => rm.CustomerRoleId).Contains(guestRole.Id)); - - if (onlyWithoutShoppingCart) - query = query.Where(c => !c.ShoppingCartItems.Any()); - - // no orders - query = JoinWith(query, x => x.CustomerId); - - // no customer content - query = JoinWith(query, x => x.CustomerId); - - // no private messages (guests can only receive but not send messages) - query = JoinWith(query, x => x.ToCustomerId); - - // no forum posts - query = JoinWith(query, x => x.CustomerId); - - // no forum topics - query = JoinWith(query, x => x.CustomerId); - - // don't delete system accounts - query = query.Where(c => !c.IsSystemAccount); - - // only distinct items - query = from c in query - group c by c.Id - into cGroup - orderby cGroup.Key - select cGroup.FirstOrDefault(); - query = query.OrderBy(c => c.Id); - - var customers = await query.Take(() => maxItemsToDelete).ToListAsync(); - - int numberOfDeletedCustomers = 0; - foreach (var c in customers) - { - try - { - // delete attributes (using GenericAttributeService would incorporate caching... which is bad in long running processes) - var gaQuery = from ga in _gaRepository.Table - where ga.EntityId == c.Id && ga.KeyGroup == "Customer" - select ga; - var attributes = await gaQuery.ToListAsync(); - - _gaRepository.DeleteRange(attributes); - - // delete customer - _customerRepository.Delete(c); - numberOfDeletedCustomers++; - - if (numberOfDeletedCustomers % 1000 == 0) - { - // save changes all 1000th item - try - { - await ctx.SaveChangesAsync(); - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } - } - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } - } - - // save the rest - await ctx.SaveChangesAsync(); - - return numberOfDeletedCustomers; - } - } - - private IQueryable JoinWith(IQueryable query, Expression> customerIdSelector) where T : BaseEntity - { - var inner = _customerRepository.Context.Set().AsNoTracking(); - - /* - * Lamda join created with LinqPad. ORIGINAL: - from c in customers - join inner in ctx.Set().AsNoTracking() on c.Id equals inner.CustomerId into c_inner - from inner in c_inner.DefaultIfEmpty() - where !c_inner.Any() - select c; - */ - query = query - .GroupJoin( - inner, - c => c.Id, - customerIdSelector, - (c, i) => new { Customer = c, Inner = i }) - .SelectMany( - x => x.Inner.DefaultIfEmpty(), - (a, b) => new { a, b } - ) - .Where(x => !(x.a.Inner.Any())) - .Select(x => x.a.Customer); - - return query; - } + + public virtual int DeleteGuestCustomers( + DateTime? registrationFrom, + DateTime? registrationTo, + bool onlyWithoutShoppingCart) + { + var genericAttributesSql = @" +DELETE TOP(50000) [g] + FROM [dbo].[GenericAttribute] AS [g] + LEFT OUTER JOIN [dbo].[Customer] AS [c] ON c.Id = g.EntityId + LEFT OUTER JOIN [dbo].[Order] AS [o] ON c.Id = o.CustomerId + LEFT OUTER JOIN [dbo].[CustomerContent] AS [cc] ON c.Id = cc.CustomerId + LEFT OUTER JOIN [dbo].[Forums_PrivateMessage] AS [pm] ON c.Id = pm.ToCustomerId + LEFT OUTER JOIN [dbo].[Forums_Post] AS [fp] ON c.Id = fp.CustomerId + LEFT OUTER JOIN [dbo].[Forums_Topic] AS [ft] ON c.Id = ft.CustomerId + WHERE g.KeyGroup = 'Customer' AND c.Username IS Null AND c.Email IS NULL AND c.IsSystemAccount = 0{0} + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Order] AS [o1] WHERE c.Id = o1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[CustomerContent] AS [cc1] WHERE c.Id = cc1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Forums_PrivateMessage] AS [pm1] WHERE c.Id = pm1.ToCustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Forums_Post] AS [fp1] WHERE c.Id = fp1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[Forums_Topic] AS [ft1] WHERE c.Id = ft1.CustomerId )) +"; + + var guestCustomersSql = @" +DELETE TOP(20000) [c] + FROM [dbo].[Customer] AS [c] + LEFT OUTER JOIN [dbo].[Order] AS [o] ON c.Id = o.CustomerId + LEFT OUTER JOIN [dbo].[CustomerContent] AS [cc] ON c.Id = cc.CustomerId + LEFT OUTER JOIN [dbo].[Forums_PrivateMessage] AS [pm] ON c.Id = pm.ToCustomerId + LEFT OUTER JOIN [dbo].[Forums_Post] AS [fp] ON c.Id = fp.CustomerId + LEFT OUTER JOIN [dbo].[Forums_Topic] AS [ft] ON c.Id = ft.CustomerId + WHERE c.Username IS Null AND c.Email IS NULL AND c.IsSystemAccount = 0{0} + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Order] AS [o1] WHERE c.Id = o1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[CustomerContent] AS [cc1] WHERE c.Id = cc1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Forums_PrivateMessage] AS [pm1] WHERE c.Id = pm1.ToCustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Forums_Post] AS [fp1] WHERE c.Id = fp1.CustomerId )) + AND (NOT EXISTS (SELECT 1 AS x FROM [dbo].[Forums_Topic] AS [ft1] WHERE c.Id = ft1.CustomerId )) +"; + + var ctx = _customerRepository.Context; + var paramClauses = new StringBuilder(); + var parameters = new List(); + var numberOfDeletedCustomers = 0; + var numberOfDeletedAttributes = 0; + var pIndex = 0; + + if (registrationFrom.HasValue) + { + paramClauses.AppendFormat(" AND @p{0} <= c.CreatedOnUtc", pIndex++); + parameters.Add(registrationFrom.Value); + } + if (registrationTo.HasValue) + { + paramClauses.AppendFormat(" AND @p{0} >= c.CreatedOnUtc", pIndex++); + parameters.Add(registrationTo.Value); + } + if (onlyWithoutShoppingCart) + { + paramClauses.Append(" AND (NOT EXISTS (SELECT 1 AS [C1] FROM [dbo].[ShoppingCartItem] AS [sci] WHERE c.Id = sci.CustomerId ))"); + } + + genericAttributesSql = genericAttributesSql.FormatInvariant(paramClauses.ToString()); + guestCustomersSql = guestCustomersSql.FormatInvariant(paramClauses.ToString()); + + + // Delete generic attributes. + while (true) + { + var numDeleted = ctx.ExecuteSqlCommand(genericAttributesSql, false, null, parameters.ToArray()); + if (numDeleted <= 0) + { + break; + } + + numberOfDeletedAttributes += numDeleted; + } + + // Delete guest customers. + while (true) + { + var numDeleted = ctx.ExecuteSqlCommand(guestCustomersSql, false, null, parameters.ToArray()); + if (numDeleted <= 0) + { + break; + } + + numberOfDeletedCustomers += numDeleted; + } + + Debug.WriteLine("Deleted {0} guest customers including {1} generic attributes.", numberOfDeletedCustomers, numberOfDeletedAttributes); + + return numberOfDeletedCustomers; + } #endregion - + #region Customer roles public virtual void DeleteCustomerRole(CustomerRole role) { - Guard.NotNull(role, nameof(role)); + Guard.NotNull(role, nameof(role)); - if (role.IsSystemRole) + if (role.IsSystemRole) throw new SmartException("System role could not be deleted"); var roleId = role.Id; @@ -691,38 +674,37 @@ public virtual CustomerRole GetCustomerRoleBySystemName(string systemName) if (String.IsNullOrWhiteSpace(systemName)) return null; - var query = from cr in _customerRoleRepository.Table - orderby cr.Id - where cr.SystemName == systemName - select cr; + var query = from cr in _customerRoleRepository.Table + orderby cr.Id + where cr.SystemName == systemName + select cr; - var customerRole = query.FirstOrDefaultCached(); - return customerRole; - } + var customerRole = query.FirstOrDefaultCached(); + return customerRole; + } - public virtual IList GetAllCustomerRoles(bool showHidden = false) + public virtual IPagedList GetAllCustomerRoles(bool showHidden = false, int pageIndex = 0, int pageSize = int.MaxValue) { - var query = from cr in _customerRoleRepository.Table - orderby cr.Name - where (showHidden || cr.Active) - select cr; + var query = from cr in _customerRoleRepository.Table + orderby cr.Name + where (showHidden || cr.Active) + select cr; + + return new PagedList(query, pageIndex, pageSize); + } - var customerRoles = query.ToListCached(); - return customerRoles; - } - public virtual void InsertCustomerRole(CustomerRole role) { - Guard.NotNull(role, nameof(role)); + Guard.NotNull(role, nameof(role)); - _customerRoleRepository.Insert(role); + _customerRoleRepository.Insert(role); } public virtual void UpdateCustomerRole(CustomerRole role) { - Guard.NotNull(role, nameof(role)); + Guard.NotNull(role, nameof(role)); - _customerRoleRepository.Update(role); + _customerRoleRepository.Update(role); _services.Cache.RemoveByPattern(PermissionService.PERMISSION_TREE_KEY.FormatInvariant(role.Id)); } @@ -769,6 +751,8 @@ public virtual IPagedList GetCustomerRoleMappings( query = query.Where(x => x.IsSystemMapping == isSystemMapping.Value); } + query = query.Where(x => !x.Customer.Deleted); + if (withCustomers) { query = query @@ -811,36 +795,38 @@ public virtual void DeleteCustomerRoleMapping(CustomerRoleMapping mapping) #region Reward points public virtual void RewardPointsForProductReview(Customer customer, Product product, bool add) - { - if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForProductReview > 0) - { - string message = T(add ? "RewardPoints.Message.EarnedForProductReview" : "RewardPoints.Message.ReducedForProductReview", product.GetLocalized(x => x.Name)); - - customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForProductReview * (add ? 1 : -1), message); - - UpdateCustomer(customer); - } - } - - public virtual Multimap GetRewardPointsHistoriesByCustomerIds(int[] customerIds) - { - Guard.NotNull(customerIds, nameof(customerIds)); - - var query = - from x in _rewardPointsHistoryRepository.TableUntracked - where customerIds.Contains(x.CustomerId) - select x; - - var map = query - .OrderBy(x => x.CustomerId) - .ThenByDescending(x => x.CreatedOnUtc) - .ThenByDescending(x => x.Id) - .ToList() - .ToMultimap(x => x.CustomerId, x => x); - - return map; - } - - #endregion - } + { + var rpSettings = _rewardPointsSettings.Value; + + if (rpSettings.Enabled && rpSettings.PointsForProductReview > 0) + { + string message = T(add ? "RewardPoints.Message.EarnedForProductReview" : "RewardPoints.Message.ReducedForProductReview", product.GetLocalized(x => x.Name)); + + customer.AddRewardPointsHistoryEntry(rpSettings.PointsForProductReview * (add ? 1 : -1), message); + + UpdateCustomer(customer); + } + } + + public virtual Multimap GetRewardPointsHistoriesByCustomerIds(int[] customerIds) + { + Guard.NotNull(customerIds, nameof(customerIds)); + + var query = + from x in _rewardPointsHistoryRepository.TableUntracked + where customerIds.Contains(x.CustomerId) + select x; + + var map = query + .OrderBy(x => x.CustomerId) + .ThenByDescending(x => x.CreatedOnUtc) + .ThenByDescending(x => x.Id) + .ToList() + .ToMultimap(x => x.CustomerId, x => x); + + return map; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Customers/Events/CustomerAnonymizedEvent.cs b/src/Libraries/SmartStore.Services/Customers/Events/CustomerAnonymizedEvent.cs index b25350d730..a33f396714 100644 --- a/src/Libraries/SmartStore.Services/Customers/Events/CustomerAnonymizedEvent.cs +++ b/src/Libraries/SmartStore.Services/Customers/Events/CustomerAnonymizedEvent.cs @@ -7,34 +7,34 @@ namespace SmartStore.Services.Customers { - /// - /// TODO - /// - public class CustomerAnonymizedEvent - { - private readonly IGdprTool _gdprTool; + /// + /// TODO + /// + public class CustomerAnonymizedEvent + { + private readonly IGdprTool _gdprTool; - public CustomerAnonymizedEvent(Customer customer, IGdprTool gdprTool) - { - Guard.NotNull(customer, nameof(customer)); + public CustomerAnonymizedEvent(Customer customer, IGdprTool gdprTool) + { + Guard.NotNull(customer, nameof(customer)); - Customer = customer; - _gdprTool = gdprTool; - } + Customer = customer; + _gdprTool = gdprTool; + } - public Customer Customer { get; private set; } + public Customer Customer { get; private set; } - /// - /// TODO - /// - /// - /// - /// - /// - public void AnonymizeData(TEntity entity, Expression> expression, IdentifierDataType type, Language language = null) - where TEntity : BaseEntity - { - _gdprTool.AnonymizeData(entity, expression, type, language); - } - } + /// + /// TODO + /// + /// + /// + /// + /// + public void AnonymizeData(TEntity entity, Expression> expression, IdentifierDataType type, Language language = null) + where TEntity : BaseEntity + { + _gdprTool.AnonymizeData(entity, expression, type, language); + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/Events/CustomerExportedEvent.cs b/src/Libraries/SmartStore.Services/Customers/Events/CustomerExportedEvent.cs index 0c143c8b2f..3c6ada03a5 100644 --- a/src/Libraries/SmartStore.Services/Customers/Events/CustomerExportedEvent.cs +++ b/src/Libraries/SmartStore.Services/Customers/Events/CustomerExportedEvent.cs @@ -4,18 +4,18 @@ namespace SmartStore.Services.Customers { - public class CustomerExportedEvent - { - public CustomerExportedEvent(Customer customer, IDictionary result) - { - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(result, nameof(result)); + public class CustomerExportedEvent + { + public CustomerExportedEvent(Customer customer, IDictionary result) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(result, nameof(result)); - Customer = customer; - Result = result; - } + Customer = customer; + Result = result; + } - public Customer Customer { get; private set; } - public IDictionary Result { get; private set; } - } + public Customer Customer { get; private set; } + public IDictionary Result { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/Events/CustomerLogedInEvent.cs b/src/Libraries/SmartStore.Services/Customers/Events/CustomerLogedInEvent.cs index b224692e64..d69f9cd24c 100644 --- a/src/Libraries/SmartStore.Services/Customers/Events/CustomerLogedInEvent.cs +++ b/src/Libraries/SmartStore.Services/Customers/Events/CustomerLogedInEvent.cs @@ -3,15 +3,15 @@ namespace SmartStore.Services.Customers { - /// - /// An event message, which will be published after customer has logged in - /// + /// + /// An event message, which will be published after customer has logged in + /// public class CustomerLogedInEvent - { + { public Customer Customer - { - get; - set; - } - } + { + get; + set; + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/Events/CustomerRegisteredEvent.cs b/src/Libraries/SmartStore.Services/Customers/Events/CustomerRegisteredEvent.cs index 920c08fca6..66b6225adf 100644 --- a/src/Libraries/SmartStore.Services/Customers/Events/CustomerRegisteredEvent.cs +++ b/src/Libraries/SmartStore.Services/Customers/Events/CustomerRegisteredEvent.cs @@ -3,15 +3,15 @@ namespace SmartStore.Services.Customers { - /// - /// An event message, which will be published after customer has registered - /// + /// + /// An event message, which will be published after customer has registered + /// public class CustomerRegisteredEvent - { + { public Customer Customer - { - get; - set; - } - } + { + get; + set; + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/GdprTool.cs b/src/Libraries/SmartStore.Services/Customers/GdprTool.cs index b6208e38aa..45fecf45a6 100644 --- a/src/Libraries/SmartStore.Services/Customers/GdprTool.cs +++ b/src/Libraries/SmartStore.Services/Customers/GdprTool.cs @@ -29,128 +29,128 @@ namespace SmartStore.Services.Customers { - public enum IdentifierDataType - { - Text, - LongText, - Name, - UserName, - EmailAddress, - Url, - IpAddress, - PhoneNumber, - Address, - PostalCode, - DateTime - } - - public partial class GdprTool : IGdprTool - { - private readonly IMessageModelProvider _messageModelProvider; - private readonly IGenericAttributeService _genericAttributeService; - private readonly IShoppingCartService _shoppingCartService; - private readonly IForumService _forumService; - private readonly IBackInStockSubscriptionService _backInStockSubscriptionService; - private readonly ILanguageService _languageService; - private readonly ICommonServices _services; - - public static DateTime MinDate = new DateTime(1900, 1, 1); - - const string AnonymousEmail = "anonymous@example.com"; - - public GdprTool( - IMessageModelProvider messageModelProvider, - IGenericAttributeService genericAttributeService, - IShoppingCartService shoppingCartService, - IForumService forumService, - IBackInStockSubscriptionService backInStockSubscriptionService, - ILanguageService languageService, - ICommonServices services) - { - _messageModelProvider = messageModelProvider; - _genericAttributeService = genericAttributeService; - _shoppingCartService = shoppingCartService; - _forumService = forumService; - _backInStockSubscriptionService = backInStockSubscriptionService; - _languageService = languageService; - _services = services; - - T = NullLocalizer.InstanceEx; - Logger = NullLogger.Instance; - } - - public LocalizerEx T { get; set; } - - public ILogger Logger { get; set; } - - public virtual IDictionary ExportCustomer(Customer customer) - { - Guard.NotNull(customer, nameof(customer)); - - var ignoreMemberNames = new string[] - { - "WishlistUrl", "EditUrl", "PasswordRecoveryURL", - "BillingAddress.NameLine", "BillingAddress.StreetLine", "BillingAddress.CityLine", "BillingAddress.CountryLine", - "ShippingAddress.NameLine", "ShippingAddress.StreetLine", "ShippingAddress.CityLine", "ShippingAddress.CountryLine" - }; - - var model = _messageModelProvider.CreateModelPart(customer, true, ignoreMemberNames) as IDictionary; - - if (model != null) - { - // Roles - model["CustomerRoles"] = customer.CustomerRoleMappings.Select(x => x.CustomerRole.Name).ToArray(); - - // Generic attributes - var attributes = _genericAttributeService.GetAttributesForEntity(customer.Id, "Customer"); - if (attributes.Any()) - { - model["Attributes"] = _messageModelProvider.CreateModelPart(attributes, true); - } - - // Order history - var orders = customer.Orders.Where(x => !x.Deleted); - if (orders.Any()) - { - ignoreMemberNames = new string[] - { - "Disclaimer", "ConditionsOfUse", "Url", "CheckoutAttributes", - "Items.DownloadUrl", - "Items.Product.Description", "Items.Product.Url", "Items.Product.Thumbnail", "Items.Product.ThumbnailLg", - "Items.BundleItems.Product.Description", "Items.BundleItems.Product.Url", "Items.BundleItems.Product.Thumbnail", "Items.BundleItems.Product.ThumbnailLg", - "Billing.NameLine", "Billing.StreetLine", "Billing.CityLine", "Billing.CountryLine", - "Shipping.NameLine", "Shipping.StreetLine", "Shipping.CityLine", "Shipping.CountryLine" - }; - model["Orders"] = orders.Select(x => _messageModelProvider.CreateModelPart(x, true, ignoreMemberNames)).ToList(); - } - - // Return Request - var returnRequests = customer.ReturnRequests; - if (returnRequests.Any()) - { - model["ReturnRequests"] = returnRequests.Select(x => _messageModelProvider.CreateModelPart(x, true, "Url")).ToList(); - } - - // Wallet - var walletHistory = customer.WalletHistory; - if (walletHistory.Any()) - { - model["WalletHistory"] = walletHistory.Select(x => _messageModelProvider.CreateModelPart(x, true, "WalletUrl")).ToList(); - } - - // Forum topics - var forumTopics = customer.ForumTopics; - if (forumTopics.Any()) - { - model["ForumTopics"] = forumTopics.Select(x => _messageModelProvider.CreateModelPart(x, true, "Url")).ToList(); - } - - // Forum posts - var forumPosts = customer.ForumPosts; - if (forumPosts.Any()) - { - model["ForumPosts"] = forumPosts.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); - } + public enum IdentifierDataType + { + Text, + LongText, + Name, + UserName, + EmailAddress, + Url, + IpAddress, + PhoneNumber, + Address, + PostalCode, + DateTime + } + + public partial class GdprTool : IGdprTool + { + private readonly IMessageModelProvider _messageModelProvider; + private readonly IGenericAttributeService _genericAttributeService; + private readonly IShoppingCartService _shoppingCartService; + private readonly IForumService _forumService; + private readonly IBackInStockSubscriptionService _backInStockSubscriptionService; + private readonly ILanguageService _languageService; + private readonly ICommonServices _services; + + public static DateTime MinDate = new DateTime(1900, 1, 1); + + const string AnonymousEmail = "anonymous@example.com"; + + public GdprTool( + IMessageModelProvider messageModelProvider, + IGenericAttributeService genericAttributeService, + IShoppingCartService shoppingCartService, + IForumService forumService, + IBackInStockSubscriptionService backInStockSubscriptionService, + ILanguageService languageService, + ICommonServices services) + { + _messageModelProvider = messageModelProvider; + _genericAttributeService = genericAttributeService; + _shoppingCartService = shoppingCartService; + _forumService = forumService; + _backInStockSubscriptionService = backInStockSubscriptionService; + _languageService = languageService; + _services = services; + + T = NullLocalizer.InstanceEx; + Logger = NullLogger.Instance; + } + + public LocalizerEx T { get; set; } + + public ILogger Logger { get; set; } + + public virtual IDictionary ExportCustomer(Customer customer) + { + Guard.NotNull(customer, nameof(customer)); + + var ignoreMemberNames = new string[] + { + "WishlistUrl", "EditUrl", "PasswordRecoveryURL", + "BillingAddress.NameLine", "BillingAddress.StreetLine", "BillingAddress.CityLine", "BillingAddress.CountryLine", + "ShippingAddress.NameLine", "ShippingAddress.StreetLine", "ShippingAddress.CityLine", "ShippingAddress.CountryLine" + }; + + var model = _messageModelProvider.CreateModelPart(customer, true, ignoreMemberNames) as IDictionary; + + if (model != null) + { + // Roles + model["CustomerRoles"] = customer.CustomerRoleMappings.Select(x => x.CustomerRole.Name).ToArray(); + + // Generic attributes + var attributes = _genericAttributeService.GetAttributesForEntity(customer.Id, "Customer"); + if (attributes.Any()) + { + model["Attributes"] = _messageModelProvider.CreateModelPart(attributes, true); + } + + // Order history + var orders = customer.Orders.Where(x => !x.Deleted); + if (orders.Any()) + { + ignoreMemberNames = new string[] + { + "Disclaimer", "ConditionsOfUse", "Url", "CheckoutAttributes", + "Items.DownloadUrl", + "Items.Product.Description", "Items.Product.Url", "Items.Product.Thumbnail", "Items.Product.ThumbnailLg", + "Items.BundleItems.Product.Description", "Items.BundleItems.Product.Url", "Items.BundleItems.Product.Thumbnail", "Items.BundleItems.Product.ThumbnailLg", + "Billing.NameLine", "Billing.StreetLine", "Billing.CityLine", "Billing.CountryLine", + "Shipping.NameLine", "Shipping.StreetLine", "Shipping.CityLine", "Shipping.CountryLine" + }; + model["Orders"] = orders.Select(x => _messageModelProvider.CreateModelPart(x, true, ignoreMemberNames)).ToList(); + } + + // Return Request + var returnRequests = customer.ReturnRequests; + if (returnRequests.Any()) + { + model["ReturnRequests"] = returnRequests.Select(x => _messageModelProvider.CreateModelPart(x, true, "Url")).ToList(); + } + + // Wallet + var walletHistory = customer.WalletHistory; + if (walletHistory.Any()) + { + model["WalletHistory"] = walletHistory.Select(x => _messageModelProvider.CreateModelPart(x, true, "WalletUrl")).ToList(); + } + + // Forum topics + var forumTopics = customer.ForumTopics; + if (forumTopics.Any()) + { + model["ForumTopics"] = forumTopics.Select(x => _messageModelProvider.CreateModelPart(x, true, "Url")).ToList(); + } + + // Forum posts + var forumPosts = customer.ForumPosts; + if (forumPosts.Any()) + { + model["ForumPosts"] = forumPosts.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); + } // Forum post votes var forumPostVotes = customer.CustomerContent.OfType(); @@ -162,78 +162,78 @@ public virtual IDictionary ExportCustomer(Customer customer) // Product reviews var productReviews = customer.CustomerContent.OfType(); - if (productReviews.Any()) - { - model["ProductReviews"] = productReviews.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); - } - - // News comments - var newsComments = customer.CustomerContent.OfType(); - if (newsComments.Any()) - { - model["NewsComments"] = newsComments.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); - } - - // Blog comments - var blogComments = customer.CustomerContent.OfType(); - if (blogComments.Any()) - { - model["BlogComments"] = blogComments.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); - } - - // Product review helpfulness - var helpfulness = customer.CustomerContent.OfType(); - if (helpfulness.Any()) - { - ignoreMemberNames = new string[] { "CustomerId", "UpdatedOn" }; - model["ProductReviewHelpfulness"] = helpfulness.Select(x => _messageModelProvider.CreateModelPart(x, true, ignoreMemberNames)).ToList(); - } - - // Poll voting - var pollVotings = customer.CustomerContent.OfType(); - if (pollVotings.Any()) - { - ignoreMemberNames = new string[] { "CustomerId", "UpdatedOn" }; - model["PollVotings"] = pollVotings.Select(x => _messageModelProvider.CreateModelPart(x, true, ignoreMemberNames)).ToList(); - } - - // Forum subscriptions - var forumSubscriptions = _forumService.GetAllSubscriptions(customer.Id, 0, 0, 0, int.MaxValue); - if (forumSubscriptions.Any()) - { - model["ForumSubscriptions"] = forumSubscriptions.Select(x => _messageModelProvider.CreateModelPart(x, true, "CustomerId")).ToList(); - } - - // BackInStock subscriptions - var backInStockSubscriptions = _backInStockSubscriptionService.GetAllSubscriptionsByCustomerId(customer.Id, 0, 0, int.MaxValue); - if (backInStockSubscriptions.Any()) - { - model["BackInStockSubscriptions"] = backInStockSubscriptions.Select(x => _messageModelProvider.CreateModelPart(x, true, "CustomerId")).ToList(); - } - - // INFO: we're not going to export: - // - Private messages - // - Activity log - // It doesn't feel right and GDPR rules are not very clear about this. Let's wait and see :-) - - // Publish event to give plugin devs a chance to attach external data. - _services.EventPublisher.Publish(new CustomerExportedEvent(customer, model)); - } - - return model; - } - - public virtual void AnonymizeCustomer(Customer customer, bool pseudomyzeContent) - { - Guard.NotNull(customer, nameof(customer)); - - var language = customer.GetLanguage(); - var customerName = customer.GetFullName() ?? customer.Username ?? customer.FindEmail(); - - using (var scope = new DbContextScope(_services.DbContext, autoCommit: false)) - { - // Set to deleted - customer.Deleted = true; + if (productReviews.Any()) + { + model["ProductReviews"] = productReviews.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); + } + + // News comments + var newsComments = customer.CustomerContent.OfType(); + if (newsComments.Any()) + { + model["NewsComments"] = newsComments.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); + } + + // Blog comments + var blogComments = customer.CustomerContent.OfType(); + if (blogComments.Any()) + { + model["BlogComments"] = blogComments.Select(x => _messageModelProvider.CreateModelPart(x, true)).ToList(); + } + + // Product review helpfulness + var helpfulness = customer.CustomerContent.OfType(); + if (helpfulness.Any()) + { + ignoreMemberNames = new string[] { "CustomerId", "UpdatedOn" }; + model["ProductReviewHelpfulness"] = helpfulness.Select(x => _messageModelProvider.CreateModelPart(x, true, ignoreMemberNames)).ToList(); + } + + // Poll voting + var pollVotings = customer.CustomerContent.OfType(); + if (pollVotings.Any()) + { + ignoreMemberNames = new string[] { "CustomerId", "UpdatedOn" }; + model["PollVotings"] = pollVotings.Select(x => _messageModelProvider.CreateModelPart(x, true, ignoreMemberNames)).ToList(); + } + + // Forum subscriptions + var forumSubscriptions = _forumService.GetAllSubscriptions(customer.Id, 0, 0, 0, int.MaxValue); + if (forumSubscriptions.Any()) + { + model["ForumSubscriptions"] = forumSubscriptions.Select(x => _messageModelProvider.CreateModelPart(x, true, "CustomerId")).ToList(); + } + + // BackInStock subscriptions + var backInStockSubscriptions = _backInStockSubscriptionService.GetAllSubscriptionsByCustomerId(customer.Id, 0, 0, int.MaxValue); + if (backInStockSubscriptions.Any()) + { + model["BackInStockSubscriptions"] = backInStockSubscriptions.Select(x => _messageModelProvider.CreateModelPart(x, true, "CustomerId")).ToList(); + } + + // INFO: we're not going to export: + // - Private messages + // - Activity log + // It doesn't feel right and GDPR rules are not very clear about this. Let's wait and see :-) + + // Publish event to give plugin devs a chance to attach external data. + _services.EventPublisher.Publish(new CustomerExportedEvent(customer, model)); + } + + return model; + } + + public virtual void AnonymizeCustomer(Customer customer, bool pseudomyzeContent) + { + Guard.NotNull(customer, nameof(customer)); + + var language = customer.GetLanguage(); + var customerName = customer.GetFullName() ?? customer.Username ?? customer.FindEmail(); + + using (var scope = new DbContextScope(_services.DbContext, autoCommit: false)) + { + // Set to deleted + customer.Deleted = true; // Unassign roles var customerService = _services.Resolve(); @@ -249,231 +249,235 @@ public virtual void AnonymizeCustomer(Customer customer, bool pseudomyzeContent) { customerService.InsertCustomerRoleMapping(new CustomerRoleMapping { CustomerId = customer.Id, CustomerRoleId = guestRole.Id }); } - - // Delete shopping cart & wishlist (TBD: (mc) Really?!?) - _shoppingCartService.DeleteExpiredShoppingCartItems(DateTime.UtcNow, customer.Id); - - // Delete forum subscriptions - var forumSubscriptions = _forumService.GetAllSubscriptions(customer.Id, 0, 0, 0, int.MaxValue); - foreach (var forumSub in forumSubscriptions) - { - _forumService.DeleteSubscription(forumSub); - } - - // Delete BackInStock subscriptions - var backInStockSubscriptions = _backInStockSubscriptionService.GetAllSubscriptionsByCustomerId(customer.Id, 0, 0, int.MaxValue); - foreach (var stockSub in backInStockSubscriptions) - { - _backInStockSubscriptionService.DeleteSubscription(stockSub); - } - - // Generic attributes - var attributes = _genericAttributeService.GetAttributesForEntity(customer.Id, "Customer"); - foreach (var attr in attributes) - { - // we don't need to mask generic attrs, we just delete them. - _genericAttributeService.DeleteAttribute(attr); - } - - // Customer Data - AnonymizeData(customer, x => x.Username, IdentifierDataType.UserName, language); - AnonymizeData(customer, x => x.Email, IdentifierDataType.EmailAddress, language); - AnonymizeData(customer, x => x.LastIpAddress, IdentifierDataType.IpAddress, language); - if (pseudomyzeContent) - { - AnonymizeData(customer, x => x.AdminComment, IdentifierDataType.LongText, language); - AnonymizeData(customer, x => x.LastLoginDateUtc, IdentifierDataType.DateTime, language); - AnonymizeData(customer, x => x.LastActivityDateUtc, IdentifierDataType.DateTime, language); - } - - // Addresses - foreach (var address in customer.Addresses) - { - AnonymizeAddress(address, language); - } - - // Private messages - if (pseudomyzeContent) - { - var privateMessages = _forumService.GetAllPrivateMessages(0, customer.Id, 0, null, null, null, 0, int.MaxValue); - foreach (var msg in privateMessages) - { - AnonymizeData(msg, x => x.Subject, IdentifierDataType.Text, language); - AnonymizeData(msg, x => x.Text, IdentifierDataType.LongText, language); - } - } - - // Forum topics - if (pseudomyzeContent) - { - foreach (var topic in customer.ForumTopics) - { - AnonymizeData(topic, x => x.Subject, IdentifierDataType.Text, language); - } - } - - // Forum posts - foreach (var post in customer.ForumPosts) - { - AnonymizeData(post, x => x.IPAddress, IdentifierDataType.IpAddress, language); - if (pseudomyzeContent) - { - AnonymizeData(post, x => x.Text, IdentifierDataType.LongText, language); - } - } - - // Customer Content - foreach (var item in customer.CustomerContent) - { - AnonymizeData(item, x => x.IpAddress, IdentifierDataType.IpAddress, language); - - if (pseudomyzeContent) - { - switch (item) - { - case ProductReview c: - AnonymizeData(c, x => x.ReviewText, IdentifierDataType.LongText, language); - AnonymizeData(c, x => x.Title, IdentifierDataType.Text, language); - break; - case NewsComment c: - AnonymizeData(c, x => x.CommentText, IdentifierDataType.LongText, language); - AnonymizeData(c, x => x.CommentTitle, IdentifierDataType.Text, language); - break; - case BlogComment c: - AnonymizeData(c, x => x.CommentText, IdentifierDataType.LongText, language); - break; - } - } - } - - //// Anonymize Order IPs - //// TBD: Don't! Doesn't feel right because of fraud detection etc. - //foreach (var order in customer.Orders) - //{ - // AnonymizeData(order, x => x.CustomerIp, IdentifierDataType.IpAddress, language); - //} - - // SAVE!!! - //_services.DbContext.DetachAll(); // TEST - scope.Commit(); - - // Log - Logger.Info(T("Gdpr.Anonymize.Success", language.Id, customerName)); - } - } - - private void AnonymizeAddress(Address address, Language language) - { - AnonymizeData(address, x => x.Address1, IdentifierDataType.Address, language); - AnonymizeData(address, x => x.Address2, IdentifierDataType.Address, language); - AnonymizeData(address, x => x.City, IdentifierDataType.Address, language); - AnonymizeData(address, x => x.Company, IdentifierDataType.Address, language); - AnonymizeData(address, x => x.Email, IdentifierDataType.EmailAddress, language); - AnonymizeData(address, x => x.FaxNumber, IdentifierDataType.PhoneNumber, language); - AnonymizeData(address, x => x.FirstName, IdentifierDataType.Name, language); - AnonymizeData(address, x => x.LastName, IdentifierDataType.Name, language); - AnonymizeData(address, x => x.PhoneNumber, IdentifierDataType.PhoneNumber, language); - AnonymizeData(address, x => x.ZipPostalCode, IdentifierDataType.PostalCode, language); - } - - public virtual void AnonymizeData(TEntity entity, Expression> expression, IdentifierDataType type, Language language = null) - where TEntity : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(expression, nameof(expression)); - - var originalValue = expression.Compile().Invoke(entity); - object maskedValue = null; - - if (originalValue is DateTime d) - { - maskedValue = MinDate; - } - else if (originalValue is string s) - { - if (s.IsEmpty()) - { - return; - } - - language = language ?? (entity as Customer)?.GetLanguage(); - - switch (type) - { - case IdentifierDataType.Address: - case IdentifierDataType.Name: - case IdentifierDataType.Text: - maskedValue = T("Gdpr.DeletedText", language.Id).Text; - break; - case IdentifierDataType.LongText: - maskedValue = T("Gdpr.DeletedLongText", language.Id).Text; - break; - case IdentifierDataType.EmailAddress: - //maskedValue = s.Hash(Encoding.ASCII, true) + "@anony.mous"; - maskedValue = HashCodeCombiner.Start() - .Add(entity.GetHashCode()) - .Add(s) - .CombinedHashString + "@anony.mous"; - break; - case IdentifierDataType.Url: - maskedValue = "https://anony.mous"; - break; - case IdentifierDataType.IpAddress: - maskedValue = AnonymizeIpAddress(s); - break; - case IdentifierDataType.UserName: - maskedValue = T("Gdpr.Anonymous", language.Id).Text.ToLower(); - break; - case IdentifierDataType.PhoneNumber: - maskedValue = "555-00000"; - break; - case IdentifierDataType.PostalCode: - maskedValue = "00000"; - break; - case IdentifierDataType.DateTime: - maskedValue = MinDate.ToString(CultureInfo.InvariantCulture); - break; - } - } - - if (maskedValue != null) - { - var pi = expression.ExtractPropertyInfo(); - pi.SetValue(entity, maskedValue); - } - } - - /// - /// Returns an anonymized IPv4 or IPv6 address. - /// - /// The IPv4 or IPv6 address to be anonymized. - /// The anonymized IP address. - protected virtual string AnonymizeIpAddress(string ipAddress) - { - try - { - var ip = IPAddress.Parse(ipAddress); - - switch (ip.AddressFamily) - { - case AddressFamily.InterNetwork: - break; - case AddressFamily.InterNetworkV6: - // Map to IPv4 first - ip = ip.MapToIPv4(); - break; - default: - // we only support IPv4 and IPv6 - return "0.0.0.0"; - } - - // Keep the first 3 bytes and append ".0" - return string.Join(".", ip.GetAddressBytes().Take(3)) + ".0"; - } - catch - { - return null; - } - } - } + + // Delete shopping cart & wishlist (TBD: (mc) Really?!?) + _shoppingCartService.DeleteExpiredShoppingCartItems(DateTime.UtcNow, customer.Id); + + // Delete forum subscriptions + var forumSubscriptions = _forumService.GetAllSubscriptions(customer.Id, 0, 0, 0, int.MaxValue); + foreach (var forumSub in forumSubscriptions) + { + _forumService.DeleteSubscription(forumSub); + } + + // Delete BackInStock subscriptions + var backInStockSubscriptions = _backInStockSubscriptionService.GetAllSubscriptionsByCustomerId(customer.Id, 0, 0, int.MaxValue); + foreach (var stockSub in backInStockSubscriptions) + { + _backInStockSubscriptionService.DeleteSubscription(stockSub); + } + + // Generic attributes + var attributes = _genericAttributeService.GetAttributesForEntity(customer.Id, "Customer"); + foreach (var attr in attributes) + { + // we don't need to mask generic attrs, we just delete them. + _genericAttributeService.DeleteAttribute(attr); + } + + // Customer Data + AnonymizeData(customer, x => x.Username, IdentifierDataType.UserName, language); + AnonymizeData(customer, x => x.Email, IdentifierDataType.EmailAddress, language); + AnonymizeData(customer, x => x.LastIpAddress, IdentifierDataType.IpAddress, language); + AnonymizeData(customer, x => x.FirstName, IdentifierDataType.Name, language); + AnonymizeData(customer, x => x.LastName, IdentifierDataType.Name, language); + AnonymizeData(customer, x => x.BirthDate, IdentifierDataType.DateTime, language); + + if (pseudomyzeContent) + { + AnonymizeData(customer, x => x.AdminComment, IdentifierDataType.LongText, language); + AnonymizeData(customer, x => x.LastLoginDateUtc, IdentifierDataType.DateTime, language); + AnonymizeData(customer, x => x.LastActivityDateUtc, IdentifierDataType.DateTime, language); + } + + // Addresses + foreach (var address in customer.Addresses) + { + AnonymizeAddress(address, language); + } + + // Private messages + if (pseudomyzeContent) + { + var privateMessages = _forumService.GetAllPrivateMessages(0, customer.Id, 0, null, null, null, 0, int.MaxValue); + foreach (var msg in privateMessages) + { + AnonymizeData(msg, x => x.Subject, IdentifierDataType.Text, language); + AnonymizeData(msg, x => x.Text, IdentifierDataType.LongText, language); + } + } + + // Forum topics + if (pseudomyzeContent) + { + foreach (var topic in customer.ForumTopics) + { + AnonymizeData(topic, x => x.Subject, IdentifierDataType.Text, language); + } + } + + // Forum posts + foreach (var post in customer.ForumPosts) + { + AnonymizeData(post, x => x.IPAddress, IdentifierDataType.IpAddress, language); + if (pseudomyzeContent) + { + AnonymizeData(post, x => x.Text, IdentifierDataType.LongText, language); + } + } + + // Customer Content + foreach (var item in customer.CustomerContent) + { + AnonymizeData(item, x => x.IpAddress, IdentifierDataType.IpAddress, language); + + if (pseudomyzeContent) + { + switch (item) + { + case ProductReview c: + AnonymizeData(c, x => x.ReviewText, IdentifierDataType.LongText, language); + AnonymizeData(c, x => x.Title, IdentifierDataType.Text, language); + break; + case NewsComment c: + AnonymizeData(c, x => x.CommentText, IdentifierDataType.LongText, language); + AnonymizeData(c, x => x.CommentTitle, IdentifierDataType.Text, language); + break; + case BlogComment c: + AnonymizeData(c, x => x.CommentText, IdentifierDataType.LongText, language); + break; + } + } + } + + //// Anonymize Order IPs + //// TBD: Don't! Doesn't feel right because of fraud detection etc. + //foreach (var order in customer.Orders) + //{ + // AnonymizeData(order, x => x.CustomerIp, IdentifierDataType.IpAddress, language); + //} + + // SAVE!!! + //_services.DbContext.DetachAll(); // TEST + scope.Commit(); + + // Log + Logger.Info(T("Gdpr.Anonymize.Success", language.Id, customerName)); + } + } + + private void AnonymizeAddress(Address address, Language language) + { + AnonymizeData(address, x => x.Address1, IdentifierDataType.Address, language); + AnonymizeData(address, x => x.Address2, IdentifierDataType.Address, language); + AnonymizeData(address, x => x.City, IdentifierDataType.Address, language); + AnonymizeData(address, x => x.Company, IdentifierDataType.Address, language); + AnonymizeData(address, x => x.Email, IdentifierDataType.EmailAddress, language); + AnonymizeData(address, x => x.FaxNumber, IdentifierDataType.PhoneNumber, language); + AnonymizeData(address, x => x.FirstName, IdentifierDataType.Name, language); + AnonymizeData(address, x => x.LastName, IdentifierDataType.Name, language); + AnonymizeData(address, x => x.PhoneNumber, IdentifierDataType.PhoneNumber, language); + AnonymizeData(address, x => x.ZipPostalCode, IdentifierDataType.PostalCode, language); + } + + public virtual void AnonymizeData(TEntity entity, Expression> expression, IdentifierDataType type, Language language = null) + where TEntity : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(expression, nameof(expression)); + + var originalValue = expression.Compile().Invoke(entity); + object maskedValue = null; + + if (originalValue is DateTime d) + { + maskedValue = MinDate; + } + else if (originalValue is string s) + { + if (s.IsEmpty()) + { + return; + } + + language = language ?? (entity as Customer)?.GetLanguage(); + + switch (type) + { + case IdentifierDataType.Address: + case IdentifierDataType.Name: + case IdentifierDataType.Text: + maskedValue = T("Gdpr.DeletedText", language.Id).Text; + break; + case IdentifierDataType.LongText: + maskedValue = T("Gdpr.DeletedLongText", language.Id).Text; + break; + case IdentifierDataType.EmailAddress: + //maskedValue = s.Hash(Encoding.ASCII, true) + "@anony.mous"; + maskedValue = HashCodeCombiner.Start() + .Add(entity.GetHashCode()) + .Add(s) + .CombinedHashString + "@anony.mous"; + break; + case IdentifierDataType.Url: + maskedValue = "https://anony.mous"; + break; + case IdentifierDataType.IpAddress: + maskedValue = AnonymizeIpAddress(s); + break; + case IdentifierDataType.UserName: + maskedValue = T("Gdpr.Anonymous", language.Id).Text.ToLower(); + break; + case IdentifierDataType.PhoneNumber: + maskedValue = "555-00000"; + break; + case IdentifierDataType.PostalCode: + maskedValue = "00000"; + break; + case IdentifierDataType.DateTime: + maskedValue = MinDate.ToString(CultureInfo.InvariantCulture); + break; + } + } + + if (maskedValue != null) + { + var pi = expression.ExtractPropertyInfo(); + pi.SetValue(entity, maskedValue); + } + } + + /// + /// Returns an anonymized IPv4 or IPv6 address. + /// + /// The IPv4 or IPv6 address to be anonymized. + /// The anonymized IP address. + protected virtual string AnonymizeIpAddress(string ipAddress) + { + try + { + var ip = IPAddress.Parse(ipAddress); + + switch (ip.AddressFamily) + { + case AddressFamily.InterNetwork: + break; + case AddressFamily.InterNetworkV6: + // Map to IPv4 first + ip = ip.MapToIPv4(); + break; + default: + // we only support IPv4 and IPv6 + return "0.0.0.0"; + } + + // Keep the first 3 bytes and append ".0" + return string.Join(".", ip.GetAddressBytes().Take(3)) + ".0"; + } + catch + { + return null; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/ICookieManager.cs b/src/Libraries/SmartStore.Services/Customers/ICookieManager.cs index 3d3a1bc267..7a246b92a4 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICookieManager.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICookieManager.cs @@ -5,34 +5,34 @@ namespace SmartStore.Services.Customers { - public interface ICookieManager - { - /// - /// Gets CookieInfos of all plugins which are publishing cookies to be display in CookieManager dialog. - /// - /// List of CookieInfos. - List GetAllCookieInfos(); - - /// - /// Gets a value which specifies whether it is allowed to set a cookie of a certain type. - /// - /// Type of the cookie. - /// Value which indicates whether cookies of a certain type are allowed to be set. - bool IsCookieAllowed(ControllerContext context, CookieType cookieType); + public interface ICookieManager + { + /// + /// Gets CookieInfos of all plugins which are publishing cookies to be display in CookieManager dialog. + /// + /// List of CookieInfos. + List GetAllCookieInfos(bool addSettingCookies = false); - /// - /// Gets the data of the cookie. - /// - /// - /// Cookie which contains data of user selection. - ConsentCookie GetCookieData(ControllerContext context); + /// + /// Gets a value which specifies whether it is allowed to set a cookie of a certain type. + /// + /// Type of the cookie. + /// Value which indicates whether cookies of a certain type are allowed to be set. + bool IsCookieAllowed(ControllerContext context, CookieType cookieType); - /// - /// Sets the cookie. - /// - /// Cookie will be set to this response. - /// Defines whether analytical cookies are allowed to be set. - /// Defines whether third party cookies are allowed to be set. - void SetConsentCookie(HttpResponseBase response, bool allowAnalytics = false, bool allowThirdParty = false); - } + /// + /// Gets the data of the cookie. + /// + /// + /// Cookie which contains data of user selection. + ConsentCookie GetCookieData(ControllerContext context); + + /// + /// Sets the cookie. + /// + /// Cookie will be set to this response. + /// Defines whether analytical cookies are allowed to be set. + /// Defines whether third party cookies are allowed to be set. + void SetConsentCookie(HttpResponseBase response, bool allowAnalytics = false, bool allowThirdParty = false); + } } diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerContentService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerContentService.cs index 42484688bc..002e76a6ca 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerContentService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerContentService.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using SmartStore.Core; using SmartStore.Core.Domain.Customers; namespace SmartStore.Services.Customers @@ -20,9 +20,15 @@ public partial interface ICustomerContentService /// /// Customer identifier; 0 to load all records /// A value indicating whether to content is approved; null to load all records + /// Page index. + /// Page size. /// Customer content - IList GetAllCustomerContent(int customerId, bool? approved); - + IPagedList GetAllCustomerContent( + int customerId, + bool? approved, + int pageIndex = 0, + int pageSize = int.MaxValue); + /// /// Gets all customer content /// @@ -31,9 +37,16 @@ public partial interface ICustomerContentService /// A value indicating whether to content is approved; null to load all records /// Item creation from; null to load all records /// Item item creation to; null to load all records + /// Page index. + /// Page size. /// Customer content - IList GetAllCustomerContent(int customerId, bool? approved, - DateTime? fromUtc = null, DateTime? toUtc = null) where T : CustomerContent; + IPagedList GetAllCustomerContent( + int customerId, + bool? approved, + DateTime? fromUtc = null, + DateTime? toUtc = null, + int pageIndex = 0, + int pageSize = int.MaxValue) where T : CustomerContent; /// /// Gets a customer content diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerReportService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerReportService.cs index a884b5097d..6095b259dc 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerReportService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerReportService.cs @@ -1,30 +1,42 @@ using System; using System.Collections.Generic; +using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; +using SmartStore.Services.Orders; using static SmartStore.Services.Customers.CustomerReportService; namespace SmartStore.Services.Customers { /// - /// Customer report service interface + /// Customer report service interface. /// public partial interface ICustomerReportService { /// - /// Get best customers + /// Get best customers. /// - /// Order start time; null to load all - /// Order end time; null to load all - /// Order status; null to load all records - /// Order payment status; null to load all records - /// Order shippment status; null to load all records - /// 1 - order by order total, 2 - order by number of orders + /// Order start time. null to load all customers. + /// Order end time. null to load all customers. + /// Order status. null to load all customers. + /// Order payment status. null to load all customers. + /// Order shippment status. null to load all customers. + /// Sorting of report items. + /// Page index. + /// Page size. /// Report - IList GetTopCustomersReport(DateTime? startTime, DateTime? endTime, OrderStatus? os, PaymentStatus? ps, ShippingStatus? ss, int orderBy, int count); - + IPagedList GetTopCustomersReport( + DateTime? startTime, + DateTime? endTime, + OrderStatus? orderStatus, + PaymentStatus? paymentStatus, + ShippingStatus? shippingStatus, + ReportSorting sorting, + int pageIndex = 0, + int pageSize = int.MaxValue); + /// /// Gets a report of customers registered in the last days /// diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs index ab374d0f5a..91b0079fc3 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; @@ -22,28 +21,28 @@ public partial interface ICustomerService /// Customer collection IPagedList SearchCustomers(CustomerSearchQuery q); - /// - /// Gets all customers by customer format (including deleted ones) - /// - /// Password format - /// Customers - IPagedList GetAllCustomersByPasswordFormat(PasswordFormat passwordFormat); - - /// - /// Gets online customers - /// - /// Customer last activity date (from) - /// A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers; - /// Page index - /// Page size - /// Customer collection - IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, int[] customerRoleIds, int pageIndex, int pageSize); - - /// - /// Delete a customer - /// - /// Customer - void DeleteCustomer(Customer customer); + /// + /// Gets all customers by customer format (including deleted ones) + /// + /// Password format + /// Customers + IPagedList GetAllCustomersByPasswordFormat(PasswordFormat passwordFormat); + + /// + /// Gets online customers + /// + /// Customer last activity date (from) + /// A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers; + /// Page index + /// Page size + /// Customer collection + IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, int[] customerRoleIds, int pageIndex, int pageSize); + + /// + /// Delete a customer + /// + /// Customer + void DeleteCustomer(Customer customer); /// /// Gets a customer @@ -59,18 +58,18 @@ public partial interface ICustomerService /// Customers IList GetCustomersByIds(int[] customerIds); - /// - /// Get system account customers - /// - /// System account customers - IList GetSystemAccountCustomers(); + /// + /// Get system account customers + /// + /// System account customers + IList GetSystemAccountCustomers(); - /// - /// Gets a customer by GUID - /// - /// Customer GUID - /// A customer - Customer GetCustomerByGuid(Guid customerGuid); + /// + /// Gets a customer by GUID + /// + /// Customer GUID + /// A customer + Customer GetCustomerByGuid(Guid customerGuid); /// /// Get customer by email @@ -78,7 +77,7 @@ public partial interface ICustomerService /// Email /// Customer Customer GetCustomerByEmail(string email); - + /// /// Get customer by system name /// @@ -93,29 +92,29 @@ public partial interface ICustomerService /// Customer Customer GetCustomerByUsername(string username); - /// - /// Insert a guest customer - /// - /// The customer GUID. Pass null to create a random one. - /// Customer - Customer InsertGuestCustomer(Guid? customerGuid = null); - - /// - /// Tries to find a guest/anonymous customer record by client ident. This method should be called when an - /// anonymous visitor rejects cookies and therefore cannot be identified automatically. - /// - /// - /// The client ident string, which is a hashed combination of client IP address and user agent. - /// Call to obtain an ident string, or pass null to let this method obtain it automatically. - /// The max age of the newly created guest customer record. The shorter, the better (default is 1 min.) - /// The identified customer or null - Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60); - - /// - /// Insert a customer - /// - /// Customer - void InsertCustomer(Customer customer); + /// + /// Insert a guest customer + /// + /// The customer GUID. Pass null to create a random one. + /// Customer + Customer InsertGuestCustomer(Guid? customerGuid = null); + + /// + /// Tries to find a guest/anonymous customer record by client ident. This method should be called when an + /// anonymous visitor rejects cookies and therefore cannot be identified automatically. + /// + /// + /// The client ident string, which is a hashed combination of client IP address and user agent. + /// Call to obtain an ident string, or pass null to let this method obtain it automatically. + /// The max age of the newly created guest customer record. The shorter, the better (default is 1 min.) + /// The identified customer or null + Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60); + + /// + /// Insert a customer + /// + /// Customer + void InsertCustomer(Customer customer); /// /// Updates the customer @@ -123,31 +122,34 @@ public partial interface ICustomerService /// Customer void UpdateCustomer(Customer customer); - /// - /// Reset data required for checkout - /// - /// Customer - /// Store identifier - /// A value indicating whether to clear coupon code - /// A value indicating whether to clear selected checkout attributes - /// A value indicating whether to clear "Use reward points" flag - /// A value indicating whether to clear selected shipping method - /// A value indicating whether to clear selected payment method - /// A value indicating whether to clear credit balance. - void ResetCheckoutData(Customer customer, int storeId, + /// + /// Reset data required for checkout + /// + /// Customer + /// Store identifier + /// A value indicating whether to clear coupon code + /// A value indicating whether to clear selected checkout attributes + /// A value indicating whether to clear "Use reward points" flag + /// A value indicating whether to clear selected shipping method + /// A value indicating whether to clear selected payment method + /// A value indicating whether to clear credit balance. + void ResetCheckoutData(Customer customer, int storeId, bool clearCouponCodes = false, bool clearCheckoutAttributes = false, bool clearRewardPoints = false, bool clearShippingMethod = true, bool clearPaymentMethod = true, - bool clearCreditBalance = false); + bool clearCreditBalance = false); - /// - /// Delete guest customer records - /// - /// Customer registration from; null to load all customers - /// Customer registration to; null to load all customers - /// A value indicating whether to delete customers only without shopping cart - /// Number of deleted customers - Task DeleteGuestCustomersAsync(DateTime? registrationFrom, DateTime? registrationTo, bool onlyWithoutShoppingCart, int maxItemsToDelete = 5000); + /// + /// Delete guest customer records including generic attributes. + /// + /// Customer registration from. null to ignore. + /// Customer registration to. null to ignore. + /// A value indicating whether to delete only customers without shopping cart. + /// Number of deleted guest customers. + int DeleteGuestCustomers( + DateTime? registrationFrom, + DateTime? registrationTo, + bool onlyWithoutShoppingCart); #endregion @@ -176,9 +178,11 @@ void ResetCheckoutData(Customer customer, int storeId, /// /// Gets all customer roles /// + /// Page index. + /// Page size. /// A value indicating whether to show hidden records /// Customer role collection - IList GetAllCustomerRoles(bool showHidden = false); + IPagedList GetAllCustomerRoles(bool showHidden = false, int pageIndex = 0, int pageSize = int.MaxValue); /// /// Inserts a customer role @@ -251,13 +255,13 @@ IPagedList GetCustomerRoleMappings( /// Whether to add or remove points void RewardPointsForProductReview(Customer customer, Product product, bool add); - /// - /// Gets reward points histories - /// - /// Customer identifiers - /// Reward points histories - Multimap GetRewardPointsHistoriesByCustomerIds(int[] customerIds); + /// + /// Gets reward points histories + /// + /// Customer identifiers + /// Reward points histories + Multimap GetRewardPointsHistoriesByCustomerIds(int[] customerIds); - #endregion - } + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Customers/IGdprTool.cs b/src/Libraries/SmartStore.Services/Customers/IGdprTool.cs index febf53638e..ca85e4f96c 100644 --- a/src/Libraries/SmartStore.Services/Customers/IGdprTool.cs +++ b/src/Libraries/SmartStore.Services/Customers/IGdprTool.cs @@ -8,33 +8,33 @@ namespace SmartStore.Services.Customers { - public interface IGdprTool - { - /// - /// Exports all data stored for a customer into a dictionary. Exported data contains all - /// personal data, addresses, order history, reviews, forum posts, private messages etc. - /// - /// The customer to export data for. - /// The exported data - /// This method fulfills the "GDPR Data Portability" requirement. - IDictionary ExportCustomer(Customer customer); + public interface IGdprTool + { + /// + /// Exports all data stored for a customer into a dictionary. Exported data contains all + /// personal data, addresses, order history, reviews, forum posts, private messages etc. + /// + /// The customer to export data for. + /// The exported data + /// This method fulfills the "GDPR Data Portability" requirement. + IDictionary ExportCustomer(Customer customer); - /// - /// Anonymizes a customer's (personal) data. - /// - /// The customer to anonymize. - /// - /// This method fulfills the "GDPR Right to be forgotten" requirement. - void AnonymizeCustomer(Customer customer, bool pseudomyzeContent); + /// + /// Anonymizes a customer's (personal) data. + /// + /// The customer to anonymize. + /// + /// This method fulfills the "GDPR Right to be forgotten" requirement. + void AnonymizeCustomer(Customer customer, bool pseudomyzeContent); - /// - /// TODO - /// - /// - /// - /// - /// - /// - void AnonymizeData(TEntity entity, Expression> expression, IdentifierDataType type, Language language = null) where TEntity : BaseEntity; - } + /// + /// TODO + /// + /// + /// + /// + /// + /// + void AnonymizeData(TEntity entity, Expression> expression, IdentifierDataType type, Language language = null) where TEntity : BaseEntity; + } } diff --git a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs index 4fb953ca0d..ce9703ed4d 100644 --- a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs +++ b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs @@ -8,8 +8,8 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Tax; -using SmartStore.Core.IO; using SmartStore.Core.Security; using SmartStore.Services.Affiliates; using SmartStore.Services.Common; @@ -23,119 +23,119 @@ namespace SmartStore.Services.Customers.Importer { public class CustomerImporter : EntityImporterBase - { - private const string _attributeKeyGroup = "Customer"; + { + private const string _attributeKeyGroup = "Customer"; - private readonly IRepository _customerRepository; - private readonly IRepository _customerRoleRepository; - private readonly ICommonServices _services; + private readonly IRepository _customerRepository; + private readonly IRepository _customerRoleRepository; + private readonly ICommonServices _services; private readonly ICustomerService _customerService; private readonly IGenericAttributeService _genericAttributeService; private readonly IMediaService _mediaService; private readonly IAffiliateService _affiliateService; - private readonly ICountryService _countryService; - private readonly IStateProvinceService _stateProvinceService; - private readonly FileDownloadManager _fileDownloadManager; - private readonly CustomerSettings _customerSettings; - private readonly DateTimeSettings _dateTimeSettings; - private readonly ForumSettings _forumSettings; + private readonly ICountryService _countryService; + private readonly IStateProvinceService _stateProvinceService; + private readonly FileDownloadManager _fileDownloadManager; + private readonly CustomerSettings _customerSettings; + private readonly DateTimeSettings _dateTimeSettings; + private readonly ForumSettings _forumSettings; private readonly TaxSettings _taxSettings; private readonly PrivacySettings _privacySettings; public CustomerImporter( - IRepository customerRepository, - IRepository customerRoleRepository, - ICommonServices services, + IRepository customerRepository, + IRepository customerRoleRepository, + ICommonServices services, ICustomerService customerService, IGenericAttributeService genericAttributeService, IMediaService mediaService, IAffiliateService affiliateService, - ICountryService countryService, - IStateProvinceService stateProvinceService, - FileDownloadManager fileDownloadManager, - CustomerSettings customerSettings, - DateTimeSettings dateTimeSettings, - ForumSettings forumSettings, + ICountryService countryService, + IStateProvinceService stateProvinceService, + FileDownloadManager fileDownloadManager, + CustomerSettings customerSettings, + DateTimeSettings dateTimeSettings, + ForumSettings forumSettings, TaxSettings taxSettings, PrivacySettings privacySettings) - { - _customerRepository = customerRepository; - _customerRoleRepository = customerRoleRepository; + { + _customerRepository = customerRepository; + _customerRoleRepository = customerRoleRepository; _mediaService = mediaService; - _services = services; + _services = services; _customerService = customerService; - _genericAttributeService = genericAttributeService; - _affiliateService = affiliateService; - _countryService = countryService; - _stateProvinceService = stateProvinceService; - _fileDownloadManager = fileDownloadManager; - _customerSettings = customerSettings; - _dateTimeSettings = dateTimeSettings; - _forumSettings = forumSettings; + _genericAttributeService = genericAttributeService; + _affiliateService = affiliateService; + _countryService = countryService; + _stateProvinceService = stateProvinceService; + _fileDownloadManager = fileDownloadManager; + _customerSettings = customerSettings; + _dateTimeSettings = dateTimeSettings; + _forumSettings = forumSettings; _taxSettings = taxSettings; _privacySettings = privacySettings; - } + } - protected override void Import(ImportExecuteContext context) - { + protected override void Import(ImportExecuteContext context) + { var customer = _services.WorkContext.CurrentCustomer; var allowManagingCustomerRoles = _services.Permissions.Authorize(Permissions.Customer.EditRole, customer); var allAffiliateIds = _affiliateService.GetAllAffiliates(true) - .Select(x => x.Id) - .ToList(); - - var allCountries = new Dictionary(); - foreach (var country in _countryService.GetAllCountries(true)) - { - if (!allCountries.ContainsKey(country.TwoLetterIsoCode)) - allCountries.Add(country.TwoLetterIsoCode, country.Id); - - if (!allCountries.ContainsKey(country.ThreeLetterIsoCode)) - allCountries.Add(country.ThreeLetterIsoCode, country.Id); - } - - var allStateProvinces = _stateProvinceService.GetAllStateProvinces(true) - .ToDictionarySafe(x => new Tuple(x.CountryId, x.Abbreviation), x => x.Id); - - var allCustomerNumbers = new HashSet( - _customerRepository.Table.Where(x => !string.IsNullOrEmpty(x.CustomerNumber)).Select(x => x.CustomerNumber), - StringComparer.OrdinalIgnoreCase); - - using (var scope = new DbContextScope(ctx: _services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) - { - var segmenter = context.DataSegmenter; - - Initialize(context); - AddInfoForDeprecatedFields(context); - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.GetCurrentBatch(); - - _customerRepository.Context.DetachAll(true); - - context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); - - // =========================================================================== - // Process customers - // =========================================================================== - try - { - ProcessCustomers(context, batch, allAffiliateIds, allCustomerNumbers); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessCustomers"); - } - - // reduce batch to saved (valid) records. - // No need to perform import operations on errored records. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); - - // update result object - context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); - context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + .Select(x => x.Id) + .ToList(); + + var allCountries = new Dictionary(); + foreach (var country in _countryService.GetAllCountries(true)) + { + if (!allCountries.ContainsKey(country.TwoLetterIsoCode)) + allCountries.Add(country.TwoLetterIsoCode, country.Id); + + if (!allCountries.ContainsKey(country.ThreeLetterIsoCode)) + allCountries.Add(country.ThreeLetterIsoCode, country.Id); + } + + var allStateProvinces = _stateProvinceService.GetAllStateProvinces(true) + .ToDictionarySafe(x => new Tuple(x.CountryId, x.Abbreviation), x => x.Id); + + var allCustomerNumbers = new HashSet( + _customerRepository.Table.Where(x => !string.IsNullOrEmpty(x.CustomerNumber)).Select(x => x.CustomerNumber), + StringComparer.OrdinalIgnoreCase); + + using (var scope = new DbContextScope(ctx: _services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + AddInfoForDeprecatedFields(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + _customerRepository.Context.DetachAll(true); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + // =========================================================================== + // Process customers + // =========================================================================== + try + { + ProcessCustomers(context, batch, allAffiliateIds, allCustomerNumbers); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessCustomers"); + } + + // reduce batch to saved (valid) records. + // No need to perform import operations on errored records. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); // =========================================================================== // Process customer roles @@ -157,172 +157,172 @@ protected override void Import(ImportExecuteContext context) } } - // =========================================================================== - // Process generic attributes - // =========================================================================== - try - { - ProcessGenericAttributes(context, batch, allCountries, allStateProvinces); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessGenericAttributes"); - } - - // =========================================================================== - // Process avatars - // =========================================================================== - if (_customerSettings.AllowCustomersToUploadAvatars) - { - try - { - ProcessAvatars(context, batch); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessAvatars"); - } - } - - // =========================================================================== - // Process addresses - // =========================================================================== - try - { - _services.DbContext.AutoDetectChangesEnabled = true; - ProcessAddresses(context, batch, allCountries, allStateProvinces); - } - catch (Exception ex) - { - context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessAddresses"); - } - finally - { - _services.DbContext.AutoDetectChangesEnabled = false; - } + // =========================================================================== + // Process generic attributes + // =========================================================================== + try + { + ProcessGenericAttributes(context, batch, allCountries, allStateProvinces); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessGenericAttributes"); + } + + // =========================================================================== + // Process avatars + // =========================================================================== + if (_customerSettings.AllowCustomersToUploadAvatars) + { + try + { + ProcessAvatars(context, batch); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessAvatars"); + } + } + + // =========================================================================== + // Process addresses + // =========================================================================== + try + { + _services.DbContext.AutoDetectChangesEnabled = true; + ProcessAddresses(context, batch, allCountries, allStateProvinces); + } + catch (Exception ex) + { + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessAddresses"); + } + finally + { + _services.DbContext.AutoDetectChangesEnabled = false; + } context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); } - } - } - - protected virtual int ProcessCustomers( - ImportExecuteContext context, - IEnumerable> batch, - List allAffiliateIds, - HashSet allCustomerNumbers) - { - _customerRepository.AutoCommitEnabled = true; - - var currentCustomer = _services.WorkContext.CurrentCustomer; + } + } + + protected virtual int ProcessCustomers( + ImportExecuteContext context, + IEnumerable> batch, + List allAffiliateIds, + HashSet allCustomerNumbers) + { + _customerRepository.AutoCommitEnabled = true; + + var currentCustomer = _services.WorkContext.CurrentCustomer; var customerQuery = _customerRepository.Table .Expand(x => x.Addresses) .Expand(x => x.CustomerRoleMappings.Select(rm => rm.CustomerRole)); - foreach (var row in batch) - { - Customer customer = null; - var id = row.GetDataValue("Id"); - var email = row.GetDataValue("Email"); + foreach (var row in batch) + { + Customer customer = null; + var id = row.GetDataValue("Id"); + var email = row.GetDataValue("Email"); var userName = row.GetDataValue("Username"); foreach (var keyName in context.KeyFieldNames) - { - switch (keyName) - { - case "Id": - if (id != 0) - { - customer = customerQuery.FirstOrDefault(x => x.Id == id); - } - break; - case "CustomerGuid": - var customerGuid = row.GetDataValue("CustomerGuid"); - if (customerGuid.HasValue()) - { - var guid = new Guid(customerGuid); - customer = customerQuery.FirstOrDefault(x => x.CustomerGuid == guid); - } - break; - case "Email": - if (email.HasValue()) - { - customer = customerQuery.FirstOrDefault(x => x.Email == email); - } - break; - case "Username": - if (userName.HasValue()) - { - customer = customerQuery.FirstOrDefault(x => x.Username == userName); - } - break; - } - - if (customer != null) - break; - } - - if (customer == null) - { - if (context.UpdateOnly) - { - ++context.Result.SkippedRecords; - continue; - } - - customer = new Customer - { - CustomerGuid = new Guid(), - AffiliateId = 0, - Active = true - }; - } - else - { - _customerRepository.Context.LoadCollection(customer, (Customer x) => x.CustomerRoleMappings); - } - - var affiliateId = row.GetDataValue("AffiliateId"); - - row.Initialize(customer, email ?? id.ToString()); - - row.SetProperty(context.Result, (x) => x.CustomerGuid); - row.SetProperty(context.Result, (x) => x.Username); - row.SetProperty(context.Result, (x) => x.Email); + { + switch (keyName) + { + case "Id": + if (id != 0) + { + customer = customerQuery.FirstOrDefault(x => x.Id == id); + } + break; + case "CustomerGuid": + var customerGuid = row.GetDataValue("CustomerGuid"); + if (customerGuid.HasValue()) + { + var guid = new Guid(customerGuid); + customer = customerQuery.FirstOrDefault(x => x.CustomerGuid == guid); + } + break; + case "Email": + if (email.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Email == email); + } + break; + case "Username": + if (userName.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Username == userName); + } + break; + } + + if (customer != null) + break; + } + + if (customer == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + customer = new Customer + { + CustomerGuid = new Guid(), + AffiliateId = 0, + Active = true + }; + } + else + { + _customerRepository.Context.LoadCollection(customer, (Customer x) => x.CustomerRoleMappings); + } + + var affiliateId = row.GetDataValue("AffiliateId"); + + row.Initialize(customer, email ?? id.ToString()); + + row.SetProperty(context.Result, (x) => x.CustomerGuid); + row.SetProperty(context.Result, (x) => x.Username); + row.SetProperty(context.Result, (x) => x.Email); row.SetProperty(context.Result, (x) => x.Salutation); row.SetProperty(context.Result, (x) => x.FullName); row.SetProperty(context.Result, (x) => x.FirstName); - row.SetProperty(context.Result, (x) => x.LastName); + row.SetProperty(context.Result, (x) => x.LastName); if (_customerSettings.TitleEnabled) - row.SetProperty(context.Result, (x) => x.Title); + row.SetProperty(context.Result, (x) => x.Title); - if (_customerSettings.CompanyEnabled) - row.SetProperty(context.Result, (x) => x.Company); + if (_customerSettings.CompanyEnabled) + row.SetProperty(context.Result, (x) => x.Company); - if (_customerSettings.DateOfBirthEnabled) - row.SetProperty(context.Result, (x) => x.BirthDate); + if (_customerSettings.DateOfBirthEnabled) + row.SetProperty(context.Result, (x) => x.BirthDate); if (_privacySettings.StoreLastIpAddress) row.SetProperty(context.Result, (x) => x.LastIpAddress); if (email.HasValue() && currentCustomer.Email.IsCaseInsensitiveEqual(email)) - { - context.Result.AddInfo("Security. Ignored password of current customer (who started this import).", row.GetRowInfo(), "Password"); - } - else - { - row.SetProperty(context.Result, (x) => x.Password); - row.SetProperty(context.Result, (x) => x.PasswordFormatId); - row.SetProperty(context.Result, (x) => x.PasswordSalt); - } - - row.SetProperty(context.Result, (x) => x.AdminComment); - row.SetProperty(context.Result, (x) => x.IsTaxExempt); - row.SetProperty(context.Result, (x) => x.Active); - - row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); - row.SetProperty(context.Result, (x) => x.LastActivityDateUtc, UtcNow); + { + context.Result.AddInfo("Security. Ignored password of current customer (who started this import).", row.GetRowInfo(), "Password"); + } + else + { + row.SetProperty(context.Result, (x) => x.Password); + row.SetProperty(context.Result, (x) => x.PasswordFormatId); + row.SetProperty(context.Result, (x) => x.PasswordSalt); + } + + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.Active); + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + row.SetProperty(context.Result, (x) => x.LastActivityDateUtc, UtcNow); if (_taxSettings.EuVatEnabled) row.SetProperty(context.Result, (x) => x.VatNumberStatusId); @@ -335,8 +335,8 @@ protected virtual int ProcessCustomers( if (affiliateId > 0 && allAffiliateIds.Contains(affiliateId)) customer.AffiliateId = affiliateId; - - string customerNumber = null; + + string customerNumber = null; if (_customerSettings.CustomerNumberMethod == CustomerNumberMethod.AutomaticallySet && row.IsTransient) { @@ -346,70 +346,70 @@ protected virtual int ProcessCustomers( { customerNumber = row.GetDataValue("CustomerNumber"); } - - if (customerNumber.HasValue() || !allCustomerNumbers.Contains(customerNumber)) - { - row.Entity.CustomerNumber = customerNumber; + + if (customerNumber.HasValue() || !allCustomerNumbers.Contains(customerNumber)) + { + row.Entity.CustomerNumber = customerNumber; if (!customerNumber.IsEmpty()) { allCustomerNumbers.Add(customerNumber); } - } - - if (row.IsTransient) - { - _customerRepository.Insert(customer); - } - else - { - _customerRepository.Update(customer); - } - } - - var num = _customerRepository.Context.SaveChanges(); - return num; - } - - protected virtual int ProcessCustomerRoles( - ImportExecuteContext context, - IEnumerable> batch) - { + } + + if (row.IsTransient) + { + _customerRepository.Insert(customer); + } + else + { + _customerRepository.Update(customer); + } + } + + var num = _customerRepository.Context.SaveChanges(); + return num; + } + + protected virtual int ProcessCustomerRoles( + ImportExecuteContext context, + IEnumerable> batch) + { Dictionary allCustomerRoles = null; - foreach (var row in batch) - { - var customer = row.Entity; + foreach (var row in batch) + { + var customer = row.Entity; var importRoleSystemNames = row.GetDataValue>("CustomerRoleSystemNames"); - var assignedRoles = customer.CustomerRoleMappings + var assignedRoles = customer.CustomerRoleMappings .Where(x => !x.IsSystemMapping) .Select(x => x.CustomerRole) .ToDictionarySafe(x => x.SystemName, StringComparer.OrdinalIgnoreCase); - // Roles to remove. - foreach (var customerRole in assignedRoles) - { - var systemName = customerRole.Key; - if (!systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.Administrators) && - !systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.SuperAdministrators) && - !importRoleSystemNames.Contains(systemName)) - { + // Roles to remove. + foreach (var customerRole in assignedRoles) + { + var systemName = customerRole.Key; + if (!systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.Administrators) && + !systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.SuperAdministrators) && + !importRoleSystemNames.Contains(systemName)) + { var mappings = customer.CustomerRoleMappings.Where(x => !x.IsSystemMapping && x.CustomerRoleId == customerRole.Value.Id).ToList(); mappings.Each(x => _customerService.DeleteCustomerRoleMapping(x)); - } - } - - // Roles to add. - foreach (var systemName in importRoleSystemNames) - { - if (systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.Administrators) || - systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.SuperAdministrators)) - { - context.Result.AddInfo("Security. Ignored administrator role.", row.GetRowInfo(), "CustomerRoleSystemNames"); - } - else if (!assignedRoles.ContainsKey(systemName)) - { + } + } + + // Roles to add. + foreach (var systemName in importRoleSystemNames) + { + if (systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.Administrators) || + systemName.IsCaseInsensitiveEqual(SystemCustomerRoleNames.SuperAdministrators)) + { + context.Result.AddInfo("Security. Ignored administrator role.", row.GetRowInfo(), "CustomerRoleSystemNames"); + } + else if (!assignedRoles.ContainsKey(systemName)) + { // Add role mapping, never insert roles. // Be careful not to insert the roles several times! if (allCustomerRoles == null) @@ -424,34 +424,34 @@ protected virtual int ProcessCustomerRoles( _customerService.InsertCustomerRoleMapping(new CustomerRoleMapping { CustomerId = customer.Id, CustomerRoleId = role.Id }); } } - } - } + } + } + + return _services.DbContext.SaveChanges(); + } + + protected virtual int ProcessAddresses( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + foreach (var row in batch) + { + ImportAddress("BillingAddress.", row, context, allCountries, allStateProvinces); + ImportAddress("ShippingAddress.", row, context, allCountries, allStateProvinces); + } return _services.DbContext.SaveChanges(); } - protected virtual int ProcessAddresses( - ImportExecuteContext context, - IEnumerable> batch, - Dictionary allCountries, - Dictionary, int> allStateProvinces) - { - foreach (var row in batch) - { - ImportAddress("BillingAddress.", row, context, allCountries, allStateProvinces); - ImportAddress("ShippingAddress.", row, context, allCountries, allStateProvinces); - } - - return _services.DbContext.SaveChanges(); - } - - private void ImportAddress( - string fieldPrefix, - ImportRow row, - ImportExecuteContext context, - Dictionary allCountries, - Dictionary, int> allStateProvinces) - { + private void ImportAddress( + string fieldPrefix, + ImportRow row, + ImportExecuteContext context, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { // Last name is mandatory for an address to be imported or updated. if (!row.HasDataValue(fieldPrefix + "LastName")) { @@ -469,32 +469,32 @@ private void ImportAddress( address = row.Entity.ShippingAddress ?? new Address { CreatedOnUtc = UtcNow }; } - var childRow = new ImportRow
(row.Segmenter, row.DataRow, row.Position); - childRow.Initialize(address, row.EntityDisplayName); + var childRow = new ImportRow
(row.Segmenter, row.DataRow, row.Position); + childRow.Initialize(address, row.EntityDisplayName); childRow.SetProperty(context.Result, fieldPrefix + "Salutation", x => x.Salutation); childRow.SetProperty(context.Result, fieldPrefix + "Title", x => x.Title); childRow.SetProperty(context.Result, fieldPrefix + "FirstName", x => x.FirstName); childRow.SetProperty(context.Result, fieldPrefix + "LastName", x => x.LastName); - childRow.SetProperty(context.Result, fieldPrefix + "Email", x => x.Email); - childRow.SetProperty(context.Result, fieldPrefix + "Company", x => x.Company); - childRow.SetProperty(context.Result, fieldPrefix + "City", x => x.City); - childRow.SetProperty(context.Result, fieldPrefix + "Address1", x => x.Address1); - childRow.SetProperty(context.Result, fieldPrefix + "Address2", x => x.Address2); - childRow.SetProperty(context.Result, fieldPrefix + "ZipPostalCode", x => x.ZipPostalCode); - childRow.SetProperty(context.Result, fieldPrefix + "PhoneNumber", x => x.PhoneNumber); - childRow.SetProperty(context.Result, fieldPrefix + "FaxNumber", x => x.FaxNumber); - - childRow.SetProperty(context.Result, fieldPrefix + "CountryId", x => x.CountryId); - if (childRow.Entity.CountryId == null) - { - // Try with country code. - childRow.SetProperty(context.Result, fieldPrefix + "CountryCode", x => x.CountryId, converter: (val, ci) => CountryCodeToId(allCountries, val.ToString())); - } + childRow.SetProperty(context.Result, fieldPrefix + "Email", x => x.Email); + childRow.SetProperty(context.Result, fieldPrefix + "Company", x => x.Company); + childRow.SetProperty(context.Result, fieldPrefix + "City", x => x.City); + childRow.SetProperty(context.Result, fieldPrefix + "Address1", x => x.Address1); + childRow.SetProperty(context.Result, fieldPrefix + "Address2", x => x.Address2); + childRow.SetProperty(context.Result, fieldPrefix + "ZipPostalCode", x => x.ZipPostalCode); + childRow.SetProperty(context.Result, fieldPrefix + "PhoneNumber", x => x.PhoneNumber); + childRow.SetProperty(context.Result, fieldPrefix + "FaxNumber", x => x.FaxNumber); + + childRow.SetProperty(context.Result, fieldPrefix + "CountryId", x => x.CountryId); + if (childRow.Entity.CountryId == null) + { + // Try with country code. + childRow.SetProperty(context.Result, fieldPrefix + "CountryCode", x => x.CountryId, converter: (val, ci) => CountryCodeToId(allCountries, val.ToString())); + } var countryId = childRow.Entity.CountryId; - if (countryId.HasValue) - { + if (countryId.HasValue) + { if (row.HasDataValue(fieldPrefix + "StateProvinceId")) { childRow.SetProperty(context.Result, fieldPrefix + "StateProvinceId", x => x.StateProvinceId); @@ -506,11 +506,11 @@ private void ImportAddress( } } - if (!childRow.IsDirty) - { - // Not one single property could be set. Get out! - return; - } + if (!childRow.IsDirty) + { + // Not one single property could be set. Get out! + return; + } if (address.Id == 0) { @@ -534,29 +534,29 @@ private void ImportAddress( } _customerRepository.Update(row.Entity); - } - - protected virtual int ProcessGenericAttributes( - ImportExecuteContext context, - IEnumerable> batch, - Dictionary allCountries, - Dictionary, int> allStateProvinces) - { - foreach (var row in batch) - { + } + + protected virtual int ProcessGenericAttributes( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + foreach (var row in batch) + { if (_taxSettings.EuVatEnabled) { SaveAttribute(row, SystemCustomerAttributeNames.VatNumber); } - if (_customerSettings.StreetAddressEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress); + if (_customerSettings.StreetAddressEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress); - if (_customerSettings.StreetAddress2Enabled) - SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress2); + if (_customerSettings.StreetAddress2Enabled) + SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress2); - if (_customerSettings.CityEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.City); + if (_customerSettings.CityEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.City); if (_customerSettings.ZipPostalCodeEnabled) SaveAttribute(row, SystemCustomerAttributeNames.ZipPostalCode); @@ -565,176 +565,172 @@ protected virtual int ProcessGenericAttributes( SaveAttribute(row, SystemCustomerAttributeNames.CountryId); if (_customerSettings.CountryEnabled && _customerSettings.StateProvinceEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId); + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId); - if (_customerSettings.PhoneEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.Phone); + if (_customerSettings.PhoneEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Phone); - if (_customerSettings.FaxEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.Fax); + if (_customerSettings.FaxEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Fax); - if (_forumSettings.ForumsEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.ForumPostCount); + if (_forumSettings.ForumsEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.ForumPostCount); - if (_forumSettings.SignaturesEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.Signature); + if (_forumSettings.SignaturesEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Signature); var countryId = CountryCodeToId(allCountries, row.GetDataValue("CountryCode")); var stateId = StateAbbreviationToId(allStateProvinces, countryId, row.GetDataValue("StateAbbreviation")); if (countryId.HasValue) SaveAttribute(row, SystemCustomerAttributeNames.CountryId, countryId.Value); - + if (stateId.HasValue) - SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId, stateId.Value); - - } - - return _services.DbContext.SaveChanges(); - } - - protected virtual int ProcessAvatars( - ImportExecuteContext context, - IEnumerable> batch) - { - foreach (var row in batch) - { - var urlOrPath = row.GetDataValue("AvatarPictureUrl"); + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId, stateId.Value); + + } + + return _services.DbContext.SaveChanges(); + } + + protected virtual int ProcessAvatars( + ImportExecuteContext context, + IEnumerable> batch) + { + foreach (var row in batch) + { + var urlOrPath = row.GetDataValue("AvatarPictureUrl"); if (urlOrPath.IsEmpty()) { continue; } - var image = CreateDownloadImage(context, urlOrPath, 1); - if (image.Url.HasValue() && !image.Success.HasValue) - { - AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); - } + var image = CreateDownloadImage(context, urlOrPath, 1); + if (image == null) + { + continue; + } + + if (image.Url.HasValue() && !image.Success.HasValue) + { + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); + } - if ((image.Success ?? false) && File.Exists(image.Path)) - { - Succeeded(image); + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); using (var stream = File.OpenRead(image.Path)) { - if ((stream?.Length ?? 0) > 0) + if (stream?.Length > 0) { + MediaFile sourceFile = null; var currentFiles = new List(); var fileId = row.Entity.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId); var file = _mediaService.GetFileById(fileId, MediaLoadFlags.AsNoTracking); + if (file != null) { currentFiles.Add(file); } - if (!_mediaService.FindEqualFile(stream, currentFiles.Select(x => x.File), true, out var _)) + if (_mediaService.FindEqualFile(stream, currentFiles.Select(x => x.File), true, out var _)) { - // Don't manage avatar files. Just overwrite existing file. - var path = _mediaService.CombinePaths(SystemAlbumProvider.Customers, image.FileName.ToValidFileName()); - - var newFile = _mediaService.SaveFile(path, stream, false, DuplicateFileHandling.Overwrite); - if ((newFile?.Id ?? 0) != 0) - { - SaveAttribute(row, SystemCustomerAttributeNames.AvatarPictureId, newFile.Id); - } + context.Result.AddInfo($"Found equal image in customer data for {image.FileName}. Skipping file.", row.GetRowInfo(), "AvatarPictureUrl"); } else { - context.Result.AddInfo("Found equal image in data store. Skipping field.", row.GetRowInfo(), "AvatarPictureUrl"); + // An avatar may not be assigned to several customers. A customer could otherwise delete the avatar of another. + // Overwriting is probably too dangerous here, because we could overwrite the avatar of another customer, so better rename. + var path = _mediaService.CombinePaths(SystemAlbumProvider.Customers, image.FileName); + sourceFile = _mediaService.SaveFile(path, stream, false, DuplicateFileHandling.Rename)?.File; + } + + if (sourceFile?.Id > 0) + { + SaveAttribute(row, SystemCustomerAttributeNames.AvatarPictureId, sourceFile.Id); } } } } - else - { - context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "AvatarPictureUrl"); - } - } - - return _services.DbContext.SaveChanges(); - } - - public static string[] SupportedKeyFields - { - get - { - return new string[] { "Id", "CustomerGuid", "Email", "Username" }; - } - } - - public static string[] DefaultKeyFields - { - get - { - return new string[] { "Email", "CustomerGuid" }; - } - } - - private int? CountryCodeToId(Dictionary allCountries, string code) - { - int countryId; - if (code.HasValue() && allCountries.TryGetValue(code, out countryId) && countryId != 0) - { - return countryId; - } - - return null; - } - - private int? StateAbbreviationToId(Dictionary, int> allStateProvinces, int? countryId, string abbreviation) - { - if (countryId.HasValue && abbreviation.HasValue()) - { - var key = Tuple.Create(countryId.Value, abbreviation); - - int stateId; - if (allStateProvinces.TryGetValue(key, out stateId) && stateId != 0) - { - return stateId; - } - } - - return null; - } - - private void SaveAttribute(ImportRow row, string key) - { - SaveAttribute(row, key, row.GetDataValue(key)); - } - - private void SaveAttribute(ImportRow row, string key) - { - SaveAttribute(row, key, row.GetDataValue(key)); - } - - private void SaveAttribute(ImportRow row, string key, TPropType value) - { - if (row.IsTransient) - return; - - if (row.IsNew || value != null) - { - _genericAttributeService.SaveAttribute(row.Entity.Id, key, _attributeKeyGroup, value); - } - } - - private void AddInfoForDeprecatedFields(ImportExecuteContext context) - { - if (context.DataSegmenter.HasColumn("IsGuest")) - { - context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsGuest"); - } - if (context.DataSegmenter.HasColumn("IsRegistered")) - { - context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsRegistered"); - } - if (context.DataSegmenter.HasColumn("IsAdministrator")) - { - context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsAdministrator"); - } - if (context.DataSegmenter.HasColumn("IsForumModerator")) - { - context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsForumModerator"); - } - } - } + else + { + context.Result.AddInfo("Download of avatar failed.", row.GetRowInfo(), "AvatarPictureUrl"); + } + } + + return _services.DbContext.SaveChanges(); + } + + public static string[] SupportedKeyFields => new string[] { "Id", "CustomerGuid", "Email", "Username" }; + + public static string[] DefaultKeyFields => new string[] { "Email", "CustomerGuid" }; + + private int? CountryCodeToId(Dictionary allCountries, string code) + { + int countryId; + if (code.HasValue() && allCountries.TryGetValue(code, out countryId) && countryId != 0) + { + return countryId; + } + + return null; + } + + private int? StateAbbreviationToId(Dictionary, int> allStateProvinces, int? countryId, string abbreviation) + { + if (countryId.HasValue && abbreviation.HasValue()) + { + var key = Tuple.Create(countryId.Value, abbreviation); + + int stateId; + if (allStateProvinces.TryGetValue(key, out stateId) && stateId != 0) + { + return stateId; + } + } + + return null; + } + + private void SaveAttribute(ImportRow row, string key) + { + SaveAttribute(row, key, row.GetDataValue(key)); + } + + private void SaveAttribute(ImportRow row, string key) + { + SaveAttribute(row, key, row.GetDataValue(key)); + } + + private void SaveAttribute(ImportRow row, string key, TPropType value) + { + if (row.IsTransient) + return; + + if (row.IsNew || value != null) + { + _genericAttributeService.SaveAttribute(row.Entity.Id, key, _attributeKeyGroup, value); + } + } + + private void AddInfoForDeprecatedFields(ImportExecuteContext context) + { + if (context.DataSegmenter.HasColumn("IsGuest")) + { + context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsGuest"); + } + if (context.DataSegmenter.HasColumn("IsRegistered")) + { + context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsRegistered"); + } + if (context.DataSegmenter.HasColumn("IsAdministrator")) + { + context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsAdministrator"); + } + if (context.DataSegmenter.HasColumn("IsForumModerator")) + { + context.Result.AddInfo("Deprecated field. Use CustomerRoleSystemNames instead.", null, "IsForumModerator"); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/PasswordChangeResult.cs b/src/Libraries/SmartStore.Services/Customers/PasswordChangeResult.cs index 6270fd3a4b..0b035a39b6 100644 --- a/src/Libraries/SmartStore.Services/Customers/PasswordChangeResult.cs +++ b/src/Libraries/SmartStore.Services/Customers/PasswordChangeResult.cs @@ -1,22 +1,19 @@ using System.Collections.Generic; -namespace SmartStore.Services.Customers +namespace SmartStore.Services.Customers { public class PasswordChangeResult { public IList Errors { get; set; } - public PasswordChangeResult() + public PasswordChangeResult() { this.Errors = new List(); } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); - public void AddError(string error) + public void AddError(string error) { this.Errors.Add(error); } diff --git a/src/Libraries/SmartStore.Services/Customers/Rules/ITargetGroupService.cs b/src/Libraries/SmartStore.Services/Customers/Rules/ITargetGroupService.cs index ab018d0f3e..ac24dc9358 100644 --- a/src/Libraries/SmartStore.Services/Customers/Rules/ITargetGroupService.cs +++ b/src/Libraries/SmartStore.Services/Customers/Rules/ITargetGroupService.cs @@ -15,8 +15,8 @@ public interface ITargetGroupService : IRuleProvider FilterExpressionGroup CreateExpressionGroup(int ruleSetId); IPagedList ProcessFilter( - FilterExpression filter, - int pageIndex = 0, + FilterExpression filter, + int pageIndex = 0, int pageSize = int.MaxValue); IPagedList ProcessFilter( diff --git a/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupFilterDescriptor.cs b/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupFilterDescriptor.cs index 9cc07202c9..266db9b3d1 100644 --- a/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupFilterDescriptor.cs +++ b/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupFilterDescriptor.cs @@ -15,7 +15,7 @@ public class TargetGroupFilterDescriptor : FilterDescriptor private readonly IRuleFactory _ruleFactory; private readonly WeakReference _ruleVisitor; - public TargetGroupFilterDescriptor(IRuleFactory ruleFactory, IRuleVisitor ruleVisitor) + public TargetGroupFilterDescriptor(IRuleFactory ruleFactory, IRuleVisitor ruleVisitor) : base(x => true) { _ruleFactory = ruleFactory; @@ -25,7 +25,7 @@ public TargetGroupFilterDescriptor(IRuleFactory ruleFactory, IRuleVisitor ruleVi public override Expression GetExpression(RuleOperator op, Expression valueExpression, bool liftToNull) { var ruleSetId = ((ConstantExpression)valueExpression).Value.Convert(); - + // Get other expression group _ruleVisitor.TryGetTarget(out var visitor); var otherGroup = _ruleFactory.CreateExpressionGroup(ruleSetId, visitor) as FilterExpressionGroup; diff --git a/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupService.cs b/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupService.cs index 665e89332b..2b54efadc3 100644 --- a/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupService.cs +++ b/src/Libraries/SmartStore.Services/Customers/Rules/TargetGroupService.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Localization; using SmartStore.Rules; using SmartStore.Rules.Domain; using SmartStore.Rules.Filters; @@ -23,7 +24,7 @@ public class TargetGroupService : RuleProviderBase, ITargetGroupService private readonly ICommonServices _services; public TargetGroupService( - IRuleFactory ruleFactory, + IRuleFactory ruleFactory, IRepository rsCustomer, ICommonServices services) : base(RuleScope.Customer) { @@ -32,6 +33,8 @@ public TargetGroupService( _services = services; } + public Localizer T { get; set; } = NullLocalizer.Instance; + public FilterExpressionGroup CreateExpressionGroup(int ruleSetId) { return _ruleFactory.CreateExpressionGroup(ruleSetId, this) as FilterExpressionGroup; @@ -62,22 +65,21 @@ public override IRuleExpressionGroup VisitRuleSet(RuleSetEntity ruleSet) } public IPagedList ProcessFilter( - FilterExpression filter, - int pageIndex = 0, + FilterExpression filter, + int pageIndex = 0, int pageSize = int.MaxValue) { Guard.NotNull(filter, nameof(filter)); return ProcessFilter( new[] { filter }, - LogicalRuleOperator.And, - pageIndex, + LogicalRuleOperator.And, + pageIndex, pageSize); } - public IPagedList ProcessFilter( - int[] ruleSetIds, + int[] ruleSetIds, LogicalRuleOperator logicalOperator, int pageIndex = 0, int pageSize = int.MaxValue) @@ -107,7 +109,7 @@ public IPagedList ProcessFilter( } // TODO: really untracked? - var query = _rsCustomer.TableUntracked.Where(x => !x.Deleted); + var query = _rsCustomer.TableUntracked.Where(x => !x.Deleted && !x.IsSystemAccount); FilterExpressionGroup group = null; @@ -160,295 +162,274 @@ protected override IEnumerable LoadDescriptors() new FilterDescriptor(x => x.Active) { Name = "Active", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Active"), - RuleType = RuleType.Boolean, - Constraints = new IRuleConstraint[0] - }, - new FilterDescriptor(x => x.IsTaxExempt) - { - Name = "TaxExempt", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.TaxExempt"), - RuleType = RuleType.Boolean, - Constraints = new IRuleConstraint[0] - }, - new FilterDescriptor(x => DbFunctions.DiffDays(x.LastActivityDateUtc, DateTime.UtcNow)) - { - Name = "LastActivityDays", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.LastActivityDays"), - RuleType = RuleType.NullableInt, - Constraints = new IRuleConstraint[0] - }, - new FilterDescriptor(x => DbFunctions.DiffDays(x.LastLoginDateUtc, DateTime.UtcNow)) - { - Name = "LastLoginDays", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.LastLoginDays"), - RuleType = RuleType.NullableInt, - Constraints = new IRuleConstraint[0] - }, - new FilterDescriptor(x => DbFunctions.DiffDays(x.CreatedOnUtc, DateTime.UtcNow)) - { - Name = "CreatedDays", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.CreatedDays"), - RuleType = RuleType.NullableInt, - Constraints = new IRuleConstraint[0] - }, - new FilterDescriptor(x => DbFunctions.DiffDays(x.BirthDate, DateTime.UtcNow)) - { - Name = "BirthDateDays", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.BirthDate"), - RuleType = RuleType.NullableInt, - Constraints = new IRuleConstraint[0] - }, - new FilterDescriptor(x => x.BillingAddress != null ? x.BillingAddress.CountryId : 0) - { - Name = "BillingCountry", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.BillingCountry"), - RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } - }, - new FilterDescriptor(x => x.ShippingAddress != null ? x.ShippingAddress.CountryId : 0) - { - Name = "ShippingCountry", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ShippingCountry"), - RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } + DisplayName = T("Admin.Rules.FilterDescriptor.Active"), + RuleType = RuleType.Boolean }, new FilterDescriptor(x => x.Salutation) { Name = "Salutation", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Salutation"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.Salutation"), + RuleType = RuleType.String }, new FilterDescriptor(x => x.Title) { Name = "Title", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Title"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.Title"), + RuleType = RuleType.String }, new FilterDescriptor(x => x.Company) { Name = "Company", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Company"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.Company"), + RuleType = RuleType.String }, new FilterDescriptor(x => x.Gender) { Name = "Gender", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Gender"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.Gender"), + RuleType = RuleType.String }, new FilterDescriptor(x => x.CustomerNumber) { Name = "CustomerNumber", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.CustomerNumber"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.CustomerNumber"), + RuleType = RuleType.String }, - new FilterDescriptor(x => x.TimeZoneId) + new AnyFilterDescriptor(x => x.CustomerRoleMappings, rm => rm.CustomerRoleId) { - Name = "TimeZone", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.TimeZone"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + Name = "IsInCustomerRole", + DisplayName = T("Admin.Rules.FilterDescriptor.IsInCustomerRole"), + RuleType = RuleType.IntArray, + SelectList = new RemoteRuleValueSelectList("CustomerRole") { Multiple = true } + }, + new FilterDescriptor(x => x.IsTaxExempt) + { + Name = "TaxExempt", + DisplayName = T("Admin.Rules.FilterDescriptor.TaxExempt"), + RuleType = RuleType.Boolean }, new FilterDescriptor(x => x.VatNumberStatusId) { Name = "VatNumberStatus", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.VatNumberStatus"), + DisplayName = T("Admin.Rules.FilterDescriptor.VatNumberStatus"), RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], SelectList = new LocalRuleValueSelectList(vatNumberStatus) }, new FilterDescriptor(x => x.TaxDisplayTypeId) { Name = "TaxDisplayType", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.TaxDisplayType"), + DisplayName = T("Admin.Rules.FilterDescriptor.TaxDisplayType"), RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], SelectList = new LocalRuleValueSelectList(taxDisplayTypes) }, - new FilterDescriptor(x => DbFunctions.DiffDays(x.LastForumVisit, DateTime.UtcNow)) + new FilterDescriptor(x => x.TimeZoneId) { - Name = "LastForumVisitDays", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.LastForumVisit"), - RuleType = RuleType.NullableInt, - Constraints = new IRuleConstraint[0] + Name = "TimeZone", + DisplayName = T("Admin.Rules.FilterDescriptor.TimeZone"), + RuleType = RuleType.String }, new FilterDescriptor(x => x.LastUserAgent) { Name = "LastUserAgent", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.LastUserAgent"), - RuleType = RuleType.String, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.LastUserAgent"), + RuleType = RuleType.String }, - - new AnyFilterDescriptor(x => x.CustomerRoleMappings, rm => rm.CustomerRoleId) + new FilterDescriptor(x => x.BillingAddress != null ? x.BillingAddress.CountryId : 0) { - Name = "IsInCustomerRole", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.IsInCustomerRole"), + Name = "BillingCountry", + DisplayName = T("Admin.Rules.FilterDescriptor.BillingCountry"), RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("CustomerRole") { Multiple = true } + SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } + }, + new FilterDescriptor(x => x.ShippingAddress != null ? x.ShippingAddress.CountryId : 0) + { + Name = "ShippingCountry", + DisplayName = T("Admin.Rules.FilterDescriptor.ShippingCountry"), + RuleType = RuleType.IntArray, + SelectList = new RemoteRuleValueSelectList("Country") { Multiple = true } }, new FilterDescriptor(x => x.ReturnRequests.Count()) { Name = "ReturnRequestCount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ReturnRequestCount"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.ReturnRequestCount"), + RuleType = RuleType.Int }, - new FilterDescriptor(x => x.Orders.Count(o => !o.Deleted && o.OrderStatusId == 30)) + new FilterDescriptor(x => DbFunctions.DiffDays(x.LastActivityDateUtc, DateTime.UtcNow)) { - Name = "CompletedOrderCount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.CompletedOrderCount"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0] + Name = "LastActivityDays", + DisplayName = T("Admin.Rules.FilterDescriptor.LastActivityDays"), + RuleType = RuleType.NullableInt }, - new FilterDescriptor(x => x.Orders.Count(o => !o.Deleted && o.OrderStatusId == 40)) + new FilterDescriptor(x => DbFunctions.DiffDays(x.LastLoginDateUtc, DateTime.UtcNow)) { - Name = "CancelledOrderCount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.CancelledOrderCount"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0] + Name = "LastLoginDays", + DisplayName = T("Admin.Rules.FilterDescriptor.LastLoginDays"), + RuleType = RuleType.NullableInt }, - new FilterDescriptor(x => x.Orders.Count(o => !o.Deleted && (o.OrderStatusId == 10 || o.OrderStatusId == 20))) + new FilterDescriptor(x => DbFunctions.DiffDays(x.LastForumVisit, DateTime.UtcNow)) { - Name = "NewOrderCount", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.NewOrderCount"), + Name = "LastForumVisitDays", + DisplayName = T("Admin.Rules.FilterDescriptor.LastForumVisit"), + RuleType = RuleType.NullableInt + }, + new FilterDescriptor(x => DbFunctions.DiffDays(x.CreatedOnUtc, DateTime.UtcNow)) + { + Name = "CreatedDays", + DisplayName = T("Admin.Rules.FilterDescriptor.CreatedDays"), + RuleType = RuleType.NullableInt + }, + new FilterDescriptor(x => DbFunctions.DiffDays(x.BirthDate, DateTime.UtcNow)) + { + Name = "BirthDateDays", + DisplayName = T("Admin.Rules.FilterDescriptor.BirthDate"), + RuleType = RuleType.NullableInt + }, + new TargetGroupFilterDescriptor(_ruleFactory, this) + { + Name = "RuleSet", + DisplayName = T("Admin.Rules.FilterDescriptor.RuleSet"), RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0] + Operators = new[] { RuleOperator.IsEqualTo, RuleOperator.IsNotEqualTo }, + SelectList = new RemoteRuleValueSelectList("TargetGroup") }, + new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.StoreId) { Name = "OrderInStore", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.OrderInStore"), + DisplayName = T("Admin.Rules.FilterDescriptor.OrderInStore"), + GroupKey = "Admin.Orders", RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], SelectList = new LocalRuleValueSelectList(stores) { Multiple = true } }, + new FilterDescriptor(x => x.Orders.Count(o => !o.Deleted && (o.OrderStatusId == 10 || o.OrderStatusId == 20))) + { + Name = "NewOrderCount", + DisplayName = T("Admin.Rules.FilterDescriptor.NewOrderCount"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Int + }, + new FilterDescriptor(x => x.Orders.Count(o => !o.Deleted && o.OrderStatusId == 30)) + { + Name = "CompletedOrderCount", + DisplayName = T("Admin.Rules.FilterDescriptor.CompletedOrderCount"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Int + }, + new FilterDescriptor(x => x.Orders.Count(o => !o.Deleted && o.OrderStatusId == 40)) + { + Name = "CancelledOrderCount", + DisplayName = T("Admin.Rules.FilterDescriptor.CancelledOrderCount"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Int + }, new FilterDescriptor(x => DbFunctions.DiffDays(x.Orders.Where(o => !o.Deleted).Max(o => o.CreatedOnUtc), DateTime.UtcNow)) { Name = "LastOrderDateDays", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.LastOrderDateDays"), - RuleType = RuleType.NullableInt, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.LastOrderDateDays"), + GroupKey = "Admin.Orders", + RuleType = RuleType.NullableInt }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.OrderTotal) { Name = "OrderTotal", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.OrderTotal"), - RuleType = RuleType.Money, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.OrderTotal"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Money }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.OrderSubtotalInclTax) { Name = "OrderSubtotalInclTax", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.OrderSubtotalInclTax"), - RuleType = RuleType.Money, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.OrderSubtotalInclTax"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Money }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.OrderSubtotalExclTax) { Name = "OrderSubtotalExclTax", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.OrderSubtotalExclTax"), - RuleType = RuleType.Money, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.OrderSubtotalExclTax"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Money + }, + new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.ShippingStatusId) + { + Name = "ShippingStatus", + DisplayName = T("Admin.Rules.FilterDescriptor.ShippingStatus"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Int, + SelectList = new LocalRuleValueSelectList(shippingStatus) + }, + new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.PaymentStatusId) + { + Name = "PaymentStatus", + DisplayName = T("Admin.Rules.FilterDescriptor.PaymentStatus"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Int, + SelectList = new LocalRuleValueSelectList(paymentStatus) }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted).SelectMany(o => o.OrderItems), oi => oi.ProductId) { Name = "HasPurchasedProduct", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.HasPurchasedProduct"), + DisplayName = T("Admin.Rules.FilterDescriptor.HasPurchasedProduct"), + GroupKey = "Admin.Orders", RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Product") { Multiple = true } }, new AllFilterDescriptor(x => x.Orders.Where(o => !o.Deleted).SelectMany(o => o.OrderItems), oi => oi.ProductId) { Name = "HasPurchasedAllProducts", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.HasPurchasedAllProducts"), + DisplayName = T("Admin.Rules.FilterDescriptor.HasPurchasedAllProducts"), + GroupKey = "Admin.Orders", RuleType = RuleType.IntArray, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Product") { Multiple = true } }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.AcceptThirdPartyEmailHandOver) { Name = "AcceptThirdPartyEmailHandOver", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.AcceptThirdPartyEmailHandOver"), - RuleType = RuleType.Boolean, - Constraints = new IRuleConstraint[0] + DisplayName = T("Admin.Rules.FilterDescriptor.AcceptThirdPartyEmailHandOver"), + GroupKey = "Admin.Orders", + RuleType = RuleType.Boolean }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.CustomerCurrencyCode) { Name = "CurrencyCode", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Currency"), + DisplayName = T("Admin.Rules.FilterDescriptor.Currency"), + GroupKey = "Admin.Orders", RuleType = RuleType.String, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Currency") }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.CustomerLanguageId) { Name = "OrderLanguage", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.Language"), + DisplayName = T("Admin.Rules.FilterDescriptor.Language"), + GroupKey = "Admin.Orders", RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("Language") }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.PaymentMethodSystemName) { Name = "PaymentMethod", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.PaidBy"), + DisplayName = T("Admin.Rules.FilterDescriptor.PaidBy"), + GroupKey = "Admin.Orders", RuleType = RuleType.String, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("PaymentMethod") }, - new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.PaymentStatusId) - { - Name = "PaymentStatus", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.PaymentStatus"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], - SelectList = new LocalRuleValueSelectList(paymentStatus) - }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.ShippingMethod) { Name = "ShippingMethod", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ShippingMethod"), + DisplayName = T("Admin.Rules.FilterDescriptor.ShippingMethod"), + GroupKey = "Admin.Orders", RuleType = RuleType.String, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("ShippingMethod") }, new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.ShippingRateComputationMethodSystemName) { Name = "ShippingRateComputationMethod", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ShippingRateComputationMethod"), + DisplayName = T("Admin.Rules.FilterDescriptor.ShippingRateComputationMethod"), + GroupKey = "Admin.Orders", RuleType = RuleType.String, - Constraints = new IRuleConstraint[0], SelectList = new RemoteRuleValueSelectList("ShippingRateComputationMethod") }, - new AnyFilterDescriptor(x => x.Orders.Where(o => !o.Deleted), o => o.ShippingStatusId) - { - Name = "ShippingStatus", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.ShippingStatus"), - RuleType = RuleType.Int, - Constraints = new IRuleConstraint[0], - SelectList = new LocalRuleValueSelectList(shippingStatus) - }, - new TargetGroupFilterDescriptor(_ruleFactory, this) - { - Name = "RuleSet", - DisplayName = _services.Localization.GetResource("Admin.Rules.FilterDescriptor.RuleSet"), - RuleType = RuleType.Int, - Operators = new[] { RuleOperator.IsEqualTo, RuleOperator.IsNotEqualTo }, - Constraints = new IRuleConstraint[0], - SelectList = new RemoteRuleValueSelectList("TargetGroup") - } }; descriptors diff --git a/src/Libraries/SmartStore.Services/Customers/Tasks/DeleteGuestsTask.cs b/src/Libraries/SmartStore.Services/Customers/Tasks/DeleteGuestsTask.cs index 1238009e0d..881138aaa0 100644 --- a/src/Libraries/SmartStore.Services/Customers/Tasks/DeleteGuestsTask.cs +++ b/src/Libraries/SmartStore.Services/Customers/Tasks/DeleteGuestsTask.cs @@ -1,26 +1,32 @@ using System; -using System.Threading.Tasks; +using SmartStore.Core.Domain.Common; using SmartStore.Services.Tasks; namespace SmartStore.Services.Customers { /// - /// Represents a task for deleting guest customers + /// Represents a task for deleting guest customers. /// - public partial class DeleteGuestsTask : AsyncTask + public partial class DeleteGuestsTask : ITask { private readonly ICustomerService _customerService; + private readonly CommonSettings _commonSettings; - public DeleteGuestsTask(ICustomerService customerService) + public DeleteGuestsTask( + ICustomerService customerService, + CommonSettings commonSettings) { _customerService = customerService; + _commonSettings = commonSettings; } - public override async Task ExecuteAsync(TaskExecutionContext ctx) - { - //60*24 = 1 day - var olderThanMinutes = 1440; // TODO: move to settings - await _customerService.DeleteGuestCustomersAsync(null, DateTime.UtcNow.AddMinutes(-olderThanMinutes), true); - } - } + public void Execute(TaskExecutionContext ctx) + { + Guard.NotNegative(_commonSettings.MaxGuestsRegistrationAgeInMinutes, nameof(_commonSettings.MaxGuestsRegistrationAgeInMinutes)); + + var registrationTo = DateTime.UtcNow.AddMinutes(-_commonSettings.MaxGuestsRegistrationAgeInMinutes); + + _customerService.DeleteGuestCustomers(null, registrationTo, true); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs index 1680cf2d55..7ac7deff8f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs @@ -4,293 +4,275 @@ namespace SmartStore.Services.DataExchange.Csv { - /// - /// Specifies the action to take when a parsing error has occured. - /// - public enum ParseErrorAction - { - /// - /// Raises the event. - /// - RaiseEvent = 0, - - /// - /// Tries to advance to next line. - /// - AdvanceToNextLine = 1, - - /// - /// Throws an exception. - /// - ThrowException = 2, - } - - /// - /// Specifies the action to take when a field is missing. - /// - public enum MissingFieldAction - { - /// - /// Treat as a parsing error. - /// - ParseError = 0, - - /// - /// Replaces by an empty value. - /// - ReplaceByEmpty = 1, - - /// - /// Replaces by a null value (). - /// - ReplaceByNull = 2, - } - - public class CsvConfiguration - { - private char _delimiter; - private char _escape; - private char _quote; - private string _quoteString; - private char[] _quotableChars; - - public CsvConfiguration() - { - _escape = '"'; - _delimiter = ';'; - _quote = '"'; - _quoteString = new String(new char[] { _escape, _quote }); - - Comment = '#'; - HasHeaders = true; - SkipEmptyLines = true; - SupportsMultiline = true; - DefaultHeaderName = "Column"; - - BuildQuotableChars(); + /// + /// Specifies the action to take when a parsing error has occured. + /// + public enum ParseErrorAction + { + /// + /// Raises the event. + /// + RaiseEvent = 0, + + /// + /// Tries to advance to next line. + /// + AdvanceToNextLine = 1, + + /// + /// Throws an exception. + /// + ThrowException = 2, + } + + /// + /// Specifies the action to take when a field is missing. + /// + public enum MissingFieldAction + { + /// + /// Treat as a parsing error. + /// + ParseError = 0, + + /// + /// Replaces by an empty value. + /// + ReplaceByEmpty = 1, + + /// + /// Replaces by a null value (). + /// + ReplaceByNull = 2, + } + + public class CsvConfiguration + { + private char _delimiter; + private char _escape; + private char _quote; + private string _quoteString; + private char[] _quotableChars; + + public CsvConfiguration() + { + _escape = '"'; + _delimiter = ';'; + _quote = '"'; + _quoteString = new String(new char[] { _escape, _quote }); + + Comment = '#'; + HasHeaders = true; + SkipEmptyLines = true; + SupportsMultiline = true; + DefaultHeaderName = "Column"; + + BuildQuotableChars(); } - private void BuildQuotableChars() - { - _quotableChars = new char[] { '\r', '\n', _delimiter, _quote }; + private void BuildQuotableChars() + { + _quotableChars = new char[] { '\r', '\n', _delimiter, _quote }; } - internal char[] QuotableChars - { - get { return _quotableChars; } - } - - /// - /// Gets an Excel friendly configuration where the result can be directly edited by Excel - /// - public static CsvConfiguration ExcelFriendlyConfiguration - { - get - { - return new CsvConfiguration - { - Delimiter = ';', - Quote = '"', - Escape = '"', - SupportsMultiline = false - }; - } - } - - /// - /// Gets an array with preset characters - /// - public static char[] PresetCharacters - { - get { return new char[] { '\n', '\r', '\0' }; } - } - - /// - /// Gets the comment character indicating that a line is commented out (default: #). - /// - /// The comment character indicating that a line is commented out. - public char Comment - { - get; - set; - } - - /// - /// Gets the escape character letting insert quotation characters inside a quoted field (default: "). - /// - /// The escape character letting insert quotation characters inside a quoted field. - public char Escape - { - get { return _escape; } - set - { - if (value == _escape) - return; - - if (PresetCharacters.Contains(value)) - { - throw new SmartException("'{0}' is not a valid escape char.".FormatInvariant(value)); - } - if (value == _delimiter) - { - throw new SmartException("Escape and delimiter chars cannot be equal in CSV files."); - } - - _escape = value; - _quoteString = new String(new char[] { _escape, _quote }); - } - } - - /// - /// Gets the delimiter character separating each field (default: ;). - /// - /// The delimiter character separating each field. - public char Delimiter - { - get { return _delimiter; } - set - { - if (value == _delimiter) - return; - - if (PresetCharacters.Contains(value)) - { - throw new SmartException("'{0}' is not a valid delimiter char.".FormatInvariant(value)); - } - if (value == _quote) - { - throw new SmartException("Quote and delimiter chars cannot be equal in CSV files."); - } - - _delimiter = value; - BuildQuotableChars(); - - } - } - - /// - /// Gets the quotation character wrapping every field (default: "). - /// - /// The quotation character wrapping every field. - public char Quote - { - get { return _quote; } - set - { - if (value == _quote) - return; - - if (PresetCharacters.Contains(value)) - { - throw new SmartException("'{0}' is not a valid quote char.".FormatInvariant(value)); - } - if (value == _delimiter) - { - throw new SmartException("Quote and delimiter chars cannot be equal in CSV files."); - } - - _quote = value; - _quoteString = new String(new char[] { _escape, _quote }); - BuildQuotableChars(); - } - } - - /// - /// Gets the concatenation of escape and quote char - /// - [JsonIgnore] - public string QuoteString - { - get - { - return _quoteString; - } - } - - public bool QuoteAllFields - { - get; - set; - } - - /// - /// Indicates if field names are located on the first non commented line (default: true). - /// - /// if field names are located on the first non commented line, otherwise, . - public bool HasHeaders - { - get; - set; - } - - /// - /// Indicates if spaces at the start and end of a field are trimmed (default: false). - /// - /// if spaces at the start and end of a field are trimmed, otherwise, . - public bool TrimValues - { - get; - set; - } - - /// - /// Contains the value which denotes a DbNull-value. - /// - public string NullValue - { - get; - set; - } - - /// - /// Gets or sets the default action to take when a parsing error has occured. - /// - /// The default action to take when a parsing error has occured. - public ParseErrorAction DefaultParseErrorAction - { - get; - set; - } - - /// - /// Gets or sets the action to take when a field is missing. - /// - /// The action to take when a field is missing. - public MissingFieldAction MissingFieldAction - { - get; - set; - } - - /// - /// Gets or sets a value indicating if the reader supports multiline fields (default: true). - /// - /// A value indicating if the reader supports multiline field. - public bool SupportsMultiline - { - get; - set; - } - - /// - /// Gets or sets a value indicating if the reader will skip empty lines (default: true). - /// - /// A value indicating if the reader will skip empty lines. - public bool SkipEmptyLines - { - get; - set; - } - - /// - /// Gets or sets the default header name when it is an empty string or only whitespaces (default: Column). - /// The header index will be appended to the specified name. - /// - /// The default header name when it is an empty string or only whitespaces. - public string DefaultHeaderName - { - get; - set; - } - } + internal char[] QuotableChars => _quotableChars; + + /// + /// Gets an Excel friendly configuration where the result can be directly edited by Excel + /// + public static CsvConfiguration ExcelFriendlyConfiguration => new CsvConfiguration + { + Delimiter = ';', + Quote = '"', + Escape = '"', + SupportsMultiline = false + }; + + /// + /// Gets an array with preset characters + /// + public static char[] PresetCharacters => new char[] { '\n', '\r', '\0' }; + + /// + /// Gets the comment character indicating that a line is commented out (default: #). + /// + /// The comment character indicating that a line is commented out. + public char Comment + { + get; + set; + } + + /// + /// Gets the escape character letting insert quotation characters inside a quoted field (default: "). + /// + /// The escape character letting insert quotation characters inside a quoted field. + public char Escape + { + get => _escape; + set + { + if (value == _escape) + return; + + if (PresetCharacters.Contains(value)) + { + throw new SmartException("'{0}' is not a valid escape char.".FormatInvariant(value)); + } + if (value == _delimiter) + { + throw new SmartException("Escape and delimiter chars cannot be equal in CSV files."); + } + + _escape = value; + _quoteString = new String(new char[] { _escape, _quote }); + } + } + + /// + /// Gets the delimiter character separating each field (default: ;). + /// + /// The delimiter character separating each field. + public char Delimiter + { + get => _delimiter; + set + { + if (value == _delimiter) + return; + + if (PresetCharacters.Contains(value)) + { + throw new SmartException("'{0}' is not a valid delimiter char.".FormatInvariant(value)); + } + if (value == _quote) + { + throw new SmartException("Quote and delimiter chars cannot be equal in CSV files."); + } + + _delimiter = value; + BuildQuotableChars(); + + } + } + + /// + /// Gets the quotation character wrapping every field (default: "). + /// + /// The quotation character wrapping every field. + public char Quote + { + get => _quote; + set + { + if (value == _quote) + return; + + if (PresetCharacters.Contains(value)) + { + throw new SmartException("'{0}' is not a valid quote char.".FormatInvariant(value)); + } + if (value == _delimiter) + { + throw new SmartException("Quote and delimiter chars cannot be equal in CSV files."); + } + + _quote = value; + _quoteString = new String(new char[] { _escape, _quote }); + BuildQuotableChars(); + } + } + + /// + /// Gets the concatenation of escape and quote char + /// + [JsonIgnore] + public string QuoteString => _quoteString; + + public bool QuoteAllFields + { + get; + set; + } + + /// + /// Indicates if field names are located on the first non commented line (default: true). + /// + /// if field names are located on the first non commented line, otherwise, . + public bool HasHeaders + { + get; + set; + } + + /// + /// Indicates if spaces at the start and end of a field are trimmed (default: false). + /// + /// if spaces at the start and end of a field are trimmed, otherwise, . + public bool TrimValues + { + get; + set; + } + + /// + /// Contains the value which denotes a DbNull-value. + /// + public string NullValue + { + get; + set; + } + + /// + /// Gets or sets the default action to take when a parsing error has occured. + /// + /// The default action to take when a parsing error has occured. + public ParseErrorAction DefaultParseErrorAction + { + get; + set; + } + + /// + /// Gets or sets the action to take when a field is missing. + /// + /// The action to take when a field is missing. + public MissingFieldAction MissingFieldAction + { + get; + set; + } + + /// + /// Gets or sets a value indicating if the reader supports multiline fields (default: true). + /// + /// A value indicating if the reader supports multiline field. + public bool SupportsMultiline + { + get; + set; + } + + /// + /// Gets or sets a value indicating if the reader will skip empty lines (default: true). + /// + /// A value indicating if the reader will skip empty lines. + public bool SkipEmptyLines + { + get; + set; + } + + /// + /// Gets or sets the default header name when it is an empty string or only whitespaces (default: Column). + /// The header index will be appended to the specified name. + /// + /// The default header name when it is an empty string or only whitespaces. + public string DefaultHeaderName + { + get; + set; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs index 9ae40e871b..1d99e78f11 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs @@ -5,61 +5,61 @@ namespace SmartStore.Services.DataExchange.Csv { - public class CsvConfigurationConverter : DefaultTypeConverter - { - public CsvConfigurationConverter() - : base(typeof(object)) - { - } + public class CsvConfigurationConverter : DefaultTypeConverter + { + public CsvConfigurationConverter() + : base(typeof(object)) + { + } - public override bool CanConvertFrom(Type type) - { - return type == typeof(string); - } + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } - public override bool CanConvertTo(Type type) - { - return type == typeof(string); - } + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is string) - { - return JsonConvert.DeserializeObject((string)value); - } + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + return JsonConvert.DeserializeObject((string)value); + } - return base.ConvertFrom(culture, value); - } + return base.ConvertFrom(culture, value); + } - public T ConvertFrom(string value) - { - if (value.HasValue()) - return (T)ConvertFrom(CultureInfo.InvariantCulture, value); + public T ConvertFrom(string value) + { + if (value.HasValue()) + return (T)ConvertFrom(CultureInfo.InvariantCulture, value); - return default(T); - } + return default(T); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (to == typeof(string)) - { - if (value is CsvConfiguration) - { - return JsonConvert.SerializeObject(value); - } - else - { - return string.Empty; - } - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value is CsvConfiguration) + { + return JsonConvert.SerializeObject(value); + } + else + { + return string.Empty; + } + } - return base.ConvertTo(culture, format, value, to); - } + return base.ConvertTo(culture, format, value, to); + } - public string ConvertTo(object value) - { - return (string)ConvertTo(CultureInfo.InvariantCulture, null, value, typeof(string)); - } - } + public string ConvertTo(object value) + { + return (string)ConvertTo(CultureInfo.InvariantCulture, null, value, typeof(string)); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs index 0a172ff592..664fb186fe 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs @@ -15,374 +15,314 @@ namespace SmartStore.Services.DataExchange.Csv { - /// - /// Represents a reader that provides fast, non-cached, forward-only access to CSV data. - /// - public class CsvDataReader : DisposableObject, IDataReader, IEnumerable - { - private readonly CsvReader _csv; - private readonly IDataReader _reader; - - /// - /// Initializes a new instance of the CsvDataReader class. - /// - /// A pointing to the CSV file. - public CsvDataReader(TextReader reader) - : this(reader, new CsvConfiguration()) - { - } - - /// - /// Initializes a new instance of the CsvDataReader class. - /// - /// A pointing to the CSV file. - public CsvDataReader(TextReader reader, CsvConfiguration configuration) - { - Guard.NotNull(reader, nameof(reader)); - Guard.NotNull(configuration, nameof(configuration)); - - this.Configuration = configuration; - - _csv = new CsvReader( - reader, - configuration.HasHeaders, - configuration.Delimiter, - configuration.Quote, - configuration.Escape, - configuration.Comment, - configuration.TrimValues ? ValueTrimmingOptions.All : ValueTrimmingOptions.None, - 4096, - configuration.NullValue) - { - SupportsMultiline = configuration.SupportsMultiline, - SkipEmptyLines = configuration.SkipEmptyLines, - DefaultHeaderName = configuration.DefaultHeaderName, - DefaultParseErrorAction = (LumenWorks.Framework.IO.Csv.ParseErrorAction)((int)configuration.DefaultParseErrorAction), - MissingFieldAction = (LumenWorks.Framework.IO.Csv.MissingFieldAction)((int)configuration.MissingFieldAction) - }; - - _reader = _csv; - } - - public CsvConfiguration Configuration - { - get; - private set; - } - - #region Public wrapper members - - /// - /// Gets the field headers. - /// - /// The field headers or an empty array if headers are not supported. - /// - /// The instance has been disposed of. - /// - public string[] GetFieldHeaders() - { - return _csv.GetFieldHeaders(); - } - - public bool EndOfStream - { - get - { - return _csv.EndOfStream; - } - } - - /// - /// Gets the current row index in the CSV file (0-based). - /// - /// The current row index in the CSV file. - public long CurrentRowIndex - { - get - { - return _csv.CurrentRecordIndex; - } - } - - public void CopyCurrentRowTo(string[] array) - { - _csv.CopyCurrentRecordTo(array); - } - - /// - /// Gets the current row's raw CSV data. - /// - /// The current raw CSV data. - public string GetRawData() - { - return _csv.GetCurrentRawData(); - } - - /// - /// Gets the field with the specified name. must be . - /// - /// - /// The field with the specified name. - /// - /// - /// is or an empty string. - /// - /// - /// The CSV does not have headers ( property is ). - /// - /// - /// not found. - /// - /// - /// The CSV appears to be corrupt at the current position. - /// - /// - /// The instance has been disposed of. - /// - public string this[string field] - { - get - { - return _csv[field]; - } - } - - /// - /// Gets the field at the specified index. - /// - /// The field at the specified index. - /// - /// must be included in [0, [. - /// - /// - /// No record read yet. Call ReadLine() first. - /// - /// - /// The CSV appears to be corrupt at the current position. - /// - /// - /// The instance has been disposed of. - /// - public virtual string this[int index] - { - get - { - return _csv[index]; - } - } - - #endregion - - #region IDataReader (implicit) - - public int FieldCount - { - get - { - return _reader.FieldCount; - } - } - - public bool Read() - { - return _csv.ReadNextRecord(); - } - - public int GetOrdinal(string name) - { - return _reader.GetOrdinal(name); - } - - #endregion - - #region IDataReader (explicit) - - object IDataRecord.this[string name] - { - get - { - return _reader[name]; - } - } - - object IDataRecord.this[int i] - { - get - { - return _reader[i]; - } - } - - int IDataReader.Depth - { - get - { - return _reader.Depth; - } - } - - bool IDataReader.IsClosed - { - get - { - return _reader.IsClosed; - } - } - - int IDataReader.RecordsAffected - { - get - { - return _reader.RecordsAffected; - } - } - - void IDataReader.Close() - { - _reader.Close(); - } - - bool IDataRecord.GetBoolean(int i) - { - return _reader.GetBoolean(i); - } - - byte IDataRecord.GetByte(int i) - { - return _reader.GetByte(i); - } - - long IDataRecord.GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) - { - return _reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); - } - - char IDataRecord.GetChar(int i) - { - return _reader.GetChar(i); - } - - long IDataRecord.GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length) - { - return _reader.GetChars(i, fieldOffset, buffer, bufferoffset, length); - } - - IDataReader IDataRecord.GetData(int i) - { - return _reader.GetData(i); - } - - string IDataRecord.GetDataTypeName(int i) - { - return _reader.GetDataTypeName(i); - } - - DateTime IDataRecord.GetDateTime(int i) - { - return _reader.GetDateTime(i); - } - - decimal IDataRecord.GetDecimal(int i) - { - return _reader.GetDecimal(i); - } - - double IDataRecord.GetDouble(int i) - { - return _reader.GetDouble(i); - } - - Type IDataRecord.GetFieldType(int i) - { - return _reader.GetFieldType(i); - } - - float IDataRecord.GetFloat(int i) - { - return _reader.GetFloat(i); - } - - Guid IDataRecord.GetGuid(int i) - { - return _reader.GetGuid(i); - } - - short IDataRecord.GetInt16(int i) - { - return _reader.GetInt16(i); - } - - int IDataRecord.GetInt32(int i) - { - return _reader.GetInt32(i); - } - - long IDataRecord.GetInt64(int i) - { - return _reader.GetInt64(i); - } - - string IDataRecord.GetName(int i) - { - return _reader.GetName(i); - } - - DataTable IDataReader.GetSchemaTable() - { - return _reader.GetSchemaTable(); - } - - string IDataRecord.GetString(int i) - { - return _reader.GetString(i); - } - - object IDataRecord.GetValue(int i) - { - return _reader.GetGuid(i); - } - - int IDataRecord.GetValues(object[] values) - { - return _reader.GetValues(values); - } - - bool IDataRecord.IsDBNull(int i) - { - return _reader.IsDBNull(i); - } - - bool IDataReader.NextResult() - { - return _reader.NextResult(); - } - - #endregion - - #region IEnumerable - - IEnumerator IEnumerable.GetEnumerator() - { - return _csv.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _csv.GetEnumerator(); - } - - #endregion - - #region Disposable - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - _csv.Dispose(); - } - } - - #endregion - } + /// + /// Represents a reader that provides fast, non-cached, forward-only access to CSV data. + /// + public class CsvDataReader : DisposableObject, IDataReader, IEnumerable + { + private readonly CsvReader _csv; + private readonly IDataReader _reader; + + /// + /// Initializes a new instance of the CsvDataReader class. + /// + /// A pointing to the CSV file. + public CsvDataReader(TextReader reader) + : this(reader, new CsvConfiguration()) + { + } + + /// + /// Initializes a new instance of the CsvDataReader class. + /// + /// A pointing to the CSV file. + public CsvDataReader(TextReader reader, CsvConfiguration configuration) + { + Guard.NotNull(reader, nameof(reader)); + Guard.NotNull(configuration, nameof(configuration)); + + this.Configuration = configuration; + + _csv = new CsvReader( + reader, + configuration.HasHeaders, + configuration.Delimiter, + configuration.Quote, + configuration.Escape, + configuration.Comment, + configuration.TrimValues ? ValueTrimmingOptions.All : ValueTrimmingOptions.None, + 4096, + configuration.NullValue) + { + SupportsMultiline = configuration.SupportsMultiline, + SkipEmptyLines = configuration.SkipEmptyLines, + DefaultHeaderName = configuration.DefaultHeaderName, + DefaultParseErrorAction = (LumenWorks.Framework.IO.Csv.ParseErrorAction)((int)configuration.DefaultParseErrorAction), + MissingFieldAction = (LumenWorks.Framework.IO.Csv.MissingFieldAction)((int)configuration.MissingFieldAction) + }; + + _reader = _csv; + } + + public CsvConfiguration Configuration + { + get; + private set; + } + + #region Public wrapper members + + /// + /// Gets the field headers. + /// + /// The field headers or an empty array if headers are not supported. + /// + /// The instance has been disposed of. + /// + public string[] GetFieldHeaders() + { + return _csv.GetFieldHeaders(); + } + + public bool EndOfStream => _csv.EndOfStream; + + /// + /// Gets the current row index in the CSV file (0-based). + /// + /// The current row index in the CSV file. + public long CurrentRowIndex => _csv.CurrentRecordIndex; + + public void CopyCurrentRowTo(string[] array) + { + _csv.CopyCurrentRecordTo(array); + } + + /// + /// Gets the current row's raw CSV data. + /// + /// The current raw CSV data. + public string GetRawData() + { + return _csv.GetCurrentRawData(); + } + + /// + /// Gets the field with the specified name. must be . + /// + /// + /// The field with the specified name. + /// + /// + /// is or an empty string. + /// + /// + /// The CSV does not have headers ( property is ). + /// + /// + /// not found. + /// + /// + /// The CSV appears to be corrupt at the current position. + /// + /// + /// The instance has been disposed of. + /// + public string this[string field] => _csv[field]; + + /// + /// Gets the field at the specified index. + /// + /// The field at the specified index. + /// + /// must be included in [0, [. + /// + /// + /// No record read yet. Call ReadLine() first. + /// + /// + /// The CSV appears to be corrupt at the current position. + /// + /// + /// The instance has been disposed of. + /// + public virtual string this[int index] => _csv[index]; + + #endregion + + #region IDataReader (implicit) + + public int FieldCount => _reader.FieldCount; + + public bool Read() + { + return _csv.ReadNextRecord(); + } + + public int GetOrdinal(string name) + { + return _reader.GetOrdinal(name); + } + + #endregion + + #region IDataReader (explicit) + + object IDataRecord.this[string name] => _reader[name]; + + object IDataRecord.this[int i] => _reader[i]; + + int IDataReader.Depth => _reader.Depth; + + bool IDataReader.IsClosed => _reader.IsClosed; + + int IDataReader.RecordsAffected => _reader.RecordsAffected; + + void IDataReader.Close() + { + _reader.Close(); + } + + bool IDataRecord.GetBoolean(int i) + { + return _reader.GetBoolean(i); + } + + byte IDataRecord.GetByte(int i) + { + return _reader.GetByte(i); + } + + long IDataRecord.GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) + { + return _reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); + } + + char IDataRecord.GetChar(int i) + { + return _reader.GetChar(i); + } + + long IDataRecord.GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length) + { + return _reader.GetChars(i, fieldOffset, buffer, bufferoffset, length); + } + + IDataReader IDataRecord.GetData(int i) + { + return _reader.GetData(i); + } + + string IDataRecord.GetDataTypeName(int i) + { + return _reader.GetDataTypeName(i); + } + + DateTime IDataRecord.GetDateTime(int i) + { + return _reader.GetDateTime(i); + } + + decimal IDataRecord.GetDecimal(int i) + { + return _reader.GetDecimal(i); + } + + double IDataRecord.GetDouble(int i) + { + return _reader.GetDouble(i); + } + + Type IDataRecord.GetFieldType(int i) + { + return _reader.GetFieldType(i); + } + + float IDataRecord.GetFloat(int i) + { + return _reader.GetFloat(i); + } + + Guid IDataRecord.GetGuid(int i) + { + return _reader.GetGuid(i); + } + + short IDataRecord.GetInt16(int i) + { + return _reader.GetInt16(i); + } + + int IDataRecord.GetInt32(int i) + { + return _reader.GetInt32(i); + } + + long IDataRecord.GetInt64(int i) + { + return _reader.GetInt64(i); + } + + string IDataRecord.GetName(int i) + { + return _reader.GetName(i); + } + + DataTable IDataReader.GetSchemaTable() + { + return _reader.GetSchemaTable(); + } + + string IDataRecord.GetString(int i) + { + return _reader.GetString(i); + } + + object IDataRecord.GetValue(int i) + { + return _reader.GetGuid(i); + } + + int IDataRecord.GetValues(object[] values) + { + return _reader.GetValues(values); + } + + bool IDataRecord.IsDBNull(int i) + { + return _reader.IsDBNull(i); + } + + bool IDataReader.NextResult() + { + return _reader.NextResult(); + } + + #endregion + + #region IEnumerable + + IEnumerator IEnumerable.GetEnumerator() + { + return _csv.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _csv.GetEnumerator(); + } + + #endregion + + #region Disposable + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + _csv.Dispose(); + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs index f2f360556b..d4cdc66359 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs @@ -14,181 +14,181 @@ namespace SmartStore.Services.DataExchange.Csv { - /// - /// SImple utility class used to write CSV files. - /// - public class CsvWriter : DisposableObject - { - private TextWriter _writer; - private readonly IList _currentRow = new List(); - private int? _fieldCount; - - public CsvWriter(TextWriter writer) - : this(writer, new CsvConfiguration()) - { - } - - public CsvWriter(TextWriter writer, CsvConfiguration configuration) - { - Guard.NotNull(writer, nameof(writer)); - Guard.NotNull(configuration, nameof(configuration)); - - _writer = writer; + /// + /// SImple utility class used to write CSV files. + /// + public class CsvWriter : DisposableObject + { + private TextWriter _writer; + private readonly IList _currentRow = new List(); + private int? _fieldCount; + + public CsvWriter(TextWriter writer) + : this(writer, new CsvConfiguration()) + { + } + + public CsvWriter(TextWriter writer, CsvConfiguration configuration) + { + Guard.NotNull(writer, nameof(writer)); + Guard.NotNull(configuration, nameof(configuration)); + + _writer = writer; this.Configuration = configuration; - } - - public CsvConfiguration Configuration - { - get; - private set; - } - - /// - /// Writes a sequence of fields to the CSV file. The fields - /// may get quotes added to it. - /// When all fields are written for a row, - /// must be called - /// to complete writing of the current row. - /// - /// The fields to write. - public virtual void WriteFields(IEnumerable fields) - { - Guard.NotNull(fields, nameof(fields)); - fields.Each(x => WriteField(x)); - } - - /// - /// Writes a sequence of fields to the CSV file. This will - /// ignore any need to quote and ignore the - /// - /// and just quote based on the shouldQuote - /// parameter. - /// When all fields are written for a row, - /// must be called - /// to complete writing of the current row. - /// - /// The fields to write. - /// True to quote the fields, otherwise false. - public virtual void WriteFields(IEnumerable fields, bool shouldQuote) - { - Guard.NotNull(fields, nameof(fields)); - fields.Each(x => WriteField(x, shouldQuote)); - } - - /// - /// Writes the field to the CSV file. The field - /// may get quotes added to it. - /// When all fields are written for a row, - /// must be called - /// to complete writing of the current row. - /// - /// The field to write. - public virtual void WriteField(string field) - { - var shouldQuote = Configuration.QuoteAllFields; - - if (!string.IsNullOrEmpty(field)) - { - if (Configuration.TrimValues) - { - field = field.Trim(); - } - - if (!Configuration.SupportsMultiline) - { - field = field.Replace('\r', ' ').Replace('\n', ' '); - } - - if (shouldQuote - || field[0] == ' ' - || field[field.Length - 1] == ' ' - || field.IndexOfAny(Configuration.QuotableChars) > -1) - { - shouldQuote = true; - } - } - - WriteField(field, shouldQuote); - } - - /// - /// Writes the field to the CSV file. This will - /// ignore any need to quote and ignore the - /// - /// and just quote based on the shouldQuote - /// parameter. - /// When all fields are written for a row, - /// must be called - /// to complete writing of the current row. - /// - /// The field to write. - /// True to quote the field, otherwise false. - public virtual void WriteField(string field, bool shouldQuote) - { - // All quotes must be escaped. - if (shouldQuote && !string.IsNullOrEmpty(field)) - { - field = field.Replace(Configuration.Quote.ToString(), Configuration.QuoteString); - } - - if (shouldQuote) - { - field = Configuration.Quote + (field ?? string.Empty) + Configuration.Quote; - } - - _currentRow.Add(field ?? string.Empty); - } - - /// - /// Ends writing of the current row - /// and starts a new row. - /// - public virtual void NextRow() - { - WriteRow(_currentRow.ToArray()); - _currentRow.Clear(); - } - - public string CurrentRawValue() - { - var row = string.Join(Configuration.Delimiter.ToString(), _currentRow); - return row; - } - - private void WriteRow(string[] fields) - { - CheckDisposed(); - - if (fields.Length == 0) - { - throw new SmartException("Cannot write an empty row to the CSV file."); - } - - if (!_fieldCount.HasValue) - { - _fieldCount = fields.Length; - } - - if (_fieldCount.Value != fields.Length) - { - throw new SmartException("The field count of the current row does not match the previous row's field count."); - } - - var row = string.Join(Configuration.Delimiter.ToString(), fields); - _writer.WriteLine(row); - } - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - if (_writer != null) - { - _writer.Dispose(); - _writer = null; - } - } - } - - } + } + + public CsvConfiguration Configuration + { + get; + private set; + } + + /// + /// Writes a sequence of fields to the CSV file. The fields + /// may get quotes added to it. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The fields to write. + public virtual void WriteFields(IEnumerable fields) + { + Guard.NotNull(fields, nameof(fields)); + fields.Each(x => WriteField(x)); + } + + /// + /// Writes a sequence of fields to the CSV file. This will + /// ignore any need to quote and ignore the + /// + /// and just quote based on the shouldQuote + /// parameter. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The fields to write. + /// True to quote the fields, otherwise false. + public virtual void WriteFields(IEnumerable fields, bool shouldQuote) + { + Guard.NotNull(fields, nameof(fields)); + fields.Each(x => WriteField(x, shouldQuote)); + } + + /// + /// Writes the field to the CSV file. The field + /// may get quotes added to it. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The field to write. + public virtual void WriteField(string field) + { + var shouldQuote = Configuration.QuoteAllFields; + + if (!string.IsNullOrEmpty(field)) + { + if (Configuration.TrimValues) + { + field = field.Trim(); + } + + if (!Configuration.SupportsMultiline) + { + field = field.Replace('\r', ' ').Replace('\n', ' '); + } + + if (shouldQuote + || field[0] == ' ' + || field[field.Length - 1] == ' ' + || field.IndexOfAny(Configuration.QuotableChars) > -1) + { + shouldQuote = true; + } + } + + WriteField(field, shouldQuote); + } + + /// + /// Writes the field to the CSV file. This will + /// ignore any need to quote and ignore the + /// + /// and just quote based on the shouldQuote + /// parameter. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The field to write. + /// True to quote the field, otherwise false. + public virtual void WriteField(string field, bool shouldQuote) + { + // All quotes must be escaped. + if (shouldQuote && !string.IsNullOrEmpty(field)) + { + field = field.Replace(Configuration.Quote.ToString(), Configuration.QuoteString); + } + + if (shouldQuote) + { + field = Configuration.Quote + (field ?? string.Empty) + Configuration.Quote; + } + + _currentRow.Add(field ?? string.Empty); + } + + /// + /// Ends writing of the current row + /// and starts a new row. + /// + public virtual void NextRow() + { + WriteRow(_currentRow.ToArray()); + _currentRow.Clear(); + } + + public string CurrentRawValue() + { + var row = string.Join(Configuration.Delimiter.ToString(), _currentRow); + return row; + } + + private void WriteRow(string[] fields) + { + CheckDisposed(); + + if (fields.Length == 0) + { + throw new SmartException("Cannot write an empty row to the CSV file."); + } + + if (!_fieldCount.HasValue) + { + _fieldCount = fields.Length; + } + + if (_fieldCount.Value != fields.Length) + { + throw new SmartException("The field count of the current row does not match the previous row's field count."); + } + + var row = string.Join(Configuration.Delimiter.ToString(), fields); + _writer.WriteLine(row); + } + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + if (_writer != null) + { + _writer.Dispose(); + _writer = null; + } + } + } + + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs index 709c6deff9..9031e7792d 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs @@ -10,368 +10,347 @@ namespace SmartStore.Services.DataExchange.Excel { - public class ExcelDataReader : DisposableObject, IDataReader - { - private ExcelPackage _package; - private ExcelWorksheet _sheet; - private int _totalRows; - private int _totalColumns; - - private bool _initialized; - private string[] _columns; - private Dictionary _columnIndexes; - private int _currentRowIndex; - private bool _eof; - - private readonly object _lock = new object(); - - public ExcelDataReader(Stream source, bool hasHeaders) - { - Guard.NotNull(source, nameof(source)); - - _package = new ExcelPackage(source); - - // get the first worksheet in the workbook - _sheet = _package.Workbook.Worksheets.FirstOrDefault(); - if (_sheet == null) - { - throw Error.InvalidOperation("The excel package does not contain any worksheet."); - } - - if (_sheet.Dimension == null) - { - throw Error.InvalidOperation("The excel worksheet does not contain any data."); - } - - HasHeaders = hasHeaders; - DefaultHeaderName = "Column"; - - _totalColumns = _sheet.Dimension.End.Column; - _totalRows = _sheet.Dimension.End.Row - (hasHeaders ? 1 : 0); - - _currentRowIndex = -1; - } - - #region Configuration - - public bool HasHeaders - { - get; - private set; - } - - public string DefaultHeaderName - { - get; - set; - } - - #endregion - - #region public members - - public bool MoveToStart() - { - return MoveTo(0); - } - - public bool MoveToEnd() - { - return MoveTo(_totalRows - 1); - } - - public bool MoveTo(int row) - { - EnsureInitialize(); - ValidateDataReader(validateInitialized: false); - - if (row < 0 || row >= _totalRows) - return false; - - _currentRowIndex = row; - _eof = false; - - return true; - } - - public bool EndOfStream - { - get { return _eof; } - } - - public int TotalRows - { - get - { - EnsureInitialize(); - return _totalRows; - } - } - - public IReadOnlyCollection GetColumnHeaders() - { - EnsureInitialize(); - return _columns.AsReadOnly(); - } - - public int CurrentRowIndex - { - get - { - return _currentRowIndex; - } - } - - public string GetFormatted(int i) - { - ValidateDataReader(); - return _sheet.Cells[ExcelRowIndex(_currentRowIndex), i + 1].Text; - } - - public int GetColumnIndex(string name) - { - Guard.NotEmpty(name, nameof(name)); - - EnsureInitialize(); - - int index; - - if (_columnIndexes != null && _columnIndexes.TryGetValue(name, out index)) - return index; - else - return -1; - } - - #endregion - - #region IDataReader - - bool IDataReader.NextResult() - { - ValidateDataReader(validateInitialized: false); - return false; - } - - public bool Read() - { - ValidateDataReader(validateInitialized: false); - return ReadNextRow(false); - } - - int IDataReader.Depth - { - get - { - ValidateDataReader(validateInitialized: false); - return 0; - } - } - - bool IDataReader.IsClosed - { - get - { - return _eof; - } - } - - int IDataReader.RecordsAffected - { - get - { - return -1; - } - } - - void IDataReader.Close() - { - Dispose(); - } - - - public int FieldCount - { - get - { - EnsureInitialize(); - return _totalColumns; - } - } - - public object this[string name] - { - get - { - int index = GetColumnIndex(name); - - if (index < 0) - Error.Argument("name", "'{0}' column header not found.".FormatInvariant(name)); - - return this[index]; - } - } - - public object this[int i] - { - get - { - ValidateDataReader(); - // Excel indexes start from 1 - return _sheet.GetValue(ExcelRowIndex(_currentRowIndex), i + 1); - } - } - - public bool GetBoolean(int i) - { - object value = this[i]; - - int result; - if (Int32.TryParse(value.ToString(), out result)) - return (result != 0); - else - return Boolean.Parse(value.ToString()); - } - - public byte GetByte(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) - { - ValidateDataReader(); - return CopyFieldToArray(i, fieldOffset, buffer, bufferoffset, length); - } - - public char GetChar(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public long GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length) - { - ValidateDataReader(); - return CopyFieldToArray(i, fieldOffset, buffer, bufferoffset, length); - } - - IDataReader IDataRecord.GetData(int i) - { - ValidateDataReader(); - - if (i == 0) - return this; - else - return null; - } - - string IDataRecord.GetDataTypeName(int i) - { - return this[i].GetType().FullName; - } - - public DateTime GetDateTime(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public decimal GetDecimal(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public double GetDouble(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public Type GetFieldType(int i) - { - return this[i].GetType(); - } - - public float GetFloat(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public Guid GetGuid(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public short GetInt16(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public int GetInt32(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public long GetInt64(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public string GetName(int i) - { - EnsureInitialize(); - ValidateDataReader(validateInitialized: false); - - if (i < 0 || i >= _columns.Length) - { - throw new ArgumentOutOfRangeException("i", i, - "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_columns.Length, i)); - } - - return _columns[i]; - } - - public int GetOrdinal(string name) - { - return GetColumnIndex(name); - } - - DataTable IDataReader.GetSchemaTable() - { - EnsureInitialize(); - ValidateDataReader(validateInitialized: false); - - var schema = new DataTable("SchemaTable") - { - Locale = CultureInfo.InvariantCulture, - MinimumCapacity = _columns.Length - }; + public class ExcelDataReader : DisposableObject, IDataReader + { + private ExcelPackage _package; + private ExcelWorksheet _sheet; + private int _totalRows; + private int _totalColumns; + + private bool _initialized; + private string[] _columns; + private Dictionary _columnIndexes; + private int _currentRowIndex; + private bool _eof; + + private readonly object _lock = new object(); + + public ExcelDataReader(Stream source, bool hasHeaders) + { + Guard.NotNull(source, nameof(source)); + + _package = new ExcelPackage(source); + + // get the first worksheet in the workbook + _sheet = _package.Workbook.Worksheets.FirstOrDefault(); + if (_sheet == null) + { + throw Error.InvalidOperation("The excel package does not contain any worksheet."); + } + + if (_sheet.Dimension == null) + { + throw Error.InvalidOperation("The excel worksheet does not contain any data."); + } + + HasHeaders = hasHeaders; + DefaultHeaderName = "Column"; + + _totalColumns = _sheet.Dimension.End.Column; + _totalRows = _sheet.Dimension.End.Row - (hasHeaders ? 1 : 0); + + _currentRowIndex = -1; + } + + #region Configuration + + public bool HasHeaders + { + get; + private set; + } + + public string DefaultHeaderName + { + get; + set; + } + + #endregion - schema.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.BaseTableName, typeof(string)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.ColumnName, typeof(string)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.DataType, typeof(object)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.IsAliased, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.IsExpression, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.IsKey, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.IsLong, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.IsUnique, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.NumericScale, typeof(short)).ReadOnly = true; - schema.Columns.Add(SchemaTableColumn.ProviderType, typeof(int)).ReadOnly = true; - - schema.Columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)).ReadOnly = true; - schema.Columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)).ReadOnly = true; - schema.Columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)).ReadOnly = true; - schema.Columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)).ReadOnly = true; - - // null marks columns that will change for each row - object[] schemaRow = - { - true, // 00- AllowDBNull + #region public members + + public bool MoveToStart() + { + return MoveTo(0); + } + + public bool MoveToEnd() + { + return MoveTo(_totalRows - 1); + } + + public bool MoveTo(int row) + { + EnsureInitialize(); + ValidateDataReader(validateInitialized: false); + + if (row < 0 || row >= _totalRows) + return false; + + _currentRowIndex = row; + _eof = false; + + return true; + } + + public bool EndOfStream => _eof; + + public int TotalRows + { + get + { + EnsureInitialize(); + return _totalRows; + } + } + + public IReadOnlyCollection GetColumnHeaders() + { + EnsureInitialize(); + return _columns.AsReadOnly(); + } + + public int CurrentRowIndex => _currentRowIndex; + + public string GetFormatted(int i) + { + ValidateDataReader(); + return _sheet.Cells[ExcelRowIndex(_currentRowIndex), i + 1].Text; + } + + public int GetColumnIndex(string name) + { + Guard.NotEmpty(name, nameof(name)); + + EnsureInitialize(); + + int index; + + if (_columnIndexes != null && _columnIndexes.TryGetValue(name, out index)) + return index; + else + return -1; + } + + #endregion + + #region IDataReader + + bool IDataReader.NextResult() + { + ValidateDataReader(validateInitialized: false); + return false; + } + + public bool Read() + { + ValidateDataReader(validateInitialized: false); + return ReadNextRow(false); + } + + int IDataReader.Depth + { + get + { + ValidateDataReader(validateInitialized: false); + return 0; + } + } + + bool IDataReader.IsClosed => _eof; + + int IDataReader.RecordsAffected => -1; + + void IDataReader.Close() + { + Dispose(); + } + + + public int FieldCount + { + get + { + EnsureInitialize(); + return _totalColumns; + } + } + + public object this[string name] + { + get + { + int index = GetColumnIndex(name); + + if (index < 0) + Error.Argument("name", "'{0}' column header not found.".FormatInvariant(name)); + + return this[index]; + } + } + + public object this[int i] + { + get + { + ValidateDataReader(); + // Excel indexes start from 1 + return _sheet.GetValue(ExcelRowIndex(_currentRowIndex), i + 1); + } + } + + public bool GetBoolean(int i) + { + object value = this[i]; + + int result; + if (Int32.TryParse(value.ToString(), out result)) + return (result != 0); + else + return Boolean.Parse(value.ToString()); + } + + public byte GetByte(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) + { + ValidateDataReader(); + return CopyFieldToArray(i, fieldOffset, buffer, bufferoffset, length); + } + + public char GetChar(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public long GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length) + { + ValidateDataReader(); + return CopyFieldToArray(i, fieldOffset, buffer, bufferoffset, length); + } + + IDataReader IDataRecord.GetData(int i) + { + ValidateDataReader(); + + if (i == 0) + return this; + else + return null; + } + + string IDataRecord.GetDataTypeName(int i) + { + return this[i].GetType().FullName; + } + + public DateTime GetDateTime(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public decimal GetDecimal(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public double GetDouble(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public Type GetFieldType(int i) + { + return this[i].GetType(); + } + + public float GetFloat(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public Guid GetGuid(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public short GetInt16(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public int GetInt32(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public long GetInt64(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public string GetName(int i) + { + EnsureInitialize(); + ValidateDataReader(validateInitialized: false); + + if (i < 0 || i >= _columns.Length) + { + throw new ArgumentOutOfRangeException("i", i, + "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_columns.Length, i)); + } + + return _columns[i]; + } + + public int GetOrdinal(string name) + { + return GetColumnIndex(name); + } + + DataTable IDataReader.GetSchemaTable() + { + EnsureInitialize(); + ValidateDataReader(validateInitialized: false); + + var schema = new DataTable("SchemaTable") + { + Locale = CultureInfo.InvariantCulture, + MinimumCapacity = _columns.Length + }; + + schema.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.BaseTableName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ColumnName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.DataType, typeof(object)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsAliased, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsExpression, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsKey, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsLong, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsUnique, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.NumericScale, typeof(short)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ProviderType, typeof(int)).ReadOnly = true; + + schema.Columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)).ReadOnly = true; + + // null marks columns that will change for each row + object[] schemaRow = + { + true, // 00- AllowDBNull null, // 01- BaseColumnName string.Empty, // 02- BaseSchemaName string.Empty, // 03- BaseTableName @@ -395,218 +374,218 @@ DataTable IDataReader.GetSchemaTable() false // 21- IsRowVersion }; - int r = ExcelRowIndex(0); - - for (int i = 0; i < _columns.Length; i++) - { - schemaRow[1] = _columns[i]; // Base column name - schemaRow[4] = _columns[i]; // Column name - schemaRow[5] = i; // Column ordinal - - // get data type from 1st row only - var firstValue = _sheet.Cells[r, i + 1].Value; - schemaRow[7] = firstValue != null ? firstValue.GetType() : typeof(string); - - schema.Rows.Add(schemaRow); - } - - return schema; - } - - public string GetString(int i) - { - return this[i].Convert(CultureInfo.CurrentCulture); - } - - public object GetValue(int i) - { - ValidateDataReader(); - return ((IDataRecord)this).IsDBNull(i) ? DBNull.Value : this[i]; - } - - int IDataRecord.GetValues(object[] values) - { - var record = (IDataRecord)this; - - for (int i = 0; i < _totalColumns; i++) - { - values[i] = record.GetValue(i); - } - - return _totalColumns; - } - - bool IDataRecord.IsDBNull(int i) - { - return this[i] == null; - } - - #endregion - - #region Helper - - /// - /// Reads the next record. - /// - /// - /// Indicates if the reader will proceed to the next record after having read headers. - /// if it stops after having read headers; otherwise, . - /// - /// if a record has been successfully reads; otherwise, . - /// - /// The instance has been disposed of. - /// - protected virtual bool ReadNextRow(bool onlyReadHeaders) - { - if (_eof) - return false; - - CheckDisposed(); - - if (!_initialized) - { - _columns = new string[_totalColumns]; - _columnIndexes = new Dictionary(_totalColumns, StringComparer.OrdinalIgnoreCase); - - for (int i = 1; i <= _totalColumns; i++) - { - // Excel indexes start from 1 - string name = null; - if (HasHeaders) - { - name = _sheet.GetValue(1, i).NullEmpty(); - } - - name = name ?? (DefaultHeaderName ?? "Column") + i; - - _columns[i - 1] = name; - _columnIndexes[name] = i - 1; - } - - if (_columns.Select(x => x.ToLower()).Distinct().ToArray().Length != _columns.Length) - { - _columns = null; - _columnIndexes = null; - throw Error.InvalidOperation("The first row cannot contain duplicate column names."); - } - - _initialized = true; - - if (!onlyReadHeaders) - { - return ReadNextRow(false); - } - } - else - { - _currentRowIndex++; - if (_currentRowIndex >= _totalRows) - { - _eof = true; - return false; - } - } - - return true; - } - - private int ExcelRowIndex(int i) - { - // Excel indexes start from 1 - return i + (HasHeaders ? 2 : 1); - } - - /// - /// Ensures that the reader is initialized. - /// - private void EnsureInitialize() - { - if (!_initialized) - { - ReadNextRow(true); - } - - Debug.Assert(_columns != null); - Debug.Assert(_columns.Length > 0 || (_columns.Length == 0 && _columnIndexes == null)); - } - - private void ValidateDataReader(bool validateInitialized = true, bool validateNotClosed = true) - { - if (validateInitialized && (!_initialized || _currentRowIndex < 0)) - throw new InvalidOperationException("No current record. Call Read() to initialize the reader."); - - if (validateNotClosed && IsDisposed) - throw new InvalidOperationException("This operation is invalid when the reader is closed."); - } - - private long CopyFieldToArray(int column, long columnOffset, Array destinationArray, int destinationOffset, int length) - { - EnsureInitialize(); - - if (column < 0 || column >= _totalColumns) - { - throw new ArgumentOutOfRangeException("column", column, - "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_totalColumns, column)); - } - - if (columnOffset < 0 || columnOffset >= int.MaxValue) - throw new ArgumentOutOfRangeException("fieldOffset"); - - // Array.Copy(...) will do the remaining argument checks - - if (length == 0) - return 0; - - string value = this[column].ToString(); - - if (value == null) - value = string.Empty; - - if (destinationArray.GetType() == typeof(char[])) - Array.Copy(value.ToCharArray((int)columnOffset, length), 0, destinationArray, destinationOffset, length); - else - { - char[] chars = value.ToCharArray((int)columnOffset, length); - byte[] source = new byte[chars.Length]; - ; - - for (int i = 0; i < chars.Length; i++) - source[i] = Convert.ToByte(chars[i]); - - Array.Copy(source, 0, destinationArray, destinationOffset, length); - } - - return length; - } - - #endregion - - #region IDisposable Support - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - try - { - _sheet = null; - if (_package != null) - { - lock (_lock) - { - if (_package != null) - { - _package.Dispose(); - _package = null; - _eof = true; - } - } - } - } - catch { } - } - } - - #endregion - } + int r = ExcelRowIndex(0); + + for (int i = 0; i < _columns.Length; i++) + { + schemaRow[1] = _columns[i]; // Base column name + schemaRow[4] = _columns[i]; // Column name + schemaRow[5] = i; // Column ordinal + + // get data type from 1st row only + var firstValue = _sheet.Cells[r, i + 1].Value; + schemaRow[7] = firstValue != null ? firstValue.GetType() : typeof(string); + + schema.Rows.Add(schemaRow); + } + + return schema; + } + + public string GetString(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public object GetValue(int i) + { + ValidateDataReader(); + return ((IDataRecord)this).IsDBNull(i) ? DBNull.Value : this[i]; + } + + int IDataRecord.GetValues(object[] values) + { + var record = (IDataRecord)this; + + for (int i = 0; i < _totalColumns; i++) + { + values[i] = record.GetValue(i); + } + + return _totalColumns; + } + + bool IDataRecord.IsDBNull(int i) + { + return this[i] == null; + } + + #endregion + + #region Helper + + /// + /// Reads the next record. + /// + /// + /// Indicates if the reader will proceed to the next record after having read headers. + /// if it stops after having read headers; otherwise, . + /// + /// if a record has been successfully reads; otherwise, . + /// + /// The instance has been disposed of. + /// + protected virtual bool ReadNextRow(bool onlyReadHeaders) + { + if (_eof) + return false; + + CheckDisposed(); + + if (!_initialized) + { + _columns = new string[_totalColumns]; + _columnIndexes = new Dictionary(_totalColumns, StringComparer.OrdinalIgnoreCase); + + for (int i = 1; i <= _totalColumns; i++) + { + // Excel indexes start from 1 + string name = null; + if (HasHeaders) + { + name = _sheet.GetValue(1, i).NullEmpty(); + } + + name = name ?? (DefaultHeaderName ?? "Column") + i; + + _columns[i - 1] = name; + _columnIndexes[name] = i - 1; + } + + if (_columns.Select(x => x.ToLower()).Distinct().ToArray().Length != _columns.Length) + { + _columns = null; + _columnIndexes = null; + throw Error.InvalidOperation("The first row cannot contain duplicate column names."); + } + + _initialized = true; + + if (!onlyReadHeaders) + { + return ReadNextRow(false); + } + } + else + { + _currentRowIndex++; + if (_currentRowIndex >= _totalRows) + { + _eof = true; + return false; + } + } + + return true; + } + + private int ExcelRowIndex(int i) + { + // Excel indexes start from 1 + return i + (HasHeaders ? 2 : 1); + } + + /// + /// Ensures that the reader is initialized. + /// + private void EnsureInitialize() + { + if (!_initialized) + { + ReadNextRow(true); + } + + Debug.Assert(_columns != null); + Debug.Assert(_columns.Length > 0 || (_columns.Length == 0 && _columnIndexes == null)); + } + + private void ValidateDataReader(bool validateInitialized = true, bool validateNotClosed = true) + { + if (validateInitialized && (!_initialized || _currentRowIndex < 0)) + throw new InvalidOperationException("No current record. Call Read() to initialize the reader."); + + if (validateNotClosed && IsDisposed) + throw new InvalidOperationException("This operation is invalid when the reader is closed."); + } + + private long CopyFieldToArray(int column, long columnOffset, Array destinationArray, int destinationOffset, int length) + { + EnsureInitialize(); + + if (column < 0 || column >= _totalColumns) + { + throw new ArgumentOutOfRangeException("column", column, + "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_totalColumns, column)); + } + + if (columnOffset < 0 || columnOffset >= int.MaxValue) + throw new ArgumentOutOfRangeException("fieldOffset"); + + // Array.Copy(...) will do the remaining argument checks + + if (length == 0) + return 0; + + string value = this[column].ToString(); + + if (value == null) + value = string.Empty; + + if (destinationArray.GetType() == typeof(char[])) + Array.Copy(value.ToCharArray((int)columnOffset, length), 0, destinationArray, destinationOffset, length); + else + { + char[] chars = value.ToCharArray((int)columnOffset, length); + byte[] source = new byte[chars.Length]; + ; + + for (int i = 0; i < chars.Length; i++) + source[i] = Convert.ToByte(chars[i]); + + Array.Copy(source, 0, destinationArray, destinationOffset, length); + } + + return length; + } + + #endregion + + #region IDisposable Support + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + try + { + _sheet = null; + if (_package != null) + { + lock (_lock) + { + if (_package != null) + { + _package.Dispose(); + _package = null; + _eof = true; + } + } + } + } + catch { } + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs index 57c85801ab..80d2c7b437 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs @@ -6,62 +6,59 @@ namespace SmartStore.Services.DataExchange.Export { [Serializable] - public class DataExportResult - { - public DataExportResult() - { - Files = new List(); - } + public class DataExportResult + { + public DataExportResult() + { + Files = new List(); + } - /// - /// Whether the export succeeded - /// - public bool Succeeded - { - get { return LastError.IsEmpty(); } - } + /// + /// Whether the export succeeded + /// + public bool Succeeded => LastError.IsEmpty(); - /// - /// Last error - /// - [XmlIgnore] - public string LastError { get; set; } + /// + /// Last error + /// + [XmlIgnore] + public string LastError { get; set; } - /// - /// Files created by last export - /// - public List Files { get; set; } + /// + /// Files created by last export + /// + public List Files { get; set; } - /// - /// The path of the folder with the export files - /// - [XmlIgnore] - public string FileFolder { get; set; } + /// + /// The path of the folder with the export files + /// + [XmlIgnore] + public string FileFolder { get; set; } - [Serializable] - public class ExportFileInfo - { - /// - /// Store identifier, can be 0. - /// - public int StoreId { get; set; } + [Serializable] + public class ExportFileInfo + { + /// + /// Store identifier, can be 0. + /// + public int StoreId { get; set; } - /// - /// Name of file. - /// - public string FileName { get; set; } + /// + /// Name of file. + /// + public string FileName { get; set; } - /// - /// Short optional text that describes the content of the file. - /// - public string Label { get; set; } + /// + /// Short optional text that describes the content of the file. + /// + public string Label { get; set; } /// /// The related entity type. /// public RelatedEntityType? RelatedType { get; set; } } - } + } public class DataExportPreviewResult diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs index 0d935149e8..08225564bd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs @@ -4,55 +4,55 @@ namespace SmartStore.Services.DataExchange.Export { - // Note, namespace persisted in ScheduleTask.Type! - public partial class DataExportTask : ITask - { - private readonly IDataExporter _exporter; - private readonly IExportProfileService _exportProfileService; - - public DataExportTask( - IDataExporter exporter, - IExportProfileService exportProfileService) - { - _exporter = exporter; - _exportProfileService = exportProfileService; + // Note, namespace persisted in ScheduleTask.Type! + public partial class DataExportTask : ITask + { + private readonly IDataExporter _exporter; + private readonly IExportProfileService _exportProfileService; + + public DataExportTask( + IDataExporter exporter, + IExportProfileService exportProfileService) + { + _exporter = exporter; + _exportProfileService = exportProfileService; } - public Localizer T { get; set; } - - public void Execute(TaskExecutionContext ctx) - { - var profileId = ctx.ScheduleTaskHistory.ScheduleTask.Alias.ToInt(); - var profile = _exportProfileService.GetExportProfileById(profileId); - - // Load provider. - var provider = _exportProfileService.LoadProvider(profile.ProviderSystemName); - if (provider == null) - throw new SmartException(T("Admin.Common.ProviderNotLoaded", profile.ProviderSystemName.NaIfEmpty())); - - // Build export request. - var request = new DataExportRequest(profile, provider); - - request.ProgressValueSetter = delegate (int val, int max, string msg) - { - ctx.SetProgress(val, max, msg, true); - }; - - if (ctx.Parameters.ContainsKey("SelectedIds")) - { - request.EntitiesToExport = ctx.Parameters["SelectedIds"] - .SplitSafe(",") - .Select(x => x.ToInt()) - .ToList(); - } - - if (ctx.Parameters.ContainsKey("ActionOrigin")) - { - request.ActionOrigin = ctx.Parameters["ActionOrigin"]; - } - - // Process! - _exporter.Export(request, ctx.CancellationToken); - } - } + public Localizer T { get; set; } + + public void Execute(TaskExecutionContext ctx) + { + var profileId = ctx.ScheduleTaskHistory.ScheduleTask.Alias.ToInt(); + var profile = _exportProfileService.GetExportProfileById(profileId); + + // Load provider. + var provider = _exportProfileService.LoadProvider(profile.ProviderSystemName); + if (provider == null) + throw new SmartException(T("Admin.Common.ProviderNotLoaded", profile.ProviderSystemName.NaIfEmpty())); + + // Build export request. + var request = new DataExportRequest(profile, provider); + + request.ProgressValueSetter = delegate (int val, int max, string msg) + { + ctx.SetProgress(val, max, msg, true); + }; + + if (ctx.Parameters.ContainsKey("SelectedIds")) + { + request.EntitiesToExport = ctx.Parameters["SelectedIds"] + .SplitSafe(",") + .Select(x => x.ToInt()) + .ToList(); + } + + if (ctx.Parameters.ContainsKey("ActionOrigin")) + { + request.ActionOrigin = ctx.Parameters["ActionOrigin"]; + } + + // Process! + _exporter.Export(request, ctx.CancellationToken); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 2833879e15..076b161bf9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -49,154 +49,154 @@ namespace SmartStore.Services.DataExchange.Export { public partial class DataExporter : IDataExporter - { - #region Dependencies - - private readonly ICommonServices _services; - private readonly IDbContext _dbContext; - private readonly HttpContextBase _httpContext; - private readonly Lazy _priceFormatter; - private readonly Lazy _exportProfileService; + { + #region Dependencies + + private readonly ICommonServices _services; + private readonly IDbContext _dbContext; + private readonly HttpContextBase _httpContext; + private readonly Lazy _priceFormatter; + private readonly Lazy _exportProfileService; private readonly Lazy _localizedEntityService; - private readonly Lazy _languageService; + private readonly Lazy _languageService; private readonly Lazy _urlRecordService; private readonly Lazy _mediaService; - private readonly Lazy _priceCalculationService; - private readonly Lazy _currencyService; - private readonly Lazy _taxService; - private readonly Lazy _categoryService; - private readonly Lazy _productAttributeParser; - private readonly Lazy _productAttributeService; + private readonly Lazy _priceCalculationService; + private readonly Lazy _currencyService; + private readonly Lazy _taxService; + private readonly Lazy _categoryService; + private readonly Lazy _productAttributeParser; + private readonly Lazy _productAttributeService; private readonly Lazy _specificationAttributeService; private readonly Lazy _productTemplateService; - private readonly Lazy _categoryTemplateService; - private readonly Lazy _productService; - private readonly Lazy _orderService; - private readonly Lazy _manufacturerService; - private readonly ICustomerService _customerService; - private readonly Lazy _addressService; - private readonly Lazy _countryService; + private readonly Lazy _categoryTemplateService; + private readonly Lazy _productService; + private readonly Lazy _orderService; + private readonly Lazy _manufacturerService; + private readonly ICustomerService _customerService; + private readonly Lazy _addressService; + private readonly Lazy _countryService; private readonly Lazy _shipmentService; - private readonly Lazy _genericAttributeService; - private readonly Lazy _emailAccountService; - private readonly Lazy _queuedEmailService; - private readonly Lazy _emailSender; - private readonly Lazy _deliveryTimeService; - private readonly Lazy _quantityUnitService; - private readonly Lazy _catalogSearchService; + private readonly Lazy _genericAttributeService; + private readonly Lazy _emailAccountService; + private readonly Lazy _queuedEmailService; + private readonly Lazy _emailSender; + private readonly Lazy _deliveryTimeService; + private readonly Lazy _quantityUnitService; + private readonly Lazy _catalogSearchService; private readonly Lazy _downloadService; private readonly Lazy _productUrlHelper; - private readonly Lazy>_customerRepository; - private readonly Lazy> _subscriptionRepository; - private readonly Lazy> _orderRepository; - private readonly Lazy> _shoppingCartItemRepository; - - private readonly Lazy _mediaSettings; - private readonly Lazy _contactDataSettings; - private readonly Lazy _customerSettings; - private readonly Lazy _catalogSettings; - private readonly Lazy _localizationSettings; - private readonly Lazy _taxSettings; + private readonly Lazy> _customerRepository; + private readonly Lazy> _subscriptionRepository; + private readonly Lazy> _orderRepository; + private readonly Lazy> _shoppingCartItemRepository; + + private readonly Lazy _mediaSettings; + private readonly Lazy _contactDataSettings; + private readonly Lazy _customerSettings; + private readonly Lazy _catalogSettings; + private readonly Lazy _localizationSettings; + private readonly Lazy _taxSettings; private readonly Lazy _seoSettings; public DataExporter( - ICommonServices services, - IDbContext dbContext, - HttpContextBase httpContext, - Lazy priceFormatter, - Lazy exportProfileService, + ICommonServices services, + IDbContext dbContext, + HttpContextBase httpContext, + Lazy priceFormatter, + Lazy exportProfileService, Lazy localizedEntityService, - Lazy languageService, - Lazy urlRecordService, + Lazy languageService, + Lazy urlRecordService, Lazy mediaService, Lazy priceCalculationService, - Lazy currencyService, - Lazy taxService, - Lazy categoryService, - Lazy productAttributeParser, - Lazy productAttributeService, + Lazy currencyService, + Lazy taxService, + Lazy categoryService, + Lazy productAttributeParser, + Lazy productAttributeService, Lazy specificationAttributeService, Lazy productTemplateService, - Lazy categoryTemplateService, - Lazy productService, - Lazy orderService, - Lazy manufacturerService, - ICustomerService customerService, - Lazy addressService, - Lazy countryService, - Lazy shipmentService, - Lazy genericAttributeService, - Lazy emailAccountService, - Lazy queuedEmailService, + Lazy categoryTemplateService, + Lazy productService, + Lazy orderService, + Lazy manufacturerService, + ICustomerService customerService, + Lazy addressService, + Lazy countryService, + Lazy shipmentService, + Lazy genericAttributeService, + Lazy emailAccountService, + Lazy queuedEmailService, Lazy emailSender, - Lazy deliveryTimeService, - Lazy quantityUnitService, - Lazy catalogSearchService, + Lazy deliveryTimeService, + Lazy quantityUnitService, + Lazy catalogSearchService, Lazy downloadService, Lazy productUrlHelper, - Lazy> customerRepository, - Lazy> subscriptionRepository, - Lazy> orderRepository, - Lazy> shoppingCartItemRepository, - Lazy mediaSettings, - Lazy contactDataSettings, - Lazy customerSettings, - Lazy catalogSettings, - Lazy localizationSettings, - Lazy taxSettings, + Lazy> customerRepository, + Lazy> subscriptionRepository, + Lazy> orderRepository, + Lazy> shoppingCartItemRepository, + Lazy mediaSettings, + Lazy contactDataSettings, + Lazy customerSettings, + Lazy catalogSettings, + Lazy localizationSettings, + Lazy taxSettings, Lazy seoSettings) - { - _services = services; - _dbContext = dbContext; - _httpContext = httpContext; - _priceFormatter = priceFormatter; - _exportProfileService = exportProfileService; - _localizedEntityService = localizedEntityService; - _languageService = languageService; - _urlRecordService = urlRecordService; + { + _services = services; + _dbContext = dbContext; + _httpContext = httpContext; + _priceFormatter = priceFormatter; + _exportProfileService = exportProfileService; + _localizedEntityService = localizedEntityService; + _languageService = languageService; + _urlRecordService = urlRecordService; _mediaService = mediaService; - _priceCalculationService = priceCalculationService; - _currencyService = currencyService; - _taxService = taxService; - _categoryService = categoryService; - _productAttributeParser = productAttributeParser; - _productAttributeService = productAttributeService; + _priceCalculationService = priceCalculationService; + _currencyService = currencyService; + _taxService = taxService; + _categoryService = categoryService; + _productAttributeParser = productAttributeParser; + _productAttributeService = productAttributeService; _specificationAttributeService = specificationAttributeService; - _productTemplateService = productTemplateService; - _categoryTemplateService = categoryTemplateService; - _productService = productService; - _orderService = orderService; - _manufacturerService = manufacturerService; - _customerService = customerService; - _addressService = addressService; - _countryService = countryService; - _shipmentService = shipmentService; - _genericAttributeService = genericAttributeService; - _emailAccountService = emailAccountService; - _queuedEmailService = queuedEmailService; - _emailSender = emailSender; - _deliveryTimeService = deliveryTimeService; - _quantityUnitService = quantityUnitService; - _catalogSearchService = catalogSearchService; + _productTemplateService = productTemplateService; + _categoryTemplateService = categoryTemplateService; + _productService = productService; + _orderService = orderService; + _manufacturerService = manufacturerService; + _customerService = customerService; + _addressService = addressService; + _countryService = countryService; + _shipmentService = shipmentService; + _genericAttributeService = genericAttributeService; + _emailAccountService = emailAccountService; + _queuedEmailService = queuedEmailService; + _emailSender = emailSender; + _deliveryTimeService = deliveryTimeService; + _quantityUnitService = quantityUnitService; + _catalogSearchService = catalogSearchService; _downloadService = downloadService; - _productUrlHelper = productUrlHelper; - - _customerRepository = customerRepository; - _subscriptionRepository = subscriptionRepository; - _orderRepository = orderRepository; - _shoppingCartItemRepository = shoppingCartItemRepository; - - _mediaSettings = mediaSettings; - _contactDataSettings = contactDataSettings; - _customerSettings = customerSettings; - _catalogSettings = catalogSettings; - _localizationSettings = localizationSettings; - _taxSettings = taxSettings; + _productUrlHelper = productUrlHelper; + + _customerRepository = customerRepository; + _subscriptionRepository = subscriptionRepository; + _orderRepository = orderRepository; + _shoppingCartItemRepository = shoppingCartItemRepository; + + _mediaSettings = mediaSettings; + _contactDataSettings = contactDataSettings; + _customerSettings = customerSettings; + _catalogSettings = catalogSettings; + _localizationSettings = localizationSettings; + _taxSettings = taxSettings; _seoSettings = seoSettings; - } + } - public Localizer T { get; set; } = NullLocalizer.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; #endregion @@ -225,11 +225,11 @@ private UrlRecordCollection CreateUrlRecordCollection(string entityName, IEnumer } private void SetProgress(DataExporterContext ctx, int loadedRecords) - { - try - { - if (!ctx.IsPreview && loadedRecords > 0) - { + { + try + { + if (!ctx.IsPreview && loadedRecords > 0) + { var totalRecords = ctx.StatsPerStore.Sum(x => x.Value.TotalRecords); if (ctx.Request.Profile.Limit > 0 && totalRecords > ctx.Request.Profile.Limit) @@ -237,51 +237,51 @@ private void SetProgress(DataExporterContext ctx, int loadedRecords) totalRecords = ctx.Request.Profile.Limit; } - ctx.RecordCount = Math.Min(ctx.RecordCount + loadedRecords, totalRecords); - var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount.ToString("N0"), totalRecords.ToString("N0")); - ctx.Request.ProgressValueSetter.Invoke(ctx.RecordCount, totalRecords, msg); - } - } - catch { } - } - - private void SetProgress(DataExporterContext ctx, string message) - { - try - { - if (!ctx.IsPreview && message.HasValue()) - { - ctx.Request.ProgressValueSetter.Invoke(0, 0, message); - } - } - catch { } - } - - private bool HasPermission(DataExporterContext ctx) - { - if (ctx.Request.HasPermission) - { - return true; - } - - var customer = _services.WorkContext.CurrentCustomer; - - if (customer.SystemName == SystemCustomerNames.BackgroundTask) - { - return true; - } + ctx.RecordCount = Math.Min(ctx.RecordCount + loadedRecords, totalRecords); + var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount.ToString("N0"), totalRecords.ToString("N0")); + ctx.Request.ProgressValueSetter.Invoke(ctx.RecordCount, totalRecords, msg); + } + } + catch { } + } + + private void SetProgress(DataExporterContext ctx, string message) + { + try + { + if (!ctx.IsPreview && message.HasValue()) + { + ctx.Request.ProgressValueSetter.Invoke(0, 0, message); + } + } + catch { } + } + + private bool HasPermission(DataExporterContext ctx) + { + if (ctx.Request.HasPermission) + { + return true; + } + + var customer = _services.WorkContext.CurrentCustomer; + + if (customer.SystemName == SystemCustomerNames.BackgroundTask) + { + return true; + } return _services.Permissions.Authorize(Permissions.Configuration.Export.Execute); - } + } private void DetachAllEntitiesAndClear(DataExporterContext ctx) - { + { try - { + { ctx.AssociatedProductContext?.Clear(); if (ctx.ProductExportContext != null) - { + { _dbContext.DetachEntities(x => { return x is Product || x is Discount || x is ProductVariantAttributeCombination || x is ProductVariantAttribute || x is ProductVariantAttributeValue || x is ProductAttribute || @@ -291,83 +291,89 @@ x is ProductMediaFile || x is ProductTag || x is ProductSpecificationAttribute | }); ctx.ProductExportContext.Clear(); - } - - if (ctx.OrderExportContext != null) - { - _dbContext.DetachEntities(x => - { - return x is Order || x is Address || x is GenericAttribute || x is Customer || - x is OrderItem || x is RewardPointsHistory || x is Shipment || x is ProductVariantAttributeCombination; - }); - - ctx.OrderExportContext.Clear(); - } - - if (ctx.CategoryExportContext != null) - { - _dbContext.DetachEntities(x => - { - return x is Category || x is MediaFile || x is ProductCategory; - }); - - ctx.CategoryExportContext.Clear(); - } - - if (ctx.ManufacturerExportContext != null) - { - _dbContext.DetachEntities(x => - { - return x is Manufacturer || x is MediaFile || x is ProductManufacturer; - }); - - ctx.ManufacturerExportContext.Clear(); - } - - if (ctx.CustomerExportContext != null) - { - _dbContext.DetachEntities(x => - { - return x is Customer || x is GenericAttribute || x is CustomerContent; - }); - - ctx.CustomerExportContext.Clear(); - } - - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.ShoppingCartItem) - { - _dbContext.DetachEntities(x => - { - return x is ShoppingCartItem || x is Customer || x is Product || x is ProductVariantAttributeCombination; - }); - } + } + + if (ctx.OrderExportContext != null) + { + _dbContext.DetachEntities(x => + { + return x is Order || x is Address || x is GenericAttribute || x is Customer || + x is OrderItem || x is RewardPointsHistory || x is Shipment || x is ProductVariantAttributeCombination; + }); + + ctx.OrderExportContext.Clear(); + } + + if (ctx.CategoryExportContext != null) + { + _dbContext.DetachEntities(x => + { + return x is Category || x is MediaFile || x is ProductCategory; + }); + + ctx.CategoryExportContext.Clear(); + } + + if (ctx.ManufacturerExportContext != null) + { + _dbContext.DetachEntities(x => + { + return x is Manufacturer || x is MediaFile || x is ProductManufacturer; + }); + + ctx.ManufacturerExportContext.Clear(); + } + + if (ctx.CustomerExportContext != null) + { + _dbContext.DetachEntities(x => + { + return x is Customer || x is GenericAttribute || x is CustomerContent; + }); + + ctx.CustomerExportContext.Clear(); + } + + switch (ctx.Request.Provider.Value.EntityType) + { + case ExportEntityType.ShoppingCartItem: + _dbContext.DetachEntities(x => + { + return x is ShoppingCartItem || x is Customer || x is Product || x is ProductVariantAttributeCombination; + }); + break; + case ExportEntityType.NewsLetterSubscription: + _dbContext.DetachEntities(x => + { + return x is NewsLetterSubscription || x is Customer; + }); + break; + } } catch (Exception ex) - { - ctx.Log.Warn(ex, "Detaching entities failed."); - } + { + ctx.Log.Warn(ex, "Detaching entities failed."); + } //((DbContext)_dbContext).DumpAttachedEntities(); } - private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) - { + private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) + { var stats = ctx.StatsPerStore[ctx.Store.Id]; var offset = Math.Max(ctx.Request.Profile.Offset, 0); - var limit = Math.Max(ctx.Request.Profile.Limit, 0); - var recordsPerSegment = ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0); + var limit = Math.Max(ctx.Request.Profile.Limit, 0); + var recordsPerSegment = ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0); var totalRecords = offset + stats.TotalRecords; - ctx.LastId = stats.OffsetId; - switch (ctx.Request.Provider.Value.EntityType) - { - case ExportEntityType.Product: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetProducts(ctx), - entities => - { + { + case ExportEntityType.Product: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetProducts(ctx), + entities => + { // Load data behind navigation properties for current queue in one go. ctx.ProductExportContext = CreateProductExportContext(entities, ctx.ContextCustomer, ctx.Store.Id); ctx.AssociatedProductContext = null; @@ -409,25 +415,25 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ctx.TranslationsPerPage[nameof(ProductAttribute)] = CreateTranslationCollection(nameof(ProductAttribute), pa); ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)] = CreateTranslationCollection(nameof(ProductVariantAttributeValue), pvav); }, - entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; - - case ExportEntityType.Order: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetOrders(ctx), - entities => - { - ctx.OrderExportContext = new OrderExportContext(entities, - x => _customerService.GetCustomersByIds(x), - x => _genericAttributeService.Value.GetAttributesForEntity(x, "Customer"), - x => _customerService.GetRewardPointsHistoriesByCustomerIds(x), - x => _addressService.Value.GetAddressByIds(x), - x => _orderService.Value.GetOrderItemsByOrderIds(x), - x => _shipmentService.Value.GetShipmentsByOrderIds(x) - ); + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; + + case ExportEntityType.Order: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetOrders(ctx), + entities => + { + ctx.OrderExportContext = new OrderExportContext(entities, + x => _customerService.GetCustomersByIds(x), + x => _genericAttributeService.Value.GetAttributesForEntity(x, "Customer"), + x => _customerService.GetRewardPointsHistoriesByCustomerIds(x), + x => _addressService.Value.GetAddressByIds(x), + x => _orderService.Value.GetOrderItemsByOrderIds(x), + x => _shipmentService.Value.GetShipmentsByOrderIds(x) + ); ctx.OrderExportContext.OrderItems.LoadAll(); @@ -437,72 +443,72 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ctx.TranslationsPerPage[nameof(Product)] = CreateTranslationCollection(nameof(Product), products); ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), products); }, - entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; - - case ExportEntityType.Manufacturer: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetManufacturers(ctx), - entities => - { - ctx.ManufacturerExportContext = new ManufacturerExportContext(entities, - x => _manufacturerService.Value.GetProductManufacturersByManufacturerIds(x, true), + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; + + case ExportEntityType.Manufacturer: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetManufacturers(ctx), + entities => + { + ctx.ManufacturerExportContext = new ManufacturerExportContext(entities, + x => _manufacturerService.Value.GetProductManufacturersByManufacturerIds(x, true), x => _mediaService.Value.GetFilesByIds(x) ); - }, - entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; - - case ExportEntityType.Category: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetCategories(ctx), - entities => - { - ctx.CategoryExportContext = new CategoryExportContext(entities, - x => _categoryService.Value.GetProductCategoriesByCategoryIds(x), + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; + + case ExportEntityType.Category: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetCategories(ctx), + entities => + { + ctx.CategoryExportContext = new CategoryExportContext(entities, + x => _categoryService.Value.GetProductCategoriesByCategoryIds(x), x => _mediaService.Value.GetFilesByIds(x) - ); - }, - entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; - - case ExportEntityType.Customer: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetCustomers(ctx), - entities => - { - ctx.CustomerExportContext = new CustomerExportContext(entities, - x => _genericAttributeService.Value.GetAttributesForEntity(x, "Customer") - ); - }, - entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; - - case ExportEntityType.NewsLetterSubscription: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetNewsLetterSubscriptions(ctx), - null, - entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; - - case ExportEntityType.ShoppingCartItem: - ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter - ( - () => GetShoppingCartItems(ctx), + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; + + case ExportEntityType.Customer: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetCustomers(ctx), + entities => + { + ctx.CustomerExportContext = new CustomerExportContext(entities, + x => _genericAttributeService.Value.GetAttributesForEntity(x, "Customer") + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; + + case ExportEntityType.NewsLetterSubscription: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetNewsLetterSubscriptions(ctx), + null, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; + + case ExportEntityType.ShoppingCartItem: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + () => GetShoppingCartItems(ctx), entities => { var products = entities.Select(x => x.Product); @@ -510,17 +516,17 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx) ctx.UrlRecordsPerPage[nameof(Product)] = CreateUrlRecordCollection(nameof(Product), products); }, entity => Convert(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalRecords - ); - break; + offset, PageSize, limit, recordsPerSegment, totalRecords + ); + break; - default: - ctx.ExecuteContext.DataSegmenter = null; - break; - } + default: + ctx.ExecuteContext.DataSegmenter = null; + break; + } - return ctx.ExecuteContext.DataSegmenter as IExportDataSegmenterProvider; - } + return ctx.ExecuteContext.DataSegmenter as IExportDataSegmenterProvider; + } private Stream CreateStream(string path) { @@ -582,15 +588,15 @@ private IEnumerable GetDataUnitsForRelatedEntities(DataExporterC return result; } - private bool CallProvider(DataExporterContext ctx, string method, string path) - { - if (method != "Execute" && method != "OnExecuted") - { - throw new SmartException($"Unknown export method {method.NaIfEmpty()}."); - } + private bool CallProvider(DataExporterContext ctx, string method, string path) + { + if (method != "Execute" && method != "OnExecuted") + { + throw new SmartException($"Unknown export method {method.NaIfEmpty()}."); + } - try - { + try + { using (ctx.ExecuteContext.DataStream = new ExportFileStream(CreateStream(ctx.IsFileBasedExport ? path : null))) { if (method == "Execute") @@ -604,13 +610,13 @@ private bool CallProvider(DataExporterContext ctx, string method, string path) } } catch (Exception ex) - { + { ctx.ExecuteContext.Abort = DataExchangeAbortion.Hard; - ctx.Log.ErrorFormat(ex, $"The provider failed at the {method.NaIfEmpty()} method."); - ctx.Result.LastError = ex.ToAllMessages(true); - } - finally - { + ctx.Log.ErrorFormat(ex, $"The provider failed at the {method.NaIfEmpty()} method."); + ctx.Result.LastError = ex.ToAllMessages(true); + } + finally + { ctx.ExecuteContext.DataStream?.Dispose(); ctx.ExecuteContext.DataStream = null; @@ -655,253 +661,260 @@ private bool CallProvider(DataExporterContext ctx, string method, string path) ctx.Log.Info(unitFileInfos.ToString()); } - } - - return ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard; - } - - private bool Deploy(DataExporterContext ctx, string zipPath) - { - var allSucceeded = true; - var deployments = ctx.Request.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled); - - if (deployments.Count() == 0) - return false; - - var context = new ExportDeploymentContext - { - T = T, - Log = ctx.Log, - FolderContent = ctx.FolderContent, - ZipPath = zipPath, - CreateZipArchive = ctx.Request.Profile.CreateZipArchive - }; - - foreach (var deployment in deployments) - { - IFilePublisher publisher = null; - - context.Result = new DataDeploymentResult - { - LastExecutionUtc = DateTime.UtcNow - }; - - try - { - switch (deployment.DeploymentType) - { - case ExportDeploymentType.Email: - publisher = new EmailFilePublisher(_emailAccountService.Value, _queuedEmailService.Value); - break; - case ExportDeploymentType.FileSystem: - publisher = new FileSystemFilePublisher(); - break; - case ExportDeploymentType.Ftp: - publisher = new FtpFilePublisher(); - break; - case ExportDeploymentType.Http: - publisher = new HttpFilePublisher(); - break; - case ExportDeploymentType.PublicFolder: - publisher = new PublicFolderPublisher(); - break; - } - - if (publisher != null) - { - publisher.Publish(context, deployment); - - if (!context.Result.Succeeded) - allSucceeded = false; - } - } - catch (Exception ex) - { - allSucceeded = false; - - if (context.Result != null) - { - context.Result.LastError = ex.ToAllMessages(true); - } - - ctx.Log.Error(ex, $"Deployment \"{deployment.Name}\" of type {deployment.DeploymentType.ToString()} failed."); - } - - deployment.ResultInfo = XmlHelper.Serialize(context.Result); - - _exportProfileService.Value.UpdateExportDeployment(deployment); - } - - return allSucceeded; - } - - private void SendCompletionEmail(DataExporterContext ctx, string zipPath) - { - var emailAccount = _emailAccountService.Value.GetEmailAccountById(ctx.Request.Profile.EmailAccountId); - if (emailAccount == null) - { - return; - } - - var downloadUrl = "{0}Admin/Export/DownloadExportFile/{1}?name=".FormatInvariant(_services.WebHelper.GetStoreLocation(ctx.Store.SslEnabled), ctx.Request.Profile.Id); - var languageId = ctx.Projection.LanguageId ?? 0; - var smtpContext = new SmtpContext(emailAccount); - var message = new EmailMessage(); - - var storeInfo = "{0} ({1})".FormatInvariant(ctx.Store.Name, ctx.Store.Url); - var intro =_services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Body", languageId).FormatInvariant(storeInfo); - var body = new StringBuilder(intro); - - if (ctx.Result.LastError.HasValue()) - { - body.AppendFormat("

{0}

", ctx.Result.LastError); - } - - if (ctx.IsFileBasedExport && File.Exists(zipPath)) - { - var fileName = Path.GetFileName(zipPath); - body.AppendFormat("

{2}

", downloadUrl, HttpUtility.UrlEncode(fileName), fileName); - } - - if (ctx.IsFileBasedExport && ctx.Result.Files.Any()) - { - body.Append("

"); - foreach (var file in ctx.Result.Files) - { - body.AppendFormat("

", downloadUrl, HttpUtility.UrlEncode(file.FileName), file.FileName); - } - body.Append("

"); - } - - message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); - - if (ctx.Request.Profile.CompletedEmailAddresses.HasValue()) - message.To.AddRange(ctx.Request.Profile.CompletedEmailAddresses.SplitSafe(",").Where(x => x.IsEmail()).Select(x => new EmailAddress(x))); - - if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) - message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); - - if (message.To.Count == 0) - message.To.Add(new EmailAddress(emailAccount.Email, emailAccount.DisplayName)); - - message.Subject = _services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Subject", languageId) - .FormatInvariant(ctx.Request.Profile.Name); - - message.Body = body.ToString(); - - _emailSender.Value.SendEmail(smtpContext, message); - - //_queuedEmailService.Value.InsertQueuedEmail(new QueuedEmail - //{ - // From = emailAccount.Email, - // FromName = emailAccount.DisplayName, - // To = message.To.First().Address, - // Subject = message.Subject, - // Body = message.Body, - // CreatedOnUtc = DateTime.UtcNow, - // EmailAccountId = emailAccount.Id, - // SendManually = true - //}); - //_dbContext.SaveChanges(); - } - - #endregion - - #region Getting data - - public virtual ProductExportContext CreateProductExportContext( - IEnumerable products = null, - Customer customer = null, - int? storeId = null, - int? maxPicturesPerProduct = null, - bool includeHidden = true) - { - if (customer == null) - customer = _services.WorkContext.CurrentCustomer; - - if (!storeId.HasValue) - storeId = _services.StoreContext.CurrentStore.Id; - - var context = new ProductExportContext(products, - x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), - x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), - x => _specificationAttributeService.Value.GetProductSpecificationAttributesByProductIds(x), - x => _productService.Value.GetTierPricesByProductIds(x, customer, storeId.GetValueOrDefault()), - x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, includeHidden), - x => _manufacturerService.Value.GetProductManufacturersByProductIds(x, includeHidden), - x => _productService.Value.GetAppliedDiscountsByProductIds(x), - x => _productService.Value.GetBundleItemsByProductIds(x, includeHidden), + } + + return ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard; + } + + private bool Deploy(DataExporterContext ctx, string zipPath) + { + var allSucceeded = true; + var deployments = ctx.Request.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled); + + if (deployments.Count() == 0) + return false; + + var context = new ExportDeploymentContext + { + T = T, + Log = ctx.Log, + FolderContent = ctx.FolderContent, + ZipPath = zipPath, + CreateZipArchive = ctx.Request.Profile.CreateZipArchive + }; + + foreach (var deployment in deployments) + { + IFilePublisher publisher = null; + + context.Result = new DataDeploymentResult + { + LastExecutionUtc = DateTime.UtcNow + }; + + try + { + switch (deployment.DeploymentType) + { + case ExportDeploymentType.Email: + publisher = new EmailFilePublisher(_emailAccountService.Value, _queuedEmailService.Value); + break; + case ExportDeploymentType.FileSystem: + publisher = new FileSystemFilePublisher(); + break; + case ExportDeploymentType.Ftp: + publisher = new FtpFilePublisher(); + break; + case ExportDeploymentType.Http: + publisher = new HttpFilePublisher(); + break; + case ExportDeploymentType.PublicFolder: + publisher = new PublicFolderPublisher(); + break; + } + + if (publisher != null) + { + publisher.Publish(context, deployment); + + if (!context.Result.Succeeded) + allSucceeded = false; + } + } + catch (Exception ex) + { + allSucceeded = false; + + if (context.Result != null) + { + context.Result.LastError = ex.ToAllMessages(true); + } + + ctx.Log.Error(ex, $"Deployment \"{deployment.Name}\" of type {deployment.DeploymentType.ToString()} failed."); + } + + deployment.ResultInfo = XmlHelper.Serialize(context.Result); + + _exportProfileService.Value.UpdateExportDeployment(deployment); + } + + return allSucceeded; + } + + private void SendCompletionEmail(DataExporterContext ctx, string zipPath) + { + var emailAccount = _emailAccountService.Value.GetEmailAccountById(ctx.Request.Profile.EmailAccountId); + if (emailAccount == null) + { + return; + } + + var downloadUrl = "{0}Admin/Export/DownloadExportFile/{1}?name=".FormatInvariant(_services.WebHelper.GetStoreLocation(ctx.Store.SslEnabled), ctx.Request.Profile.Id); + var languageId = ctx.Projection.LanguageId ?? 0; + var smtpContext = new SmtpContext(emailAccount); + var message = new EmailMessage(); + + var storeInfo = "{0} ({1})".FormatInvariant(ctx.Store.Name, ctx.Store.Url); + var intro = _services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Body", languageId).FormatInvariant(storeInfo); + var body = new StringBuilder(intro); + + if (ctx.Result.LastError.HasValue()) + { + body.AppendFormat("

{0}

", ctx.Result.LastError); + } + + if (ctx.IsFileBasedExport && File.Exists(zipPath)) + { + var fileName = Path.GetFileName(zipPath); + body.AppendFormat("

{2}

", downloadUrl, HttpUtility.UrlEncode(fileName), fileName); + } + + if (ctx.IsFileBasedExport && ctx.Result.Files.Any()) + { + body.Append("

"); + foreach (var file in ctx.Result.Files) + { + body.AppendFormat("

", downloadUrl, HttpUtility.UrlEncode(file.FileName), file.FileName); + } + body.Append("

"); + } + + message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + + if (ctx.Request.Profile.CompletedEmailAddresses.HasValue()) + message.To.AddRange(ctx.Request.Profile.CompletedEmailAddresses.SplitSafe(",").Where(x => x.IsEmail()).Select(x => new EmailAddress(x))); + + if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) + message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); + + if (message.To.Count == 0) + message.To.Add(new EmailAddress(emailAccount.Email, emailAccount.DisplayName)); + + message.Subject = _services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Subject", languageId) + .FormatInvariant(ctx.Request.Profile.Name); + + message.Body = body.ToString(); + + _emailSender.Value.SendEmail(smtpContext, message); + + //_queuedEmailService.Value.InsertQueuedEmail(new QueuedEmail + //{ + // From = emailAccount.Email, + // FromName = emailAccount.DisplayName, + // To = message.To.First().Address, + // Subject = message.Subject, + // Body = message.Body, + // CreatedOnUtc = DateTime.UtcNow, + // EmailAccountId = emailAccount.Id, + // SendManually = true + //}); + //_dbContext.SaveChanges(); + } + + #endregion + + #region Getting data + + public virtual ProductExportContext CreateProductExportContext( + IEnumerable products = null, + Customer customer = null, + int? storeId = null, + int? maxPicturesPerProduct = null, + bool includeHidden = true) + { + if (customer == null) + customer = _services.WorkContext.CurrentCustomer; + + if (!storeId.HasValue) + storeId = _services.StoreContext.CurrentStore.Id; + + var context = new ProductExportContext(products, + x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), + x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), + x => _specificationAttributeService.Value.GetProductSpecificationAttributesByProductIds(x), + x => _productService.Value.GetTierPricesByProductIds(x, customer, storeId.GetValueOrDefault()), + x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, includeHidden), + x => _manufacturerService.Value.GetProductManufacturersByProductIds(x, includeHidden), + x => _productService.Value.GetAppliedDiscountsByProductIds(x), + x => _productService.Value.GetBundleItemsByProductIds(x, includeHidden), x => _productService.Value.GetAssociatedProductsByProductIds(x, includeHidden), - x => _productService.Value.GetProductPicturesByProductIds(x, maxPicturesPerProduct), - x => _productService.Value.GetProductTagsByProductIds(x, includeHidden), + x => _productService.Value.GetProductPicturesByProductIds(x, maxPicturesPerProduct), + x => _productService.Value.GetProductTagsByProductIds(x, includeHidden), x => _downloadService.Value.GetDownloadsByEntityIds(x, nameof(Product)) - ); + ); - return context; - } + return context; + } private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, int take) - { - IQueryable query = null; - - if (ctx.Request.ProductQuery == null) - { - var f = ctx.Filter; - var createdFrom = f.CreatedFrom.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(f.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone) : null; - var createdTo = f.CreatedTo.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(f.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone) : null; - - var searchQuery = new CatalogSearchQuery() - .WithCurrency(ctx.ContextCurrency) - .WithLanguage(ctx.ContextLanguage) + { + IQueryable query = null; + + // Skip used for preview (ordinary paging) or initial query (offset option). + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + // Initial query. Apply offset. + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + if (ctx.Request.ProductQuery == null) + { + var f = ctx.Filter; + var createdFrom = f.CreatedFrom.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(f.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone) : null; + var createdTo = f.CreatedTo.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(f.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone) : null; + + var searchQuery = new CatalogSearchQuery() + .WithCurrency(ctx.ContextCurrency) + .WithLanguage(ctx.ContextLanguage) .HasStoreId(ctx.Request.Profile.PerStore ? ctx.Store.Id : f.StoreId) - .PriceBetween(f.PriceMinimum, f.PriceMaximum) - .WithStockQuantity(f.AvailabilityMinimum, f.AvailabilityMaximum) - .CreatedBetween(createdFrom, createdTo); + .PriceBetween(f.PriceMinimum, f.PriceMaximum) + .WithStockQuantity(f.AvailabilityMinimum, f.AvailabilityMaximum) + .CreatedBetween(createdFrom, createdTo); if (f.Visibility.HasValue) searchQuery = searchQuery.WithVisibility(f.Visibility.Value); - if (f.IsPublished.HasValue) - searchQuery = searchQuery.PublishedOnly(f.IsPublished.Value); + if (f.IsPublished.HasValue) + searchQuery = searchQuery.PublishedOnly(f.IsPublished.Value); - if (f.ProductType.HasValue) - searchQuery = searchQuery.IsProductType(f.ProductType.Value); + if (f.ProductType.HasValue) + searchQuery = searchQuery.IsProductType(f.ProductType.Value); - if (f.ProductTagId.HasValue) - searchQuery = searchQuery.WithProductTagIds(f.ProductTagId.Value); + if (f.ProductTagId.HasValue) + searchQuery = searchQuery.WithProductTagIds(f.ProductTagId.Value); - if (f.WithoutManufacturers.HasValue) - searchQuery = searchQuery.HasAnyManufacturer(!f.WithoutManufacturers.Value); - else if (f.ManufacturerId.HasValue) - searchQuery = searchQuery.WithManufacturerIds(f.FeaturedProducts, f.ManufacturerId.Value); + if (f.WithoutManufacturers.HasValue) + searchQuery = searchQuery.HasAnyManufacturer(!f.WithoutManufacturers.Value); + else if (f.ManufacturerId.HasValue) + searchQuery = searchQuery.WithManufacturerIds(f.FeaturedProducts, f.ManufacturerId.Value); - if (f.WithoutCategories.HasValue) - searchQuery = searchQuery.HasAnyCategory(!f.WithoutCategories.Value); - else if (f.CategoryIds != null && f.CategoryIds.Length > 0) - searchQuery = searchQuery.WithCategoryIds(f.FeaturedProducts, f.CategoryIds); + if (f.WithoutCategories.HasValue) + searchQuery = searchQuery.HasAnyCategory(!f.WithoutCategories.Value); + else if (f.CategoryIds != null && f.CategoryIds.Length > 0) + searchQuery = searchQuery.WithCategoryIds(f.FeaturedProducts, f.CategoryIds); - if (ctx.Request.EntitiesToExport.Count > 0) - searchQuery = searchQuery.WithProductIds(ctx.Request.EntitiesToExport.ToArray()); - else - searchQuery = searchQuery.WithProductId(f.IdMinimum, f.IdMaximum); + if (ctx.Request.EntitiesToExport.Count > 0) + searchQuery = searchQuery.WithProductIds(ctx.Request.EntitiesToExport.ToArray()); + else + searchQuery = searchQuery.WithProductId(f.IdMinimum, f.IdMaximum); - query = _catalogSearchService.Value.PrepareQuery(searchQuery); - } - else - { - query = ctx.Request.ProductQuery; - } + query = _catalogSearchService.Value.PrepareQuery(searchQuery); + } + else + { + query = ctx.Request.ProductQuery; + } query = query.OrderBy(x => x.Id); - // Skip required for preview. - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); } else if (ctx.LastId > 0) { + // Fast, ID based paging. query = query.Where(x => x.Id > ctx.LastId); } @@ -910,11 +923,11 @@ private IQueryable GetProductQuery(DataExporterContext ctx, int? skip, query = query.Take(() => take); } - return query; - } + return query; + } - private List GetProducts(DataExporterContext ctx) - { + private List GetProducts(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -924,7 +937,6 @@ private List GetProducts(DataExporterContext ctx) return null; } - var products = GetProductQuery(ctx, null, PageSize).ToList(); if (!products.Any()) { @@ -940,20 +952,20 @@ private List GetProducts(DataExporterContext ctx) associatedProducts = _productService.Value.GetAssociatedProductsByProductIds(groupedProductIds, true); } - foreach (var product in products) - { - if (product.ProductType == ProductType.SimpleProduct || product.ProductType == ProductType.BundledProduct) - { + foreach (var product in products) + { + if (product.ProductType == ProductType.SimpleProduct || product.ProductType == ProductType.BundledProduct) + { // We use ctx.EntityIdsPerSegment to avoid exporting products multiple times per segment\file (cause of associated products). if (ctx.EntityIdsPerSegment.Add(product.Id)) { result.Add(product); } - } - else if (product.ProductType == ProductType.GroupedProduct) - { - if (ctx.Projection.NoGroupedProducts) - { + } + else if (product.ProductType == ProductType.GroupedProduct) + { + if (ctx.Projection.NoGroupedProducts) + { if (associatedProducts.ContainsKey(product.Id)) { foreach (var associatedProduct in associatedProducts[product.Id]) @@ -973,36 +985,42 @@ private List GetProducts(DataExporterContext ctx) } } } - } - else - { - if (ctx.EntityIdsPerSegment.Add(product.Id)) - { - result.Add(product); - } - } - } - } + } + else + { + if (ctx.EntityIdsPerSegment.Add(product.Id)) + { + result.Add(product); + } + } + } + } ctx.LastId = products.Last().Id; SetProgress(ctx, products.Count); return result; - } - - private IQueryable GetOrderQuery(DataExporterContext ctx, int? skip, int take) - { - var query = _orderService.Value.GetOrders( - ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId, - ctx.Projection.CustomerId ?? 0, - ctx.Filter.CreatedFrom.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone) : null, - ctx.Filter.CreatedTo.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone) : null, - ctx.Filter.OrderStatusIds, - ctx.Filter.PaymentStatusIds, - ctx.Filter.ShippingStatusIds, - null, - null, - null); + } + + private IQueryable GetOrderQuery(DataExporterContext ctx, int? skip, int take) + { + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + var query = _orderService.Value.GetOrders( + ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId, + ctx.Projection.CustomerId ?? 0, + ctx.Filter.CreatedFrom.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone) : null, + ctx.Filter.CreatedTo.HasValue ? (DateTime?)_services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone) : null, + ctx.Filter.OrderStatusIds, + ctx.Filter.PaymentStatusIds, + ctx.Filter.ShippingStatusIds, + null, + null, + null); if (ctx.Request.EntitiesToExport.Any()) { @@ -1011,7 +1029,6 @@ private IQueryable GetOrderQuery(DataExporterContext ctx, int? skip, int query = query.OrderBy(x => x.Id); - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); @@ -1026,11 +1043,11 @@ private IQueryable GetOrderQuery(DataExporterContext ctx, int? skip, int query = query.Take(() => take); } - return query; - } + return query; + } - private List GetOrders(DataExporterContext ctx) - { + private List GetOrders(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -1046,21 +1063,28 @@ private List GetOrders(DataExporterContext ctx) return null; } - if (ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) - { - ctx.SetLoadedEntityIds(orders.Select(x => x.Id)); - } + if (ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) + { + ctx.SetLoadedEntityIds(orders.Select(x => x.Id)); + } ctx.LastId = orders.Last().Id; SetProgress(ctx, orders.Count); return orders; - } + } - private IQueryable GetManufacturerQuery(DataExporterContext ctx, int? skip, int take) - { - var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; - var query = _manufacturerService.Value.GetManufacturers(true, storeId); + private IQueryable GetManufacturerQuery(DataExporterContext ctx, int? skip, int take) + { + var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; + + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + var query = _manufacturerService.Value.GetManufacturers(true, storeId); if (ctx.Request.EntitiesToExport.Any()) { @@ -1069,7 +1093,6 @@ private IQueryable GetManufacturerQuery(DataExporterContext ctx, i query = query.OrderBy(x => x.Id); - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); @@ -1084,11 +1107,11 @@ private IQueryable GetManufacturerQuery(DataExporterContext ctx, i query = query.Take(() => take); } - return query; - } + return query; + } - private List GetManufacturers(DataExporterContext ctx) - { + private List GetManufacturers(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -1108,12 +1131,19 @@ private List GetManufacturers(DataExporterContext ctx) SetProgress(ctx, manus.Count); return manus; - } + } - private IQueryable GetCategoryQuery(DataExporterContext ctx, int? skip, int take) - { - var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; - var query = _categoryService.Value.BuildCategoriesQuery(null, true, null, storeId); + private IQueryable GetCategoryQuery(DataExporterContext ctx, int? skip, int take) + { + var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; + + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + var query = _categoryService.Value.BuildCategoriesQuery(null, true, null, storeId); if (ctx.Request.EntitiesToExport.Any()) { @@ -1122,7 +1152,6 @@ private IQueryable GetCategoryQuery(DataExporterContext ctx, int? skip query = query.OrderBy(x => x.Id); - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); @@ -1137,11 +1166,11 @@ private IQueryable GetCategoryQuery(DataExporterContext ctx, int? skip query = query.Take(() => take); } - return query; - } + return query; + } - private List GetCategories(DataExporterContext ctx) - { + private List GetCategories(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -1161,17 +1190,23 @@ private List GetCategories(DataExporterContext ctx) SetProgress(ctx, categories.Count); return categories; - } - - private IQueryable GetCustomerQuery(DataExporterContext ctx, int? skip, int take) - { - var query = _customerRepository.Value.TableUntracked - .Expand(x => x.BillingAddress) - .Expand(x => x.ShippingAddress) - .Expand(x => x.Addresses.Select(y => y.Country)) - .Expand(x => x.Addresses.Select(y => y.StateProvince)) - .Expand(x => x.CustomerRoleMappings.Select(rm => rm.CustomerRole)) - .Where(x => !x.Deleted); + } + + private IQueryable GetCustomerQuery(DataExporterContext ctx, int? skip, int take) + { + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + var query = _customerRepository.Value.TableUntracked + .Expand(x => x.BillingAddress) + .Expand(x => x.ShippingAddress) + .Expand(x => x.Addresses.Select(y => y.Country)) + .Expand(x => x.Addresses.Select(y => y.StateProvince)) + .Expand(x => x.CustomerRoleMappings.Select(rm => rm.CustomerRole)) + .Where(x => !x.Deleted); if (ctx.Filter.IsActiveCustomer.HasValue) { @@ -1190,53 +1225,53 @@ private IQueryable GetCustomerQuery(DataExporterContext ctx, int? skip if (ctx.Filter.BillingCountryIds != null && ctx.Filter.BillingCountryIds.Any()) { - query = query.Where(x => x.BillingAddress != null && ctx.Filter.BillingCountryIds.Contains(x.BillingAddress.Id)); + query = query.Where(x => x.BillingAddress != null && ctx.Filter.BillingCountryIds.Contains(x.BillingAddress.CountryId ?? 0)); } if (ctx.Filter.ShippingCountryIds != null && ctx.Filter.ShippingCountryIds.Any()) { - query = query.Where(x => x.ShippingAddress != null && ctx.Filter.ShippingCountryIds.Contains(x.ShippingAddress.Id)); - } - - if (ctx.Filter.LastActivityFrom.HasValue) - { - var activityFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityFrom.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => activityFrom <= x.LastActivityDateUtc); - } - - if (ctx.Filter.LastActivityTo.HasValue) - { - var activityTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityTo.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => activityTo >= x.LastActivityDateUtc); - } - - if (ctx.Filter.HasSpentAtLeastAmount.HasValue) - { - query = query - .Join(_orderRepository.Value.Table, x => x.Id, y => y.CustomerId, (x, y) => new { Customer = x, Order = y }) - .GroupBy(x => x.Customer.Id) - .Select(x => new - { - Customer = x.FirstOrDefault().Customer, - OrderTotal = x.Sum(y => y.Order.OrderTotal) - }) - .Where(x => x.OrderTotal >= ctx.Filter.HasSpentAtLeastAmount.Value) - .Select(x => x.Customer); - } - - if (ctx.Filter.HasPlacedAtLeastOrders.HasValue) - { - query = query - .Join(_orderRepository.Value.Table, x => x.Id, y => y.CustomerId, (x, y) => new { Customer = x, Order = y }) - .GroupBy(x => x.Customer.Id) - .Select(x => new - { - Customer = x.FirstOrDefault().Customer, - OrderCount = x.Count() - }) - .Where(x => x.OrderCount >= ctx.Filter.HasPlacedAtLeastOrders.Value) - .Select(x => x.Customer); - } + query = query.Where(x => x.ShippingAddress != null && ctx.Filter.ShippingCountryIds.Contains(x.ShippingAddress.CountryId ?? 0)); + } + + if (ctx.Filter.LastActivityFrom.HasValue) + { + var activityFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityFrom.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => activityFrom <= x.LastActivityDateUtc); + } + + if (ctx.Filter.LastActivityTo.HasValue) + { + var activityTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityTo.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => activityTo >= x.LastActivityDateUtc); + } + + if (ctx.Filter.HasSpentAtLeastAmount.HasValue) + { + query = query + .Join(_orderRepository.Value.Table, x => x.Id, y => y.CustomerId, (x, y) => new { Customer = x, Order = y }) + .GroupBy(x => x.Customer.Id) + .Select(x => new + { + Customer = x.FirstOrDefault().Customer, + OrderTotal = x.Sum(y => y.Order.OrderTotal) + }) + .Where(x => x.OrderTotal >= ctx.Filter.HasSpentAtLeastAmount.Value) + .Select(x => x.Customer); + } + + if (ctx.Filter.HasPlacedAtLeastOrders.HasValue) + { + query = query + .Join(_orderRepository.Value.Table, x => x.Id, y => y.CustomerId, (x, y) => new { Customer = x, Order = y }) + .GroupBy(x => x.Customer.Id) + .Select(x => new + { + Customer = x.FirstOrDefault().Customer, + OrderCount = x.Count() + }) + .Where(x => x.OrderCount >= ctx.Filter.HasPlacedAtLeastOrders.Value) + .Select(x => x.Customer); + } if (ctx.Request.EntitiesToExport.Any()) { @@ -1245,7 +1280,6 @@ private IQueryable GetCustomerQuery(DataExporterContext ctx, int? skip query = query.OrderBy(x => x.Id); - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); @@ -1260,11 +1294,11 @@ private IQueryable GetCustomerQuery(DataExporterContext ctx, int? skip query = query.Take(() => take); } - return query; - } + return query; + } - private List GetCustomers(DataExporterContext ctx) - { + private List GetCustomers(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -1284,22 +1318,38 @@ private List GetCustomers(DataExporterContext ctx) SetProgress(ctx, customers.Count); return customers; - } + } - private IQueryable GetNewsLetterSubscriptionQuery(DataExporterContext ctx, int? skip, int take) - { - var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId; + private IQueryable GetNewsLetterSubscriptionQuery(DataExporterContext ctx, int? skip, int take) + { + var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId; - var query = _subscriptionRepository.Value.TableUntracked; + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + var customerQuery = _customerRepository.Value.TableUntracked.Where(x => !x.Deleted); + + var query = + from ns in _subscriptionRepository.Value.TableUntracked + join c in customerQuery on ns.Email equals c.Email into customers + from c in customers.DefaultIfEmpty() + select new NewsletterSubscriber + { + Subscription = ns, + Customer = c + }; if (storeId > 0) { - query = query.Where(x => x.StoreId == storeId); + query = query.Where(x => x.Subscription.StoreId == storeId); } if (ctx.Filter.IsActiveSubscriber.HasValue) { - query = query.Where(x => x.Active == ctx.Filter.IsActiveSubscriber.Value); + query = query.Where(x => x.Subscription.Active == ctx.Filter.IsActiveSubscriber.Value); } if (ctx.Filter.WorkingLanguageId != null && ctx.Filter.WorkingLanguageId != 0) @@ -1309,41 +1359,45 @@ private IQueryable GetNewsLetterSubscriptionQuery(DataEx if (isDefaultLanguage) { - query = query.Where(x => x.WorkingLanguageId == 0 || x.WorkingLanguageId == ctx.Filter.WorkingLanguageId); + query = query.Where(x => x.Subscription.WorkingLanguageId == 0 || x.Subscription.WorkingLanguageId == ctx.Filter.WorkingLanguageId); } else { - query = query.Where(x => x.WorkingLanguageId == ctx.Filter.WorkingLanguageId); + query = query.Where(x => x.Subscription.WorkingLanguageId == ctx.Filter.WorkingLanguageId); } } if (ctx.Filter.CreatedFrom.HasValue) - { - var createdFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => createdFrom <= x.CreatedOnUtc); - } + { + var createdFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => createdFrom <= x.Subscription.CreatedOnUtc); + } - if (ctx.Filter.CreatedTo.HasValue) - { - var createdTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => createdTo >= x.CreatedOnUtc); - } + if (ctx.Filter.CreatedTo.HasValue) + { + var createdTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => createdTo >= x.Subscription.CreatedOnUtc); + } + + if (ctx.Filter.CustomerRoleIds != null && ctx.Filter.CustomerRoleIds.Any()) + { + query = query.Where(x => x.Customer.CustomerRoleMappings.Select(y => y.CustomerRoleId).Intersect(ctx.Filter.CustomerRoleIds).Any()); + } if (ctx.Request.EntitiesToExport.Any()) { - query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Subscription.Id)); } - query = query.OrderBy(x => x.Id); + query = query.OrderBy(x => x.Subscription.Id); - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); } else if (ctx.LastId > 0) { - query = query.Where(x => x.Id > ctx.LastId); + query = query.Where(x => x.Subscription.Id > ctx.LastId); } if (take != int.MaxValue) @@ -1351,11 +1405,11 @@ private IQueryable GetNewsLetterSubscriptionQuery(DataEx query = query.Take(() => take); } - return query; - } + return query.Select(x => x.Subscription); + } - private List GetNewsLetterSubscriptions(DataExporterContext ctx) - { + private List GetNewsLetterSubscriptions(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -1375,35 +1429,41 @@ private List GetNewsLetterSubscriptions(DataExporterCont SetProgress(ctx, subscriptions.Count); return subscriptions; - } + } - private IQueryable GetShoppingCartItemQuery(DataExporterContext ctx, int? skip, int take) - { - var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId; + private IQueryable GetShoppingCartItemQuery(DataExporterContext ctx, int? skip, int take) + { + var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId; - var query = _shoppingCartItemRepository.Value.TableUntracked - .Expand(x => x.Customer) - .Expand(x => x.Customer.CustomerRoleMappings.Select(rm => rm.CustomerRole)) - .Expand(x => x.Product) - .Where(x => !x.Customer.Deleted); + var skipValue = skip.GetValueOrDefault(); + if (skipValue == 0 && ctx.LastId == 0) + { + skipValue = Math.Max(ctx.Request.Profile.Offset, 0); + } + + var query = _shoppingCartItemRepository.Value.TableUntracked + .Expand(x => x.Customer) + .Expand(x => x.Customer.CustomerRoleMappings.Select(rm => rm.CustomerRole)) + .Expand(x => x.Product) + .Where(x => !x.Customer.Deleted); if (storeId > 0) { query = query.Where(x => x.StoreId == storeId); } - if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentCarts")) - { - query = query.Where(x => x.ShoppingCartTypeId == (int)ShoppingCartType.ShoppingCart); - } - else if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentWishlists")) - { - query = query.Where(x => x.ShoppingCartTypeId == (int)ShoppingCartType.Wishlist); - } - else if (ctx.Filter.ShoppingCartTypeId.HasValue) - { - query = query.Where(x => x.ShoppingCartTypeId == ctx.Filter.ShoppingCartTypeId.Value); - } + if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentCarts")) + { + query = query.Where(x => x.ShoppingCartTypeId == (int)ShoppingCartType.ShoppingCart); + } + else if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentWishlists")) + { + query = query.Where(x => x.ShoppingCartTypeId == (int)ShoppingCartType.Wishlist); + } + else if (ctx.Filter.ShoppingCartTypeId.HasValue) + { + query = query.Where(x => x.ShoppingCartTypeId == ctx.Filter.ShoppingCartTypeId.Value); + } if (ctx.Filter.IsActiveCustomer.HasValue) { @@ -1420,38 +1480,38 @@ private IQueryable GetShoppingCartItemQuery(DataExporterContex query = query.Where(x => x.Customer.CustomerRoleMappings.Select(y => y.CustomerRoleId).Intersect(ctx.Filter.CustomerRoleIds).Any()); } - if (ctx.Filter.LastActivityFrom.HasValue) - { - var activityFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityFrom.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => activityFrom <= x.Customer.LastActivityDateUtc); - } - - if (ctx.Filter.LastActivityTo.HasValue) - { - var activityTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityTo.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => activityTo >= x.Customer.LastActivityDateUtc); - } - - if (ctx.Filter.CreatedFrom.HasValue) - { - var createdFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => createdFrom <= x.CreatedOnUtc); - } - - if (ctx.Filter.CreatedTo.HasValue) - { - var createdTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone); - query = query.Where(x => createdTo >= x.CreatedOnUtc); - } - - if (ctx.Projection.NoBundleProducts) - { - query = query.Where(x => x.Product.ProductTypeId != (int)ProductType.BundledProduct); - } - else - { - query = query.Where(x => x.BundleItemId == null); - } + if (ctx.Filter.LastActivityFrom.HasValue) + { + var activityFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityFrom.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => activityFrom <= x.Customer.LastActivityDateUtc); + } + + if (ctx.Filter.LastActivityTo.HasValue) + { + var activityTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityTo.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => activityTo >= x.Customer.LastActivityDateUtc); + } + + if (ctx.Filter.CreatedFrom.HasValue) + { + var createdFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => createdFrom <= x.CreatedOnUtc); + } + + if (ctx.Filter.CreatedTo.HasValue) + { + var createdTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => createdTo >= x.CreatedOnUtc); + } + + if (ctx.Projection.NoBundleProducts) + { + query = query.Where(x => x.Product.ProductTypeId != (int)ProductType.BundledProduct); + } + else + { + query = query.Where(x => x.BundleItemId == null); + } if (ctx.Request.EntitiesToExport.Any()) { @@ -1460,7 +1520,6 @@ private IQueryable GetShoppingCartItemQuery(DataExporterContex query = query.OrderBy(x => x.Id); - var skipValue = skip.GetValueOrDefault(); if (skipValue > 0) { query = query.Skip(() => skipValue); @@ -1475,11 +1534,11 @@ private IQueryable GetShoppingCartItemQuery(DataExporterContex query = query.Take(take); } - return query; - } + return query; + } - private List GetShoppingCartItems(DataExporterContext ctx) - { + private List GetShoppingCartItems(DataExporterContext ctx) + { DetachAllEntitiesAndClear(ctx); var stats = ctx.StatsPerStore[ctx.Store.Id]; @@ -1499,20 +1558,20 @@ private List GetShoppingCartItems(DataExporterContext ctx) SetProgress(ctx, cartItems.Count); return cartItems; - } + } - #endregion + #endregion - private List Init(DataExporterContext ctx) - { - List result = null; + private List Init(DataExporterContext ctx) + { + List result = null; - ctx.ContextCurrency = _currencyService.Value.GetCurrencyById(ctx.Projection.CurrencyId ?? 0) ?? _services.WorkContext.WorkingCurrency; - ctx.ContextCustomer = _customerService.GetCustomerById(ctx.Projection.CustomerId ?? 0) ?? _services.WorkContext.CurrentCustomer; - ctx.ContextLanguage = _languageService.Value.GetLanguageById(ctx.Projection.LanguageId ?? 0) ?? _services.WorkContext.WorkingLanguage; + ctx.ContextCurrency = _currencyService.Value.GetCurrencyById(ctx.Projection.CurrencyId ?? 0) ?? _services.WorkContext.WorkingCurrency; + ctx.ContextCustomer = _customerService.GetCustomerById(ctx.Projection.CustomerId ?? 0) ?? _services.WorkContext.CurrentCustomer; + ctx.ContextLanguage = _languageService.Value.GetLanguageById(ctx.Projection.LanguageId ?? 0) ?? _services.WorkContext.WorkingLanguage; - ctx.Stores = _services.StoreService.GetAllStores().ToDictionary(x => x.Id, x => x); - ctx.Languages = _languageService.Value.GetAllLanguages(true).ToDictionary(x => x.Id, x => x); + ctx.Stores = _services.StoreService.GetAllStores().ToDictionary(x => x.Id, x => x); + ctx.Languages = _languageService.Value.GetAllLanguages(true).ToDictionary(x => x.Id, x => x); if (ctx.IsPreview) { @@ -1521,7 +1580,7 @@ private List Init(DataExporterContext ctx) ctx.Translations[name] = new LocalizedPropertyCollection(name, null, Enumerable.Empty()); } - foreach (var name in new string[] { nameof(Product), nameof(ProductTag), nameof(ProductBundleItem), nameof(SpecificationAttribute), nameof(SpecificationAttributeOption), + foreach (var name in new string[] { nameof(Product), nameof(ProductTag), nameof(ProductBundleItem), nameof(SpecificationAttribute), nameof(SpecificationAttributeOption), nameof(ProductAttribute), nameof(ProductVariantAttributeValue) }) { ctx.TranslationsPerPage[name] = new LocalizedPropertyCollection(name, null, Enumerable.Empty()); @@ -1547,21 +1606,21 @@ private List Init(DataExporterContext ctx) } if (!ctx.IsPreview && ctx.Request.Profile.PerStore) - { - result = new List(ctx.Stores.Values.Where(x => x.Id == ctx.Filter.StoreId || ctx.Filter.StoreId == 0)); - } - else - { - int? storeId = ctx.Filter.StoreId == 0 ? ctx.Projection.StoreId : ctx.Filter.StoreId; - ctx.Store = ctx.Stores.Values.FirstOrDefault(x => x.Id == (storeId ?? _services.StoreContext.CurrentStore.Id)); - - result = new List { ctx.Store }; - } - - // Get record stats for each store. - foreach (var store in result) - { - ctx.Store = store; + { + result = new List(ctx.Stores.Values.Where(x => x.Id == ctx.Filter.StoreId || ctx.Filter.StoreId == 0)); + } + else + { + int? storeId = ctx.Filter.StoreId == 0 ? ctx.Projection.StoreId : ctx.Filter.StoreId; + ctx.Store = ctx.Stores.Values.FirstOrDefault(x => x.Id == (storeId ?? _services.StoreContext.CurrentStore.Id)); + + result = new List { ctx.Store }; + } + + // Get record stats for each store. + foreach (var store in result) + { + ctx.Store = store; IQueryable query = null; @@ -1598,20 +1657,16 @@ private List Init(DataExporterContext ctx) if (!ctx.IsPreview) { stats.MaxId = query.Max(x => (int?)x.Id) ?? 0; - if (ctx.Request.Profile.Offset > 0) - { - stats.OffsetId = query.Select(x => x.Id).FirstOrDefault(); - } } ctx.StatsPerStore[store.Id] = stats; - } + } return result; - } + } - private void ExportCoreInner(DataExporterContext ctx, Store store) - { + private void ExportCoreInner(DataExporterContext ctx, Store store) + { var context = ctx.ExecuteContext; var profile = ctx.Request.Profile; var provider = ctx.Request.Provider; @@ -1621,13 +1676,13 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) return; } - { - var logHead = new StringBuilder(); - logHead.AppendLine(); - logHead.AppendLine(new string('-', 40)); - logHead.AppendLine("SmartStore.NET: v." + SmartStoreVersion.CurrentFullVersion); - logHead.Append("Export profile: " + profile.Name); - logHead.AppendLine(profile.Id == 0 ? " (volatile)" : $" (Id {profile.Id})"); + { + var logHead = new StringBuilder(); + logHead.AppendLine(); + logHead.AppendLine(new string('-', 40)); + logHead.AppendLine("Smartstore: v." + SmartStoreVersion.CurrentFullVersion); + logHead.Append("Export profile: " + profile.Name); + logHead.AppendLine(profile.Id == 0 ? " (volatile)" : $" (Id {profile.Id})"); if (provider.Metadata.FriendlyName.HasValue()) { @@ -1638,79 +1693,80 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) logHead.AppendLine("Export provider: " + profile.ProviderSystemName); } - var plugin = provider.Metadata.PluginDescriptor; - logHead.Append("Plugin: "); - logHead.AppendLine(plugin == null ? "".NaIfEmpty() : $"{plugin.FriendlyName} ({plugin.SystemName}) v.{plugin.Version.ToString()}"); - logHead.AppendLine("Entity: " + provider.Value.EntityType.ToString()); + var plugin = provider.Metadata.PluginDescriptor; + logHead.Append("Plugin: "); + logHead.AppendLine(plugin == null ? "".NaIfEmpty() : $"{plugin.FriendlyName} ({plugin.SystemName}) v.{plugin.Version.ToString()}"); + logHead.AppendLine("Entity: " + provider.Value.EntityType.ToString()); - try - { - var uri = new Uri(store.Url); - logHead.AppendLine($"Store: {uri.DnsSafeHost.NaIfEmpty()} (Id {store.Id})"); - } - catch { } + try + { + var uri = new Uri(store.Url); + logHead.AppendLine($"Store: {uri.DnsSafeHost.NaIfEmpty()} (Id {store.Id})"); + } + catch { } - var customer = _services.WorkContext.CurrentCustomer; - logHead.Append("Executed by: " + (customer.Email.HasValue() ? customer.Email : customer.SystemName)); + var customer = _services.WorkContext.CurrentCustomer; + logHead.Append("Executed by: " + (customer.Email.HasValue() ? customer.Email : customer.SystemName)); - ctx.Log.Info(logHead.ToString()); - } + ctx.Log.Info(logHead.ToString()); + } var dataExchangeSettings = _services.Settings.LoadSetting(store.Id); var publicDeployment = profile.Deployments.FirstOrDefault(x => x.DeploymentType == ExportDeploymentType.PublicFolder); ctx.Store = store; + ctx.LastId = 0; context.FileIndex = 0; context.Store = ToDynamic(ctx, ctx.Store); - context.MaxFileNameLength = dataExchangeSettings.MaxFileNameLength; - context.HasPublicDeployment = publicDeployment != null; - context.PublicFolderPath = publicDeployment.GetDeploymentFolder(true); - context.PublicFolderUrl = publicDeployment.GetPublicFolderUrl(_services, ctx.Store); + context.MaxFileNameLength = dataExchangeSettings.MaxFileNameLength; + context.HasPublicDeployment = publicDeployment != null; + context.PublicFolderPath = publicDeployment.GetDeploymentFolder(true); + context.PublicFolderUrl = publicDeployment.GetPublicFolderUrl(_services, ctx.Store); var fileExtension = provider.Value.FileExtension.HasValue() ? provider.Value.FileExtension.ToLower().EnsureStartsWith(".") : ""; - using (var segmenter = CreateSegmenter(ctx)) - { - if (segmenter == null) - { - throw new SmartException($"Unsupported entity type '{provider.Value.EntityType.ToString()}'."); - } + using (var segmenter = CreateSegmenter(ctx)) + { + if (segmenter == null) + { + throw new SmartException($"Unsupported entity type '{provider.Value.EntityType.ToString()}'."); + } - if (segmenter.TotalRecords <= 0) - { - ctx.Log.Info("There are no records to export."); - } + if (segmenter.TotalRecords <= 0) + { + ctx.Log.Info("There are no records to export."); + } - while (context.Abort == DataExchangeAbortion.None && segmenter.HasData) - { - segmenter.RecordPerSegmentCount = 0; - context.RecordsSucceeded = 0; + while (context.Abort == DataExchangeAbortion.None && segmenter.HasData) + { + segmenter.RecordPerSegmentCount = 0; + context.RecordsSucceeded = 0; - string path = null; + string path = null; - if (ctx.IsFileBasedExport) - { + if (ctx.IsFileBasedExport) + { context.FileIndex = context.FileIndex + 1; context.FileName = profile.ResolveFileNamePattern(ctx.Store, context.FileIndex, context.MaxFileNameLength) + fileExtension; - path = Path.Combine(context.Folder, context.FileName); + path = Path.Combine(context.Folder, context.FileName); if (profile.ExportRelatedData && ctx.Supports(ExportFeatures.UsesRelatedDataUnits)) { context.ExtraDataUnits.AddRange(GetDataUnitsForRelatedEntities(ctx)); } - } - - if (CallProvider(ctx, "Execute", path)) - { - if (ctx.IsFileBasedExport && File.Exists(path)) - { - ctx.Result.Files.Add(new DataExportResult.ExportFileInfo - { - StoreId = ctx.Store.Id, - FileName = context.FileName + } + + if (CallProvider(ctx, "Execute", path)) + { + if (ctx.IsFileBasedExport && File.Exists(path)) + { + ctx.Result.Files.Add(new DataExportResult.ExportFileInfo + { + StoreId = ctx.Store.Id, + FileName = context.FileName }); } - } + } ctx.EntityIdsPerSegment.Clear(); @@ -1722,14 +1778,14 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) { ctx.Log.Warn("Export aborted. A cancellation has been requested."); } - } + } if (context.Abort != DataExchangeAbortion.Hard) - { + { var calledExecuted = false; - context.ExtraDataUnits.ForEach(x => - { + context.ExtraDataUnits.ForEach(x => + { context.DataStreamId = x.Id; var success = true; @@ -1763,45 +1819,45 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) context.ExtraDataUnits.Clear(); } - } + } - private void ExportCoreOuter(DataExporterContext ctx) - { + private void ExportCoreOuter(DataExporterContext ctx) + { var profile = ctx.Request.Profile; - var logPath = profile.GetExportLogPath(); - var zipPath = profile.GetExportZipPath(); + var logPath = profile.GetExportLogPath(); + var zipPath = profile.GetExportZipPath(); FileSystemHelper.DeleteFile(logPath); - FileSystemHelper.DeleteFile(zipPath); - FileSystemHelper.ClearDirectory(ctx.FolderContent, false); - - using (var logger = new TraceLogger(logPath)) - { - try - { - ctx.Log = logger; - ctx.ExecuteContext.Log = logger; - ctx.ProgressInfo = T("Admin.DataExchange.Export.ProgressInfo"); - - if (!ctx.Request.Provider.IsValid()) - throw new SmartException("Export aborted because the export provider is not valid."); - - if (!HasPermission(ctx)) - throw new SmartException("You do not have permission to perform the selected export."); - - foreach (var item in ctx.Request.CustomData) - { - ctx.ExecuteContext.CustomProperties.Add(item.Key, item.Value); - } - - if (profile.ProviderConfigData.HasValue()) - { - var configInfo = ctx.Request.Provider.Value.ConfigurationInfo; - if (configInfo != null) - { - ctx.ExecuteContext.ConfigurationData = XmlHelper.Deserialize(profile.ProviderConfigData, configInfo.ModelType); - } - } + FileSystemHelper.DeleteFile(zipPath); + FileSystemHelper.ClearDirectory(ctx.FolderContent, false); + + using (var logger = new TraceLogger(logPath)) + { + try + { + ctx.Log = logger; + ctx.ExecuteContext.Log = logger; + ctx.ProgressInfo = T("Admin.DataExchange.Export.ProgressInfo"); + + if (!ctx.Request.Provider.IsValid()) + throw new SmartException("Export aborted because the export provider is not valid."); + + if (!HasPermission(ctx)) + throw new SmartException("You do not have permission to perform the selected export."); + + foreach (var item in ctx.Request.CustomData) + { + ctx.ExecuteContext.CustomProperties.Add(item.Key, item.Value); + } + + if (profile.ProviderConfigData.HasValue()) + { + var configInfo = ctx.Request.Provider.Value.ConfigurationInfo; + if (configInfo != null) + { + ctx.ExecuteContext.ConfigurationData = XmlHelper.Deserialize(profile.ProviderConfigData, configInfo.ModelType); + } + } // lazyLoading: false, proxyCreation: false impossible due to price calculation. using (var scope = new DbContextScope(_dbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) @@ -1838,70 +1894,70 @@ private void ExportCoreOuter(DataExporterContext ctx) stores.ForEach(x => ExportCoreInner(ctx, x)); } - if (!ctx.IsPreview && ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) - { - if (ctx.IsFileBasedExport) - { - if (profile.CreateZipArchive) - { - ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, false); - } - - if (profile.Deployments.Any(x => x.Enabled)) - { - SetProgress(ctx, T("Common.Publishing")); - - var allDeploymentsSucceeded = Deploy(ctx, zipPath); - - if (allDeploymentsSucceeded && profile.Cleanup) - { - logger.Info("Cleaning up export folder"); - - FileSystemHelper.ClearDirectory(ctx.FolderContent, false); - } - } - } - - if (profile.EmailAccountId != 0 && !ctx.Supports(ExportFeatures.CanOmitCompletionMail)) - { - SendCompletionEmail(ctx, zipPath); - } - } - } - catch (Exception ex) - { - logger.ErrorsAll(ex); - ctx.Result.LastError = ex.ToAllMessages(true); - } - finally - { - try - { - if (!ctx.IsPreview && profile.Id != 0) - { + if (!ctx.IsPreview && ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) + { + if (ctx.IsFileBasedExport) + { + if (profile.CreateZipArchive) + { + ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, false); + } + + if (profile.Deployments.Any(x => x.Enabled)) + { + SetProgress(ctx, T("Common.Publishing")); + + var allDeploymentsSucceeded = Deploy(ctx, zipPath); + + if (allDeploymentsSucceeded && profile.Cleanup) + { + logger.Info("Cleaning up export folder"); + + FileSystemHelper.ClearDirectory(ctx.FolderContent, false); + } + } + } + + if (profile.EmailAccountId != 0 && !ctx.Supports(ExportFeatures.CanOmitCompletionMail)) + { + SendCompletionEmail(ctx, zipPath); + } + } + } + catch (Exception ex) + { + logger.ErrorsAll(ex); + ctx.Result.LastError = ex.ToAllMessages(true); + } + finally + { + try + { + if (!ctx.IsPreview && profile.Id != 0) + { ctx.Result.Files = ctx.Result.Files.OrderBy(x => x.RelatedType).ToList(); - profile.ResultInfo = XmlHelper.Serialize(ctx.Result); + profile.ResultInfo = XmlHelper.Serialize(ctx.Result); - _exportProfileService.Value.UpdateExportProfile(profile); - } - } - catch (Exception ex) - { - logger.ErrorsAll(ex); - } + _exportProfileService.Value.UpdateExportProfile(profile); + } + } + catch (Exception ex) + { + logger.ErrorsAll(ex); + } - DetachAllEntitiesAndClear(ctx); + DetachAllEntitiesAndClear(ctx); try - { - ctx.NewsletterSubscriptions.Clear(); - ctx.ProductTemplates.Clear(); - ctx.CategoryTemplates.Clear(); - ctx.Countries.Clear(); - ctx.Languages.Clear(); - ctx.QuantityUnits.Clear(); - ctx.DeliveryTimes.Clear(); - ctx.Stores.Clear(); + { + ctx.NewsletterSubscriptions.Clear(); + ctx.ProductTemplates.Clear(); + ctx.CategoryTemplates.Clear(); + ctx.Countries.Clear(); + ctx.Languages.Clear(); + ctx.QuantityUnits.Clear(); + ctx.DeliveryTimes.Clear(); + ctx.Stores.Clear(); ctx.Translations.Clear(); ctx.UrlRecords.Clear(); @@ -1911,29 +1967,29 @@ private void ExportCoreOuter(DataExporterContext ctx) ctx.Request.CustomData.Clear(); ctx.ExecuteContext.CustomProperties.Clear(); - ctx.ExecuteContext.Log = null; - ctx.Log = null; - } - catch (Exception ex) - { - logger.ErrorsAll(ex); - } - } - } + ctx.ExecuteContext.Log = null; + ctx.Log = null; + } + catch (Exception ex) + { + logger.ErrorsAll(ex); + } + } + } if (ctx.IsPreview || ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard) { return; } - // Post process order entities. - if (ctx.EntityIdsLoaded.Any() && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) - { - using (var logger = new TraceLogger(logPath)) - { - try - { - int? orderStatusId = null; + // Post process order entities. + if (ctx.EntityIdsLoaded.Any() && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) + { + using (var logger = new TraceLogger(logPath)) + { + try + { + int? orderStatusId = null; if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Processing) { @@ -1944,38 +2000,38 @@ private void ExportCoreOuter(DataExporterContext ctx) orderStatusId = (int)OrderStatus.Complete; } - using (var scope = new DbContextScope(_dbContext, false, null, false, false, false, false)) - { - foreach (var chunk in ctx.EntityIdsLoaded.Slice(128)) - { - var entities = _orderRepository.Value.Table.Where(x => chunk.Contains(x.Id)).ToList(); + using (var scope = new DbContextScope(_dbContext, false, null, false, false, false, false)) + { + foreach (var chunk in ctx.EntityIdsLoaded.Slice(128)) + { + var entities = _orderRepository.Value.Table.Where(x => chunk.Contains(x.Id)).ToList(); - entities.ForEach(x => x.OrderStatusId = (orderStatusId ?? x.OrderStatusId)); + entities.ForEach(x => x.OrderStatusId = (orderStatusId ?? x.OrderStatusId)); - _dbContext.SaveChanges(); - } - } + _dbContext.SaveChanges(); + } + } - logger.Info($"Updated order status for {ctx.EntityIdsLoaded.Count()} order(s)."); - } - catch (Exception ex) - { - logger.ErrorsAll(ex); + logger.Info($"Updated order status for {ctx.EntityIdsLoaded.Count()} order(s)."); + } + catch (Exception ex) + { + logger.ErrorsAll(ex); ctx.Result.LastError = ex.ToAllMessages(true); - } - } - } - } + } + } + } + } - /// - /// The name of the public export folder - /// - public static string PublicFolder => "Exchange"; + /// + /// The name of the public export folder + /// + public static string PublicFolder => "Exchange"; - public static int PageSize => 100; + public static int PageSize => 100; - public DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken) - { + public DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken) + { var ctx = new DataExporterContext(request, cancellationToken); if (!(request?.Profile?.Enabled ?? false)) @@ -1995,73 +2051,76 @@ public DataExportResult Export(DataExportRequest request, CancellationToken canc ExportCoreOuter(ctx); } - cancellationToken.ThrowIfCancellationRequested(); - return ctx.Result; - } + cancellationToken.ThrowIfCancellationRequested(); + return ctx.Result; + } - public DataExportPreviewResult Preview(DataExportRequest request, int pageIndex) - { + public DataExportPreviewResult Preview(DataExportRequest request, int pageIndex) + { var result = new DataExportPreviewResult(); var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); - var ctx = new DataExporterContext(request, cancellation.Token, true); + var ctx = new DataExporterContext(request, cancellation.Token, true); - var unused = Init(ctx); - var skip = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); + Init(ctx); - if (!HasPermission(ctx)) - { - throw new SmartException(T("Admin.AccessDenied")); - } + var limit = Math.Max(request.Profile.Limit, 0); + var take = limit > 0 && limit < PageSize ? limit : PageSize; + var skip = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * take); + + if (!HasPermission(ctx)) + { + throw new SmartException(T("Admin.AccessDenied")); + } result.TotalRecords = ctx.StatsPerStore.First().Value.TotalRecords; switch (request.Provider.Value.EntityType) - { - case ExportEntityType.Product: - { - var items = GetProductQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - case ExportEntityType.Order: - { - var items = GetOrderQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - case ExportEntityType.Category: - { - var items = GetCategoryQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - case ExportEntityType.Manufacturer: - { - var items = GetManufacturerQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - case ExportEntityType.Customer: - { - var items = GetCustomerQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - case ExportEntityType.NewsLetterSubscription: - { - var items = GetNewsLetterSubscriptionQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - case ExportEntityType.ShoppingCartItem: - { - var items = GetShoppingCartItemQuery(ctx, skip, PageSize).ToList(); - items.Each(x => result.Data.Add(ToDynamic(ctx, x))); - } - break; - } + { + case ExportEntityType.Product: + { + var items = GetProductQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Order: + { + var items = GetOrderQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Category: + { + var items = GetCategoryQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Manufacturer: + { + var items = GetManufacturerQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Customer: + { + var items = GetCustomerQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.NewsLetterSubscription: + { + var items = GetNewsLetterSubscriptionQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.ShoppingCartItem: + { + var items = GetShoppingCartItemQuery(ctx, skip, take).ToList(); + items.Each(x => result.Data.Add(ToDynamic(ctx, x))); + } + break; + } return result; - } - } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs index 7fff7a46e5..9b901f138f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs @@ -2,22 +2,19 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - [Serializable] - public class DataDeploymentResult - { - /// - /// Whether the deployment succeeded - /// - public bool Succeeded - { - get { return LastError.IsEmpty(); } - } + [Serializable] + public class DataDeploymentResult + { + /// + /// Whether the deployment succeeded + /// + public bool Succeeded => LastError.IsEmpty(); - public string LastError { get; set; } + public string LastError { get; set; } - /// - /// Last execution date - /// - public DateTime LastExecutionUtc { get; set; } - } + /// + /// Last execution date + /// + public DateTime LastExecutionUtc { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs index 3415948ae2..ccb036af4d 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs @@ -11,56 +11,56 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - public class EmailFilePublisher : IFilePublisher - { - private IEmailAccountService _emailAccountService; - private IQueuedEmailService _queuedEmailService; + public class EmailFilePublisher : IFilePublisher + { + private IEmailAccountService _emailAccountService; + private IQueuedEmailService _queuedEmailService; - public EmailFilePublisher( - IEmailAccountService emailAccountService, - IQueuedEmailService queuedEmailService) - { - _emailAccountService = emailAccountService; - _queuedEmailService = queuedEmailService; - } + public EmailFilePublisher( + IEmailAccountService emailAccountService, + IQueuedEmailService queuedEmailService) + { + _emailAccountService = emailAccountService; + _queuedEmailService = queuedEmailService; + } - public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) - { - var emailAccount = _emailAccountService.GetEmailAccountById(deployment.EmailAccountId); - var smtpContext = new SmtpContext(emailAccount); - var count = 0; + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var emailAccount = _emailAccountService.GetEmailAccountById(deployment.EmailAccountId); + var smtpContext = new SmtpContext(emailAccount); + var count = 0; - foreach (var email in deployment.EmailAddresses.SplitSafe(",").Where(x => x.IsEmail())) - { - var queuedEmail = new QueuedEmail - { - From = emailAccount.ToEmailAddress(), - SendManually = false, - To = email, - Subject = deployment.EmailSubject.NaIfEmpty(), - Body = deployment.EmailSubject.NaIfEmpty(), - CreatedOnUtc = DateTime.UtcNow, - EmailAccountId = deployment.EmailAccountId - }; + foreach (var email in deployment.EmailAddresses.SplitSafe(",").Where(x => x.IsEmail())) + { + var queuedEmail = new QueuedEmail + { + From = emailAccount.ToEmailAddress(), + SendManually = false, + To = email, + Subject = deployment.EmailSubject.NaIfEmpty(), + Body = deployment.EmailSubject.NaIfEmpty(), + CreatedOnUtc = DateTime.UtcNow, + EmailAccountId = deployment.EmailAccountId + }; - foreach (var path in context.GetDeploymentFiles()) - { - var name = Path.GetFileName(path); + foreach (var path in context.GetDeploymentFiles()) + { + var name = Path.GetFileName(path); - queuedEmail.Attachments.Add(new QueuedEmailAttachment - { - StorageLocation = EmailAttachmentStorageLocation.Blob, - MediaStorage = new MediaStorage { Data = File.ReadAllBytes(path) }, - Name = name, - MimeType = MimeTypes.MapNameToMimeType(name) - }); - } + queuedEmail.Attachments.Add(new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.Blob, + MediaStorage = new MediaStorage { Data = File.ReadAllBytes(path) }, + Name = name, + MimeType = MimeTypes.MapNameToMimeType(name) + }); + } - _queuedEmailService.InsertQueuedEmail(queuedEmail); - ++count; - } + _queuedEmailService.InsertQueuedEmail(queuedEmail); + ++count; + } - context.Log.Info("{0} email(s) created and queued for deployment.".FormatInvariant(count)); - } - } + context.Log.Info("{0} email(s) created and queued for deployment.".FormatInvariant(count)); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs index 346cf02176..04363d9663 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs @@ -5,21 +5,21 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - public class FileSystemFilePublisher : IFilePublisher - { - public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) - { - var targetFolder = deployment.GetDeploymentFolder(true); + public class FileSystemFilePublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var targetFolder = deployment.GetDeploymentFolder(true); - if (targetFolder.IsEmpty()) - return; + if (targetFolder.IsEmpty()) + return; - if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(targetFolder))) - { - context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); - } + if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(targetFolder))) + { + context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); + } - context.Log.Info($"Copied export data files to {targetFolder}."); - } - } + context.Log.Info($"Copied export data files to {targetFolder}."); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs index 64bf43b067..bd6a2e8ad8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs @@ -6,170 +6,170 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - public class FtpFilePublisher : IFilePublisher - { - private ExportDeploymentContext _context; - private ExportDeployment _deployment; - private int _succeededFiles; - private string _ftpRootUrl; - - private string GetRelativePath(string path) - { - var relativePath = path.Substring(_context.FolderContent.Length).Replace("\\", "/"); - if (relativePath.StartsWith("/")) - { - return relativePath.Substring(1); - } - - return relativePath; - } - - private FtpWebRequest CreateRequest(string url, bool keepAlive = true, long? contentLength = null) - { - var request = (FtpWebRequest)WebRequest.Create(url); - request.Method = WebRequestMethods.Ftp.UploadFile; - request.KeepAlive = keepAlive; - request.UseBinary = true; - request.Proxy = null; - request.UsePassive = _deployment.PassiveMode; - request.EnableSsl = _deployment.UseSsl; - - if (_deployment.Username.HasValue()) - { - request.Credentials = new NetworkCredential(_deployment.Username, _deployment.Password); - } - - if (contentLength.HasValue) - { - request.ContentLength = contentLength.Value; - } - - return request; - } - - private bool UploadFile(string path, string fileUrl, bool keepAlive = true) - { - var succeeded = false; - var bytesRead = 0; - var buffLength = 32768; - byte[] buff = new byte[buffLength]; - - var request = CreateRequest(fileUrl, keepAlive, (new FileInfo(path)).Length); - request.Method = WebRequestMethods.Ftp.UploadFile; - - var requestStream = request.GetRequestStream(); - using (var stream = new FileStream(path, FileMode.Open)) - { - while ((bytesRead = stream.Read(buff, 0, buffLength)) != 0) - { - requestStream.Write(buff, 0, bytesRead); - } - } - - requestStream.Close(); - - using (var response = (FtpWebResponse)request.GetResponse()) - { - var statusCode = (int)response.StatusCode; - succeeded = statusCode >= 200 && statusCode <= 299; - - if (succeeded) - { - ++_succeededFiles; - } - else - { - _context.Result.LastError = _context.T("Admin.Common.FtpStatus", statusCode, response.StatusCode.ToString()); - _context.Log.Error("The FTP transfer failed. FTP status {0} ({1}). File {3}".FormatInvariant(statusCode, response.StatusCode.ToString(), path)); - } - } - - return succeeded; - } - - private bool DirectoryExists(string directoryUrl) - { - var result = false; - - try - { - var request = CreateRequest(directoryUrl.EnsureEndsWith("/")); - request.Method = WebRequestMethods.Ftp.ListDirectory; - - using (request.GetResponse()) - { - result = true; - } - } - catch (WebException) - { - result = false; - } - - return result; - } - - private bool FtpCopyDirectory(DirectoryInfo source) - { - var files = source.GetFiles(); - var len = files.Length; - - for (var i = 0; i < len; ++i) - { - var path = files[i].FullName; - var relativePath = GetRelativePath(path); - - UploadFile(path, _ftpRootUrl + relativePath, i != (len - 1)); - } - - foreach (var sourceSubDir in source.GetDirectories()) - { - var relativePath = GetRelativePath(sourceSubDir.FullName); - - if (!DirectoryExists(_ftpRootUrl + relativePath)) - { - var request = CreateRequest(_ftpRootUrl + relativePath, true); - request.Method = WebRequestMethods.Ftp.MakeDirectory; - using (var response = (FtpWebResponse)request.GetResponse()) - { - response.Close(); - } - } - - FtpCopyDirectory(sourceSubDir); - } - - return true; - } - - public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) - { - _context = context; - _deployment = deployment; - _succeededFiles = 0; - _ftpRootUrl = deployment.Url; - - if (!_ftpRootUrl.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase)) - { - _ftpRootUrl = "ftp://" + _ftpRootUrl; - } - - _ftpRootUrl = _ftpRootUrl.EnsureEndsWith("/"); - - if (context.CreateZipArchive) - { - if (File.Exists(context.ZipPath)) - { - var fileUrl = _ftpRootUrl + Path.GetFileName(context.ZipPath); - UploadFile(context.ZipPath, fileUrl, false); - } - } - else - { - FtpCopyDirectory(new DirectoryInfo(context.FolderContent)); - } - - context.Log.Info("{0} file(s) successfully uploaded via FTP.".FormatInvariant(_succeededFiles)); - } - } + public class FtpFilePublisher : IFilePublisher + { + private ExportDeploymentContext _context; + private ExportDeployment _deployment; + private int _succeededFiles; + private string _ftpRootUrl; + + private string GetRelativePath(string path) + { + var relativePath = path.Substring(_context.FolderContent.Length).Replace("\\", "/"); + if (relativePath.StartsWith("/")) + { + return relativePath.Substring(1); + } + + return relativePath; + } + + private FtpWebRequest CreateRequest(string url, bool keepAlive = true, long? contentLength = null) + { + var request = (FtpWebRequest)WebRequest.Create(url); + request.Method = WebRequestMethods.Ftp.UploadFile; + request.KeepAlive = keepAlive; + request.UseBinary = true; + request.Proxy = null; + request.UsePassive = _deployment.PassiveMode; + request.EnableSsl = _deployment.UseSsl; + + if (_deployment.Username.HasValue()) + { + request.Credentials = new NetworkCredential(_deployment.Username, _deployment.Password); + } + + if (contentLength.HasValue) + { + request.ContentLength = contentLength.Value; + } + + return request; + } + + private bool UploadFile(string path, string fileUrl, bool keepAlive = true) + { + var succeeded = false; + var bytesRead = 0; + var buffLength = 32768; + byte[] buff = new byte[buffLength]; + + var request = CreateRequest(fileUrl, keepAlive, (new FileInfo(path)).Length); + request.Method = WebRequestMethods.Ftp.UploadFile; + + var requestStream = request.GetRequestStream(); + using (var stream = new FileStream(path, FileMode.Open)) + { + while ((bytesRead = stream.Read(buff, 0, buffLength)) != 0) + { + requestStream.Write(buff, 0, bytesRead); + } + } + + requestStream.Close(); + + using (var response = (FtpWebResponse)request.GetResponse()) + { + var statusCode = (int)response.StatusCode; + succeeded = statusCode >= 200 && statusCode <= 299; + + if (succeeded) + { + ++_succeededFiles; + } + else + { + _context.Result.LastError = _context.T("Admin.Common.FtpStatus", statusCode, response.StatusCode.ToString()); + _context.Log.Error("The FTP transfer failed. FTP status {0} ({1}). File {3}".FormatInvariant(statusCode, response.StatusCode.ToString(), path)); + } + } + + return succeeded; + } + + private bool DirectoryExists(string directoryUrl) + { + var result = false; + + try + { + var request = CreateRequest(directoryUrl.EnsureEndsWith("/")); + request.Method = WebRequestMethods.Ftp.ListDirectory; + + using (request.GetResponse()) + { + result = true; + } + } + catch (WebException) + { + result = false; + } + + return result; + } + + private bool FtpCopyDirectory(DirectoryInfo source) + { + var files = source.GetFiles(); + var len = files.Length; + + for (var i = 0; i < len; ++i) + { + var path = files[i].FullName; + var relativePath = GetRelativePath(path); + + UploadFile(path, _ftpRootUrl + relativePath, i != (len - 1)); + } + + foreach (var sourceSubDir in source.GetDirectories()) + { + var relativePath = GetRelativePath(sourceSubDir.FullName); + + if (!DirectoryExists(_ftpRootUrl + relativePath)) + { + var request = CreateRequest(_ftpRootUrl + relativePath, true); + request.Method = WebRequestMethods.Ftp.MakeDirectory; + using (var response = (FtpWebResponse)request.GetResponse()) + { + response.Close(); + } + } + + FtpCopyDirectory(sourceSubDir); + } + + return true; + } + + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + _context = context; + _deployment = deployment; + _succeededFiles = 0; + _ftpRootUrl = deployment.Url; + + if (!_ftpRootUrl.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase)) + { + _ftpRootUrl = "ftp://" + _ftpRootUrl; + } + + _ftpRootUrl = _ftpRootUrl.EnsureEndsWith("/"); + + if (context.CreateZipArchive) + { + if (File.Exists(context.ZipPath)) + { + var fileUrl = _ftpRootUrl + Path.GetFileName(context.ZipPath); + UploadFile(context.ZipPath, fileUrl, false); + } + } + else + { + FtpCopyDirectory(new DirectoryInfo(context.FolderContent)); + } + + context.Log.Info("{0} file(s) successfully uploaded via FTP.".FormatInvariant(_succeededFiles)); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs index 9bcdf2f5fd..a484b85a50 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs @@ -8,70 +8,70 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - public class HttpFilePublisher : IFilePublisher - { - public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) - { - var succeededFiles = 0; - var url = deployment.Url; + public class HttpFilePublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var succeededFiles = 0; + var url = deployment.Url; - if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) - url = "http://" + url; + if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) + url = "http://" + url; - if (deployment.HttpTransmissionType == ExportHttpTransmissionType.MultipartFormDataPost) - { - var countFiles = 0; - ICredentials credentials = null; + if (deployment.HttpTransmissionType == ExportHttpTransmissionType.MultipartFormDataPost) + { + var countFiles = 0; + ICredentials credentials = null; - if (deployment.Username.HasValue()) - credentials = new NetworkCredential(deployment.Username, deployment.Password); + if (deployment.Username.HasValue()) + credentials = new NetworkCredential(deployment.Username, deployment.Password); - using (var handler = new HttpClientHandler { Credentials = credentials }) - using (var client = new HttpClient(handler)) - using (var formData = new MultipartFormDataContent()) - { - foreach (var path in context.GetDeploymentFiles()) - { - byte[] fileData = File.ReadAllBytes(path); - formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++countFiles), Path.GetFileName(path)); - } + using (var handler = new HttpClientHandler { Credentials = credentials }) + using (var client = new HttpClient(handler)) + using (var formData = new MultipartFormDataContent()) + { + foreach (var path in context.GetDeploymentFiles()) + { + byte[] fileData = File.ReadAllBytes(path); + formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++countFiles), Path.GetFileName(path)); + } - var response = client.PostAsync(url, formData).Result; + var response = client.PostAsync(url, formData).Result; - if (response.IsSuccessStatusCode) - { - succeededFiles = countFiles; - } - else if (response.Content != null) - { - context.Result.LastError = context.T("Admin.Common.HttpStatus", (int)response.StatusCode, response.StatusCode.ToString()); + if (response.IsSuccessStatusCode) + { + succeededFiles = countFiles; + } + else if (response.Content != null) + { + context.Result.LastError = context.T("Admin.Common.HttpStatus", (int)response.StatusCode, response.StatusCode.ToString()); - var content = response.Content.ReadAsStringAsync().Result; + var content = response.Content.ReadAsStringAsync().Result; - var msg = "Multipart form data upload failed. HTTP status {0} ({1}). Response: {2}".FormatInvariant( - (int)response.StatusCode, response.StatusCode.ToString(), content.NaIfEmpty().Truncate(2000, "...")); + var msg = "Multipart form data upload failed. HTTP status {0} ({1}). Response: {2}".FormatInvariant( + (int)response.StatusCode, response.StatusCode.ToString(), content.NaIfEmpty().Truncate(2000, "...")); - context.Log.Error(msg); - } - } - } - else - { - using (var webClient = new WebClient()) - { - if (deployment.Username.HasValue()) - webClient.Credentials = new NetworkCredential(deployment.Username, deployment.Password); + context.Log.Error(msg); + } + } + } + else + { + using (var webClient = new WebClient()) + { + if (deployment.Username.HasValue()) + webClient.Credentials = new NetworkCredential(deployment.Username, deployment.Password); - foreach (var path in context.GetDeploymentFiles()) - { - webClient.UploadFile(url, path); + foreach (var path in context.GetDeploymentFiles()) + { + webClient.UploadFile(url, path); - ++succeededFiles; - } - } - } + ++succeededFiles; + } + } + } - context.Log.Info("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeededFiles, deployment.HttpTransmissionType.ToString())); - } - } + context.Log.Info("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeededFiles, deployment.HttpTransmissionType.ToString())); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs index 1ae3301ec4..16bfa4d000 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs @@ -6,37 +6,37 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - public interface IFilePublisher - { - void Publish(ExportDeploymentContext context, ExportDeployment deployment); + public interface IFilePublisher + { + void Publish(ExportDeploymentContext context, ExportDeployment deployment); } - public class ExportDeploymentContext - { - public Localizer T { get; set; } - public ILogger Log { get; set; } + public class ExportDeploymentContext + { + public Localizer T { get; set; } + public ILogger Log { get; set; } - public string FolderContent { get; set; } + public string FolderContent { get; set; } - public string ZipPath { get; set; } - public bool CreateZipArchive { get; set; } + public string ZipPath { get; set; } + public bool CreateZipArchive { get; set; } - public DataDeploymentResult Result { get; set; } + public DataDeploymentResult Result { get; set; } - public IEnumerable GetDeploymentFiles() - { - if (!CreateZipArchive) - { - return System.IO.Directory.EnumerateFiles(FolderContent, "*", SearchOption.AllDirectories); - } + public IEnumerable GetDeploymentFiles() + { + if (!CreateZipArchive) + { + return System.IO.Directory.EnumerateFiles(FolderContent, "*", SearchOption.AllDirectories); + } - if (File.Exists(ZipPath)) - { - return new string[] { ZipPath }; - } + if (File.Exists(ZipPath)) + { + return new string[] { ZipPath }; + } - return new string[0]; - } - } + return new string[0]; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs index 7e9d67beb3..f5143a2033 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs @@ -5,40 +5,40 @@ namespace SmartStore.Services.DataExchange.Export.Deployment { - public class PublicFolderPublisher : IFilePublisher - { - public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) - { - var destinationFolder = deployment.GetDeploymentFolder(true); + public class PublicFolderPublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var destinationFolder = deployment.GetDeploymentFolder(true); - if (destinationFolder.IsEmpty()) - return; + if (destinationFolder.IsEmpty()) + return; - if (!System.IO.Directory.Exists(destinationFolder)) - { - System.IO.Directory.CreateDirectory(destinationFolder); - } + if (!System.IO.Directory.Exists(destinationFolder)) + { + System.IO.Directory.CreateDirectory(destinationFolder); + } - if (context.CreateZipArchive) - { - if (File.Exists(context.ZipPath)) - { - var destinationFile = Path.Combine(destinationFolder, Path.GetFileName(context.ZipPath)); + if (context.CreateZipArchive) + { + if (File.Exists(context.ZipPath)) + { + var destinationFile = Path.Combine(destinationFolder, Path.GetFileName(context.ZipPath)); - File.Copy(context.ZipPath, destinationFile, true); + File.Copy(context.ZipPath, destinationFile, true); - context.Log.Info($"Copied zipped export data to {destinationFile}."); - } - } - else - { - if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(destinationFolder))) - { - context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); - } + context.Log.Info($"Copied zipped export data to {destinationFile}."); + } + } + else + { + if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(destinationFolder))) + { + context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); + } - context.Log.Info($"Copied export data files to {destinationFolder}."); - } - } - } + context.Log.Info($"Copied export data files to {destinationFolder}."); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index a76f2a865f..946769b317 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -32,60 +32,61 @@ using SmartStore.Services.DataExchange.Export.Internal; using SmartStore.Services.Localization; using SmartStore.Services.Media; +using SmartStore.Services.Media.Imaging; using SmartStore.Services.Seo; using SmartStore.Utilities; namespace SmartStore.Services.DataExchange.Export { public partial class DataExporter - { - private readonly string[] _orderCustomerAttributes = new string[] - { - SystemCustomerAttributeNames.VatNumber, + { + private readonly string[] _orderCustomerAttributes = new string[] + { + SystemCustomerAttributeNames.VatNumber, SystemCustomerAttributeNames.ImpersonatedCustomerId - }; - - private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObject, Product product) - { - try - { - var languageId = ctx.LanguageId; - string description = ""; - - // Description merging. - if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.None) - { - // Export empty description. - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty) - { - description = dynObject.FullDescription; - - if (description.IsEmpty()) - description = dynObject.ShortDescription; - if (description.IsEmpty()) - description = dynObject.Name; - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescription) - { - description = dynObject.ShortDescription; - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.Description) - { - description = dynObject.FullDescription; - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndShortDescription) - { - description = ((string)dynObject.Name).Grow((string)dynObject.ShortDescription, " "); - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndDescription) - { - description = ((string)dynObject.Name).Grow((string)dynObject.FullDescription, " "); - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription || - ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndDescription) - { - var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); + }; + + private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObject, Product product) + { + try + { + var languageId = ctx.LanguageId; + string description = ""; + + // Description merging. + if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.None) + { + // Export empty description. + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty) + { + description = dynObject.FullDescription; + + if (description.IsEmpty()) + description = dynObject.ShortDescription; + if (description.IsEmpty()) + description = dynObject.Name; + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescription) + { + description = dynObject.ShortDescription; + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.Description) + { + description = dynObject.FullDescription; + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndShortDescription) + { + description = ((string)dynObject.Name).Grow((string)dynObject.ShortDescription, " "); + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndDescription) + { + description = ((string)dynObject.Name).Grow((string)dynObject.FullDescription, " "); + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription || + ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndDescription) + { + var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); if (productManus != null && productManus.Any()) { @@ -94,90 +95,90 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec description = translations.GetValue(languageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; } - description = description.Grow((string)dynObject.Name, " "); + description = description.Grow((string)dynObject.Name, " "); description = ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription ? description.Grow((string)dynObject.ShortDescription, " ") : description.Grow((string)dynObject.FullDescription, " "); - } - - // Append text. - if (ctx.Projection.AppendDescriptionText.HasValue() && ((string)dynObject.ShortDescription).IsEmpty() && ((string)dynObject.FullDescription).IsEmpty()) - { - string[] appendText = ctx.Projection.AppendDescriptionText.SplitSafe(","); - if (appendText.Length > 0) - { - var rnd = CommonHelper.GenerateRandomInteger(0, appendText.Length - 1); - description = description.Grow(appendText.SafeGet(rnd), " "); - } - } - - // Remove critical characters. - if (description.HasValue() && ctx.Projection.RemoveCriticalCharacters) - { + } + + // Append text. + if (ctx.Projection.AppendDescriptionText.HasValue() && ((string)dynObject.ShortDescription).IsEmpty() && ((string)dynObject.FullDescription).IsEmpty()) + { + string[] appendText = ctx.Projection.AppendDescriptionText.SplitSafe(","); + if (appendText.Length > 0) + { + var rnd = CommonHelper.GenerateRandomInteger(0, appendText.Length - 1); + description = description.Grow(appendText.SafeGet(rnd), " "); + } + } + + // Remove critical characters. + if (description.HasValue() && ctx.Projection.RemoveCriticalCharacters) + { foreach (var str in ctx.Projection.CriticalCharacters.SplitSafe(",")) { description = description.Replace(str, ""); } - } - - // Convert to plain text. - if (description.HasValue() && ctx.Projection.DescriptionToPlainText) - { - //Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); - //description = HttpUtility.HtmlDecode(reg.Replace(description, "")); - - description = HtmlUtils.ConvertHtmlToPlainText(description); - description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); - } - - dynObject.FullDescription = description.TrimSafe(); - } - catch { } - } - - private decimal? ConvertPrice(DataExporterContext ctx, Product product, decimal? price) - { - if (price.HasValue) - { - if (ctx.Projection.ConvertNetToGrossPrices) - { - decimal taxRate; - price = _taxService.Value.GetProductPrice(product, product.TaxCategoryId, price.Value, true, ctx.ContextCustomer, ctx.ContextCurrency, - _taxSettings.Value.PricesIncludeTax, out taxRate); - } - - if (price != decimal.Zero) - { - price = _currencyService.Value.ConvertFromPrimaryStoreCurrency(price.Value, ctx.ContextCurrency, ctx.Store); - } - } - - return price; - } - - private decimal CalculatePrice( - DataExporterContext ctx, - Product product, - ProductVariantAttributeCombination combination, - ICollection attributeValues) - { - var price = product.Price; - var productContext = ctx.ProductExportContext as PriceCalculationContext; + } + + // Convert to plain text. + if (description.HasValue() && ctx.Projection.DescriptionToPlainText) + { + //Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); + //description = HttpUtility.HtmlDecode(reg.Replace(description, "")); + + description = HtmlUtils.ConvertHtmlToPlainText(description); + description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); + } + + dynObject.FullDescription = description.TrimSafe(); + } + catch { } + } + + private decimal? ConvertPrice(DataExporterContext ctx, Product product, decimal? price) + { + if (price.HasValue) + { + if (ctx.Projection.ConvertNetToGrossPrices) + { + decimal taxRate; + price = _taxService.Value.GetProductPrice(product, product.TaxCategoryId, price.Value, true, ctx.ContextCustomer, ctx.ContextCurrency, + _taxSettings.Value.PricesIncludeTax, out taxRate); + } + + if (price != decimal.Zero) + { + price = _currencyService.Value.ConvertFromPrimaryStoreCurrency(price.Value, ctx.ContextCurrency, ctx.Store); + } + } + + return price; + } + + private decimal CalculatePrice( + DataExporterContext ctx, + Product product, + ProductVariantAttributeCombination combination, + ICollection attributeValues) + { + var price = product.Price; + var productContext = ctx.ProductExportContext as PriceCalculationContext; var associatedProductContext = ctx.AssociatedProductContext as PriceCalculationContext; if (combination != null) - { - // price for attribute combination - var attributesTotalPriceBase = decimal.Zero; + { + // price for attribute combination + var attributesTotalPriceBase = decimal.Zero; - if (attributeValues != null) - { - attributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.Value.GetProductVariantAttributeValuePriceAdjustment(x, product, ctx.ContextCustomer, productContext)); - } + if (attributeValues != null) + { + attributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.Value.GetProductVariantAttributeValuePriceAdjustment(x, product, ctx.ContextCustomer, productContext)); + } - price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, attributesTotalPriceBase, true, 1, null, productContext); - } - else if (ctx.Projection.PriceType.HasValue) + price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, attributesTotalPriceBase, true, 1, null, productContext); + } + else if (ctx.Projection.PriceType.HasValue) { var priceType = ctx.Projection.PriceType.Value; @@ -217,10 +218,10 @@ private decimal CalculatePrice( price = _priceCalculationService.Value.GetLowestPrice(product, ctx.ContextCustomer, productContext, out _); } } - } + } - return ConvertPrice(ctx, product, price) ?? price; - } + return ConvertPrice(ctx, product, price) ?? price; + } private List GetLocalized( DataExporterContext ctx, @@ -285,205 +286,205 @@ private List GetLocalized( } private dynamic ToDynamic(DataExporterContext ctx, ExportProfile profile) - { - if (profile == null) - return null; + { + if (profile == null) + return null; - dynamic result = new DynamicEntity(profile); - return result; - } + dynamic result = new DynamicEntity(profile); + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Currency currency) - { + private dynamic ToDynamic(DataExporterContext ctx, Currency currency) + { if (currency == null) { return null; } - dynamic result = new DynamicEntity(currency); + dynamic result = new DynamicEntity(currency); var translations = ctx.Translations[nameof(Currency)]; result.Name = translations.GetValue(ctx.LanguageId, currency.Id, nameof(currency.Name)) ?? currency.Name; result._Localized = GetLocalized(ctx, translations, null, currency, x => x.Name); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Language language) - { + private dynamic ToDynamic(DataExporterContext ctx, Language language) + { if (language == null) { return null; } - dynamic result = new DynamicEntity(language); - return result; - } + dynamic result = new DynamicEntity(language); + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Country country) - { + private dynamic ToDynamic(DataExporterContext ctx, Country country) + { if (country == null) { return null; } - dynamic result = new DynamicEntity(country); + dynamic result = new DynamicEntity(country); var translations = ctx.Translations[nameof(Country)]; result.Name = translations.GetValue(ctx.LanguageId, country.Id, nameof(country.Name)) ?? country.Name; - result._Localized = GetLocalized(ctx, translations, null, country, x => x.Name); + result._Localized = GetLocalized(ctx, translations, null, country, x => x.Name); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Address address) - { + private dynamic ToDynamic(DataExporterContext ctx, Address address) + { if (address == null) { return null; } - dynamic result = new DynamicEntity(address); + dynamic result = new DynamicEntity(address); result.Country = ToDynamic(ctx, address.Country); - if (address.StateProvinceId.GetValueOrDefault() > 0) - { - dynamic sp = new DynamicEntity(address.StateProvince); + if (address.StateProvinceId.GetValueOrDefault() > 0) + { + dynamic sp = new DynamicEntity(address.StateProvince); var translations = ctx.Translations[nameof(StateProvince)]; - sp.Name = translations.GetValue(ctx.LanguageId, address.StateProvince.Id, nameof(StateProvince)) ?? address.StateProvince.Name; - sp._Localized = GetLocalized(ctx, translations, null, address.StateProvince, x => x.Name); + sp.Name = translations.GetValue(ctx.LanguageId, address.StateProvince.Id, nameof(StateProvince.Name)) ?? address.StateProvince.Name; + sp._Localized = GetLocalized(ctx, translations, null, address.StateProvince, x => x.Name); - result.StateProvince = sp; - } - else - { - result.StateProvince = null; - } + result.StateProvince = sp; + } + else + { + result.StateProvince = null; + } - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, RewardPointsHistory points) - { + private dynamic ToDynamic(DataExporterContext ctx, RewardPointsHistory points) + { if (points == null) { return null; } - dynamic result = new DynamicEntity(points); - return result; - } + dynamic result = new DynamicEntity(points); + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Customer customer) - { + private dynamic ToDynamic(DataExporterContext ctx, Customer customer) + { if (customer == null) { return null; } - dynamic result = new DynamicEntity(customer); + dynamic result = new DynamicEntity(customer); - result.BillingAddress = null; - result.ShippingAddress = null; - result.Addresses = null; + result.BillingAddress = null; + result.ShippingAddress = null; + result.Addresses = null; - result.RewardPointsHistory = null; - result._RewardPointsBalance = 0; + result.RewardPointsHistory = null; + result._RewardPointsBalance = 0; - result._GenericAttributes = null; - result._HasNewsletterSubscription = false; + result._GenericAttributes = null; + result._HasNewsletterSubscription = false; - result._FullName = null; - result._AvatarPictureUrl = null; + result._FullName = null; + result._AvatarPictureUrl = null; - result.CustomerRoles = customer.CustomerRoleMappings - .Select(x => - { - dynamic dyn = new DynamicEntity(x.CustomerRole); - return dyn; - }) - .ToList(); + result.CustomerRoles = customer.CustomerRoleMappings + .Select(x => + { + dynamic dyn = new DynamicEntity(x.CustomerRole); + return dyn; + }) + .ToList(); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Store store) - { + private dynamic ToDynamic(DataExporterContext ctx, Store store) + { if (store == null) { return null; } - dynamic result = new DynamicEntity(store); + dynamic result = new DynamicEntity(store); - result.PrimaryStoreCurrency = ToDynamic(ctx, store.PrimaryStoreCurrency); - result.PrimaryExchangeRateCurrency = ToDynamic(ctx, store.PrimaryExchangeRateCurrency); + result.PrimaryStoreCurrency = ToDynamic(ctx, store.PrimaryStoreCurrency); + result.PrimaryExchangeRateCurrency = ToDynamic(ctx, store.PrimaryExchangeRateCurrency); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, DeliveryTime deliveryTime) - { + private dynamic ToDynamic(DataExporterContext ctx, DeliveryTime deliveryTime) + { if (deliveryTime == null) { return null; } - dynamic result = new DynamicEntity(deliveryTime); + dynamic result = new DynamicEntity(deliveryTime); var translations = ctx.Translations[nameof(DeliveryTime)]; result.Name = translations.GetValue(ctx.LanguageId, deliveryTime.Id, nameof(deliveryTime.Name)) ?? deliveryTime.Name; - result._Localized = GetLocalized(ctx, translations, null, deliveryTime, x => x.Name); + result._Localized = GetLocalized(ctx, translations, null, deliveryTime, x => x.Name); - return result; - } + return result; + } - private void ToDeliveryTime(DataExporterContext ctx, dynamic parent, int? deliveryTimeId) - { - if (ctx.DeliveryTimes != null) - { + private void ToDeliveryTime(DataExporterContext ctx, dynamic parent, int? deliveryTimeId) + { + if (ctx.DeliveryTimes != null) + { parent.DeliveryTime = deliveryTimeId.HasValue && ctx.DeliveryTimes.ContainsKey(deliveryTimeId.Value) ? ToDynamic(ctx, ctx.DeliveryTimes[deliveryTimeId.Value]) : null; - } - } + } + } - private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) - { + private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) + { if (quantityUnit == null) { return null; } - dynamic result = new DynamicEntity(quantityUnit); + dynamic result = new DynamicEntity(quantityUnit); var translations = ctx.Translations[nameof(QuantityUnit)]; result.Name = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Name)) ?? quantityUnit.Name; result.NamePlural = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.NamePlural)) ?? quantityUnit.NamePlural; result.Description = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; - result._Localized = GetLocalized(ctx, translations, null, quantityUnit, - x => x.Name, + result._Localized = GetLocalized(ctx, translations, null, quantityUnit, + x => x.Name, x => x.NamePlural, - x => x.Description); + x => x.Description); - return result; - } + return result; + } - private void ToQuantityUnit(DataExporterContext ctx, dynamic parent, int? quantityUnitId) - { - if (ctx.QuantityUnits != null) - { + private void ToQuantityUnit(DataExporterContext ctx, dynamic parent, int? quantityUnitId) + { + if (ctx.QuantityUnits != null) + { parent.QuantityUnit = quantityUnitId.HasValue && ctx.QuantityUnits.ContainsKey(quantityUnitId.Value) ? ToDynamic(ctx, ctx.QuantityUnits[quantityUnitId.Value]) : null; - } - } + } + } - private dynamic ToDynamic(DataExporterContext ctx, MediaFile file, int thumbPictureSize, int detailsPictureSize) - { + private dynamic ToDynamic(DataExporterContext ctx, MediaFile file, int thumbPictureSize, int detailsPictureSize) + { return ToDynamic(ctx, _mediaService.Value.ConvertMediaFile(file), thumbPictureSize, detailsPictureSize); } @@ -516,7 +517,7 @@ private dynamic ToDynamic(DataExporterContext ctx, MediaFileInfo file, int thumb } private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttribute pva) - { + { if (pva == null) { return null; @@ -526,56 +527,56 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttribute pva) var attribute = pva.ProductAttribute; dynamic result = new DynamicEntity(pva); - dynamic dynAttribute = new DynamicEntity(attribute); + dynamic dynAttribute = new DynamicEntity(attribute); var paTranslations = ctx.TranslationsPerPage[nameof(ProductAttribute)]; var pvavTranslations = ctx.TranslationsPerPage[nameof(ProductVariantAttributeValue)]; dynAttribute.Name = paTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Name)) ?? attribute.Name; - dynAttribute.Description = paTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Description)) ?? attribute.Description; + dynAttribute.Description = paTranslations.GetValue(languageId, attribute.Id, nameof(attribute.Description)) ?? attribute.Description; - dynAttribute.Values = pva.ProductVariantAttributeValues - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic dyn = new DynamicEntity(x); + dynAttribute.Values = pva.ProductVariantAttributeValues + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); dyn.Name = pvavTranslations.GetValue(languageId, x.Id, nameof(x.Name)) ?? x.Name; - dyn._Localized = GetLocalized(ctx, pvavTranslations, null, x, y => y.Name); + dyn._Localized = GetLocalized(ctx, pvavTranslations, null, x, y => y.Name); - return dyn; - }) - .ToList(); + return dyn; + }) + .ToList(); - dynAttribute._Localized = GetLocalized(ctx, paTranslations, null, attribute, - x => x.Name, - x => x.Description); + dynAttribute._Localized = GetLocalized(ctx, paTranslations, null, attribute, + x => x.Name, + x => x.Description); - result.Attribute = dynAttribute; + result.Attribute = dynAttribute; - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttributeCombination pvac) - { - if (pvac == null) - return null; + private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttributeCombination pvac) + { + if (pvac == null) + return null; - dynamic result = new DynamicEntity(pvac); + dynamic result = new DynamicEntity(pvac); - ToDeliveryTime(ctx, result, pvac.DeliveryTimeId); - ToQuantityUnit(ctx, result, pvac.QuantityUnitId); + ToDeliveryTime(ctx, result, pvac.DeliveryTimeId); + ToQuantityUnit(ctx, result, pvac.QuantityUnitId); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) - { + private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) + { if (manufacturer == null) { return null; } - dynamic result = new DynamicEntity(manufacturer); + dynamic result = new DynamicEntity(manufacturer); var translations = ctx.Translations[nameof(Manufacturer)]; var urlRecords = ctx.UrlRecords[nameof(Manufacturer)]; @@ -583,105 +584,105 @@ private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) result.Name = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.Name)) ?? manufacturer.Name; if (!ctx.IsPreview) - { - result.SeName = ctx.UrlRecords[nameof(Manufacturer)].GetSlug(ctx.LanguageId, manufacturer.Id); + { + result.SeName = ctx.UrlRecords[nameof(Manufacturer)].GetSlug(ctx.LanguageId, manufacturer.Id); result.Description = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.Description)) ?? manufacturer.Description; result.BottomDescription = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.BottomDescription)) ?? manufacturer.BottomDescription; result.MetaKeywords = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaKeywords)) ?? manufacturer.MetaKeywords; - result.MetaDescription = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaDescription)) ?? manufacturer.MetaDescription; - result.MetaTitle = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaTitle)) ?? manufacturer.MetaTitle; + result.MetaDescription = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaDescription)) ?? manufacturer.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.LanguageId, manufacturer.Id, nameof(manufacturer.MetaTitle)) ?? manufacturer.MetaTitle; - result._Localized = GetLocalized(ctx, translations, urlRecords, manufacturer, - x => x.Name, - x => x.Description, + result._Localized = GetLocalized(ctx, translations, urlRecords, manufacturer, + x => x.Name, + x => x.Description, x => x.BottomDescription, x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle); - } + x => x.MetaDescription, + x => x.MetaTitle); + } - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Category category) - { + private dynamic ToDynamic(DataExporterContext ctx, Category category) + { if (category == null) { return null; } - dynamic result = new DynamicEntity(category); + dynamic result = new DynamicEntity(category); var translations = ctx.Translations[nameof(Category)]; var urlRecords = ctx.UrlRecords[nameof(Category)]; result.Picture = null; - result.Name = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.Name)) ?? category.Name; + result.Name = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.Name)) ?? category.Name; result.FullName = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.FullName)) ?? category.FullName; - if (!ctx.IsPreview) - { + if (!ctx.IsPreview) + { result.SeName = ctx.UrlRecords[nameof(Category)].GetSlug(ctx.LanguageId, category.Id); result.Description = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.Description)) ?? category.Description; - result.BottomDescription = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.BottomDescription)) ?? category.BottomDescription; - result.MetaKeywords = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaKeywords)) ?? category.MetaKeywords; - result.MetaDescription = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaDescription)) ?? category.MetaDescription; - result.MetaTitle = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaTitle)) ?? category.MetaTitle; - - result._CategoryTemplateViewPath = ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId) - ? ctx.CategoryTemplates[category.CategoryTemplateId] - : ""; - - result._Localized = GetLocalized(ctx, translations, urlRecords, category, - x => x.Name, - x => x.FullName, - x => x.Description, - x => x.BottomDescription, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle); - } - - return result; - } - - private dynamic ToDynamic(DataExporterContext ctx, Product product, string seName = null) - { + result.BottomDescription = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.BottomDescription)) ?? category.BottomDescription; + result.MetaKeywords = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaKeywords)) ?? category.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaDescription)) ?? category.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.LanguageId, category.Id, nameof(category.MetaTitle)) ?? category.MetaTitle; + + result._CategoryTemplateViewPath = ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId) + ? ctx.CategoryTemplates[category.CategoryTemplateId] + : ""; + + result._Localized = GetLocalized(ctx, translations, urlRecords, category, + x => x.Name, + x => x.FullName, + x => x.Description, + x => x.BottomDescription, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle); + } + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Product product, string seName = null) + { if (product == null) { return null; } - dynamic result = new DynamicEntity(product); + dynamic result = new DynamicEntity(product); var translations = ctx.TranslationsPerPage[nameof(Product)]; var urlRecords = ctx.UrlRecordsPerPage[nameof(Product)]; var localizedName = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.Name)) ?? product.Name; result.AppliedDiscounts = null; result.Downloads = null; - result.TierPrices = null; - result.ProductAttributes = null; - result.ProductAttributeCombinations = null; - result.ProductPictures = null; - result.ProductCategories = null; - result.ProductManufacturers = null; - result.ProductTags = null; - result.ProductSpecificationAttributes = null; - result.ProductBundleItems = null; + result.TierPrices = null; + result.ProductAttributes = null; + result.ProductAttributeCombinations = null; + result.ProductPictures = null; + result.ProductCategories = null; + result.ProductManufacturers = null; + result.ProductTags = null; + result.ProductSpecificationAttributes = null; + result.ProductBundleItems = null; result.Name = localizedName; - if (!ctx.IsPreview) - { - result.SeName = seName ?? ctx.UrlRecordsPerPage[nameof(Product)].GetSlug(ctx.LanguageId, product.Id); + if (!ctx.IsPreview) + { + result.SeName = seName ?? ctx.UrlRecordsPerPage[nameof(Product)].GetSlug(ctx.LanguageId, product.Id); result.ShortDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.ShortDescription)) ?? product.ShortDescription; - result.FullDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.FullDescription)) ?? product.FullDescription; - result.MetaKeywords = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaKeywords)) ?? product.MetaKeywords; - result.MetaDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaDescription)) ?? product.MetaDescription; - result.MetaTitle = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaTitle)) ?? product.MetaTitle; - result.BundleTitleText = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.BundleTitleText)) ?? product.BundleTitleText; + result.FullDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.FullDescription)) ?? product.FullDescription; + result.MetaKeywords = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaKeywords)) ?? product.MetaKeywords; + result.MetaDescription = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaDescription)) ?? product.MetaDescription; + result.MetaTitle = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.MetaTitle)) ?? product.MetaTitle; + result.BundleTitleText = translations.GetValue(ctx.LanguageId, product.Id, nameof(product.BundleTitleText)) ?? product.BundleTitleText; - result._ProductTemplateViewPath = ctx.ProductTemplates.ContainsKey(product.ProductTemplateId) - ? ctx.ProductTemplates[product.ProductTemplateId] - : string.Empty; + result._ProductTemplateViewPath = ctx.ProductTemplates.ContainsKey(product.ProductTemplateId) + ? ctx.ProductTemplates[product.ProductTemplateId] + : string.Empty; // A standard, language independent base price formatting: / var basePriceInfo = string.Empty; @@ -703,35 +704,45 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam result._BasePriceInfo = basePriceInfo; ToDeliveryTime(ctx, result, product.DeliveryTimeId); - ToQuantityUnit(ctx, result, product.QuantityUnitId); + ToQuantityUnit(ctx, result, product.QuantityUnitId); + + result.CountryOfOrigin = product.CountryOfOriginId.HasValue + ? ToDynamic(ctx, product.CountryOfOrigin) + : null; result._Localized = GetLocalized(ctx, translations, urlRecords, product, - x => x.Name, - x => x.ShortDescription, - x => x.FullDescription, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle, - x => x.BundleTitleText); - } - - return result; - } - - private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParent, DynamicProductContext productContext) - { - product.MergeWithCombination(productContext.Combination); - - var numberOfPictures = ctx.Projection.NumberOfMediaFiles ?? int.MaxValue; - var productDetailsPictureSize = ctx.Projection.PictureSize > 0 ? ctx.Projection.PictureSize : _mediaSettings.Value.ProductDetailsPictureSize; - var imageQuery = ctx.Projection.PictureSize > 0 ? new ProcessImageQuery { MaxWidth = ctx.Projection.PictureSize } : null; + x => x.Name, + x => x.ShortDescription, + x => x.FullDescription, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle, + x => x.BundleTitleText); + } + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParent, DynamicProductContext productContext) + { + product.MergeWithCombination(productContext.Combination); + + var numberOfPictures = ctx.Projection.NumberOfMediaFiles ?? int.MaxValue; + var productDetailsPictureSize = _mediaSettings.Value.ProductDetailsPictureSize; + ProcessImageQuery imageQuery = null; + + if (ctx.Projection.PictureSize > 0) + { + productDetailsPictureSize = _mediaSettings.Value.GetNextValidThumbnailSize(ctx.Projection.PictureSize); + imageQuery = new ProcessImageQuery { MaxWidth = productDetailsPictureSize }; + } IEnumerable productPictures = ctx.ProductExportContext.ProductPictures.GetOrLoad(product.Id); - var productManufacturers = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); - var productCategories = ctx.ProductExportContext.ProductCategories.GetOrLoad(product.Id); - var productAttributes = ctx.ProductExportContext.Attributes.GetOrLoad(product.Id); - var productTags = ctx.ProductExportContext.ProductTags.GetOrLoad(product.Id); - var specificationAttributes = ctx.ProductExportContext.SpecificationAttributes.GetOrLoad(product.Id); + var productManufacturers = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); + var productCategories = ctx.ProductExportContext.ProductCategories.GetOrLoad(product.Id); + var productAttributes = ctx.ProductExportContext.Attributes.GetOrLoad(product.Id); + var productTags = ctx.ProductExportContext.ProductTags.GetOrLoad(product.Id); + var specificationAttributes = ctx.ProductExportContext.SpecificationAttributes.GetOrLoad(product.Id); Multimap variantAttributes = null; ICollection variantAttributeValues = null; string attributesXml = null; @@ -747,10 +758,10 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen if (productContext.Combination != null) { var mediaIds = productContext.Combination.GetAssignedMediaIds(); - if (mediaIds.Any()) - { - productPictures = productPictures.Where(x => mediaIds.Contains(x.MediaFileId)); - } + if (mediaIds.Any()) + { + productPictures = productPictures.Where(x => mediaIds.Contains(x.MediaFileId)); + } attributesXml = productContext.Combination.AttributesXml; variantAttributes = _productAttributeParser.Value.DeserializeProductVariantAttributes(attributesXml); @@ -794,136 +805,136 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen else { dynObject._DetailUrl = productContext.AbsoluteProductUrl; - } - - dynObject.Price = CalculatePrice(ctx, product, productContext.Combination, variantAttributeValues); - - // Category path - { - var categoryPath = string.Empty; - var pc = productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault(); - - if (pc != null) - { - var node = _categoryService.Value.GetCategoryTree(pc.CategoryId, true, ctx.Store.Id); - if (node != null) - { - categoryPath = _categoryService.Value.GetCategoryPath(node, ctx.Projection.LanguageId, null, " > "); - } - } - - dynObject._CategoryPath = categoryPath; - } - - if (ctx.Countries != null) - { - dynObject.CountryOfOrigin = product.CountryOfOriginId.HasValue && ctx.Countries.ContainsKey(product.CountryOfOriginId.Value) - ? ToDynamic(ctx, ctx.Countries[product.CountryOfOriginId.Value]) - : null; - } - - dynObject.ProductPictures = productPictures - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic dyn = new DynamicEntity(x); - - dyn.Picture = ToDynamic(ctx, x.MediaFile, _mediaSettings.Value.ProductThumbPictureSize, productDetailsPictureSize); + } + + dynObject.Price = CalculatePrice(ctx, product, productContext.Combination, variantAttributeValues); + + // Category path + { + var categoryPath = string.Empty; + var pc = productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault(); + + if (pc != null) + { + var node = _categoryService.Value.GetCategoryTree(pc.CategoryId, true, ctx.Store.Id); + if (node != null) + { + categoryPath = _categoryService.Value.GetCategoryPath(node, ctx.Projection.LanguageId, null, " > "); + } + } + + dynObject._CategoryPath = categoryPath; + } + + if (ctx.Countries != null) + { + dynObject.CountryOfOrigin = product.CountryOfOriginId.HasValue && ctx.Countries.ContainsKey(product.CountryOfOriginId.Value) + ? ToDynamic(ctx, ctx.Countries[product.CountryOfOriginId.Value]) + : null; + } + + dynObject.ProductPictures = productPictures + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Picture = ToDynamic(ctx, x.MediaFile, _mediaSettings.Value.ProductThumbPictureSize, productDetailsPictureSize); + + return dyn; + }) + .ToList(); + + dynObject.ProductManufacturers = productManufacturers + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Manufacturer = ToDynamic(ctx, x.Manufacturer); + + dyn.Manufacturer.Picture = x.Manufacturer != null && x.Manufacturer.MediaFileId.HasValue + ? ToDynamic(ctx, x.Manufacturer.MediaFile, _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize) + : null; return dyn; - }) - .ToList(); + }) + .ToList(); + + dynObject.ProductCategories = productCategories + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); - dynObject.ProductManufacturers = productManufacturers - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic dyn = new DynamicEntity(x); + dyn.Category = ToDynamic(ctx, x.Category); - dyn.Manufacturer = ToDynamic(ctx, x.Manufacturer); + if (x.Category != null && x.Category.MediaFileId.HasValue) + dyn.Category.Picture = ToDynamic(ctx, x.Category.MediaFile, _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); - dyn.Manufacturer.Picture = x.Manufacturer != null && x.Manufacturer.MediaFileId.HasValue - ? ToDynamic(ctx, x.Manufacturer.MediaFile, _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize) - : null; + if (dynObject._CategoryName == null) + dynObject._CategoryName = (string)dyn.Category.Name; - return dyn; - }) - .ToList(); + return dyn; + }) + .ToList(); - dynObject.ProductCategories = productCategories - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic dyn = new DynamicEntity(x); + dynObject.ProductAttributes = productAttributes + .OrderBy(x => x.DisplayOrder) + .Select(x => ToDynamic(ctx, x)) + .ToList(); - dyn.Category = ToDynamic(ctx, x.Category); + // Do not export combinations if a combination is exported as a product. + if (productContext.Combinations != null && productContext.Combination == null) + { + dynObject.ProductAttributeCombinations = productContext.Combinations + .Select(x => + { + dynamic dyn = ToDynamic(ctx, x); + var assignedPictures = new List(); - if (x.Category != null && x.Category.MediaFileId.HasValue) - dyn.Category.Picture = ToDynamic(ctx, x.Category.MediaFile, _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); + foreach (int pictureId in x.GetAssignedMediaIds().Take(numberOfPictures)) + { + var assignedPicture = productPictures.FirstOrDefault(y => y.MediaFileId == pictureId); + if (assignedPicture != null && assignedPicture.MediaFile != null) + { + assignedPictures.Add(ToDynamic(ctx, assignedPicture.MediaFile, _mediaSettings.Value.ProductThumbPictureSize, productDetailsPictureSize)); + } + } - if (dynObject._CategoryName == null) - dynObject._CategoryName = (string)dyn.Category.Name; + dyn.Pictures = assignedPictures; - return dyn; - }) - .ToList(); + return dyn; + }) + .ToList(); + } + else + { + dynObject.ProductAttributeCombinations = Enumerable.Empty(); + } - dynObject.ProductAttributes = productAttributes - .OrderBy(x => x.DisplayOrder) - .Select(x => ToDynamic(ctx, x)) - .ToList(); + if (product.HasTierPrices) + { + var tierPrices = ctx.ProductExportContext.TierPrices.GetOrLoad(product.Id) + .RemoveDuplicatedQuantities(); - // Do not export combinations if a combination is exported as a product. - if (productContext.Combinations != null && productContext.Combination == null) - { - dynObject.ProductAttributeCombinations = productContext.Combinations + dynObject.TierPrices = tierPrices .Select(x => - { - dynamic dyn = ToDynamic(ctx, x); - var assignedPictures = new List(); - - foreach (int pictureId in x.GetAssignedMediaIds().Take(numberOfPictures)) - { - var assignedPicture = productPictures.FirstOrDefault(y => y.MediaFileId == pictureId); - if (assignedPicture != null && assignedPicture.MediaFile != null) - { - assignedPictures.Add(ToDynamic(ctx, assignedPicture.MediaFile, _mediaSettings.Value.ProductThumbPictureSize, productDetailsPictureSize)); - } - } - - dyn.Pictures = assignedPictures; - - return dyn; - }) - .ToList(); - } - else - { - dynObject.ProductAttributeCombinations = Enumerable.Empty(); - } - - if (product.HasTierPrices) - { - var tierPrices = ctx.ProductExportContext.TierPrices.GetOrLoad(product.Id) - .RemoveDuplicatedQuantities(); - - dynObject.TierPrices = tierPrices - .Select(x => - { - dynamic dyn = new DynamicEntity(x); - return dyn; - }) - .ToList(); - } - - if (product.HasDiscountsApplied) - { - var appliedDiscounts = ctx.ProductExportContext.AppliedDiscounts.GetOrLoad(product.Id); - - dynObject.AppliedDiscounts = appliedDiscounts - .Select(x => ToDynamic(ctx, x)) - .ToList(); - } + { + dynamic dyn = new DynamicEntity(x); + return dyn; + }) + .ToList(); + } + + if (product.HasDiscountsApplied) + { + var appliedDiscounts = ctx.ProductExportContext.AppliedDiscounts.GetOrLoad(product.Id); + + dynObject.AppliedDiscounts = appliedDiscounts + .Select(x => ToDynamic(ctx, x)) + .ToList(); + } if (product.IsDownload) { @@ -935,56 +946,56 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen } dynObject.ProductTags = productTags - .Select(x => - { - dynamic dyn = new DynamicEntity(x); + .Select(x => + { + dynamic dyn = new DynamicEntity(x); var translations = ctx.TranslationsPerPage[nameof(ProductTag)]; var localizedName = translations.GetValue(ctx.LanguageId, x.Id, nameof(x.Name)) ?? x.Name; dyn.Name = localizedName; - dyn.SeName = SeoExtensions.GetSeName(localizedName, _seoSettings.Value); - dyn._Localized = GetLocalized(ctx, translations, null, x, y => y.Name); + dyn.SeName = SeoExtensions.GetSeName(localizedName, _seoSettings.Value); + dyn._Localized = GetLocalized(ctx, translations, null, x, y => y.Name); - return dyn; - }) - .ToList(); + return dyn; + }) + .ToList(); - dynObject.ProductSpecificationAttributes = specificationAttributes - .Select(x => ToDynamic(ctx, x)) - .ToList(); + dynObject.ProductSpecificationAttributes = specificationAttributes + .Select(x => ToDynamic(ctx, x)) + .ToList(); - if (product.ProductType == ProductType.BundledProduct) - { - var bundleItems = ctx.ProductExportContext.ProductBundleItems.GetOrLoad(product.Id); + if (product.ProductType == ProductType.BundledProduct) + { + var bundleItems = ctx.ProductExportContext.ProductBundleItems.GetOrLoad(product.Id); - dynObject.ProductBundleItems = bundleItems - .Select(x => - { - dynamic dyn = new DynamicEntity(x); + dynObject.ProductBundleItems = bundleItems + .Select(x => + { + dynamic dyn = new DynamicEntity(x); var translations = ctx.TranslationsPerPage[nameof(ProductBundleItem)]; dyn.Name = translations.GetValue(ctx.LanguageId, x.Id, nameof(x.Name)) ?? x.Name; dyn.ShortDescription = translations.GetValue(ctx.LanguageId, x.Id, nameof(x.ShortDescription)) ?? x.ShortDescription; - dyn._Localized = GetLocalized(ctx, translations, null, x, y => y.Name, y => y.ShortDescription); + dyn._Localized = GetLocalized(ctx, translations, null, x, y => y.Name, y => y.ShortDescription); - return dyn; - }) - .ToList(); - } + return dyn; + }) + .ToList(); + } - #endregion + #endregion - #region More data based on export features + #region More data based on export features - if (ctx.Supports(ExportFeatures.CanProjectDescription)) - { - PrepareProductDescription(ctx, dynObject, product); - } + if (ctx.Supports(ExportFeatures.CanProjectDescription)) + { + PrepareProductDescription(ctx, dynObject, product); + } - if (ctx.Supports(ExportFeatures.OffersBrandFallback)) - { - string brand = null; - var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); + if (ctx.Supports(ExportFeatures.OffersBrandFallback)) + { + string brand = null; + var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); if (productManus != null && productManus.Any()) { @@ -997,187 +1008,187 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParen brand = ctx.Projection.Brand; } - dynObject._Brand = brand; - } - - if (ctx.Supports(ExportFeatures.CanIncludeMainPicture)) - { - if (productPictures != null && productPictures.Any()) - { + dynObject._Brand = brand; + } + + if (ctx.Supports(ExportFeatures.CanIncludeMainPicture)) + { + if (productPictures != null && productPictures.Any()) + { var file = _mediaService.Value.ConvertMediaFile(productPictures.Select(x => x.MediaFile).First()); dynObject._MainPictureUrl = _mediaService.Value.GetUrl(file, imageQuery, _services.StoreService.GetHost(ctx.Store)); dynObject._MainPictureRelativeUrl = _mediaService.Value.GetUrl(file, imageQuery); - } - else if (!_catalogSettings.Value.HideProductDefaultPictures) - { + } + else if (!_catalogSettings.Value.HideProductDefaultPictures) + { // Get fallback image URL. dynObject._MainPictureUrl = _mediaService.Value.GetUrl(null, imageQuery, _services.StoreService.GetHost(ctx.Store)); dynObject._MainPictureRelativeUrl = _mediaService.Value.GetUrl(null, imageQuery); } - else - { - dynObject._MainPictureUrl = null; - dynObject._MainPictureRelativeUrl = null; - } - } - - if (ctx.Supports(ExportFeatures.UsesSkuAsMpnFallback) && product.ManufacturerPartNumber.IsEmpty()) - { - dynObject.ManufacturerPartNumber = product.Sku; - } - - if (ctx.Supports(ExportFeatures.OffersShippingTimeFallback)) - { - dynamic deliveryTime = dynObject.DeliveryTime; - dynObject._ShippingTime = deliveryTime == null ? ctx.Projection.ShippingTime : deliveryTime.Name; - } - - if (ctx.Supports(ExportFeatures.OffersShippingCostsFallback)) - { - dynObject._FreeShippingThreshold = ctx.Projection.FreeShippingThreshold; - - if (product.IsFreeShipping || (ctx.Projection.FreeShippingThreshold.HasValue && (decimal)dynObject.Price >= ctx.Projection.FreeShippingThreshold.Value)) - dynObject._ShippingCosts = decimal.Zero; - else - dynObject._ShippingCosts = ctx.Projection.ShippingCosts; - } - - if (ctx.Supports(ExportFeatures.UsesOldPrice)) - { - if (product.OldPrice != decimal.Zero && product.OldPrice != (decimal)dynObject.Price && !(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) - { - if (ctx.Projection.ConvertNetToGrossPrices) - { - decimal taxRate; - dynObject._OldPrice = _taxService.Value.GetProductPrice(product, product.TaxCategoryId, product.OldPrice, true, ctx.ContextCustomer, - ctx.ContextCurrency, _taxSettings.Value.PricesIncludeTax, out taxRate); - } - else - { - dynObject._OldPrice = product.OldPrice; - } - } - else - { - dynObject._OldPrice = null; - } - } - - if (ctx.Supports(ExportFeatures.UsesSpecialPrice)) - { - dynObject._SpecialPrice = null; // special price which is valid now - dynObject._FutureSpecialPrice = null; // special price which is valid now and in future - dynObject._RegularPrice = null; // price as if a special price would not exist - - if (!(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) - { - if (product.SpecialPrice.HasValue && product.SpecialPriceEndDateTimeUtc.HasValue) - { - var endDate = DateTime.SpecifyKind(product.SpecialPriceEndDateTimeUtc.Value, DateTimeKind.Utc); - if (endDate > DateTime.UtcNow) - { - dynObject._FutureSpecialPrice = ConvertPrice(ctx, product, product.SpecialPrice.Value); - } - } - - var specialPrice = _priceCalculationService.Value.GetSpecialPrice(product); - - dynObject._SpecialPrice = ConvertPrice(ctx, product, specialPrice); - - if (specialPrice.HasValue || dynObject._FutureSpecialPrice != null) - { - decimal tmpSpecialPrice = product.SpecialPrice.Value; - product.SpecialPrice = null; - - dynObject._RegularPrice = CalculatePrice(ctx, product, productContext.Combination, variantAttributeValues); - - product.SpecialPrice = tmpSpecialPrice; - } - } - } - - #endregion - - return dynObject; - } - - private dynamic ToDynamic(DataExporterContext ctx, Order order) - { + else + { + dynObject._MainPictureUrl = null; + dynObject._MainPictureRelativeUrl = null; + } + } + + if (ctx.Supports(ExportFeatures.UsesSkuAsMpnFallback) && product.ManufacturerPartNumber.IsEmpty()) + { + dynObject.ManufacturerPartNumber = product.Sku; + } + + if (ctx.Supports(ExportFeatures.OffersShippingTimeFallback)) + { + dynamic deliveryTime = dynObject.DeliveryTime; + dynObject._ShippingTime = deliveryTime == null ? ctx.Projection.ShippingTime : deliveryTime.Name; + } + + if (ctx.Supports(ExportFeatures.OffersShippingCostsFallback)) + { + dynObject._FreeShippingThreshold = ctx.Projection.FreeShippingThreshold; + + if (product.IsFreeShipping || (ctx.Projection.FreeShippingThreshold.HasValue && (decimal)dynObject.Price >= ctx.Projection.FreeShippingThreshold.Value)) + dynObject._ShippingCosts = decimal.Zero; + else + dynObject._ShippingCosts = ctx.Projection.ShippingCosts; + } + + if (ctx.Supports(ExportFeatures.UsesOldPrice)) + { + if (product.OldPrice != decimal.Zero && product.OldPrice != (decimal)dynObject.Price && !(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) + { + if (ctx.Projection.ConvertNetToGrossPrices) + { + decimal taxRate; + dynObject._OldPrice = _taxService.Value.GetProductPrice(product, product.TaxCategoryId, product.OldPrice, true, ctx.ContextCustomer, + ctx.ContextCurrency, _taxSettings.Value.PricesIncludeTax, out taxRate); + } + else + { + dynObject._OldPrice = product.OldPrice; + } + } + else + { + dynObject._OldPrice = null; + } + } + + if (ctx.Supports(ExportFeatures.UsesSpecialPrice)) + { + dynObject._SpecialPrice = null; // special price which is valid now + dynObject._FutureSpecialPrice = null; // special price which is valid now and in future + dynObject._RegularPrice = null; // price as if a special price would not exist + + if (!(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) + { + if (product.SpecialPrice.HasValue && product.SpecialPriceEndDateTimeUtc.HasValue) + { + var endDate = DateTime.SpecifyKind(product.SpecialPriceEndDateTimeUtc.Value, DateTimeKind.Utc); + if (endDate > DateTime.UtcNow) + { + dynObject._FutureSpecialPrice = ConvertPrice(ctx, product, product.SpecialPrice.Value); + } + } + + var specialPrice = _priceCalculationService.Value.GetSpecialPrice(product); + + dynObject._SpecialPrice = ConvertPrice(ctx, product, specialPrice); + + if (specialPrice.HasValue || dynObject._FutureSpecialPrice != null) + { + decimal tmpSpecialPrice = product.SpecialPrice.Value; + product.SpecialPrice = null; + + dynObject._RegularPrice = CalculatePrice(ctx, product, productContext.Combination, variantAttributeValues); + + product.SpecialPrice = tmpSpecialPrice; + } + } + } + + #endregion + + return dynObject; + } + + private dynamic ToDynamic(DataExporterContext ctx, Order order) + { if (order == null) { return null; } - dynamic result = new DynamicEntity(order); + dynamic result = new DynamicEntity(order); - result.OrderNumber = order.GetOrderNumber(); - result.OrderStatus = order.OrderStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); - result.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); - result.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); + result.OrderNumber = order.GetOrderNumber(); + result.OrderStatus = order.OrderStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); + result.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); + result.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_services.Localization, ctx.LanguageId); - result.Customer = null; - result.BillingAddress = null; - result.ShippingAddress = null; - result.Shipments = null; + result.Customer = null; + result.BillingAddress = null; + result.ShippingAddress = null; + result.Shipments = null; - result.Store = ctx.Stores.ContainsKey(order.StoreId) - ? ToDynamic(ctx, ctx.Stores[order.StoreId]) - : null; + result.Store = ctx.Stores.ContainsKey(order.StoreId) + ? ToDynamic(ctx, ctx.Stores[order.StoreId]) + : null; - if (!ctx.IsPreview) - { - result.RedeemedRewardPointsEntry = ToDynamic(ctx, order.RedeemedRewardPointsEntry); - } + if (!ctx.IsPreview) + { + result.RedeemedRewardPointsEntry = ToDynamic(ctx, order.RedeemedRewardPointsEntry); + } - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) - { + private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) + { if (orderItem == null) { return null; } - dynamic result = new DynamicEntity(orderItem); + dynamic result = new DynamicEntity(orderItem); - orderItem.Product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser.Value); - result.Product = ToDynamic(ctx, orderItem.Product); + orderItem.Product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser.Value); + result.Product = ToDynamic(ctx, orderItem.Product); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Shipment shipment) - { + private dynamic ToDynamic(DataExporterContext ctx, Shipment shipment) + { if (shipment == null) { return null; } - dynamic result = new DynamicEntity(shipment); + dynamic result = new DynamicEntity(shipment); - result.ShipmentItems = shipment.ShipmentItems - .Select(x => - { - dynamic exp = new DynamicEntity(x); - return exp; - }) - .ToList(); + result.ShipmentItems = shipment.ShipmentItems + .Select(x => + { + dynamic exp = new DynamicEntity(x); + return exp; + }) + .ToList(); - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, Discount discount) - { + private dynamic ToDynamic(DataExporterContext ctx, Discount discount) + { if (discount == null) { return null; } - dynamic result = new DynamicEntity(discount); - return result; - } + dynamic result = new DynamicEntity(discount); + return result; + } private dynamic ToDynamic(DataExporterContext ctx, Download download) { @@ -1191,7 +1202,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Download download) } private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute psa) - { + { if (psa == null) { return null; @@ -1201,81 +1212,81 @@ private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute var attribute = option.SpecificationAttribute; dynamic result = new DynamicEntity(psa); - dynamic dynAttribute = new DynamicEntity(attribute); + dynamic dynAttribute = new DynamicEntity(attribute); var saTranslations = ctx.TranslationsPerPage[nameof(SpecificationAttribute)]; var saoTranslations = ctx.TranslationsPerPage[nameof(SpecificationAttributeOption)]; dynAttribute.Name = saTranslations.GetValue(ctx.LanguageId, attribute.Id, nameof(attribute.Name)) ?? attribute.Name; - dynAttribute._Localized = GetLocalized(ctx, saTranslations, null, attribute, x => x.Name); + dynAttribute._Localized = GetLocalized(ctx, saTranslations, null, attribute, x => x.Name); - dynAttribute.Alias = saTranslations.GetValue(ctx.LanguageId, attribute.Id, nameof(attribute.Alias)) ?? attribute.Alias; - dynAttribute._Localized = GetLocalized(ctx, saTranslations, null, attribute, x => x.Alias); + dynAttribute.Alias = saTranslations.GetValue(ctx.LanguageId, attribute.Id, nameof(attribute.Alias)) ?? attribute.Alias; + dynAttribute._Localized = GetLocalized(ctx, saTranslations, null, attribute, x => x.Alias); dynamic dynOption = new DynamicEntity(option); dynOption.Name = saoTranslations.GetValue(ctx.LanguageId, option.Id, nameof(option.Name)) ?? option.Name; - dynOption._Localized = GetLocalized(ctx, saoTranslations, null, option, x => x.Name); + dynOption._Localized = GetLocalized(ctx, saoTranslations, null, option, x => x.Name); - dynOption.Alias = saoTranslations.GetValue(ctx.LanguageId, option.Id, nameof(option.Alias)) ?? option.Alias; - dynOption._Localized = GetLocalized(ctx, saoTranslations, null, option, x => x.Alias); + dynOption.Alias = saoTranslations.GetValue(ctx.LanguageId, option.Id, nameof(option.Alias)) ?? option.Alias; + dynOption._Localized = GetLocalized(ctx, saoTranslations, null, option, x => x.Alias); - dynOption.SpecificationAttribute = dynAttribute; - result.SpecificationAttributeOption = dynOption; + dynOption.SpecificationAttribute = dynAttribute; + result.SpecificationAttributeOption = dynOption; - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, GenericAttribute genericAttribute) - { + private dynamic ToDynamic(DataExporterContext ctx, GenericAttribute genericAttribute) + { if (genericAttribute == null) { return null; } - dynamic result = new DynamicEntity(genericAttribute); - return result; - } + dynamic result = new DynamicEntity(genericAttribute); + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, NewsLetterSubscription subscription) - { + private dynamic ToDynamic(DataExporterContext ctx, NewsLetterSubscription subscription) + { if (subscription == null) { return null; } - dynamic result = new DynamicEntity(subscription); + dynamic result = new DynamicEntity(subscription); - result.Store = ctx.Stores.ContainsKey(subscription.StoreId) - ? ToDynamic(ctx, ctx.Stores[subscription.StoreId]) - : null; + result.Store = ctx.Stores.ContainsKey(subscription.StoreId) + ? ToDynamic(ctx, ctx.Stores[subscription.StoreId]) + : null; - return result; - } + return result; + } - private dynamic ToDynamic(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) - { + private dynamic ToDynamic(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) + { if (shoppingCartItem == null) { return null; } - dynamic result = new DynamicEntity(shoppingCartItem); + dynamic result = new DynamicEntity(shoppingCartItem); - shoppingCartItem.Product.MergeWithCombination(shoppingCartItem.AttributesXml, _productAttributeParser.Value); + shoppingCartItem.Product.MergeWithCombination(shoppingCartItem.AttributesXml, _productAttributeParser.Value); - result.Store = ctx.Stores.ContainsKey(shoppingCartItem.StoreId) - ? ToDynamic(ctx, ctx.Stores[shoppingCartItem.StoreId]) - : null; + result.Store = ctx.Stores.ContainsKey(shoppingCartItem.StoreId) + ? ToDynamic(ctx, ctx.Stores[shoppingCartItem.StoreId]) + : null; - result.Customer = ToDynamic(ctx, shoppingCartItem.Customer); - result.Product = ToDynamic(ctx, shoppingCartItem.Product); + result.Customer = ToDynamic(ctx, shoppingCartItem.Customer); + result.Product = ToDynamic(ctx, shoppingCartItem.Product); - return result; - } + return result; + } - private List Convert(DataExporterContext ctx, Product product) - { + private List Convert(DataExporterContext ctx, Product product) + { var result = new List(); var productContext = new DynamicProductContext(); @@ -1290,281 +1301,281 @@ private List Convert(DataExporterContext ctx, Product product) ctx.ContextLanguage); if (ctx.Projection.AttributeCombinationAsProduct && productContext.Combinations.Where(x => x.IsActive).Any()) - { - if (ctx.Supports(ExportFeatures.UsesAttributeCombinationParent)) - { - var dynObject = ToDynamic(ctx, product, true, productContext); - result.Add(dynObject); - } - - var dbContext = _dbContext as DbContext; - - foreach (var combination in productContext.Combinations.Where(x => x.IsActive)) - { - product = _dbContext.Attach(product); - var entry = dbContext.Entry(product); - - // The returned object is not the entity and is not being tracked by the context. - // It also does not have any relationships set to other objects. - // CurrentValues only includes database (thus primitive) values. - var productClone = entry.CurrentValues.ToObject() as Product; - _dbContext.DetachEntity(product); + { + if (ctx.Supports(ExportFeatures.UsesAttributeCombinationParent)) + { + var dynObject = ToDynamic(ctx, product, true, productContext); + result.Add(dynObject); + } + + var dbContext = _dbContext as DbContext; + + foreach (var combination in productContext.Combinations.Where(x => x.IsActive)) + { + product = _dbContext.Attach(product); + var entry = dbContext.Entry(product); + + // The returned object is not the entity and is not being tracked by the context. + // It also does not have any relationships set to other objects. + // CurrentValues only includes database (thus primitive) values. + var productClone = entry.CurrentValues.ToObject() as Product; + _dbContext.DetachEntity(product); productContext.Combination = combination; - var dynObject = ToDynamic(ctx, productClone, false, productContext); - result.Add(dynObject); - } - } - else - { - var dynObject = ToDynamic(ctx, product, false, productContext); - result.Add(dynObject); - } - - if (result.Any()) - { - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = result.First(), - EntityType = ExportEntityType.Product, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); - } - - return result; - } - - private List Convert(DataExporterContext ctx, Order order) - { - var result = new List(); - - ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId.HasValue ? order.ShippingAddressId.Value : 0); - ctx.OrderExportContext.Addresses.GetOrLoad(order.BillingAddressId); - - var customers = ctx.OrderExportContext.Customers.GetOrLoad(order.CustomerId); - var genericAttributes = ctx.OrderExportContext.CustomerGenericAttributes.GetOrLoad(order.CustomerId); - var rewardPointsHistories = ctx.OrderExportContext.RewardPointsHistories.GetOrLoad(order.CustomerId); - var orderItems = ctx.OrderExportContext.OrderItems.GetOrLoad(order.Id); - var shipments = ctx.OrderExportContext.Shipments.GetOrLoad(order.Id); - - dynamic dynObject = ToDynamic(ctx, order); - - dynObject.Customer = ToDynamic(ctx, customers.FirstOrDefault(x => x.Id == order.CustomerId)); - - // We do not export all customer generic attributes because otherwise the export file gets too large. - dynObject.Customer._GenericAttributes = genericAttributes - .Where(x => x.Value.HasValue() && _orderCustomerAttributes.Contains(x.Key)) - .Select(x => ToDynamic(ctx, x)) - .ToList(); - - dynObject.Customer.RewardPointsHistory = rewardPointsHistories - .Select(x => ToDynamic(ctx, x)) - .ToList(); - - if (rewardPointsHistories.Count > 0) - { - dynObject.Customer._RewardPointsBalance = rewardPointsHistories - .OrderByDescending(x => x.CreatedOnUtc) - .ThenByDescending(x => x.Id) - .FirstOrDefault() - .PointsBalance; - } - - if (ctx.OrderExportContext.Addresses.ContainsKey(order.BillingAddressId)) - { - dynObject.BillingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.BillingAddressId].FirstOrDefault()); - } - - if (order.ShippingAddressId.HasValue && ctx.OrderExportContext.Addresses.ContainsKey(order.ShippingAddressId.Value)) - { - dynObject.ShippingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.ShippingAddressId.Value].FirstOrDefault()); - } - - dynObject.OrderItems = orderItems - .Select(x => ToDynamic(ctx, x)) - .ToList(); - - dynObject.Shipments = shipments - .Select(x => ToDynamic(ctx, x)) - .ToList(); - - result.Add(dynObject); - - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = dynObject, - EntityType = ExportEntityType.Order, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); - - return result; - } - - private List Convert(DataExporterContext ctx, Manufacturer manufacturer) - { - var result = new List(); - var productManufacturers = ctx.ManufacturerExportContext.ProductManufacturers.GetOrLoad(manufacturer.Id); - - dynamic dynObject = ToDynamic(ctx, manufacturer); - - if (manufacturer.MediaFileId.HasValue) - { - var numberOfFiles = ctx.Projection.NumberOfMediaFiles ?? int.MaxValue; - var files = ctx.ManufacturerExportContext.Files.GetOrLoad(manufacturer.MediaFileId.Value).Take(numberOfFiles); + var dynObject = ToDynamic(ctx, productClone, false, productContext); + result.Add(dynObject); + } + } + else + { + var dynObject = ToDynamic(ctx, product, false, productContext); + result.Add(dynObject); + } + + if (result.Any()) + { + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = result.First(), + EntityType = ExportEntityType.Product, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + } + + return result; + } + + private List Convert(DataExporterContext ctx, Order order) + { + var result = new List(); + + ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId.HasValue ? order.ShippingAddressId.Value : 0); + ctx.OrderExportContext.Addresses.GetOrLoad(order.BillingAddressId); + + var customers = ctx.OrderExportContext.Customers.GetOrLoad(order.CustomerId); + var genericAttributes = ctx.OrderExportContext.CustomerGenericAttributes.GetOrLoad(order.CustomerId); + var rewardPointsHistories = ctx.OrderExportContext.RewardPointsHistories.GetOrLoad(order.CustomerId); + var orderItems = ctx.OrderExportContext.OrderItems.GetOrLoad(order.Id); + var shipments = ctx.OrderExportContext.Shipments.GetOrLoad(order.Id); + + dynamic dynObject = ToDynamic(ctx, order); + + dynObject.Customer = ToDynamic(ctx, customers.FirstOrDefault(x => x.Id == order.CustomerId)); + + // We do not export all customer generic attributes because otherwise the export file gets too large. + dynObject.Customer._GenericAttributes = genericAttributes + .Where(x => x.Value.HasValue() && _orderCustomerAttributes.Contains(x.Key)) + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + dynObject.Customer.RewardPointsHistory = rewardPointsHistories + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + if (rewardPointsHistories.Count > 0) + { + dynObject.Customer._RewardPointsBalance = rewardPointsHistories + .OrderByDescending(x => x.CreatedOnUtc) + .ThenByDescending(x => x.Id) + .FirstOrDefault() + .PointsBalance; + } + + if (ctx.OrderExportContext.Addresses.ContainsKey(order.BillingAddressId)) + { + dynObject.BillingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.BillingAddressId].FirstOrDefault()); + } + + if (order.ShippingAddressId.HasValue && ctx.OrderExportContext.Addresses.ContainsKey(order.ShippingAddressId.Value)) + { + dynObject.ShippingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.ShippingAddressId.Value].FirstOrDefault()); + } + + dynObject.OrderItems = orderItems + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + dynObject.Shipments = shipments + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Order, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, Manufacturer manufacturer) + { + var result = new List(); + var productManufacturers = ctx.ManufacturerExportContext.ProductManufacturers.GetOrLoad(manufacturer.Id); + + dynamic dynObject = ToDynamic(ctx, manufacturer); + + if (manufacturer.MediaFileId.HasValue) + { + var numberOfFiles = ctx.Projection.NumberOfMediaFiles ?? int.MaxValue; + var files = ctx.ManufacturerExportContext.Files.GetOrLoad(manufacturer.MediaFileId.Value).Take(numberOfFiles); if (files.Any()) { dynObject.Picture = ToDynamic(ctx, files.First(), _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); } - } - - dynObject.ProductManufacturers = productManufacturers - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic dyn = new DynamicEntity(x); - return dyn; - }) - .ToList(); - - result.Add(dynObject); - - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = dynObject, - EntityType = ExportEntityType.Manufacturer, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); - - return result; - } - - private List Convert(DataExporterContext ctx, Category category) - { - var result = new List(); - var productCategories = ctx.CategoryExportContext.ProductCategories.GetOrLoad(category.Id); - - dynamic dynObject = ToDynamic(ctx, category); - - if (category.MediaFileId.HasValue) - { - var numberOfFiles = ctx.Projection.NumberOfMediaFiles ?? int.MaxValue; - var files = ctx.CategoryExportContext.Files.GetOrLoad(category.MediaFileId.Value).Take(numberOfFiles); + } + + dynObject.ProductManufacturers = productManufacturers + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + return dyn; + }) + .ToList(); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Manufacturer, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, Category category) + { + var result = new List(); + var productCategories = ctx.CategoryExportContext.ProductCategories.GetOrLoad(category.Id); + + dynamic dynObject = ToDynamic(ctx, category); + + if (category.MediaFileId.HasValue) + { + var numberOfFiles = ctx.Projection.NumberOfMediaFiles ?? int.MaxValue; + var files = ctx.CategoryExportContext.Files.GetOrLoad(category.MediaFileId.Value).Take(numberOfFiles); if (files.Any()) { dynObject.Picture = ToDynamic(ctx, files.First(), _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); } - } + } - dynObject.ProductCategories = productCategories - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic dyn = new DynamicEntity(x); - return dyn; - }) - .ToList(); + dynObject.ProductCategories = productCategories + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + return dyn; + }) + .ToList(); - result.Add(dynObject); + result.Add(dynObject); - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = dynObject, - EntityType = ExportEntityType.Category, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Category, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); - return result; - } + return result; + } - private List Convert(DataExporterContext ctx, Customer customer) - { - var result = new List(); + private List Convert(DataExporterContext ctx, Customer customer) + { + var result = new List(); - var genericAttributes = ctx.CustomerExportContext.GenericAttributes.GetOrLoad(customer.Id); + var genericAttributes = ctx.CustomerExportContext.GenericAttributes.GetOrLoad(customer.Id); - dynamic dynObject = ToDynamic(ctx, customer); + dynamic dynObject = ToDynamic(ctx, customer); - dynObject.BillingAddress = ToDynamic(ctx, customer.BillingAddress); - dynObject.ShippingAddress = ToDynamic(ctx, customer.ShippingAddress); + dynObject.BillingAddress = ToDynamic(ctx, customer.BillingAddress); + dynObject.ShippingAddress = ToDynamic(ctx, customer.ShippingAddress); - dynObject.Addresses = customer.Addresses - .Select(x => ToDynamic(ctx, x)) - .ToList(); + dynObject.Addresses = customer.Addresses + .Select(x => ToDynamic(ctx, x)) + .ToList(); - dynObject._GenericAttributes = genericAttributes - .Select(x => ToDynamic(ctx, x)) - .ToList(); + dynObject._GenericAttributes = genericAttributes + .Select(x => ToDynamic(ctx, x)) + .ToList(); - dynObject._HasNewsletterSubscription = ctx.NewsletterSubscriptions.Contains(customer.Email, StringComparer.CurrentCultureIgnoreCase); - dynObject._FullName = customer.GetFullName(); + dynObject._HasNewsletterSubscription = ctx.NewsletterSubscriptions.Contains(customer.Email, StringComparer.CurrentCultureIgnoreCase); + dynObject._FullName = customer.GetFullName(); dynObject._AvatarPictureUrl = null; if (_customerSettings.Value.AllowCustomersToUploadAvatars) { // Reduce traffic and do not export default avatar. var fileId = genericAttributes.FirstOrDefault(x => x.Key == SystemCustomerAttributeNames.AvatarPictureId)?.Value?.ToInt() ?? 0; - var file = _mediaService.Value.GetFileById(fileId); + var file = _mediaService.Value.GetFileById(fileId, MediaLoadFlags.AsNoTracking); if (file != null) { dynObject._AvatarPictureUrl = _mediaService.Value.GetUrl(file, new ProcessImageQuery { MaxSize = _mediaSettings.Value.AvatarPictureSize }, _services.StoreService.GetHost(ctx.Store)); } } - result.Add(dynObject); - - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = dynObject, - EntityType = ExportEntityType.Customer, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); - - return result; - } - - private List Convert(DataExporterContext ctx, NewsLetterSubscription subscription) - { - var result = new List(); - dynamic dynObject = ToDynamic(ctx, subscription); - result.Add(dynObject); - - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = dynObject, - EntityType = ExportEntityType.NewsLetterSubscription, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); - - return result; - } - - private List Convert(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) - { - var result = new List(); - dynamic dynObject = ToDynamic(ctx, shoppingCartItem); - - result.Add(dynObject); - - _services.EventPublisher.Publish(new RowExportingEvent - { - Row = dynObject, - EntityType = ExportEntityType.ShoppingCartItem, - ExportRequest = ctx.Request, - ExecuteContext = ctx.ExecuteContext - }); - - return result; - } - } + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Customer, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, NewsLetterSubscription subscription) + { + var result = new List(); + dynamic dynObject = ToDynamic(ctx, subscription); + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.NewsLetterSubscription, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) + { + var result = new List(); + dynamic dynObject = ToDynamic(ctx, shoppingCartItem); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.ShoppingCartItem, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + } internal class DynamicProductContext diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs index 0b96526e00..38db26a02b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs @@ -2,34 +2,34 @@ namespace SmartStore.Services.DataExchange.Export.Events { - // TODO: Another event message must be implemented, say 'ColumnsBuildingEvent' - // The consumer of this event (most likely a plugin) could push a list of specific column headers - // into the global export definition. + // TODO: Another event message must be implemented, say 'ColumnsBuildingEvent' + // The consumer of this event (most likely a plugin) could push a list of specific column headers + // into the global export definition. - public class RowExportingEvent - { - public dynamic Row - { - get; - internal set; - } + public class RowExportingEvent + { + public dynamic Row + { + get; + internal set; + } - public ExportEntityType EntityType - { - get; - internal set; - } + public ExportEntityType EntityType + { + get; + internal set; + } - public DataExportRequest ExportRequest - { - get; - internal set; - } + public DataExportRequest ExportRequest + { + get; + internal set; + } - public ExportExecuteContext ExecuteContext - { - get; - internal set; - } - } + public ExportExecuteContext ExecuteContext + { + get; + internal set; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs index 2cfb17796d..f9406c82ff 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs @@ -2,24 +2,24 @@ namespace SmartStore.Services.DataExchange.Export { - /// - /// Serves information about export provider specific configuration - /// - public class ExportConfigurationInfo - { - /// - /// The partial view name for the configuration - /// - public string PartialViewName { get; set; } + /// + /// Serves information about export provider specific configuration + /// + public class ExportConfigurationInfo + { + /// + /// The partial view name for the configuration + /// + public string PartialViewName { get; set; } - /// - /// Type of the view model - /// - public Type ModelType { get; set; } + /// + /// Type of the view model + /// + public Type ModelType { get; set; } - /// - /// Callback to initialize the view model. Can be null. - /// - public Action Initialize { get; set; } - } + /// + /// Callback to initialize the view model. Can be null. + /// + public Action Initialize { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs index cb44a39618..6fe190a54e 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs @@ -4,80 +4,80 @@ namespace SmartStore.Services.DataExchange.Export { - public interface IExportDataSegmenterConsumer - { - /// - /// Total number of records - /// - int TotalRecords { get; } - - /// - /// Gets current data segment - /// - IReadOnlyCollection CurrentSegment { get; } - - /// - /// Reads the next segment - /// - /// - bool ReadNextSegment(); - } - - internal interface IExportDataSegmenterProvider : IExportDataSegmenterConsumer, IDisposable - { - /// - /// Whether there is data available - /// - bool HasData { get; } - - /// - /// Record per segment counter - /// - int RecordPerSegmentCount { get; set; } - } - - public class ExportDataSegmenter : IExportDataSegmenterProvider where T : BaseEntity - { - private readonly Func> _load; - private readonly Action> _loaded; - private readonly Func> _convert; - - private readonly int _offset; - private readonly int _take; - private readonly int _limit; - private readonly int _recordsPerSegment; - private readonly int _totalRecords; + public interface IExportDataSegmenterConsumer + { + /// + /// Total number of records + /// + int TotalRecords { get; } + + /// + /// Gets current data segment + /// + IReadOnlyCollection CurrentSegment { get; } + + /// + /// Reads the next segment + /// + /// + bool ReadNextSegment(); + } + + internal interface IExportDataSegmenterProvider : IExportDataSegmenterConsumer, IDisposable + { + /// + /// Whether there is data available + /// + bool HasData { get; } + + /// + /// Record per segment counter + /// + int RecordPerSegmentCount { get; set; } + } + + public class ExportDataSegmenter : IExportDataSegmenterProvider where T : BaseEntity + { + private readonly Func> _load; + private readonly Action> _loaded; + private readonly Func> _convert; + + private readonly int _offset; + private readonly int _take; + private readonly int _limit; + private readonly int _recordsPerSegment; + private readonly int _totalRecords; private Queue _data; private bool _endOfData; - public ExportDataSegmenter( - Func> load, - Action> loaded, - Func> convert, - int offset, - int take, - int limit, - int recordsPerSegment, - int totalRecords) - { - _load = load; - _loaded = loaded; - _convert = convert; - _offset = offset; - _take = take; - _limit = limit; - _recordsPerSegment = recordsPerSegment; - _totalRecords = totalRecords; - } - - /// - /// Total number of records. - /// - public int TotalRecords - { - get - { + public ExportDataSegmenter( + Func> load, + Action> loaded, + Func> convert, + int offset, + int take, + int limit, + int recordsPerSegment, + int totalRecords) + { + _load = load; + _loaded = loaded; + _convert = convert; + _offset = offset; + _take = take; + _limit = limit; + _recordsPerSegment = recordsPerSegment; + _totalRecords = totalRecords; + } + + /// + /// Total number of records. + /// + public int TotalRecords + { + get + { var total = Math.Max(_totalRecords - _offset, 0); if (_limit > 0 && _limit < total) @@ -85,9 +85,9 @@ public int TotalRecords return _limit; } - return total; - } - } + return total; + } + } /// /// Number of processed records. @@ -99,13 +99,13 @@ public int TotalRecords /// public int RecordPerSegmentCount { get; set; } - /// - /// Whether there is data available. - /// - public bool HasData - { - get - { + /// + /// Whether there is data available. + /// + public bool HasData + { + get + { if (_limit > 0 && RecordCount >= _limit) { return false; @@ -121,23 +121,23 @@ public bool HasData return false; } - return true; - } - } + return RecordCount < TotalRecords; + } + } - /// - /// Gets current data segment. - /// - public IReadOnlyCollection CurrentSegment - { - get - { - T entity; - var records = new List(); + /// + /// Gets current data segment. + /// + public IReadOnlyCollection CurrentSegment + { + get + { + T entity; + var records = new List(); - while (_data.Count > 0 && (entity = _data.Dequeue()) != null) - { - _convert(entity).Each(x => records.Add(x)); + while (_data.Count > 0 && (entity = _data.Dequeue()) != null) + { + _convert(entity).Each(x => records.Add(x)); if (++RecordCount >= _limit && _limit > 0) { @@ -148,18 +148,18 @@ public IReadOnlyCollection CurrentSegment { return records; } - } + } - return records; - } - } + return records; + } + } /// /// Read next segment. /// /// true next segment available. false no more data. public bool ReadNextSegment() - { + { if (_limit > 0 && RecordCount >= _limit) { return false; @@ -200,22 +200,22 @@ public bool ReadNextSegment() _data = new Queue(newData); } - // Give provider the opportunity to make something with entity ids. - _loaded?.Invoke(_data.AsReadOnly()); + // Give provider the opportunity to make something with entity ids. + _loaded?.Invoke(_data.AsReadOnly()); - return _data.Count > 0; - } + return _data.Count > 0; + } + + /// + /// Dispose and reset segmenter instance. + /// + public void Dispose() + { + RecordCount = 0; + RecordPerSegmentCount = 0; - /// - /// Dispose and reset segmenter instance. - /// - public void Dispose() - { - RecordCount = 0; - RecordPerSegmentCount = 0; - _endOfData = false; _data?.Clear(); } - } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs index f28ec5b11f..7b18db60da 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs @@ -10,18 +10,18 @@ namespace SmartStore.Services.DataExchange.Export { public class ExportExecuteContext - { - private DataExportResult _result; - private CancellationToken _cancellation; - private DataExchangeAbortion _providerAbort; - - internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) - { - _result = result; - _cancellation = cancellation; - Folder = folder; - ExtraDataUnits = new List(); - CustomProperties = new Dictionary(); + { + private DataExportResult _result; + private CancellationToken _cancellation; + private DataExchangeAbortion _providerAbort; + + internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) + { + _result = result; + _cancellation = cancellation; + Folder = folder; + ExtraDataUnits = new List(); + CustomProperties = new Dictionary(); } /// @@ -29,93 +29,87 @@ internal ExportExecuteContext(DataExportResult result, CancellationToken cancell /// public int ProfileId { get; internal set; } - /// - /// The export profile - /// - public dynamic Profile { get; internal set; } - - /// - /// Provides the data to be exported - /// - public IExportDataSegmenterConsumer DataSegmenter { get; set; } - - /// - /// The store context to be used for the export - /// - public dynamic Store { get; internal set; } - - /// - /// The customer context to be used for the export - /// - public dynamic Customer { get; internal set; } - - /// - /// The currency context to be used for the export - /// - public dynamic Currency { get; internal set; } - - /// - /// The language context to be used for the export - /// - public dynamic Language { get; internal set; } - - /// - /// Filter settings - /// - public ExportFilter Filter { get; internal set; } - - /// - /// Projection settings - /// - public ExportProjection Projection { get; internal set; } - - /// - /// To log information into the export log file - /// - public ILogger Log { get; internal set; } - - /// - /// Indicates whether and how to abort the export - /// - public DataExchangeAbortion Abort - { - get - { - if (_cancellation.IsCancellationRequested || IsMaxFailures) - return DataExchangeAbortion.Hard; - - return _providerAbort; - } - set - { - _providerAbort = value; - } - } - - public bool IsMaxFailures - { - get { return RecordsFailed > 11; } - } - - /// - /// Identifier of current data stream. Can be null. - /// - public string DataStreamId { get; set; } - - /// - /// Stream used to write data to - /// - public Stream DataStream { get; internal set; } - - /// - /// List with extra data units/streams required by provider - /// - public List ExtraDataUnits { get; private set; } - - /// - /// The maximum allowed file name length - /// - public int MaxFileNameLength { get; internal set; } + /// + /// The export profile + /// + public dynamic Profile { get; internal set; } + + /// + /// Provides the data to be exported + /// + public IExportDataSegmenterConsumer DataSegmenter { get; set; } + + /// + /// The store context to be used for the export + /// + public dynamic Store { get; internal set; } + + /// + /// The customer context to be used for the export + /// + public dynamic Customer { get; internal set; } + + /// + /// The currency context to be used for the export + /// + public dynamic Currency { get; internal set; } + + /// + /// The language context to be used for the export + /// + public dynamic Language { get; internal set; } + + /// + /// Filter settings + /// + public ExportFilter Filter { get; internal set; } + + /// + /// Projection settings + /// + public ExportProjection Projection { get; internal set; } + + /// + /// To log information into the export log file + /// + public ILogger Log { get; internal set; } + + /// + /// Indicates whether and how to abort the export + /// + public DataExchangeAbortion Abort + { + get + { + if (_cancellation.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + + return _providerAbort; + } + set => _providerAbort = value; + } + + public bool IsMaxFailures => RecordsFailed > 11; + + /// + /// Identifier of current data stream. Can be null. + /// + public string DataStreamId { get; set; } + + /// + /// Stream used to write data to + /// + public Stream DataStream { get; internal set; } + + /// + /// List with extra data units/streams required by provider + /// + public List ExtraDataUnits { get; private set; } + + /// + /// The maximum allowed file name length + /// + public int MaxFileNameLength { get; internal set; } /// /// Index of current export file. @@ -127,60 +121,60 @@ public bool IsMaxFailures /// public string FileName { get; internal set; } - /// - /// The path of the export content folder - /// - public string Folder { get; private set; } - - /// - /// Whether the profile has a public deployment into "Exchange" folder - /// - public bool HasPublicDeployment { get; internal set; } - - /// - /// The local path to the public export folder "Exchange". null if the profile has no public deployment. - /// - public string PublicFolderPath { get; internal set; } - - /// - /// The URL of the public export folder "Exchange". null if the profile has no public deployment. - /// - public string PublicFolderUrl { get; internal set; } - - /// - /// Provider specific configuration data - /// - public object ConfigurationData { get; internal set; } - - /// - /// Use this dictionary for any custom data required along the export - /// - public Dictionary CustomProperties { get; set; } - - /// - /// Number of successful processed records - /// - public int RecordsSucceeded { get; set; } - - /// - /// Number of failed records - /// - public int RecordsFailed { get; set; } - - /// - /// Processes an exception that occurred while exporting a record - /// + /// + /// The path of the export content folder + /// + public string Folder { get; private set; } + + /// + /// Whether the profile has a public deployment into "Exchange" folder + /// + public bool HasPublicDeployment { get; internal set; } + + /// + /// The local path to the public export folder "Exchange". null if the profile has no public deployment. + /// + public string PublicFolderPath { get; internal set; } + + /// + /// The URL of the public export folder "Exchange". null if the profile has no public deployment. + /// + public string PublicFolderUrl { get; internal set; } + + /// + /// Provider specific configuration data + /// + public object ConfigurationData { get; internal set; } + + /// + /// Use this dictionary for any custom data required along the export + /// + public Dictionary CustomProperties { get; set; } + + /// + /// Number of successful processed records + /// + public int RecordsSucceeded { get; set; } + + /// + /// Number of failed records + /// + public int RecordsFailed { get; set; } + + /// + /// Processes an exception that occurred while exporting a record + /// /// Identifier of the current entity - /// Exception - public void RecordException(Exception exception, int entityId) - { - ++RecordsFailed; + /// Exception + public void RecordException(Exception exception, int entityId) + { + ++RecordsFailed; - Log.ErrorFormat("Error while processing record with id {0}. {1}".FormatInvariant(entityId, exception.ToString())); + Log.ErrorFormat("Error while processing record with id {0}. {1}".FormatInvariant(entityId, exception.ToString())); - if (IsMaxFailures) - _result.LastError = exception.ToString(); - } + if (IsMaxFailures) + _result.LastError = exception.ToString(); + } /// /// Processes an out-of-memory exception and hard aborts the export @@ -202,34 +196,34 @@ public void RecordOutOfMemoryException(OutOfMemoryException exception, int entit public ProgressValueSetter ProgressValueSetter { get; internal set; } - /// - /// Allows to set a progress message - /// - /// Output message - public void SetProgress(string message) - { - if (ProgressValueSetter != null && message.HasValue()) - { - try - { - ProgressValueSetter.Invoke(0, 0, message); - } - catch { } - } - } - } - - public class ExportDataUnit - { + /// + /// Allows to set a progress message + /// + /// Output message + public void SetProgress(string message) + { + if (ProgressValueSetter != null && message.HasValue()) + { + try + { + ProgressValueSetter.Invoke(0, 0, message); + } + catch { } + } + } + } + + public class ExportDataUnit + { /// /// Your Id to identify this stream within a list of streams /// public string Id { get; set; } - /// - /// Stream used to write data to - /// - public Stream DataStream { get; internal set; } + /// + /// Stream used to write data to + /// + public Stream DataStream { get; internal set; } /// /// The related entity type. @@ -241,15 +235,15 @@ public class ExportDataUnit /// public string FileName { get; set; } - /// - /// Short optional text that describes the content of the file - /// - public string Label { get; set; } + /// + /// Short optional text that describes the content of the file + /// + public string Label { get; set; } - /// - /// Whether to display the file in the profile file dialog - /// - public bool DisplayInFileDialog { get; set; } + /// + /// Whether to display the file in the profile file dialog + /// + public bool DisplayInFileDialog { get; set; } /// /// Number of successful processed records. diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs index 4c9fd6fdb1..6fc708c721 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs @@ -16,39 +16,39 @@ namespace SmartStore.Services.DataExchange.Export { public static class ExportExtensions - { - /// - /// Returns a value indicating whether the export provider is valid - /// - /// Export provider - /// true provider is valid, false provider is invalid. - public static bool IsValid(this Provider provider) - { - return provider != null && provider.Value != null; - } - - /// - /// Gets the localized friendly name or the system name as fallback - /// - /// Export provider - /// Localization service - /// Provider name - public static string GetName(this Provider provider, ILocalizationService localizationService) - { - var systemName = provider.Metadata.SystemName; - var resourceName = provider.Metadata.ResourceKeyPattern.FormatInvariant(systemName, "FriendlyName"); - var name = localizationService.GetResource(resourceName, 0, false, systemName, true); - - return (name.IsEmpty() ? systemName : name); - } - - /// - /// Get temporary folder for an export profile - /// - /// Export profile - /// Folder path - public static string GetExportFolder(this ExportProfile profile, bool content = false, bool create = false) - { + { + /// + /// Returns a value indicating whether the export provider is valid + /// + /// Export provider + /// true provider is valid, false provider is invalid. + public static bool IsValid(this Provider provider) + { + return provider != null && provider.Value != null; + } + + /// + /// Gets the localized friendly name or the system name as fallback + /// + /// Export provider + /// Localization service + /// Provider name + public static string GetName(this Provider provider, ILocalizationService localizationService) + { + var systemName = provider.Metadata.SystemName; + var resourceName = provider.Metadata.ResourceKeyPattern.FormatInvariant(systemName, "FriendlyName"); + var name = localizationService.GetResource(resourceName, 0, false, systemName, true); + + return (name.IsEmpty() ? systemName : name); + } + + /// + /// Get temporary folder for an export profile + /// + /// Export profile + /// Folder path + public static string GetExportFolder(this ExportProfile profile, bool content = false, bool create = false) + { Guard.IsTrue(profile.FolderName.EmptyNull().Length > 2, nameof(profile.FolderName), "The export folder name must be at least 3 characters long."); var path = CommonHelper.MapPath(string.Concat(profile.FolderName, content ? "/Content" : "")); @@ -67,76 +67,76 @@ public static string GetExportFolder(this ExportProfile profile, bool content = /// Export profile /// Log file path public static string GetExportLogPath(this ExportProfile profile) - { - return Path.Combine(profile.GetExportFolder(), "log.txt"); - } - - /// - /// Gets the ZIP path for a profile - /// - /// Export profile - /// ZIP file path - public static string GetExportZipPath(this ExportProfile profile) - { - var name = (new DirectoryInfo(profile.FolderName)).Name; - if (name.IsEmpty()) - name = "ExportData"; - - return Path.Combine(profile.GetExportFolder(), name.ToValidFileName() + ".zip"); - } - - /// - /// Gets existing export files for an export profile - /// - /// Export profile - /// Export provider - /// List of file names - public static IEnumerable GetExportFiles(this ExportProfile profile, Provider provider) - { - var exportFolder = profile.GetExportFolder(true); - - if (System.IO.Directory.Exists(exportFolder) && provider.Value.FileExtension.HasValue()) - { - var filter = "*.{0}".FormatInvariant(provider.Value.FileExtension.ToLower()); - - return System.IO.Directory.EnumerateFiles(exportFolder, filter, SearchOption.AllDirectories).OrderBy(x => x); - } - - return Enumerable.Empty(); - } - - /// - /// Get number of existing export files - /// - /// Export profile - /// Export provider - /// Number of export files - public static int GetExportFileCount(this ExportProfile profile, Provider provider) - { - var result = profile.GetExportFiles(provider).Count(); - - if (File.Exists(profile.GetExportZipPath())) - ++result; - - return result; - } - - /// - /// Resolves the file name pattern for an export profile - /// - /// Export profile - /// Store - /// One based file index - /// The maximum length of the file name - /// Resolved file name pattern - public static string ResolveFileNamePattern(this ExportProfile profile, Store store, int fileIndex, int maxFileNameLength) - { - var sb = new StringBuilder(profile.FileNamePattern); - - sb.Replace("%Profile.Id%", profile.Id.ToString()); - sb.Replace("%Profile.FolderName%", profile.FolderName); - sb.Replace("%Store.Id%", store.Id.ToString()); - sb.Replace("%File.Index%", fileIndex.ToString("D4")); + { + return Path.Combine(profile.GetExportFolder(), "log.txt"); + } + + /// + /// Gets the ZIP path for a profile + /// + /// Export profile + /// ZIP file path + public static string GetExportZipPath(this ExportProfile profile) + { + var name = (new DirectoryInfo(profile.FolderName)).Name; + if (name.IsEmpty()) + name = "ExportData"; + + return Path.Combine(profile.GetExportFolder(), name.ToValidFileName() + ".zip"); + } + + /// + /// Gets existing export files for an export profile + /// + /// Export profile + /// Export provider + /// List of file names + public static IEnumerable GetExportFiles(this ExportProfile profile, Provider provider) + { + var exportFolder = profile.GetExportFolder(true); + + if (System.IO.Directory.Exists(exportFolder) && provider.Value.FileExtension.HasValue()) + { + var filter = "*.{0}".FormatInvariant(provider.Value.FileExtension.ToLower()); + + return System.IO.Directory.EnumerateFiles(exportFolder, filter, SearchOption.AllDirectories).OrderBy(x => x); + } + + return Enumerable.Empty(); + } + + /// + /// Get number of existing export files + /// + /// Export profile + /// Export provider + /// Number of export files + public static int GetExportFileCount(this ExportProfile profile, Provider provider) + { + var result = profile.GetExportFiles(provider).Count(); + + if (File.Exists(profile.GetExportZipPath())) + ++result; + + return result; + } + + /// + /// Resolves the file name pattern for an export profile + /// + /// Export profile + /// Store + /// One based file index + /// The maximum length of the file name + /// Resolved file name pattern + public static string ResolveFileNamePattern(this ExportProfile profile, Store store, int fileIndex, int maxFileNameLength) + { + var sb = new StringBuilder(profile.FileNamePattern); + + sb.Replace("%Profile.Id%", profile.Id.ToString()); + sb.Replace("%Profile.FolderName%", profile.FolderName); + sb.Replace("%Store.Id%", store.Id.ToString()); + sb.Replace("%File.Index%", fileIndex.ToString("D4")); if (profile.FileNamePattern.Contains("%Profile.SeoName%")) { @@ -155,113 +155,113 @@ public static string ResolveFileNamePattern(this ExportProfile profile, Store st sb.Replace("%Timestamp%", DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture)); } - var result = sb.ToString() - .ToValidFileName("") - .Truncate(maxFileNameLength); - - return result; - } - - /// - /// Get path of the deployment folder - /// - /// Export deployment - /// Deployment folder path - public static string GetDeploymentFolder(this ExportDeployment deployment, bool create = false) - { - if (deployment == null) - return null; - - string path = null; - - if (deployment.DeploymentType == ExportDeploymentType.PublicFolder) - { - if (deployment.SubFolder.HasValue()) - path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder, deployment.SubFolder); - else - path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); - } - else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) - { - if (deployment.FileSystemPath.IsEmpty()) - return null; - - if (Path.IsPathRooted(deployment.FileSystemPath)) - { - path = deployment.FileSystemPath; - } - else - { - path = PathHelper.NormalizeAppRelativePath(deployment.FileSystemPath); - path = CommonHelper.MapPath(path); - } - } - - if (create && !System.IO.Directory.Exists(path)) - System.IO.Directory.CreateDirectory(path); - - return path; - } - - /// - /// Get url of the public folder and take filtering and projection into consideration - /// - /// Export deployment - /// Common services - /// Store entity - /// Absolute URL of the public folder (always ends with /) or null - public static string GetPublicFolderUrl(this ExportDeployment deployment, ICommonServices services, Store store = null) - { - if (deployment != null && deployment.DeploymentType == ExportDeploymentType.PublicFolder) - { - if (store == null) - { - var filter = XmlHelper.Deserialize(deployment.Profile.Filtering); - var storeId = filter.StoreId; - - if (storeId == 0) - { - var projection = XmlHelper.Deserialize(deployment.Profile.Projection); - storeId = (projection.StoreId ?? 0); - } - - store = (storeId == 0 ? services.StoreContext.CurrentStore : services.StoreService.GetStoreById(storeId)); - } - - var url = string.Concat( - store.Url.EnsureEndsWith("/"), - DataExporter.PublicFolder.EnsureEndsWith("/"), - deployment.SubFolder.HasValue() ? deployment.SubFolder.EnsureEndsWith("/") : "" - ); - - return url; - } - - return null; - } - - /// - /// Get icon class for a deployment type - /// - /// Deployment type - /// Icon class - public static string GetIconClass(this ExportDeploymentType type) - { - switch (type) - { - case ExportDeploymentType.FileSystem: - return "far fa-folder-open"; - case ExportDeploymentType.Email: - return "far fa-envelope"; - case ExportDeploymentType.Http: - return "fa fa-globe"; - case ExportDeploymentType.Ftp: - return "far fa-copy"; - case ExportDeploymentType.PublicFolder: - return "fa fa-unlock"; - default: - return "fa fa-question"; - } - } - } + var result = sb.ToString() + .ToValidFileName("") + .Truncate(maxFileNameLength); + + return result; + } + + /// + /// Get path of the deployment folder + /// + /// Export deployment + /// Deployment folder path + public static string GetDeploymentFolder(this ExportDeployment deployment, bool create = false) + { + if (deployment == null) + return null; + + string path = null; + + if (deployment.DeploymentType == ExportDeploymentType.PublicFolder) + { + if (deployment.SubFolder.HasValue()) + path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder, deployment.SubFolder); + else + path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); + } + else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) + { + if (deployment.FileSystemPath.IsEmpty()) + return null; + + if (Path.IsPathRooted(deployment.FileSystemPath)) + { + path = deployment.FileSystemPath; + } + else + { + path = PathHelper.NormalizeAppRelativePath(deployment.FileSystemPath); + path = CommonHelper.MapPath(path); + } + } + + if (create && !System.IO.Directory.Exists(path)) + System.IO.Directory.CreateDirectory(path); + + return path; + } + + /// + /// Get url of the public folder and take filtering and projection into consideration + /// + /// Export deployment + /// Common services + /// Store entity + /// Absolute URL of the public folder (always ends with /) or null + public static string GetPublicFolderUrl(this ExportDeployment deployment, ICommonServices services, Store store = null) + { + if (deployment != null && deployment.DeploymentType == ExportDeploymentType.PublicFolder) + { + if (store == null) + { + var filter = XmlHelper.Deserialize(deployment.Profile.Filtering); + var storeId = filter.StoreId; + + if (storeId == 0) + { + var projection = XmlHelper.Deserialize(deployment.Profile.Projection); + storeId = (projection.StoreId ?? 0); + } + + store = (storeId == 0 ? services.StoreContext.CurrentStore : services.StoreService.GetStoreById(storeId)); + } + + var url = string.Concat( + store.Url.EnsureEndsWith("/"), + DataExporter.PublicFolder.EnsureEndsWith("/"), + deployment.SubFolder.HasValue() ? deployment.SubFolder.EnsureEndsWith("/") : "" + ); + + return url; + } + + return null; + } + + /// + /// Get icon class for a deployment type + /// + /// Deployment type + /// Icon class + public static string GetIconClass(this ExportDeploymentType type) + { + switch (type) + { + case ExportDeploymentType.FileSystem: + return "far fa-folder-open"; + case ExportDeploymentType.Email: + return "far fa-envelope"; + case ExportDeploymentType.Http: + return "fa fa-globe"; + case ExportDeploymentType.Ftp: + return "far fa-copy"; + case ExportDeploymentType.PublicFolder: + return "fa fa-unlock"; + default: + return "fa fa-question"; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs index eee6f1554e..3595e44867 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs @@ -3,13 +3,13 @@ namespace SmartStore.Services.DataExchange.Export { - /// - /// Declares data processing types supported by an export provider. - /// Projection type controls whether to display corresponding projection fields while editing an export profile. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ExportFeaturesAttribute : Attribute - { - public ExportFeatures Features { get; set; } - } + /// + /// Declares data processing types supported by an export provider. + /// Projection type controls whether to display corresponding projection fields while editing an export profile. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class ExportFeaturesAttribute : Attribute + { + public ExportFeatures Features { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index e3a4f541b6..d5c2c24001 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -16,232 +16,232 @@ namespace SmartStore.Services.DataExchange.Export { - public partial class ExportProfileService : IExportProfileService - { - private const string _defaultFileNamePattern = "%Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"; - - private readonly IRepository _exportProfileRepository; - private readonly IRepository _exportDeploymentRepository; - private readonly IEventPublisher _eventPublisher; - private readonly IScheduleTaskService _scheduleTaskService; - private readonly IProviderManager _providerManager; - private readonly DataExchangeSettings _dataExchangeSettings; - private readonly ILocalizationService _localizationService; - - public ExportProfileService( - IRepository exportProfileRepository, - IRepository exportDeploymentRepository, - IEventPublisher eventPublisher, - IScheduleTaskService scheduleTaskService, - IProviderManager providerManager, - DataExchangeSettings dataExchangeSettings, - ILocalizationService localizationService) - { - _exportProfileRepository = exportProfileRepository; - _exportDeploymentRepository = exportDeploymentRepository; - _eventPublisher = eventPublisher; - _scheduleTaskService = scheduleTaskService; - _providerManager = providerManager; - _dataExchangeSettings = dataExchangeSettings; - _localizationService = localizationService; - } - - public virtual ExportProfile InsertExportProfile( - string providerSystemName, - string name, - string fileExtension, - ExportFeatures features, - bool isSystemProfile = false, - string profileSystemName = null, - int cloneFromProfileId = 0) - { - Guard.NotEmpty(providerSystemName, nameof(providerSystemName)); - - if (name.IsEmpty()) - { - name = providerSystemName; - } - - if (!isSystemProfile) - { - var profileCount = _exportProfileRepository.Table.Count(x => x.ProviderSystemName == providerSystemName); - - name = string.Concat(_localizationService.GetResource("Common.My"), " ", name, " ", profileCount + 1); - } - - var cloneProfile = GetExportProfileById(cloneFromProfileId); - - ScheduleTask task = null; - ExportProfile profile = null; - - if (cloneProfile == null) - { - task = new ScheduleTask - { - CronExpression = "0 */6 * * *", // Every six hours. - Type = typeof(DataExportTask).AssemblyQualifiedNameWithoutVersion(), - Enabled = false, - StopOnError = false, - IsHidden = true - }; - } - else - { - task = cloneProfile.ScheduleTask.Clone(); - } - - task.Name = string.Concat(name, " Task"); - - _scheduleTaskService.InsertTask(task); - - if (cloneProfile == null) - { - profile = new ExportProfile - { - FileNamePattern = _defaultFileNamePattern - }; - - if (isSystemProfile) - { - profile.Enabled = true; - profile.PerStore = false; - profile.CreateZipArchive = false; - profile.Cleanup = false; - } - else - { - // What we do here is to preset typical settings for feed creation - // but on the other hand they may be untypical for generic data export\exchange. - var projection = new ExportProjection - { - RemoveCriticalCharacters = true, - CriticalCharacters = "¼,½,¾", - PriceType = PriceDisplayType.PreSelectedPrice, - NoGroupedProducts = features.HasFlag(ExportFeatures.CanOmitGroupedProducts) ? true : false, - OnlyIndividuallyVisibleAssociated = true, - DescriptionMerging = ExportDescriptionMerging.Description - }; - - var filter = new ExportFilter - { - IsPublished = true, - ShoppingCartTypeId = (int)ShoppingCartType.ShoppingCart - }; - - profile.Projection = XmlHelper.Serialize(projection); - profile.Filtering = XmlHelper.Serialize(filter); - } - } - else - { - profile = cloneProfile.Clone(); - } - - profile.IsSystemProfile = isSystemProfile; - profile.Name = name; - profile.ProviderSystemName = providerSystemName; - profile.SchedulingTaskId = task.Id; - - var cleanedSystemName = providerSystemName - .Replace("Exports.", "") - .Replace("Feeds.", "") - .Replace("/", "") - .Replace("-", ""); - - var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false, false) - .ToValidPath() - .Truncate(_dataExchangeSettings.MaxFileNameLength); - - var path = DataSettings.Current.TenantPath + "/ExportProfiles"; - profile.FolderName = path + "/" + FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath(path), folderName); - - profile.SystemName = profileSystemName.IsEmpty() && isSystemProfile - ? cleanedSystemName - : profileSystemName; - - _exportProfileRepository.Insert(profile); - - - task.Alias = profile.Id.ToString(); - _scheduleTaskService.UpdateTask(task); - - if (fileExtension.HasValue() && !isSystemProfile) - { - if (cloneProfile == null) - { - if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) - { - var subFolder = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/" + DataExporter.PublicFolder), folderName); - - profile.Deployments.Add(new ExportDeployment - { - ProfileId = profile.Id, - Enabled = true, - DeploymentType = ExportDeploymentType.PublicFolder, - Name = profile.Name, - SubFolder = subFolder - }); - - UpdateExportProfile(profile); - } - } - else - { - foreach (var deployment in cloneProfile.Deployments) - { - profile.Deployments.Add(deployment.Clone()); - } - - UpdateExportProfile(profile); - } - } - - return profile; - } - - public virtual ExportProfile InsertExportProfile( - Provider provider, - bool isSystemProfile = false, - string profileSystemName = null, - int cloneFromProfileId = 0) - { - Guard.NotNull(provider, nameof(provider)); - - var profile = InsertExportProfile( - provider.Metadata.SystemName, - provider.GetName(_localizationService), - provider.Value.FileExtension, - provider.Metadata.ExportFeatures, - isSystemProfile, - profileSystemName, - cloneFromProfileId); - - return profile; - } - - public virtual void UpdateExportProfile(ExportProfile profile) - { - Guard.NotNull(profile, nameof(profile)); - - profile.FolderName = PathHelper.NormalizeAppRelativePath(profile.FolderName); + public partial class ExportProfileService : IExportProfileService + { + private const string _defaultFileNamePattern = "%Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"; + + private readonly IRepository _exportProfileRepository; + private readonly IRepository _exportDeploymentRepository; + private readonly IEventPublisher _eventPublisher; + private readonly IScheduleTaskService _scheduleTaskService; + private readonly IProviderManager _providerManager; + private readonly DataExchangeSettings _dataExchangeSettings; + private readonly ILocalizationService _localizationService; + + public ExportProfileService( + IRepository exportProfileRepository, + IRepository exportDeploymentRepository, + IEventPublisher eventPublisher, + IScheduleTaskService scheduleTaskService, + IProviderManager providerManager, + DataExchangeSettings dataExchangeSettings, + ILocalizationService localizationService) + { + _exportProfileRepository = exportProfileRepository; + _exportDeploymentRepository = exportDeploymentRepository; + _eventPublisher = eventPublisher; + _scheduleTaskService = scheduleTaskService; + _providerManager = providerManager; + _dataExchangeSettings = dataExchangeSettings; + _localizationService = localizationService; + } + + public virtual ExportProfile InsertExportProfile( + string providerSystemName, + string name, + string fileExtension, + ExportFeatures features, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0) + { + Guard.NotEmpty(providerSystemName, nameof(providerSystemName)); + + if (name.IsEmpty()) + { + name = providerSystemName; + } + + if (!isSystemProfile) + { + var profileCount = _exportProfileRepository.Table.Count(x => x.ProviderSystemName == providerSystemName); + + name = string.Concat(_localizationService.GetResource("Common.My"), " ", name, " ", profileCount + 1); + } + + var cloneProfile = GetExportProfileById(cloneFromProfileId); + + ScheduleTask task = null; + ExportProfile profile = null; + + if (cloneProfile == null) + { + task = new ScheduleTask + { + CronExpression = "0 */6 * * *", // Every six hours. + Type = typeof(DataExportTask).AssemblyQualifiedNameWithoutVersion(), + Enabled = false, + StopOnError = false, + IsHidden = true + }; + } + else + { + task = cloneProfile.ScheduleTask.Clone(); + } + + task.Name = string.Concat(name, " Task"); + + _scheduleTaskService.InsertTask(task); + + if (cloneProfile == null) + { + profile = new ExportProfile + { + FileNamePattern = _defaultFileNamePattern + }; + + if (isSystemProfile) + { + profile.Enabled = true; + profile.PerStore = false; + profile.CreateZipArchive = false; + profile.Cleanup = false; + } + else + { + // What we do here is to preset typical settings for feed creation + // but on the other hand they may be untypical for generic data export\exchange. + var projection = new ExportProjection + { + RemoveCriticalCharacters = true, + CriticalCharacters = "¼,½,¾", + PriceType = PriceDisplayType.PreSelectedPrice, + NoGroupedProducts = features.HasFlag(ExportFeatures.CanOmitGroupedProducts) ? true : false, + OnlyIndividuallyVisibleAssociated = true, + DescriptionMerging = ExportDescriptionMerging.Description + }; + + var filter = new ExportFilter + { + IsPublished = true, + ShoppingCartTypeId = (int)ShoppingCartType.ShoppingCart + }; + + profile.Projection = XmlHelper.Serialize(projection); + profile.Filtering = XmlHelper.Serialize(filter); + } + } + else + { + profile = cloneProfile.Clone(); + } + + profile.IsSystemProfile = isSystemProfile; + profile.Name = name; + profile.ProviderSystemName = providerSystemName; + profile.SchedulingTaskId = task.Id; + + var cleanedSystemName = providerSystemName + .Replace("Exports.", "") + .Replace("Feeds.", "") + .Replace("/", "") + .Replace("-", ""); + + var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false, false) + .ToValidPath() + .Truncate(_dataExchangeSettings.MaxFileNameLength); + + var path = DataSettings.Current.TenantPath + "/ExportProfiles"; + profile.FolderName = path + "/" + FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath(path), folderName); + + profile.SystemName = profileSystemName.IsEmpty() && isSystemProfile + ? cleanedSystemName + : profileSystemName; + + _exportProfileRepository.Insert(profile); + + + task.Alias = profile.Id.ToString(); + _scheduleTaskService.UpdateTask(task); + + if (fileExtension.HasValue() && !isSystemProfile) + { + if (cloneProfile == null) + { + if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) + { + var subFolder = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/" + DataExporter.PublicFolder), folderName); + + profile.Deployments.Add(new ExportDeployment + { + ProfileId = profile.Id, + Enabled = true, + DeploymentType = ExportDeploymentType.PublicFolder, + Name = profile.Name, + SubFolder = subFolder + }); + + UpdateExportProfile(profile); + } + } + else + { + foreach (var deployment in cloneProfile.Deployments) + { + profile.Deployments.Add(deployment.Clone()); + } + + UpdateExportProfile(profile); + } + } + + return profile; + } + + public virtual ExportProfile InsertExportProfile( + Provider provider, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0) + { + Guard.NotNull(provider, nameof(provider)); + + var profile = InsertExportProfile( + provider.Metadata.SystemName, + provider.GetName(_localizationService), + provider.Value.FileExtension, + provider.Metadata.ExportFeatures, + isSystemProfile, + profileSystemName, + cloneFromProfileId); + + return profile; + } + + public virtual void UpdateExportProfile(ExportProfile profile) + { + Guard.NotNull(profile, nameof(profile)); + + profile.FolderName = PathHelper.NormalizeAppRelativePath(profile.FolderName); if (!PathHelper.IsSafeAppRootPath(profile.FolderName)) { throw new SmartException(_localizationService.GetResource("Admin.DataExchange.Export.FolderName.Validate")); } - _exportProfileRepository.Update(profile); - } + _exportProfileRepository.Update(profile); + } - public virtual void DeleteExportProfile(ExportProfile profile, bool force = false) - { - Guard.NotNull(profile, nameof(profile)); + public virtual void DeleteExportProfile(ExportProfile profile, bool force = false) + { + Guard.NotNull(profile, nameof(profile)); - if (!force && profile.IsSystemProfile) - throw new SmartException(_localizationService.GetResource("Admin.DataExchange.Export.CannotDeleteSystemProfile")); + if (!force && profile.IsSystemProfile) + throw new SmartException(_localizationService.GetResource("Admin.DataExchange.Export.CannotDeleteSystemProfile")); - int scheduleTaskId = profile.SchedulingTaskId; - var folder = profile.GetExportFolder(); + int scheduleTaskId = profile.SchedulingTaskId; + var folder = profile.GetExportFolder(); var deployments = profile.Deployments.Where(x => !x.IsTransientRecord()).ToList(); if (deployments.Any()) @@ -252,123 +252,123 @@ public virtual void DeleteExportProfile(ExportProfile profile, bool force = fals _exportProfileRepository.Delete(profile); - var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); - _scheduleTaskService.DeleteTask(scheduleTask); + var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); + _scheduleTaskService.DeleteTask(scheduleTask); + + if (System.IO.Directory.Exists(folder)) + { + FileSystemHelper.ClearDirectory(folder, true); + } + } + + public virtual IQueryable GetExportProfiles(bool? enabled = null) + { + var query = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments); + + if (enabled.HasValue) + { + query = query.Where(x => x.Enabled == enabled.Value); + } + + query = query + .OrderBy(x => x.IsSystemProfile) + .ThenBy(x => x.Name); + + return query; + } + + public virtual ExportProfile GetExportProfileById(int id) + { + if (id == 0) + return null; + + var profile = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments) + .FirstOrDefault(x => x.Id == id); + + return profile; + } + + public virtual ExportProfile GetSystemExportProfile(string providerSystemName) + { + if (providerSystemName.IsEmpty()) + return null; + + var query = GetExportProfiles(true); + + var profile = query + .Where(x => x.IsSystemProfile && x.ProviderSystemName == providerSystemName) + .FirstOrDefault(); + + return profile; + } + + public virtual IList GetExportProfilesBySystemName(string providerSystemName) + { + if (providerSystemName.IsEmpty()) + return new List(); + + var profiles = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments) + .Where(x => x.ProviderSystemName == providerSystemName) + .ToList(); + + return profiles; + } + + + public virtual IEnumerable> LoadAllExportProviders(int storeId = 0, bool showHidden = true) + { + var allProviders = _providerManager.GetAllProviders(storeId) + .Where(x => x.IsValid() && (showHidden || !x.Metadata.IsHidden)) + //.OrderBy(x => x.Metadata.SystemName) + .OrderBy(x => x.Metadata.FriendlyName); + + return allProviders; + } + + public virtual Provider LoadProvider(string systemName, int storeId = 0) + { + var provider = _providerManager.GetProvider(systemName, storeId); + + return (provider.IsValid() ? provider : null); + } + + public virtual ExportDeployment GetExportDeploymentById(int id) + { + if (id == 0) + return null; + + var deployment = _exportDeploymentRepository.Table + .Expand(x => x.Profile) + .FirstOrDefault(x => x.Id == id); + + return deployment; + } + + public virtual void UpdateExportDeployment(ExportDeployment deployment) + { + if (deployment == null) + throw new ArgumentNullException("deployment"); + + if (deployment.DeploymentType == ExportDeploymentType.FileSystem && deployment.FileSystemPath == "~/") + { + throw new SmartException("Invalid deployment path."); + } + + _exportDeploymentRepository.Update(deployment); + } - if (System.IO.Directory.Exists(folder)) - { - FileSystemHelper.ClearDirectory(folder, true); - } - } + public virtual void DeleteExportDeployment(ExportDeployment deployment) + { + if (deployment == null) + throw new ArgumentNullException("deployment"); - public virtual IQueryable GetExportProfiles(bool? enabled = null) - { - var query = _exportProfileRepository.Table - .Expand(x => x.ScheduleTask) - .Expand(x => x.Deployments); - - if (enabled.HasValue) - { - query = query.Where(x => x.Enabled == enabled.Value); - } - - query = query - .OrderBy(x => x.IsSystemProfile) - .ThenBy(x => x.Name); - - return query; - } - - public virtual ExportProfile GetExportProfileById(int id) - { - if (id == 0) - return null; - - var profile = _exportProfileRepository.Table - .Expand(x => x.ScheduleTask) - .Expand(x => x.Deployments) - .FirstOrDefault(x => x.Id == id); - - return profile; - } - - public virtual ExportProfile GetSystemExportProfile(string providerSystemName) - { - if (providerSystemName.IsEmpty()) - return null; - - var query = GetExportProfiles(true); - - var profile = query - .Where(x => x.IsSystemProfile && x.ProviderSystemName == providerSystemName) - .FirstOrDefault(); - - return profile; - } - - public virtual IList GetExportProfilesBySystemName(string providerSystemName) - { - if (providerSystemName.IsEmpty()) - return new List(); - - var profiles = _exportProfileRepository.Table - .Expand(x => x.ScheduleTask) - .Expand(x => x.Deployments) - .Where(x => x.ProviderSystemName == providerSystemName) - .ToList(); - - return profiles; - } - - - public virtual IEnumerable> LoadAllExportProviders(int storeId = 0, bool showHidden = true) - { - var allProviders = _providerManager.GetAllProviders(storeId) - .Where(x => x.IsValid() && (showHidden || !x.Metadata.IsHidden)) - //.OrderBy(x => x.Metadata.SystemName) - .OrderBy(x => x.Metadata.FriendlyName); - - return allProviders; - } - - public virtual Provider LoadProvider(string systemName, int storeId = 0) - { - var provider = _providerManager.GetProvider(systemName, storeId); - - return (provider.IsValid() ? provider : null); - } - - public virtual ExportDeployment GetExportDeploymentById(int id) - { - if (id == 0) - return null; - - var deployment = _exportDeploymentRepository.Table - .Expand(x => x.Profile) - .FirstOrDefault(x => x.Id == id); - - return deployment; - } - - public virtual void UpdateExportDeployment(ExportDeployment deployment) - { - if (deployment == null) - throw new ArgumentNullException("deployment"); - - if (deployment.DeploymentType == ExportDeploymentType.FileSystem && deployment.FileSystemPath == "~/") - { - throw new SmartException("Invalid deployment path."); - } - - _exportDeploymentRepository.Update(deployment); - } - - public virtual void DeleteExportDeployment(ExportDeployment deployment) - { - if (deployment == null) - throw new ArgumentNullException("deployment"); - - _exportDeploymentRepository.Delete(deployment); - } - } + _exportDeploymentRepository.Delete(deployment); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs index c77638f60b..6bed98e1d8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs @@ -2,49 +2,40 @@ namespace SmartStore.Services.DataExchange.Export { - public abstract class ExportProviderBase : IExportProvider - { - /// - /// The exported entity type - /// - public virtual ExportEntityType EntityType - { - get { return ExportEntityType.Product; } - } + public abstract class ExportProviderBase : IExportProvider + { + /// + /// The exported entity type + /// + public virtual ExportEntityType EntityType => ExportEntityType.Product; - /// - /// File extension of the export files (without dot). Return null for a non file based, on-the-fly export. - /// - public virtual string FileExtension - { - get { return null; } - } + /// + /// File extension of the export files (without dot). Return null for a non file based, on-the-fly export. + /// + public virtual string FileExtension => null; - /// - /// Get provider specific configuration information. Return null when no provider specific configuration is required. - /// - public virtual ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } + /// + /// Get provider specific configuration information. Return null when no provider specific configuration is required. + /// + public virtual ExportConfigurationInfo ConfigurationInfo => null; - public void Execute(ExportExecuteContext context) - { - Export(context); - } + public void Execute(ExportExecuteContext context) + { + Export(context); + } - /// - /// Export data to a file - /// - /// Export execution context - protected abstract void Export(ExportExecuteContext context); + /// + /// Export data to a file + /// + /// Export execution context + protected abstract void Export(ExportExecuteContext context); - /// - /// Called once per store when export execution ended - /// - /// Export execution context - public virtual void OnExecuted(ExportExecuteContext context) - { - } - } + /// + /// Called once per store when export execution ended + /// + /// Export execution context + public virtual void OnExecuted(ExportExecuteContext context) + { + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index 7165d88a1d..1ba4e09442 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -13,193 +13,187 @@ namespace SmartStore.Services.DataExchange.Export { - public class ExportXmlHelper : IDisposable - { - protected XmlWriter _writer; - protected CultureInfo _culture; - protected bool _doNotDispose; - - public ExportXmlHelper(XmlWriter writer, bool doNotDispose = false, CultureInfo culture = null) - { - _writer = writer; - _doNotDispose = doNotDispose; - _culture = culture == null ? CultureInfo.InvariantCulture : culture; - } - public ExportXmlHelper(Stream stream, XmlWriterSettings settings = null, CultureInfo culture = null) - { - if (settings == null) - { - settings = DefaultSettings; - } - - _writer = XmlWriter.Create(stream, settings); - _culture = culture == null ? CultureInfo.InvariantCulture : culture; - } - - public static XmlWriterSettings DefaultSettings - { - get - { - return new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false, - Indent = false, - NewLineHandling = NewLineHandling.None - }; - } - } - - public ExportXmlExclude Exclude { get; set; } - - public XmlWriter Writer => _writer; - - public void Dispose() - { - if (_writer != null && !_doNotDispose) - { - _writer.Dispose(); - } - } - - public void WriteLocalized(dynamic parentNode) - { - if (parentNode == null || parentNode._Localized == null) - return; - - _writer.WriteStartElement("Localized"); - foreach (dynamic item in parentNode._Localized) - { - _writer.WriteStartElement((string)item.LocaleKey); - _writer.WriteAttributeString("culture", (string)item.Culture); - _writer.WriteString(((string)item.LocaleValue).RemoveInvalidXmlChars()); - _writer.WriteEndElement(); // item.LocaleKey - } - _writer.WriteEndElement(); // Localized - } - - public void WriteGenericAttributes(dynamic parentNode) - { - if (parentNode == null || parentNode._GenericAttributes == null) - return; - - _writer.WriteStartElement("GenericAttributes"); - foreach (dynamic genericAttribute in parentNode._GenericAttributes) - { - GenericAttribute entity = genericAttribute.Entity; - - _writer.WriteStartElement("GenericAttribute"); - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("EntityId", entity.EntityId.ToString()); - _writer.Write("KeyGroup", entity.KeyGroup); - _writer.Write("Key", entity.Key); - _writer.Write("Value", (string)genericAttribute.Value); - _writer.Write("StoreId", entity.StoreId.ToString()); - _writer.WriteEndElement(); // GenericAttribute - } - _writer.WriteEndElement(); // GenericAttributes - } - - public void WriteAddress(dynamic address, string node) - { - if (address == null) - return; - - Address entity = address.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Salutation", entity.Salutation); - _writer.Write("Title", entity.Title); - _writer.Write("FirstName", entity.FirstName); - _writer.Write("LastName", entity.LastName); - _writer.Write("Email", entity.Email); - _writer.Write("Company", entity.Company); - _writer.Write("CountryId", entity.CountryId.HasValue ? entity.CountryId.Value.ToString() : ""); - _writer.Write("StateProvinceId", entity.StateProvinceId.HasValue ? entity.StateProvinceId.Value.ToString() : ""); - _writer.Write("City", entity.City); - _writer.Write("Address1", entity.Address1); - _writer.Write("Address2", entity.Address2); - _writer.Write("ZipPostalCode", entity.ZipPostalCode); - _writer.Write("PhoneNumber", entity.PhoneNumber); - _writer.Write("FaxNumber", entity.FaxNumber); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - - if (address.Country != null) - { - dynamic country = address.Country; - Country entityCountry = address.Country.Entity; - - _writer.WriteStartElement("Country"); - _writer.Write("Id", entityCountry.Id.ToString()); - _writer.Write("Name", (string)country.Name); - _writer.Write("AllowsBilling", entityCountry.AllowsBilling.ToString()); - _writer.Write("AllowsShipping", entityCountry.AllowsShipping.ToString()); - _writer.Write("TwoLetterIsoCode", entityCountry.TwoLetterIsoCode); - _writer.Write("ThreeLetterIsoCode", entityCountry.ThreeLetterIsoCode); - _writer.Write("NumericIsoCode", entityCountry.NumericIsoCode.ToString()); - _writer.Write("SubjectToVat", entityCountry.SubjectToVat.ToString()); - _writer.Write("Published", entityCountry.Published.ToString()); - _writer.Write("DisplayOrder", entityCountry.DisplayOrder.ToString()); - _writer.Write("LimitedToStores", entityCountry.LimitedToStores.ToString()); - - WriteLocalized(country); - - _writer.WriteEndElement(); // Country - } - - if (address.StateProvince != null) - { - dynamic stateProvince = address.StateProvince; - StateProvince entityStateProvince = address.StateProvince.Entity; - - _writer.WriteStartElement("StateProvince"); - _writer.Write("Id", entityStateProvince.Id.ToString()); - _writer.Write("CountryId", entityStateProvince.CountryId.ToString()); - _writer.Write("Name", (string)stateProvince.Name); - _writer.Write("Abbreviation", (string)stateProvince.Abbreviation); - _writer.Write("Published", entityStateProvince.Published.ToString()); - _writer.Write("DisplayOrder", entityStateProvince.DisplayOrder.ToString()); - - WriteLocalized(stateProvince); - - _writer.WriteEndElement(); // StateProvince - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCurrency(dynamic currency, string node) - { - if (currency == null) - return; - - Currency entity = currency.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)currency.Name); - _writer.Write("CurrencyCode", entity.CurrencyCode); - _writer.Write("Rate", entity.Rate.ToString(_culture)); - _writer.Write("DisplayLocale", entity.DisplayLocale); - _writer.Write("CustomFormatting", entity.CustomFormatting); - _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); - _writer.Write("Published", entity.Published.ToString()); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); - _writer.Write("DomainEndings", entity.DomainEndings); + public class ExportXmlHelper : IDisposable + { + protected XmlWriter _writer; + protected CultureInfo _culture; + protected bool _doNotDispose; + + public ExportXmlHelper(XmlWriter writer, bool doNotDispose = false, CultureInfo culture = null) + { + _writer = writer; + _doNotDispose = doNotDispose; + _culture = culture == null ? CultureInfo.InvariantCulture : culture; + } + public ExportXmlHelper(Stream stream, XmlWriterSettings settings = null, CultureInfo culture = null) + { + if (settings == null) + { + settings = DefaultSettings; + } + + _writer = XmlWriter.Create(stream, settings); + _culture = culture == null ? CultureInfo.InvariantCulture : culture; + } + + public static XmlWriterSettings DefaultSettings => new XmlWriterSettings + { + Encoding = Encoding.UTF8, + CheckCharacters = false, + Indent = false, + NewLineHandling = NewLineHandling.None + }; + + public ExportXmlExclude Exclude { get; set; } + + public XmlWriter Writer => _writer; + + public void Dispose() + { + if (_writer != null && !_doNotDispose) + { + _writer.Dispose(); + } + } + + public void WriteLocalized(dynamic parentNode) + { + if (parentNode == null || parentNode._Localized == null) + return; + + _writer.WriteStartElement("Localized"); + foreach (dynamic item in parentNode._Localized) + { + _writer.WriteStartElement((string)item.LocaleKey); + _writer.WriteAttributeString("culture", (string)item.Culture); + _writer.WriteString(((string)item.LocaleValue).RemoveInvalidXmlChars()); + _writer.WriteEndElement(); // item.LocaleKey + } + _writer.WriteEndElement(); // Localized + } + + public void WriteGenericAttributes(dynamic parentNode) + { + if (parentNode == null || parentNode._GenericAttributes == null) + return; + + _writer.WriteStartElement("GenericAttributes"); + foreach (dynamic genericAttribute in parentNode._GenericAttributes) + { + GenericAttribute entity = genericAttribute.Entity; + + _writer.WriteStartElement("GenericAttribute"); + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("EntityId", entity.EntityId.ToString()); + _writer.Write("KeyGroup", entity.KeyGroup); + _writer.Write("Key", entity.Key); + _writer.Write("Value", (string)genericAttribute.Value); + _writer.Write("StoreId", entity.StoreId.ToString()); + _writer.WriteEndElement(); // GenericAttribute + } + _writer.WriteEndElement(); // GenericAttributes + } + + public void WriteAddress(dynamic address, string node) + { + if (address == null) + return; + + Address entity = address.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Salutation", entity.Salutation); + _writer.Write("Title", entity.Title); + _writer.Write("FirstName", entity.FirstName); + _writer.Write("LastName", entity.LastName); + _writer.Write("Email", entity.Email); + _writer.Write("Company", entity.Company); + _writer.Write("CountryId", entity.CountryId.HasValue ? entity.CountryId.Value.ToString() : ""); + _writer.Write("StateProvinceId", entity.StateProvinceId.HasValue ? entity.StateProvinceId.Value.ToString() : ""); + _writer.Write("City", entity.City); + _writer.Write("Address1", entity.Address1); + _writer.Write("Address2", entity.Address2); + _writer.Write("ZipPostalCode", entity.ZipPostalCode); + _writer.Write("PhoneNumber", entity.PhoneNumber); + _writer.Write("FaxNumber", entity.FaxNumber); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + + if (address.Country != null) + { + dynamic country = address.Country; + Country entityCountry = address.Country.Entity; + + _writer.WriteStartElement("Country"); + _writer.Write("Id", entityCountry.Id.ToString()); + _writer.Write("Name", (string)country.Name); + _writer.Write("AllowsBilling", entityCountry.AllowsBilling.ToString()); + _writer.Write("AllowsShipping", entityCountry.AllowsShipping.ToString()); + _writer.Write("TwoLetterIsoCode", entityCountry.TwoLetterIsoCode); + _writer.Write("ThreeLetterIsoCode", entityCountry.ThreeLetterIsoCode); + _writer.Write("NumericIsoCode", entityCountry.NumericIsoCode.ToString()); + _writer.Write("SubjectToVat", entityCountry.SubjectToVat.ToString()); + _writer.Write("Published", entityCountry.Published.ToString()); + _writer.Write("DisplayOrder", entityCountry.DisplayOrder.ToString()); + _writer.Write("LimitedToStores", entityCountry.LimitedToStores.ToString()); + + WriteLocalized(country); + + _writer.WriteEndElement(); // Country + } + + if (address.StateProvince != null) + { + dynamic stateProvince = address.StateProvince; + StateProvince entityStateProvince = address.StateProvince.Entity; + + _writer.WriteStartElement("StateProvince"); + _writer.Write("Id", entityStateProvince.Id.ToString()); + _writer.Write("CountryId", entityStateProvince.CountryId.ToString()); + _writer.Write("Name", (string)stateProvince.Name); + _writer.Write("Abbreviation", (string)stateProvince.Abbreviation); + _writer.Write("Published", entityStateProvince.Published.ToString()); + _writer.Write("DisplayOrder", entityStateProvince.DisplayOrder.ToString()); + + WriteLocalized(stateProvince); + + _writer.WriteEndElement(); // StateProvince + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCurrency(dynamic currency, string node) + { + if (currency == null) + return; + + Currency entity = currency.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)currency.Name); + _writer.Write("CurrencyCode", entity.CurrencyCode); + _writer.Write("Rate", entity.Rate.ToString(_culture)); + _writer.Write("DisplayLocale", entity.DisplayLocale); + _writer.Write("CustomFormatting", entity.CustomFormatting); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("DomainEndings", entity.DomainEndings); _writer.Write("RoundOrderItemsEnabled", entity.RoundOrderItemsEnabled.ToString()); _writer.Write("RoundNumDecimals", entity.RoundNumDecimals.ToString()); _writer.Write("RoundOrderTotalEnabled", entity.RoundOrderTotalEnabled.ToString()); @@ -208,408 +202,414 @@ public void WriteCurrency(dynamic currency, string node) WriteLocalized(currency); - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCountry(dynamic country, string node) - { - if (country == null) - return; - - Country entity = country.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", entity.Name); - _writer.Write("AllowsBilling", entity.AllowsBilling.ToString()); - _writer.Write("AllowsShipping", entity.AllowsShipping.ToString()); - _writer.Write("TwoLetterIsoCode", entity.TwoLetterIsoCode); - _writer.Write("ThreeLetterIsoCode", entity.ThreeLetterIsoCode); - _writer.Write("NumericIsoCode", entity.NumericIsoCode.ToString()); - _writer.Write("SubjectToVat", entity.SubjectToVat.ToString()); - _writer.Write("Published", entity.Published.ToString()); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); - - WriteLocalized(country); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteRewardPointsHistory(dynamic rewardPoints, string node) - { - if (rewardPoints == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - foreach (dynamic rewardPoint in rewardPoints) - { - RewardPointsHistory entity = rewardPoint.Entity; - - _writer.WriteStartElement("RewardPointsHistory"); - _writer.Write("Id", entity.ToString()); - _writer.Write("CustomerId", entity.ToString()); - _writer.Write("Points", entity.Points.ToString()); - _writer.Write("PointsBalance", entity.PointsBalance.ToString()); - _writer.Write("UsedAmount", entity.UsedAmount.ToString(_culture)); - _writer.Write("Message", (string)rewardPoint.Message); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.WriteEndElement(); // RewardPointsHistory - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteDeliveryTime(dynamic deliveryTime, string node) - { - if (deliveryTime == null) - return; - - DeliveryTime entity = deliveryTime.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)deliveryTime.Name); - _writer.Write("DisplayLocale", entity.DisplayLocale); - _writer.Write("ColorHexValue", entity.ColorHexValue); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("IsDefault", entity.IsDefault.ToString()); - - WriteLocalized(deliveryTime); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteQuantityUnit(dynamic quantityUnit, string node) - { - if (quantityUnit == null) - return; - - QuantityUnit entity = quantityUnit.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)quantityUnit.Name); + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCountry(dynamic country, string node) + { + if (country == null) + return; + + Country entity = country.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", entity.Name); + _writer.Write("AllowsBilling", entity.AllowsBilling.ToString()); + _writer.Write("AllowsShipping", entity.AllowsShipping.ToString()); + _writer.Write("TwoLetterIsoCode", entity.TwoLetterIsoCode); + _writer.Write("ThreeLetterIsoCode", entity.ThreeLetterIsoCode); + _writer.Write("NumericIsoCode", entity.NumericIsoCode.ToString()); + _writer.Write("SubjectToVat", entity.SubjectToVat.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + + WriteLocalized(country); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteRewardPointsHistory(dynamic rewardPoints, string node) + { + if (rewardPoints == null) + return; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + foreach (dynamic rewardPoint in rewardPoints) + { + RewardPointsHistory entity = rewardPoint.Entity; + + _writer.WriteStartElement("RewardPointsHistory"); + _writer.Write("Id", entity.ToString()); + _writer.Write("CustomerId", entity.ToString()); + _writer.Write("Points", entity.Points.ToString()); + _writer.Write("PointsBalance", entity.PointsBalance.ToString()); + _writer.Write("UsedAmount", entity.UsedAmount.ToString(_culture)); + _writer.Write("Message", (string)rewardPoint.Message); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.WriteEndElement(); // RewardPointsHistory + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteDeliveryTime(dynamic deliveryTime, string node) + { + if (deliveryTime == null) + return; + + DeliveryTime entity = deliveryTime.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)deliveryTime.Name); + _writer.Write("DisplayLocale", entity.DisplayLocale); + _writer.Write("ColorHexValue", entity.ColorHexValue); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("IsDefault", entity.IsDefault.ToString()); + _writer.Write("MinDays", entity.MinDays.HasValue ? entity.MinDays.Value.ToString() : ""); + _writer.Write("MaxDays", entity.MaxDays.HasValue ? entity.MaxDays.Value.ToString() : ""); + + WriteLocalized(deliveryTime); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteQuantityUnit(dynamic quantityUnit, string node) + { + if (quantityUnit == null) + return; + + QuantityUnit entity = quantityUnit.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)quantityUnit.Name); _writer.Write("NamePlural", (string)quantityUnit.NamePlural); _writer.Write("Description", (string)quantityUnit.Description); - _writer.Write("DisplayLocale", entity.DisplayLocale); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("IsDefault", entity.IsDefault.ToString()); - - WriteLocalized(quantityUnit); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WritePicture(dynamic picture, string node) - { - if (picture == null) - return; - - MediaFile entity = picture.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("SeoFilename", (string)picture.Name); - _writer.Write("MimeType", (string)picture.MimeType); - _writer.Write("ThumbImageUrl", (string)picture._ThumbImageUrl); - _writer.Write("ImageUrl", (string)picture._ImageUrl); - _writer.Write("FullSizeImageUrl", (string)picture._FullSizeImageUrl); - _writer.Write("FileName", (string)picture._FileName); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCategory(dynamic category, string node) - { - if (category == null) - return; - - Category entity = category.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - - if (!Exclude.HasFlag(ExportXmlExclude.Category)) - { - _writer.Write("Name", (string)category.Name); - _writer.Write("FullName", (string)category.FullName); - _writer.Write("Description", (string)category.Description); - _writer.Write("BottomDescription", (string)category.BottomDescription); - _writer.Write("CategoryTemplateId", entity.CategoryTemplateId.ToString()); - _writer.Write("CategoryTemplateViewPath", (string)category._CategoryTemplateViewPath); - _writer.Write("MetaKeywords", (string)category.MetaKeywords); - _writer.Write("MetaDescription", (string)category.MetaDescription); - _writer.Write("MetaTitle", (string)category.MetaTitle); - _writer.Write("SeName", (string)category.SeName); - _writer.Write("ParentCategoryId", entity.ParentCategoryId.ToString()); - _writer.Write("PictureId", entity.MediaFileId.ToString()); - _writer.Write("PageSize", entity.PageSize.ToString()); - _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); - _writer.Write("PageSizeOptions", entity.PageSizeOptions); - _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); - _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); - _writer.Write("Published", entity.Published.ToString()); - _writer.Write("Deleted", entity.Deleted.ToString()); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); - _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); - _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); - _writer.Write("Alias", (string)category.Alias); - _writer.Write("DefaultViewMode", entity.DefaultViewMode); - - WritePicture(category.Picture, "Picture"); - - WriteLocalized(category); - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteManufacturer(dynamic manufacturer, string node) - { - if (manufacturer == null) - return; - - Manufacturer entity = manufacturer.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)manufacturer.Name); - _writer.Write("SeName", (string)manufacturer.SeName); - _writer.Write("Description", (string)manufacturer.Description); + _writer.Write("DisplayLocale", entity.DisplayLocale); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("IsDefault", entity.IsDefault.ToString()); + + WriteLocalized(quantityUnit); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WritePicture(dynamic picture, string node) + { + if (picture == null) + return; + + MediaFile entity = picture.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + var seoName = (string)picture.Name; + seoName = Path.GetFileNameWithoutExtension(seoName.EmptyNull()); + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("SeoFilename", seoName); + _writer.Write("MimeType", (string)picture.MimeType); + _writer.Write("ThumbImageUrl", (string)picture._ThumbImageUrl); + _writer.Write("ImageUrl", (string)picture._ImageUrl); + _writer.Write("FullSizeImageUrl", (string)picture._FullSizeImageUrl); + _writer.Write("FileName", (string)picture._FileName); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCategory(dynamic category, string node) + { + if (category == null) + return; + + Category entity = category.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + + if (!Exclude.HasFlag(ExportXmlExclude.Category)) + { + _writer.Write("Name", (string)category.Name); + _writer.Write("FullName", (string)category.FullName); + _writer.Write("Description", (string)category.Description); + _writer.Write("BottomDescription", (string)category.BottomDescription); + _writer.Write("CategoryTemplateId", entity.CategoryTemplateId.ToString()); + _writer.Write("CategoryTemplateViewPath", (string)category._CategoryTemplateViewPath); + _writer.Write("MetaKeywords", (string)category.MetaKeywords); + _writer.Write("MetaDescription", (string)category.MetaDescription); + _writer.Write("MetaTitle", (string)category.MetaTitle); + _writer.Write("SeName", (string)category.SeName); + _writer.Write("ParentCategoryId", entity.ParentCategoryId.ToString()); + _writer.Write("PictureId", entity.MediaFileId.ToString()); + _writer.Write("PageSize", entity.PageSize.ToString()); + _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); + _writer.Write("PageSizeOptions", entity.PageSizeOptions); + _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); + _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("Alias", (string)category.Alias); + _writer.Write("DefaultViewMode", entity.DefaultViewMode); + + WritePicture(category.Picture, "Picture"); + + WriteLocalized(category); + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteManufacturer(dynamic manufacturer, string node) + { + if (manufacturer == null) + return; + + Manufacturer entity = manufacturer.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)manufacturer.Name); + _writer.Write("SeName", (string)manufacturer.SeName); + _writer.Write("Description", (string)manufacturer.Description); _writer.Write("BottomDescription", (string)manufacturer.BottomDescription); _writer.Write("ManufacturerTemplateId", entity.ManufacturerTemplateId.ToString()); - _writer.Write("MetaKeywords", (string)manufacturer.MetaKeywords); - _writer.Write("MetaDescription", (string)manufacturer.MetaDescription); - _writer.Write("MetaTitle", (string)manufacturer.MetaTitle); - _writer.Write("PictureId", entity.MediaFileId.ToString()); - _writer.Write("PageSize", entity.PageSize.ToString()); - _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); - _writer.Write("PageSizeOptions", entity.PageSizeOptions); - _writer.Write("Published", entity.Published.ToString()); - _writer.Write("Deleted", entity.Deleted.ToString()); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); - _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("MetaKeywords", (string)manufacturer.MetaKeywords); + _writer.Write("MetaDescription", (string)manufacturer.MetaDescription); + _writer.Write("MetaTitle", (string)manufacturer.MetaTitle); + _writer.Write("PictureId", entity.MediaFileId.ToString()); + _writer.Write("PageSize", entity.PageSize.ToString()); + _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); + _writer.Write("PageSizeOptions", entity.PageSizeOptions); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); WritePicture(manufacturer.Picture, "Picture"); - WriteLocalized(manufacturer); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteProduct(dynamic product, string node) - { - if (product == null) - return; - - Product entity = product.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - decimal? basePriceAmount = product.BasePriceAmount; - int? basePriceBaseAmount = product.BasePriceBaseAmount; - decimal? lowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice; - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)product.Name); - _writer.Write("SeName", (string)product.SeName); - _writer.Write("ShortDescription", (string)product.ShortDescription); - _writer.Write("FullDescription", (string)product.FullDescription); - _writer.Write("AdminComment", (string)product.AdminComment); - _writer.Write("ProductTemplateId", entity.ProductTemplateId.ToString()); - _writer.Write("ProductTemplateViewPath", (string)product._ProductTemplateViewPath); - _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); - _writer.Write("HomePageDisplayOrder", entity.HomePageDisplayOrder.ToString()); - _writer.Write("MetaKeywords", (string)product.MetaKeywords); - _writer.Write("MetaDescription", (string)product.MetaDescription); - _writer.Write("MetaTitle", (string)product.MetaTitle); - _writer.Write("AllowCustomerReviews", entity.AllowCustomerReviews.ToString()); - _writer.Write("ApprovedRatingSum", entity.ApprovedRatingSum.ToString()); - _writer.Write("NotApprovedRatingSum", entity.NotApprovedRatingSum.ToString()); - _writer.Write("ApprovedTotalReviews", entity.ApprovedTotalReviews.ToString()); - _writer.Write("NotApprovedTotalReviews", entity.NotApprovedTotalReviews.ToString()); - _writer.Write("Published", entity.Published.ToString()); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); - _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); - _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); - _writer.Write("ProductTypeId", entity.ProductTypeId.ToString()); - _writer.Write("ParentGroupedProductId", entity.ParentGroupedProductId.ToString()); - _writer.Write("Sku", (string)product.Sku); - _writer.Write("ManufacturerPartNumber", (string)product.ManufacturerPartNumber); - _writer.Write("Gtin", (string)product.Gtin); - _writer.Write("IsGiftCard", entity.IsGiftCard.ToString()); - _writer.Write("GiftCardTypeId", entity.GiftCardTypeId.ToString()); - _writer.Write("RequireOtherProducts", entity.RequireOtherProducts.ToString()); - _writer.Write("RequiredProductIds", entity.RequiredProductIds); - _writer.Write("AutomaticallyAddRequiredProducts", entity.AutomaticallyAddRequiredProducts.ToString()); - _writer.Write("IsDownload", entity.IsDownload.ToString()); - _writer.Write("UnlimitedDownloads", entity.UnlimitedDownloads.ToString()); - _writer.Write("MaxNumberOfDownloads", entity.MaxNumberOfDownloads.ToString()); - _writer.Write("DownloadExpirationDays", entity.DownloadExpirationDays.HasValue ? entity.DownloadExpirationDays.Value.ToString() : ""); - _writer.Write("DownloadActivationTypeId", entity.DownloadActivationTypeId.ToString()); - _writer.Write("HasSampleDownload", entity.HasSampleDownload.ToString()); - _writer.Write("SampleDownloadId", entity.SampleDownloadId.HasValue ? entity.SampleDownloadId.Value.ToString() : ""); - _writer.Write("HasUserAgreement", entity.HasUserAgreement.ToString()); - _writer.Write("UserAgreementText", entity.UserAgreementText); - _writer.Write("IsRecurring", entity.IsRecurring.ToString()); - _writer.Write("RecurringCycleLength", entity.RecurringCycleLength.ToString()); - _writer.Write("RecurringCyclePeriodId", entity.RecurringCyclePeriodId.ToString()); - _writer.Write("RecurringTotalCycles", entity.RecurringTotalCycles.ToString()); - _writer.Write("IsShipEnabled", entity.IsShipEnabled.ToString()); - _writer.Write("IsFreeShipping", entity.IsFreeShipping.ToString()); - _writer.Write("AdditionalShippingCharge", entity.AdditionalShippingCharge.ToString(_culture)); - _writer.Write("IsTaxExempt", entity.IsTaxExempt.ToString()); - _writer.Write("TaxCategoryId", entity.TaxCategoryId.ToString()); - _writer.Write("ManageInventoryMethodId", entity.ManageInventoryMethodId.ToString()); - _writer.Write("StockQuantity", entity.StockQuantity.ToString()); - _writer.Write("DisplayStockAvailability", entity.DisplayStockAvailability.ToString()); - _writer.Write("DisplayStockQuantity", entity.DisplayStockQuantity.ToString()); - _writer.Write("MinStockQuantity", entity.MinStockQuantity.ToString()); - _writer.Write("LowStockActivityId", entity.LowStockActivityId.ToString()); - _writer.Write("NotifyAdminForQuantityBelow", entity.NotifyAdminForQuantityBelow.ToString()); - _writer.Write("BackorderModeId", entity.BackorderModeId.ToString()); - _writer.Write("AllowBackInStockSubscriptions", entity.AllowBackInStockSubscriptions.ToString()); - _writer.Write("OrderMinimumQuantity", entity.OrderMinimumQuantity.ToString()); - _writer.Write("OrderMaximumQuantity", entity.OrderMaximumQuantity.ToString()); - _writer.Write("QuantityStep", entity.QuantityStep.ToString()); - _writer.Write("QuantiyControlType", ((int)entity.QuantiyControlType).ToString()); - _writer.Write("HideQuantityControl", entity.HideQuantityControl.ToString()); + WriteLocalized(manufacturer); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteProduct(dynamic product, string node) + { + if (product == null) + return; + + Product entity = product.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + decimal? basePriceAmount = product.BasePriceAmount; + int? basePriceBaseAmount = product.BasePriceBaseAmount; + decimal? lowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice; + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)product.Name); + _writer.Write("SeName", (string)product.SeName); + _writer.Write("ShortDescription", (string)product.ShortDescription); + _writer.Write("FullDescription", (string)product.FullDescription); + _writer.Write("AdminComment", (string)product.AdminComment); + _writer.Write("ProductTemplateId", entity.ProductTemplateId.ToString()); + _writer.Write("ProductTemplateViewPath", (string)product._ProductTemplateViewPath); + _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); + _writer.Write("HomePageDisplayOrder", entity.HomePageDisplayOrder.ToString()); + _writer.Write("MetaKeywords", (string)product.MetaKeywords); + _writer.Write("MetaDescription", (string)product.MetaDescription); + _writer.Write("MetaTitle", (string)product.MetaTitle); + _writer.Write("AllowCustomerReviews", entity.AllowCustomerReviews.ToString()); + _writer.Write("ApprovedRatingSum", entity.ApprovedRatingSum.ToString()); + _writer.Write("NotApprovedRatingSum", entity.NotApprovedRatingSum.ToString()); + _writer.Write("ApprovedTotalReviews", entity.ApprovedTotalReviews.ToString()); + _writer.Write("NotApprovedTotalReviews", entity.NotApprovedTotalReviews.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("ProductTypeId", entity.ProductTypeId.ToString()); + _writer.Write("ParentGroupedProductId", entity.ParentGroupedProductId.ToString()); + _writer.Write("Sku", (string)product.Sku); + _writer.Write("ManufacturerPartNumber", (string)product.ManufacturerPartNumber); + _writer.Write("Gtin", (string)product.Gtin); + _writer.Write("IsGiftCard", entity.IsGiftCard.ToString()); + _writer.Write("GiftCardTypeId", entity.GiftCardTypeId.ToString()); + _writer.Write("RequireOtherProducts", entity.RequireOtherProducts.ToString()); + _writer.Write("RequiredProductIds", entity.RequiredProductIds); + _writer.Write("AutomaticallyAddRequiredProducts", entity.AutomaticallyAddRequiredProducts.ToString()); + _writer.Write("IsDownload", entity.IsDownload.ToString()); + _writer.Write("UnlimitedDownloads", entity.UnlimitedDownloads.ToString()); + _writer.Write("MaxNumberOfDownloads", entity.MaxNumberOfDownloads.ToString()); + _writer.Write("DownloadExpirationDays", entity.DownloadExpirationDays.HasValue ? entity.DownloadExpirationDays.Value.ToString() : ""); + _writer.Write("DownloadActivationTypeId", entity.DownloadActivationTypeId.ToString()); + _writer.Write("HasSampleDownload", entity.HasSampleDownload.ToString()); + _writer.Write("SampleDownloadId", entity.SampleDownloadId.HasValue ? entity.SampleDownloadId.Value.ToString() : ""); + _writer.Write("HasUserAgreement", entity.HasUserAgreement.ToString()); + _writer.Write("UserAgreementText", entity.UserAgreementText); + _writer.Write("IsRecurring", entity.IsRecurring.ToString()); + _writer.Write("RecurringCycleLength", entity.RecurringCycleLength.ToString()); + _writer.Write("RecurringCyclePeriodId", entity.RecurringCyclePeriodId.ToString()); + _writer.Write("RecurringTotalCycles", entity.RecurringTotalCycles.ToString()); + _writer.Write("IsShipEnabled", entity.IsShipEnabled.ToString()); + _writer.Write("IsFreeShipping", entity.IsFreeShipping.ToString()); + _writer.Write("AdditionalShippingCharge", entity.AdditionalShippingCharge.ToString(_culture)); + _writer.Write("IsTaxExempt", entity.IsTaxExempt.ToString()); + _writer.Write("TaxCategoryId", entity.TaxCategoryId.ToString()); + _writer.Write("ManageInventoryMethodId", entity.ManageInventoryMethodId.ToString()); + _writer.Write("StockQuantity", entity.StockQuantity.ToString()); + _writer.Write("DisplayStockAvailability", entity.DisplayStockAvailability.ToString()); + _writer.Write("DisplayStockQuantity", entity.DisplayStockQuantity.ToString()); + _writer.Write("MinStockQuantity", entity.MinStockQuantity.ToString()); + _writer.Write("LowStockActivityId", entity.LowStockActivityId.ToString()); + _writer.Write("NotifyAdminForQuantityBelow", entity.NotifyAdminForQuantityBelow.ToString()); + _writer.Write("BackorderModeId", entity.BackorderModeId.ToString()); + _writer.Write("AllowBackInStockSubscriptions", entity.AllowBackInStockSubscriptions.ToString()); + _writer.Write("OrderMinimumQuantity", entity.OrderMinimumQuantity.ToString()); + _writer.Write("OrderMaximumQuantity", entity.OrderMaximumQuantity.ToString()); + _writer.Write("QuantityStep", entity.QuantityStep.ToString()); + _writer.Write("QuantiyControlType", ((int)entity.QuantiyControlType).ToString()); + _writer.Write("HideQuantityControl", entity.HideQuantityControl.ToString()); _writer.Write("AllowedQuantities", entity.AllowedQuantities); - _writer.Write("DisableBuyButton", entity.DisableBuyButton.ToString()); - _writer.Write("DisableWishlistButton", entity.DisableWishlistButton.ToString()); - _writer.Write("AvailableForPreOrder", entity.AvailableForPreOrder.ToString()); - _writer.Write("CallForPrice", entity.CallForPrice.ToString()); - _writer.Write("Price", entity.Price.ToString(_culture)); - _writer.Write("OldPrice", entity.OldPrice.ToString(_culture)); - _writer.Write("ProductCost", entity.ProductCost.ToString(_culture)); - _writer.Write("SpecialPrice", entity.SpecialPrice.HasValue ? entity.SpecialPrice.Value.ToString(_culture) : ""); - _writer.Write("SpecialPriceStartDateTimeUtc", entity.SpecialPriceStartDateTimeUtc.HasValue ? entity.SpecialPriceStartDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("SpecialPriceEndDateTimeUtc", entity.SpecialPriceEndDateTimeUtc.HasValue ? entity.SpecialPriceEndDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("CustomerEntersPrice", entity.CustomerEntersPrice.ToString()); - _writer.Write("MinimumCustomerEnteredPrice", entity.MinimumCustomerEnteredPrice.ToString(_culture)); - _writer.Write("MaximumCustomerEnteredPrice", entity.MaximumCustomerEnteredPrice.ToString(_culture)); - _writer.Write("HasTierPrices", entity.HasTierPrices.ToString()); - _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); - _writer.Write("MainPictureId", entity.MainPictureId.HasValue ? entity.MainPictureId.Value.ToString() : ""); - _writer.Write("Weight", ((decimal)product.Weight).ToString(_culture)); - _writer.Write("Length", ((decimal)product.Length).ToString(_culture)); - _writer.Write("Width", ((decimal)product.Width).ToString(_culture)); - _writer.Write("Height", ((decimal)product.Height).ToString(_culture)); - _writer.Write("AvailableStartDateTimeUtc", entity.AvailableStartDateTimeUtc.HasValue ? entity.AvailableStartDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("AvailableEndDateTimeUtc", entity.AvailableEndDateTimeUtc.HasValue ? entity.AvailableEndDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("BasePriceEnabled", ((bool)product.BasePriceEnabled).ToString()); - _writer.Write("BasePriceMeasureUnit", (string)product.BasePriceMeasureUnit); - _writer.Write("BasePriceAmount", basePriceAmount.HasValue ? basePriceAmount.Value.ToString(_culture) : ""); - _writer.Write("BasePriceBaseAmount", basePriceBaseAmount.HasValue ? basePriceBaseAmount.Value.ToString() : ""); - _writer.Write("BasePriceHasValue", ((bool)product.BasePriceHasValue).ToString()); - _writer.Write("BasePriceInfo", (string)product._BasePriceInfo); + _writer.Write("DisableBuyButton", entity.DisableBuyButton.ToString()); + _writer.Write("DisableWishlistButton", entity.DisableWishlistButton.ToString()); + _writer.Write("AvailableForPreOrder", entity.AvailableForPreOrder.ToString()); + _writer.Write("CallForPrice", entity.CallForPrice.ToString()); + _writer.Write("Price", entity.Price.ToString(_culture)); + _writer.Write("OldPrice", entity.OldPrice.ToString(_culture)); + _writer.Write("ProductCost", entity.ProductCost.ToString(_culture)); + _writer.Write("SpecialPrice", entity.SpecialPrice.HasValue ? entity.SpecialPrice.Value.ToString(_culture) : ""); + _writer.Write("SpecialPriceStartDateTimeUtc", entity.SpecialPriceStartDateTimeUtc.HasValue ? entity.SpecialPriceStartDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("SpecialPriceEndDateTimeUtc", entity.SpecialPriceEndDateTimeUtc.HasValue ? entity.SpecialPriceEndDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("CustomerEntersPrice", entity.CustomerEntersPrice.ToString()); + _writer.Write("MinimumCustomerEnteredPrice", entity.MinimumCustomerEnteredPrice.ToString(_culture)); + _writer.Write("MaximumCustomerEnteredPrice", entity.MaximumCustomerEnteredPrice.ToString(_culture)); + _writer.Write("HasTierPrices", entity.HasTierPrices.ToString()); + _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("MainPictureId", entity.MainPictureId.HasValue ? entity.MainPictureId.Value.ToString() : ""); + _writer.Write("Weight", ((decimal)product.Weight).ToString(_culture)); + _writer.Write("Length", ((decimal)product.Length).ToString(_culture)); + _writer.Write("Width", ((decimal)product.Width).ToString(_culture)); + _writer.Write("Height", ((decimal)product.Height).ToString(_culture)); + _writer.Write("AvailableStartDateTimeUtc", entity.AvailableStartDateTimeUtc.HasValue ? entity.AvailableStartDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("AvailableEndDateTimeUtc", entity.AvailableEndDateTimeUtc.HasValue ? entity.AvailableEndDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("BasePriceEnabled", ((bool)product.BasePriceEnabled).ToString()); + _writer.Write("BasePriceMeasureUnit", (string)product.BasePriceMeasureUnit); + _writer.Write("BasePriceAmount", basePriceAmount.HasValue ? basePriceAmount.Value.ToString(_culture) : ""); + _writer.Write("BasePriceBaseAmount", basePriceBaseAmount.HasValue ? basePriceBaseAmount.Value.ToString() : ""); + _writer.Write("BasePriceHasValue", ((bool)product.BasePriceHasValue).ToString()); + _writer.Write("BasePriceInfo", (string)product._BasePriceInfo); _writer.Write("Visibility", ((int)entity.Visibility).ToString()); _writer.Write("Condition", ((int)entity.Condition).ToString()); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("IsSystemProduct", entity.IsSystemProduct.ToString()); - _writer.Write("BundleTitleText", entity.BundleTitleText); - _writer.Write("BundlePerItemPricing", entity.BundlePerItemPricing.ToString()); - _writer.Write("BundlePerItemShipping", entity.BundlePerItemShipping.ToString()); - _writer.Write("BundlePerItemShoppingCart", entity.BundlePerItemShoppingCart.ToString()); - _writer.Write("LowestAttributeCombinationPrice", lowestAttributeCombinationPrice.HasValue ? lowestAttributeCombinationPrice.Value.ToString(_culture) : ""); - _writer.Write("IsEsd", entity.IsEsd.ToString()); - _writer.Write("CustomsTariffNumber", entity.CustomsTariffNumber); - - WriteLocalized(product); - - WriteDeliveryTime(product.DeliveryTime, "DeliveryTime"); - - WriteQuantityUnit(product.QuantityUnit, "QuantityUnit"); - - WriteCountry(product.CountryOfOrigin, "CountryOfOrigin"); - - if (product.AppliedDiscounts != null) - { - _writer.WriteStartElement("AppliedDiscounts"); - foreach (dynamic discount in product.AppliedDiscounts) - { - Discount entityDiscount = discount.Entity; - - _writer.WriteStartElement("AppliedDiscount"); - _writer.Write("Id", entityDiscount.Id.ToString()); - _writer.Write("Name", (string)discount.Name); - _writer.Write("DiscountTypeId", entityDiscount.DiscountTypeId.ToString()); - _writer.Write("UsePercentage", entityDiscount.UsePercentage.ToString()); - _writer.Write("DiscountPercentage", entityDiscount.DiscountPercentage.ToString(_culture)); - _writer.Write("DiscountAmount", entityDiscount.DiscountAmount.ToString(_culture)); - _writer.Write("StartDateUtc", entityDiscount.StartDateUtc.HasValue ? entityDiscount.StartDateUtc.Value.ToString(_culture) : ""); - _writer.Write("EndDateUtc", entityDiscount.EndDateUtc.HasValue ? entityDiscount.EndDateUtc.Value.ToString(_culture) : ""); - _writer.Write("RequiresCouponCode", entityDiscount.RequiresCouponCode.ToString()); - _writer.Write("CouponCode", entityDiscount.CouponCode); - _writer.Write("DiscountLimitationId", entityDiscount.DiscountLimitationId.ToString()); - _writer.Write("LimitationTimes", entityDiscount.LimitationTimes.ToString()); - _writer.WriteEndElement(); // AppliedDiscount - } - _writer.WriteEndElement(); // AppliedDiscounts - } + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("IsSystemProduct", entity.IsSystemProduct.ToString()); + _writer.Write("BundleTitleText", entity.BundleTitleText); + _writer.Write("BundlePerItemPricing", entity.BundlePerItemPricing.ToString()); + _writer.Write("BundlePerItemShipping", entity.BundlePerItemShipping.ToString()); + _writer.Write("BundlePerItemShoppingCart", entity.BundlePerItemShoppingCart.ToString()); + _writer.Write("LowestAttributeCombinationPrice", lowestAttributeCombinationPrice.HasValue ? lowestAttributeCombinationPrice.Value.ToString(_culture) : ""); + _writer.Write("AttributeChoiceBehaviour", ((int)entity.AttributeChoiceBehaviour).ToString()); + _writer.Write("IsEsd", entity.IsEsd.ToString()); + _writer.Write("CustomsTariffNumber", entity.CustomsTariffNumber); + + WriteLocalized(product); + + WriteDeliveryTime(product.DeliveryTime, "DeliveryTime"); + + WriteQuantityUnit(product.QuantityUnit, "QuantityUnit"); + + WriteCountry(product.CountryOfOrigin, "CountryOfOrigin"); + + if (product.AppliedDiscounts != null) + { + _writer.WriteStartElement("AppliedDiscounts"); + foreach (dynamic discount in product.AppliedDiscounts) + { + Discount entityDiscount = discount.Entity; + + _writer.WriteStartElement("AppliedDiscount"); + _writer.Write("Id", entityDiscount.Id.ToString()); + _writer.Write("Name", (string)discount.Name); + _writer.Write("DiscountTypeId", entityDiscount.DiscountTypeId.ToString()); + _writer.Write("UsePercentage", entityDiscount.UsePercentage.ToString()); + _writer.Write("DiscountPercentage", entityDiscount.DiscountPercentage.ToString(_culture)); + _writer.Write("DiscountAmount", entityDiscount.DiscountAmount.ToString(_culture)); + _writer.Write("StartDateUtc", entityDiscount.StartDateUtc.HasValue ? entityDiscount.StartDateUtc.Value.ToString(_culture) : ""); + _writer.Write("EndDateUtc", entityDiscount.EndDateUtc.HasValue ? entityDiscount.EndDateUtc.Value.ToString(_culture) : ""); + _writer.Write("RequiresCouponCode", entityDiscount.RequiresCouponCode.ToString()); + _writer.Write("CouponCode", entityDiscount.CouponCode); + _writer.Write("DiscountLimitationId", entityDiscount.DiscountLimitationId.ToString()); + _writer.Write("LimitationTimes", entityDiscount.LimitationTimes.ToString()); + _writer.WriteEndElement(); // AppliedDiscount + } + _writer.WriteEndElement(); // AppliedDiscounts + } if (product.Downloads != null) { @@ -617,7 +617,7 @@ public void WriteProduct(dynamic product, string node) foreach (dynamic download in product.Downloads) { Download downloadEntity = download.Entity; - var mediaFile = downloadEntity.MediaFile; + var mediaFile = downloadEntity.MediaFile; _writer.WriteStartElement("Download"); _writer.Write("Id", downloadEntity.Id.ToString()); @@ -630,79 +630,79 @@ public void WriteProduct(dynamic product, string node) _writer.Write("EntityName", downloadEntity.EntityName); _writer.Write("FileVersion", downloadEntity.FileVersion); _writer.Write("Changelog", downloadEntity.Changelog); - if (!downloadEntity.UseDownloadUrl && mediaFile != null) - { - _writer.Write("ContentType", mediaFile.MimeType); - _writer.Write("Filename", mediaFile.Name); - _writer.Write("Extension", mediaFile.Extension); - _writer.Write("MediaStorageId", mediaFile.MediaStorageId.HasValue ? mediaFile.MediaStorageId.Value.ToString() : ""); - } - - _writer.WriteEndElement(); // Download + if (!downloadEntity.UseDownloadUrl && mediaFile != null) + { + _writer.Write("ContentType", mediaFile.MimeType); + _writer.Write("Filename", mediaFile.Name); + _writer.Write("Extension", mediaFile.Extension); + _writer.Write("MediaStorageId", mediaFile.MediaStorageId.HasValue ? mediaFile.MediaStorageId.Value.ToString() : ""); + } + + _writer.WriteEndElement(); // Download } _writer.WriteEndElement(); // Downloads } - if (product.TierPrices != null) - { - _writer.WriteStartElement("TierPrices"); - foreach (dynamic tierPrice in product.TierPrices) - { - TierPrice entityTierPrice = tierPrice.Entity; - - _writer.WriteStartElement("TierPrice"); - _writer.Write("Id", entityTierPrice.Id.ToString()); - _writer.Write("ProductId", entityTierPrice.ProductId.ToString()); - _writer.Write("StoreId", entityTierPrice.StoreId.ToString()); - _writer.Write("CustomerRoleId", entityTierPrice.CustomerRoleId.HasValue ? entityTierPrice.CustomerRoleId.Value.ToString() : ""); - _writer.Write("Quantity", entityTierPrice.Quantity.ToString()); - _writer.Write("Price", entityTierPrice.Price.ToString(_culture)); + if (product.TierPrices != null) + { + _writer.WriteStartElement("TierPrices"); + foreach (dynamic tierPrice in product.TierPrices) + { + TierPrice entityTierPrice = tierPrice.Entity; + + _writer.WriteStartElement("TierPrice"); + _writer.Write("Id", entityTierPrice.Id.ToString()); + _writer.Write("ProductId", entityTierPrice.ProductId.ToString()); + _writer.Write("StoreId", entityTierPrice.StoreId.ToString()); + _writer.Write("CustomerRoleId", entityTierPrice.CustomerRoleId.HasValue ? entityTierPrice.CustomerRoleId.Value.ToString() : ""); + _writer.Write("Quantity", entityTierPrice.Quantity.ToString()); + _writer.Write("Price", entityTierPrice.Price.ToString(_culture)); _writer.Write("CalculationMethod", ((int)entityTierPrice.CalculationMethod).ToString()); - _writer.WriteEndElement(); // TierPrice - } - _writer.WriteEndElement(); // TierPrices - } - - if (product.ProductTags != null) - { - _writer.WriteStartElement("ProductTags"); - foreach (dynamic tag in product.ProductTags) - { + _writer.WriteEndElement(); // TierPrice + } + _writer.WriteEndElement(); // TierPrices + } + + if (product.ProductTags != null) + { + _writer.WriteStartElement("ProductTags"); + foreach (dynamic tag in product.ProductTags) + { ProductTag entityTag = tag.Entity; - _writer.WriteStartElement("ProductTag"); - _writer.Write("Id", ((int)tag.Id).ToString()); - _writer.Write("Name", (string)tag.Name); - _writer.Write("SeName", (string)tag.SeName); + _writer.WriteStartElement("ProductTag"); + _writer.Write("Id", ((int)tag.Id).ToString()); + _writer.Write("Name", (string)tag.Name); + _writer.Write("SeName", (string)tag.SeName); _writer.Write("Published", entityTag.Published.ToString()); WriteLocalized(tag); - _writer.WriteEndElement(); // ProductTag - } - _writer.WriteEndElement(); // ProductTags - } - - if (product.ProductAttributes != null) - { - _writer.WriteStartElement("ProductAttributes"); - foreach (dynamic pva in product.ProductAttributes) - { - ProductVariantAttribute entityPva = pva.Entity; + _writer.WriteEndElement(); // ProductTag + } + _writer.WriteEndElement(); // ProductTags + } + + if (product.ProductAttributes != null) + { + _writer.WriteStartElement("ProductAttributes"); + foreach (dynamic pva in product.ProductAttributes) + { + ProductVariantAttribute entityPva = pva.Entity; ProductAttribute entityPa = pva.Attribute.Entity; _writer.WriteStartElement("ProductAttribute"); - _writer.Write("Id", entityPva.Id.ToString()); - _writer.Write("TextPrompt", (string)pva.TextPrompt); - _writer.Write("IsRequired", entityPva.IsRequired.ToString()); - _writer.Write("AttributeControlTypeId", entityPva.AttributeControlTypeId.ToString()); - _writer.Write("DisplayOrder", entityPva.DisplayOrder.ToString()); - - _writer.WriteStartElement("Attribute"); - _writer.Write("Id", entityPa.Id.ToString()); - _writer.Write("Alias", entityPa.Alias); - _writer.Write("Name", entityPa.Name); - _writer.Write("Description", entityPa.Description); + _writer.Write("Id", entityPva.Id.ToString()); + _writer.Write("TextPrompt", (string)pva.TextPrompt); + _writer.Write("IsRequired", entityPva.IsRequired.ToString()); + _writer.Write("AttributeControlTypeId", entityPva.AttributeControlTypeId.ToString()); + _writer.Write("DisplayOrder", entityPva.DisplayOrder.ToString()); + + _writer.WriteStartElement("Attribute"); + _writer.Write("Id", entityPa.Id.ToString()); + _writer.Write("Alias", entityPa.Alias); + _writer.Write("Name", entityPa.Name); + _writer.Write("Description", entityPa.Description); _writer.Write("AllowFiltering", entityPa.AllowFiltering.ToString()); _writer.Write("DisplayOrder", entityPa.DisplayOrder.ToString()); _writer.Write("FacetTemplateHint", ((int)entityPa.FacetTemplateHint).ToString()); @@ -710,250 +710,250 @@ public void WriteProduct(dynamic product, string node) WriteLocalized(pva.Attribute); - _writer.WriteEndElement(); // Attribute - - _writer.WriteStartElement("AttributeValues"); - foreach (dynamic value in pva.Attribute.Values) - { - ProductVariantAttributeValue entityPvav = value.Entity; - - _writer.WriteStartElement("AttributeValue"); - _writer.Write("Id", entityPvav.Id.ToString()); - _writer.Write("Alias", (string)value.Alias); - _writer.Write("Name", (string)value.Name); - _writer.Write("Color", (string)value.Color); - _writer.Write("PriceAdjustment", ((decimal)value.PriceAdjustment).ToString(_culture)); - _writer.Write("WeightAdjustment", ((decimal)value.WeightAdjustment).ToString(_culture)); - _writer.Write("IsPreSelected", entityPvav.IsPreSelected.ToString()); - _writer.Write("DisplayOrder", entityPvav.DisplayOrder.ToString()); - _writer.Write("ValueTypeId", entityPvav.ValueTypeId.ToString()); - _writer.Write("LinkedProductId", entityPvav.LinkedProductId.ToString()); - _writer.Write("Quantity", entityPvav.Quantity.ToString()); - - WriteLocalized(value); - - _writer.WriteEndElement(); // AttributeValue - } - _writer.WriteEndElement(); // AttributeValues - - _writer.WriteEndElement(); // ProductAttribute - } - _writer.WriteEndElement(); // ProductAttributes - } - - if (product.ProductAttributeCombinations != null) - { - _writer.WriteStartElement("ProductAttributeCombinations"); - foreach (dynamic combination in product.ProductAttributeCombinations) - { - ProductVariantAttributeCombination entityPvac = combination.Entity; - - _writer.WriteStartElement("ProductAttributeCombination"); - _writer.Write("Id", entityPvac.Id.ToString()); - _writer.Write("StockQuantity", entityPvac.StockQuantity.ToString()); - _writer.Write("AllowOutOfStockOrders", entityPvac.AllowOutOfStockOrders.ToString()); - _writer.Write("AttributesXml", entityPvac.AttributesXml); - _writer.Write("Sku", entityPvac.Sku); - _writer.Write("Gtin", entityPvac.Gtin); - _writer.Write("ManufacturerPartNumber", entityPvac.ManufacturerPartNumber); - _writer.Write("Price", entityPvac.Price.HasValue ? entityPvac.Price.Value.ToString(_culture) : ""); - _writer.Write("Length", entityPvac.Length.HasValue ? entityPvac.Length.Value.ToString(_culture) : ""); - _writer.Write("Width", entityPvac.Width.HasValue ? entityPvac.Width.Value.ToString(_culture) : ""); - _writer.Write("Height", entityPvac.Height.HasValue ? entityPvac.Height.Value.ToString(_culture) : ""); - _writer.Write("BasePriceAmount", entityPvac.BasePriceAmount.HasValue ? entityPvac.BasePriceAmount.Value.ToString(_culture) : ""); - _writer.Write("BasePriceBaseAmount", entityPvac.BasePriceBaseAmount.HasValue ? entityPvac.BasePriceBaseAmount.Value.ToString() : ""); - _writer.Write("AssignedPictureIds", entityPvac.AssignedMediaFileIds); - _writer.Write("IsActive", entityPvac.IsActive.ToString()); - - WriteDeliveryTime(combination.DeliveryTime, "DeliveryTime"); - - WriteQuantityUnit(combination.QuantityUnit, "QuantityUnit"); - - _writer.WriteStartElement("Pictures"); - foreach (dynamic assignedPicture in combination.Pictures) - { - WritePicture(assignedPicture, "Picture"); - } - _writer.WriteEndElement(); // Pictures - - _writer.WriteEndElement(); // ProductAttributeCombination - } - _writer.WriteEndElement(); // ProductAttributeCombinations - } - - if (product.ProductPictures != null) - { - _writer.WriteStartElement("ProductPictures"); - foreach (dynamic productPicture in product.ProductPictures) - { - ProductMediaFile entityProductPicture = productPicture.Entity; - - _writer.WriteStartElement("ProductPicture"); - _writer.Write("Id", entityProductPicture.Id.ToString()); - _writer.Write("DisplayOrder", entityProductPicture.DisplayOrder.ToString()); - - WritePicture(productPicture.Picture, "Picture"); - - _writer.WriteEndElement(); // ProductPicture - } - _writer.WriteEndElement(); // ProductPictures - } - - if (product.ProductCategories != null) - { - _writer.WriteStartElement("ProductCategories"); - foreach (dynamic productCategory in product.ProductCategories) - { - ProductCategory entityProductCategory = productCategory.Entity; - - _writer.WriteStartElement("ProductCategory"); - _writer.Write("Id", entityProductCategory.Id.ToString()); - _writer.Write("DisplayOrder", entityProductCategory.DisplayOrder.ToString()); - _writer.Write("IsFeaturedProduct", entityProductCategory.IsFeaturedProduct.ToString()); - - WriteCategory(productCategory.Category, "Category"); - - _writer.WriteEndElement(); // ProductCategory - } - _writer.WriteEndElement(); // ProductCategories - } - - if (product.ProductManufacturers != null) - { - _writer.WriteStartElement("ProductManufacturers"); - foreach (dynamic productManu in product.ProductManufacturers) - { - ProductManufacturer entityProductManu = productManu.Entity; - - _writer.WriteStartElement("ProductManufacturer"); - - _writer.Write("Id", entityProductManu.Id.ToString()); - _writer.Write("DisplayOrder", entityProductManu.DisplayOrder.ToString()); - _writer.Write("IsFeaturedProduct", entityProductManu.IsFeaturedProduct.ToString()); - - WriteManufacturer(productManu.Manufacturer, "Manufacturer"); - - _writer.WriteEndElement(); // ProductManufacturer - } - _writer.WriteEndElement(); // ProductManufacturers - } - - if (product.ProductSpecificationAttributes != null) - { - _writer.WriteStartElement("ProductSpecificationAttributes"); - foreach (dynamic psa in product.ProductSpecificationAttributes) - { - ProductSpecificationAttribute entityPsa = psa.Entity; - - _writer.WriteStartElement("ProductSpecificationAttribute"); - - _writer.Write("Id", entityPsa.Id.ToString()); - _writer.Write("ProductId", entityPsa.ProductId.ToString()); - _writer.Write("SpecificationAttributeOptionId", entityPsa.SpecificationAttributeOptionId.ToString()); - _writer.Write("AllowFiltering", entityPsa.AllowFiltering.ToString()); - _writer.Write("ShowOnProductPage", entityPsa.ShowOnProductPage.ToString()); - _writer.Write("DisplayOrder", entityPsa.DisplayOrder.ToString()); - - dynamic option = psa.SpecificationAttributeOption; - SpecificationAttributeOption entitySao = option.Entity; - SpecificationAttribute entitySa = option.SpecificationAttribute.Entity; - - _writer.WriteStartElement("SpecificationAttributeOption"); - _writer.Write("Id", entitySao.Id.ToString()); - _writer.Write("SpecificationAttributeId", entitySao.SpecificationAttributeId.ToString()); - _writer.Write("DisplayOrder", entitySao.DisplayOrder.ToString()); + _writer.WriteEndElement(); // Attribute + + _writer.WriteStartElement("AttributeValues"); + foreach (dynamic value in pva.Attribute.Values) + { + ProductVariantAttributeValue entityPvav = value.Entity; + + _writer.WriteStartElement("AttributeValue"); + _writer.Write("Id", entityPvav.Id.ToString()); + _writer.Write("Alias", (string)value.Alias); + _writer.Write("Name", (string)value.Name); + _writer.Write("Color", (string)value.Color); + _writer.Write("PriceAdjustment", ((decimal)value.PriceAdjustment).ToString(_culture)); + _writer.Write("WeightAdjustment", ((decimal)value.WeightAdjustment).ToString(_culture)); + _writer.Write("IsPreSelected", entityPvav.IsPreSelected.ToString()); + _writer.Write("DisplayOrder", entityPvav.DisplayOrder.ToString()); + _writer.Write("ValueTypeId", entityPvav.ValueTypeId.ToString()); + _writer.Write("LinkedProductId", entityPvav.LinkedProductId.ToString()); + _writer.Write("Quantity", entityPvav.Quantity.ToString()); + + WriteLocalized(value); + + _writer.WriteEndElement(); // AttributeValue + } + _writer.WriteEndElement(); // AttributeValues + + _writer.WriteEndElement(); // ProductAttribute + } + _writer.WriteEndElement(); // ProductAttributes + } + + if (product.ProductAttributeCombinations != null) + { + _writer.WriteStartElement("ProductAttributeCombinations"); + foreach (dynamic combination in product.ProductAttributeCombinations) + { + ProductVariantAttributeCombination entityPvac = combination.Entity; + + _writer.WriteStartElement("ProductAttributeCombination"); + _writer.Write("Id", entityPvac.Id.ToString()); + _writer.Write("StockQuantity", entityPvac.StockQuantity.ToString()); + _writer.Write("AllowOutOfStockOrders", entityPvac.AllowOutOfStockOrders.ToString()); + _writer.Write("AttributesXml", entityPvac.AttributesXml); + _writer.Write("Sku", entityPvac.Sku); + _writer.Write("Gtin", entityPvac.Gtin); + _writer.Write("ManufacturerPartNumber", entityPvac.ManufacturerPartNumber); + _writer.Write("Price", entityPvac.Price.HasValue ? entityPvac.Price.Value.ToString(_culture) : ""); + _writer.Write("Length", entityPvac.Length.HasValue ? entityPvac.Length.Value.ToString(_culture) : ""); + _writer.Write("Width", entityPvac.Width.HasValue ? entityPvac.Width.Value.ToString(_culture) : ""); + _writer.Write("Height", entityPvac.Height.HasValue ? entityPvac.Height.Value.ToString(_culture) : ""); + _writer.Write("BasePriceAmount", entityPvac.BasePriceAmount.HasValue ? entityPvac.BasePriceAmount.Value.ToString(_culture) : ""); + _writer.Write("BasePriceBaseAmount", entityPvac.BasePriceBaseAmount.HasValue ? entityPvac.BasePriceBaseAmount.Value.ToString() : ""); + _writer.Write("AssignedPictureIds", entityPvac.AssignedMediaFileIds); + _writer.Write("IsActive", entityPvac.IsActive.ToString()); + + WriteDeliveryTime(combination.DeliveryTime, "DeliveryTime"); + + WriteQuantityUnit(combination.QuantityUnit, "QuantityUnit"); + + _writer.WriteStartElement("Pictures"); + foreach (dynamic assignedPicture in combination.Pictures) + { + WritePicture(assignedPicture, "Picture"); + } + _writer.WriteEndElement(); // Pictures + + _writer.WriteEndElement(); // ProductAttributeCombination + } + _writer.WriteEndElement(); // ProductAttributeCombinations + } + + if (product.ProductPictures != null) + { + _writer.WriteStartElement("ProductPictures"); + foreach (dynamic productPicture in product.ProductPictures) + { + ProductMediaFile entityProductPicture = productPicture.Entity; + + _writer.WriteStartElement("ProductPicture"); + _writer.Write("Id", entityProductPicture.Id.ToString()); + _writer.Write("DisplayOrder", entityProductPicture.DisplayOrder.ToString()); + + WritePicture(productPicture.Picture, "Picture"); + + _writer.WriteEndElement(); // ProductPicture + } + _writer.WriteEndElement(); // ProductPictures + } + + if (product.ProductCategories != null) + { + _writer.WriteStartElement("ProductCategories"); + foreach (dynamic productCategory in product.ProductCategories) + { + ProductCategory entityProductCategory = productCategory.Entity; + + _writer.WriteStartElement("ProductCategory"); + _writer.Write("Id", entityProductCategory.Id.ToString()); + _writer.Write("DisplayOrder", entityProductCategory.DisplayOrder.ToString()); + _writer.Write("IsFeaturedProduct", entityProductCategory.IsFeaturedProduct.ToString()); + + WriteCategory(productCategory.Category, "Category"); + + _writer.WriteEndElement(); // ProductCategory + } + _writer.WriteEndElement(); // ProductCategories + } + + if (product.ProductManufacturers != null) + { + _writer.WriteStartElement("ProductManufacturers"); + foreach (dynamic productManu in product.ProductManufacturers) + { + ProductManufacturer entityProductManu = productManu.Entity; + + _writer.WriteStartElement("ProductManufacturer"); + + _writer.Write("Id", entityProductManu.Id.ToString()); + _writer.Write("DisplayOrder", entityProductManu.DisplayOrder.ToString()); + _writer.Write("IsFeaturedProduct", entityProductManu.IsFeaturedProduct.ToString()); + + WriteManufacturer(productManu.Manufacturer, "Manufacturer"); + + _writer.WriteEndElement(); // ProductManufacturer + } + _writer.WriteEndElement(); // ProductManufacturers + } + + if (product.ProductSpecificationAttributes != null) + { + _writer.WriteStartElement("ProductSpecificationAttributes"); + foreach (dynamic psa in product.ProductSpecificationAttributes) + { + ProductSpecificationAttribute entityPsa = psa.Entity; + + _writer.WriteStartElement("ProductSpecificationAttribute"); + + _writer.Write("Id", entityPsa.Id.ToString()); + _writer.Write("ProductId", entityPsa.ProductId.ToString()); + _writer.Write("SpecificationAttributeOptionId", entityPsa.SpecificationAttributeOptionId.ToString()); + _writer.Write("AllowFiltering", entityPsa.AllowFiltering.ToString()); + _writer.Write("ShowOnProductPage", entityPsa.ShowOnProductPage.ToString()); + _writer.Write("DisplayOrder", entityPsa.DisplayOrder.ToString()); + + dynamic option = psa.SpecificationAttributeOption; + SpecificationAttributeOption entitySao = option.Entity; + SpecificationAttribute entitySa = option.SpecificationAttribute.Entity; + + _writer.WriteStartElement("SpecificationAttributeOption"); + _writer.Write("Id", entitySao.Id.ToString()); + _writer.Write("SpecificationAttributeId", entitySao.SpecificationAttributeId.ToString()); + _writer.Write("DisplayOrder", entitySao.DisplayOrder.ToString()); _writer.Write("NumberValue", ((decimal)option.NumberValue).ToString(_culture)); _writer.Write("Color", (string)option.Color); _writer.Write("Name", (string)option.Name); - _writer.Write("Alias", (string)option.Alias); - - WriteLocalized(option); - - _writer.WriteStartElement("SpecificationAttribute"); - _writer.Write("Id", entitySa.Id.ToString()); - _writer.Write("Name", (string)option.SpecificationAttribute.Name); - _writer.Write("Alias", (string)option.SpecificationAttribute.Alias); - _writer.Write("DisplayOrder", entitySa.DisplayOrder.ToString()); - _writer.Write("AllowFiltering", entitySa.AllowFiltering.ToString()); - _writer.Write("ShowOnProductPage", entitySa.ShowOnProductPage.ToString()); - _writer.Write("FacetSorting", ((int)entitySa.FacetSorting).ToString()); - _writer.Write("FacetTemplateHint", ((int)entitySa.FacetTemplateHint).ToString()); + _writer.Write("Alias", (string)option.Alias); + + WriteLocalized(option); + + _writer.WriteStartElement("SpecificationAttribute"); + _writer.Write("Id", entitySa.Id.ToString()); + _writer.Write("Name", (string)option.SpecificationAttribute.Name); + _writer.Write("Alias", (string)option.SpecificationAttribute.Alias); + _writer.Write("DisplayOrder", entitySa.DisplayOrder.ToString()); + _writer.Write("AllowFiltering", entitySa.AllowFiltering.ToString()); + _writer.Write("ShowOnProductPage", entitySa.ShowOnProductPage.ToString()); + _writer.Write("FacetSorting", ((int)entitySa.FacetSorting).ToString()); + _writer.Write("FacetTemplateHint", ((int)entitySa.FacetTemplateHint).ToString()); _writer.Write("IndexOptionNames", entitySa.IndexOptionNames.ToString()); - WriteLocalized(option.SpecificationAttribute); - - _writer.WriteEndElement(); // SpecificationAttribute - _writer.WriteEndElement(); // SpecificationAttributeOption - - _writer.WriteEndElement(); // ProductSpecificationAttribute - } - _writer.WriteEndElement(); // ProductSpecificationAttributes - } - - if (product.ProductBundleItems != null) - { - _writer.WriteStartElement("ProductBundleItems"); - foreach (dynamic bundleItem in product.ProductBundleItems) - { - ProductBundleItem entityPbi = bundleItem.Entity; - - _writer.WriteStartElement("ProductBundleItem"); - _writer.Write("Id", entityPbi.Id.ToString()); - _writer.Write("ProductId", entityPbi.ProductId.ToString()); - _writer.Write("BundleProductId", entityPbi.BundleProductId.ToString()); - _writer.Write("Quantity", entityPbi.Quantity.ToString()); - _writer.Write("Discount", entityPbi.Discount.HasValue ? entityPbi.Discount.Value.ToString(_culture) : ""); - _writer.Write("DiscountPercentage", entityPbi.DiscountPercentage.ToString()); - _writer.Write("Name", (string)bundleItem.Name); - _writer.Write("ShortDescription", (string)bundleItem.ShortDescription); - _writer.Write("FilterAttributes", entityPbi.FilterAttributes.ToString()); - _writer.Write("HideThumbnail", entityPbi.HideThumbnail.ToString()); - _writer.Write("Visible", entityPbi.Visible.ToString()); - _writer.Write("Published", entityPbi.Published.ToString()); - _writer.Write("DisplayOrder", ((int)bundleItem.DisplayOrder).ToString()); - _writer.Write("CreatedOnUtc", entityPbi.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entityPbi.UpdatedOnUtc.ToString(_culture)); - - WriteLocalized(bundleItem); - - _writer.WriteEndElement(); // ProductBundleItem - } - _writer.WriteEndElement(); // ProductBundleItems - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCustomer(dynamic customer, string node) - { - if (customer == null) - return; - - Customer entity = customer.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("CustomerGuid", entity.CustomerGuid.ToString()); - _writer.Write("Username", entity.Username); - _writer.Write("Email", entity.Email); - _writer.Write("AdminComment", entity.AdminComment); - _writer.Write("IsTaxExempt", entity.IsTaxExempt.ToString()); - _writer.Write("AffiliateId", entity.AffiliateId.ToString()); - _writer.Write("Active", entity.Active.ToString()); - _writer.Write("Deleted", entity.Deleted.ToString()); - _writer.Write("IsSystemAccount", entity.IsSystemAccount.ToString()); - _writer.Write("SystemName", entity.SystemName); - _writer.Write("LastIpAddress", entity.LastIpAddress); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("LastLoginDateUtc", entity.LastLoginDateUtc.HasValue ? entity.LastLoginDateUtc.Value.ToString(_culture) : ""); - _writer.Write("LastActivityDateUtc", entity.LastActivityDateUtc.ToString(_culture)); + WriteLocalized(option.SpecificationAttribute); + + _writer.WriteEndElement(); // SpecificationAttribute + _writer.WriteEndElement(); // SpecificationAttributeOption + + _writer.WriteEndElement(); // ProductSpecificationAttribute + } + _writer.WriteEndElement(); // ProductSpecificationAttributes + } + + if (product.ProductBundleItems != null) + { + _writer.WriteStartElement("ProductBundleItems"); + foreach (dynamic bundleItem in product.ProductBundleItems) + { + ProductBundleItem entityPbi = bundleItem.Entity; + + _writer.WriteStartElement("ProductBundleItem"); + _writer.Write("Id", entityPbi.Id.ToString()); + _writer.Write("ProductId", entityPbi.ProductId.ToString()); + _writer.Write("BundleProductId", entityPbi.BundleProductId.ToString()); + _writer.Write("Quantity", entityPbi.Quantity.ToString()); + _writer.Write("Discount", entityPbi.Discount.HasValue ? entityPbi.Discount.Value.ToString(_culture) : ""); + _writer.Write("DiscountPercentage", entityPbi.DiscountPercentage.ToString()); + _writer.Write("Name", (string)bundleItem.Name); + _writer.Write("ShortDescription", (string)bundleItem.ShortDescription); + _writer.Write("FilterAttributes", entityPbi.FilterAttributes.ToString()); + _writer.Write("HideThumbnail", entityPbi.HideThumbnail.ToString()); + _writer.Write("Visible", entityPbi.Visible.ToString()); + _writer.Write("Published", entityPbi.Published.ToString()); + _writer.Write("DisplayOrder", ((int)bundleItem.DisplayOrder).ToString()); + _writer.Write("CreatedOnUtc", entityPbi.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entityPbi.UpdatedOnUtc.ToString(_culture)); + + WriteLocalized(bundleItem); + + _writer.WriteEndElement(); // ProductBundleItem + } + _writer.WriteEndElement(); // ProductBundleItems + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCustomer(dynamic customer, string node) + { + if (customer == null) + return; + + Customer entity = customer.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("CustomerGuid", entity.CustomerGuid.ToString()); + _writer.Write("Username", entity.Username); + _writer.Write("Email", entity.Email); + _writer.Write("AdminComment", entity.AdminComment); + _writer.Write("IsTaxExempt", entity.IsTaxExempt.ToString()); + _writer.Write("AffiliateId", entity.AffiliateId.ToString()); + _writer.Write("Active", entity.Active.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("IsSystemAccount", entity.IsSystemAccount.ToString()); + _writer.Write("SystemName", entity.SystemName); + _writer.Write("LastIpAddress", entity.LastIpAddress); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("LastLoginDateUtc", entity.LastLoginDateUtc.HasValue ? entity.LastLoginDateUtc.Value.ToString(_culture) : ""); + _writer.Write("LastActivityDateUtc", entity.LastActivityDateUtc.ToString(_culture)); _writer.Write("Salutation", entity.Salutation); _writer.Write("Title", entity.Title); _writer.Write("FirstName", entity.FirstName); @@ -964,92 +964,92 @@ public void WriteCustomer(dynamic customer, string node) _writer.Write("BirthDate", entity.BirthDate.HasValue ? entity.BirthDate.Value.ToString(_culture) : ""); _writer.Write("RewardPointsBalance", ((int)customer._RewardPointsBalance).ToString()); - if (customer.CustomerRoles != null) - { - _writer.WriteStartElement("CustomerRoles"); - foreach (dynamic role in customer.CustomerRoles) - { - CustomerRole entityRole = role.Entity; - - _writer.WriteStartElement("CustomerRole"); - _writer.Write("Id", entityRole.Id.ToString()); - _writer.Write("Name", (string)role.Name); - _writer.Write("FreeShipping", entityRole.FreeShipping.ToString()); - _writer.Write("TaxExempt", entityRole.TaxExempt.ToString()); - _writer.Write("TaxDisplayType", entityRole.TaxDisplayType.HasValue ? entityRole.TaxDisplayType.Value.ToString() : ""); - _writer.Write("Active", entityRole.Active.ToString()); - _writer.Write("IsSystemRole", entityRole.IsSystemRole.ToString()); - _writer.Write("SystemName", entityRole.SystemName); - _writer.WriteEndElement(); // CustomerRole - } - _writer.WriteEndElement(); // CustomerRoles - } - - WriteRewardPointsHistory(customer.RewardPointsHistory, "RewardPointsHistories"); - WriteAddress(customer.BillingAddress, "BillingAddress"); - WriteAddress(customer.ShippingAddress, "ShippingAddress"); - - if (customer.Addresses != null) - { - _writer.WriteStartElement("Addresses"); - foreach (dynamic address in customer.Addresses) - { - WriteAddress(address, "Address"); - } - _writer.WriteEndElement(); // Addresses - } - - WriteGenericAttributes(customer); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteShoppingCartItem(dynamic shoppingCartItem, string node) - { - if (shoppingCartItem == null) - return; - - ShoppingCartItem entity = shoppingCartItem.Entity; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", entity.Id.ToString()); - _writer.Write("StoreId", entity.StoreId.ToString()); - _writer.Write("ParentItemId", entity.ParentItemId.HasValue ? entity.ParentItemId.Value.ToString() : ""); - _writer.Write("BundleItemId", entity.BundleItemId.HasValue ? entity.BundleItemId.Value.ToString() : ""); - _writer.Write("ShoppingCartTypeId", entity.ShoppingCartTypeId.ToString()); - _writer.Write("CustomerId", entity.CustomerId.ToString()); - _writer.Write("ProductId", entity.ProductId.ToString()); - _writer.Write("AttributesXml", entity.AttributesXml, null, true); - _writer.Write("CustomerEnteredPrice", entity.CustomerEnteredPrice.ToString(_culture)); - _writer.Write("Quantity", entity.Quantity.ToString()); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); - - WriteCustomer(shoppingCartItem.Customer, "Customer"); - WriteProduct(shoppingCartItem.Product, "Product"); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - } - - - /// - /// Allows to exclude XML nodes from export - /// - [Flags] - public enum ExportXmlExclude - { - None = 0, - Category = 1 - } + if (customer.CustomerRoles != null) + { + _writer.WriteStartElement("CustomerRoles"); + foreach (dynamic role in customer.CustomerRoles) + { + CustomerRole entityRole = role.Entity; + + _writer.WriteStartElement("CustomerRole"); + _writer.Write("Id", entityRole.Id.ToString()); + _writer.Write("Name", (string)role.Name); + _writer.Write("FreeShipping", entityRole.FreeShipping.ToString()); + _writer.Write("TaxExempt", entityRole.TaxExempt.ToString()); + _writer.Write("TaxDisplayType", entityRole.TaxDisplayType.HasValue ? entityRole.TaxDisplayType.Value.ToString() : ""); + _writer.Write("Active", entityRole.Active.ToString()); + _writer.Write("IsSystemRole", entityRole.IsSystemRole.ToString()); + _writer.Write("SystemName", entityRole.SystemName); + _writer.WriteEndElement(); // CustomerRole + } + _writer.WriteEndElement(); // CustomerRoles + } + + WriteRewardPointsHistory(customer.RewardPointsHistory, "RewardPointsHistories"); + WriteAddress(customer.BillingAddress, "BillingAddress"); + WriteAddress(customer.ShippingAddress, "ShippingAddress"); + + if (customer.Addresses != null) + { + _writer.WriteStartElement("Addresses"); + foreach (dynamic address in customer.Addresses) + { + WriteAddress(address, "Address"); + } + _writer.WriteEndElement(); // Addresses + } + + WriteGenericAttributes(customer); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteShoppingCartItem(dynamic shoppingCartItem, string node) + { + if (shoppingCartItem == null) + return; + + ShoppingCartItem entity = shoppingCartItem.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("StoreId", entity.StoreId.ToString()); + _writer.Write("ParentItemId", entity.ParentItemId.HasValue ? entity.ParentItemId.Value.ToString() : ""); + _writer.Write("BundleItemId", entity.BundleItemId.HasValue ? entity.BundleItemId.Value.ToString() : ""); + _writer.Write("ShoppingCartTypeId", entity.ShoppingCartTypeId.ToString()); + _writer.Write("CustomerId", entity.CustomerId.ToString()); + _writer.Write("ProductId", entity.ProductId.ToString()); + _writer.Write("AttributesXml", entity.AttributesXml, null, true); + _writer.Write("CustomerEnteredPrice", entity.CustomerEnteredPrice.ToString(_culture)); + _writer.Write("Quantity", entity.Quantity.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + + WriteCustomer(shoppingCartItem.Customer, "Customer"); + WriteProduct(shoppingCartItem.Product, "Product"); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + } + + + /// + /// Allows to exclude XML nodes from export + /// + [Flags] + public enum ExportXmlExclude + { + None = 0, + Category = 1 + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs index 19ff8e74b1..428048be79 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs @@ -10,67 +10,67 @@ namespace SmartStore.Services.DataExchange.Export { - public interface IDataExporter - { - DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken); + public interface IDataExporter + { + DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken); DataExportPreviewResult Preview(DataExportRequest request, int pageIndex); - /// - /// Creates a product export context for fast retrieval (eager loading) of product navigation properties. - /// - /// Products. null to lazy load data if required. - /// Customer, null to use current customer. - /// Store identifier, null to use current store. - /// Pictures per product, null to load all pictures per product. - /// A value indicating whether to include hidden records. - /// Product export context - ProductExportContext CreateProductExportContext( - IEnumerable products = null, - Customer customer = null, - int? storeId = null, - int? maxPicturesPerProduct = null, - bool includeHidden = true); - } + /// + /// Creates a product export context for fast retrieval (eager loading) of product navigation properties. + /// + /// Products. null to lazy load data if required. + /// Customer, null to use current customer. + /// Store identifier, null to use current store. + /// Pictures per product, null to load all pictures per product. + /// A value indicating whether to include hidden records. + /// Product export context + ProductExportContext CreateProductExportContext( + IEnumerable products = null, + Customer customer = null, + int? storeId = null, + int? maxPicturesPerProduct = null, + bool includeHidden = true); + } - public class DataExportRequest - { - private readonly static ProgressValueSetter _voidProgressValueSetter = DataExportRequest.SetProgress; + public class DataExportRequest + { + private readonly static ProgressValueSetter _voidProgressValueSetter = DataExportRequest.SetProgress; - public DataExportRequest(ExportProfile profile, Provider provider) - { - Guard.NotNull(profile, nameof(profile)); - Guard.NotNull(provider, nameof(provider)); + public DataExportRequest(ExportProfile profile, Provider provider) + { + Guard.NotNull(profile, nameof(profile)); + Guard.NotNull(provider, nameof(provider)); - Profile = profile; - Provider = provider; - ProgressValueSetter = _voidProgressValueSetter; + Profile = profile; + Provider = provider; + ProgressValueSetter = _voidProgressValueSetter; - EntitiesToExport = new List(); - CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + EntitiesToExport = new List(); + CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - public ExportProfile Profile { get; private set; } + public ExportProfile Profile { get; private set; } - public Provider Provider { get; private set; } + public Provider Provider { get; private set; } - public ProgressValueSetter ProgressValueSetter { get; set; } + public ProgressValueSetter ProgressValueSetter { get; set; } - public bool HasPermission { get; set; } + public bool HasPermission { get; set; } - public IList EntitiesToExport { get; set; } + public IList EntitiesToExport { get; set; } - public string ActionOrigin { get; set; } + public string ActionOrigin { get; set; } - public IDictionary CustomData { get; private set; } + public IDictionary CustomData { get; private set; } - public IQueryable ProductQuery { get; set; } + public IQueryable ProductQuery { get; set; } - private static void SetProgress(int val, int max, string msg) - { - // do nothing - } - } + private static void SetProgress(int val, int max, string msg) + { + // do nothing + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs index b0b0c8a6bd..f5c0ab977c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs @@ -6,117 +6,117 @@ namespace SmartStore.Services.DataExchange.Export { - public interface IExportProfileService - { - /// - /// Inserts an export profile - /// - /// Provider system name - /// The name of the profile - /// File extension supported by provider - /// Features supportde by provider - /// Whether the new profile is a system profile - /// Profile system name - /// Identifier of a profile the settings should be copied from - /// New export profile - ExportProfile InsertExportProfile( - string providerSystemName, - string name, - string fileExtension, - ExportFeatures features, - bool isSystemProfile = false, - string profileSystemName = null, - int cloneFromProfileId = 0); + public interface IExportProfileService + { + /// + /// Inserts an export profile + /// + /// Provider system name + /// The name of the profile + /// File extension supported by provider + /// Features supportde by provider + /// Whether the new profile is a system profile + /// Profile system name + /// Identifier of a profile the settings should be copied from + /// New export profile + ExportProfile InsertExportProfile( + string providerSystemName, + string name, + string fileExtension, + ExportFeatures features, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0); - /// - /// Inserts an export profile - /// - /// Export provider - /// Whether the new profile is a system profile - /// Profile system name - /// Identifier of a profile the settings should be copied from - /// New export profile - ExportProfile InsertExportProfile( - Provider provider, - bool isSystemProfile = false, - string profileSystemName = null, - int cloneFromProfileId = 0); + /// + /// Inserts an export profile + /// + /// Export provider + /// Whether the new profile is a system profile + /// Profile system name + /// Identifier of a profile the settings should be copied from + /// New export profile + ExportProfile InsertExportProfile( + Provider provider, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0); - /// - /// Updates an export profile - /// - /// Export profile - void UpdateExportProfile(ExportProfile profile); + /// + /// Updates an export profile + /// + /// Export profile + void UpdateExportProfile(ExportProfile profile); - /// - /// Deletes an export profile - /// - /// Export profile - /// Whether to delete system profiles - void DeleteExportProfile(ExportProfile profile, bool force = false); + /// + /// Deletes an export profile + /// + /// Export profile + /// Whether to delete system profiles + void DeleteExportProfile(ExportProfile profile, bool force = false); - /// - /// Get queryable export profiles - /// - /// Whether to filter enabled or disabled profiles - /// Export profiles - IQueryable GetExportProfiles(bool? enabled = null); + /// + /// Get queryable export profiles + /// + /// Whether to filter enabled or disabled profiles + /// Export profiles + IQueryable GetExportProfiles(bool? enabled = null); - /// - /// Gets an export profile by identifier - /// - /// Export profile identifier - /// Export profile - ExportProfile GetExportProfileById(int id); + /// + /// Gets an export profile by identifier + /// + /// Export profile identifier + /// Export profile + ExportProfile GetExportProfileById(int id); - /// - /// Gets system export profile by provider system name - /// - /// Provider system name - /// - ExportProfile GetSystemExportProfile(string providerSystemName); + /// + /// Gets system export profile by provider system name + /// + /// Provider system name + /// + ExportProfile GetSystemExportProfile(string providerSystemName); - /// - /// Gets export profiles by provider system name - /// - /// Provider system name - /// List of export profiles - IList GetExportProfilesBySystemName(string providerSystemName); + /// + /// Gets export profiles by provider system name + /// + /// Provider system name + /// List of export profiles + IList GetExportProfilesBySystemName(string providerSystemName); - /// - /// Load all export providers - /// - /// Store identifier - /// Whether to load hidden providers - /// Export providers - IEnumerable> LoadAllExportProviders(int storeId = 0, bool showHidden = true); + /// + /// Load all export providers + /// + /// Store identifier + /// Whether to load hidden providers + /// Export providers + IEnumerable> LoadAllExportProviders(int storeId = 0, bool showHidden = true); - /// - /// Load export provider by system name - /// - /// Provider system name - /// Store identifier - /// Export provider - Provider LoadProvider(string systemName, int storeId = 0); + /// + /// Load export provider by system name + /// + /// Provider system name + /// Store identifier + /// Export provider + Provider LoadProvider(string systemName, int storeId = 0); - /// - /// Get export deployment by identifier - /// - /// Export deployment identifier - /// Export deployment - ExportDeployment GetExportDeploymentById(int id); + /// + /// Get export deployment by identifier + /// + /// Export deployment identifier + /// Export deployment + ExportDeployment GetExportDeploymentById(int id); - /// - /// Update an export deployment - /// - /// Export deployment - void UpdateExportDeployment(ExportDeployment deployment); + /// + /// Update an export deployment + /// + /// Export deployment + void UpdateExportDeployment(ExportDeployment deployment); - /// - /// Deleted an export deployment - /// - /// Export deployment - void DeleteExportDeployment(ExportDeployment deployment); - } + /// + /// Deleted an export deployment + /// + /// Export deployment + void DeleteExportDeployment(ExportDeployment deployment); + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs index 60be2e11a9..f73c64a3ea 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs @@ -3,33 +3,33 @@ namespace SmartStore.Services.DataExchange.Export { - public partial interface IExportProvider : IProvider, IUserEditable - { - /// - /// The exported entity type - /// - ExportEntityType EntityType { get; } + public partial interface IExportProvider : IProvider, IUserEditable + { + /// + /// The exported entity type + /// + ExportEntityType EntityType { get; } - /// - /// File extension of the export files (without dot). Return null for a non file based, on-the-fly export. - /// - string FileExtension { get; } + /// + /// File extension of the export files (without dot). Return null for a non file based, on-the-fly export. + /// + string FileExtension { get; } - /// - /// Get provider specific configuration information. Return null when no provider specific configuration is required. - /// - ExportConfigurationInfo ConfigurationInfo { get; } + /// + /// Get provider specific configuration information. Return null when no provider specific configuration is required. + /// + ExportConfigurationInfo ConfigurationInfo { get; } - /// - /// Export data to a file - /// - /// Export execution context - void Execute(ExportExecuteContext context); + /// + /// Export data to a file + /// + /// Export execution context + void Execute(ExportExecuteContext context); - /// - /// Called once per store when export execution ended - /// - /// Export execution context - void OnExecuted(ExportExecuteContext context); - } + /// + /// Called once per store when export execution ended + /// + /// Export execution context + void OnExecuted(ExportExecuteContext context); + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs index 2ef0903f5c..db7aa9e92f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs @@ -8,70 +8,70 @@ namespace SmartStore.Services.DataExchange.Export.Internal { internal class CategoryExportContext - { - protected List _categoryIds; - protected List _fileIds; + { + protected List _categoryIds; + protected List _fileIds; - private Func> _funcProductCategories; - private Func> _funcFiles; + private Func> _funcProductCategories; + private Func> _funcFiles; - private LazyMultimap _productCategories; - private LazyMultimap _files; + private LazyMultimap _productCategories; + private LazyMultimap _files; - public CategoryExportContext( - IEnumerable categories, - Func> productCategories, - Func> files) - { - if (categories == null) - { - _categoryIds = new List(); - _fileIds = new List(); - } - else - { - _categoryIds = new List(categories.Select(x => x.Id)); - _fileIds = categories.Select(x => x.MediaFileId ?? 0) + public CategoryExportContext( + IEnumerable categories, + Func> productCategories, + Func> files) + { + if (categories == null) + { + _categoryIds = new List(); + _fileIds = new List(); + } + else + { + _categoryIds = new List(categories.Select(x => x.Id)); + _fileIds = categories.Select(x => x.MediaFileId ?? 0) .Where(x => x != 0) .Distinct() .ToList(); - } + } - _funcProductCategories = productCategories; - _funcFiles = files; - } + _funcProductCategories = productCategories; + _funcFiles = files; + } - public void Clear() - { - _productCategories?.Clear(); - _files?.Clear(); + public void Clear() + { + _productCategories?.Clear(); + _files?.Clear(); - _categoryIds?.Clear(); - _fileIds?.Clear(); - } + _categoryIds?.Clear(); + _fileIds?.Clear(); + } - public LazyMultimap ProductCategories - { - get - { - if (_productCategories == null) - { - _productCategories = new LazyMultimap(keys => _funcProductCategories(keys), _categoryIds); - } - return _productCategories; - } - } + public LazyMultimap ProductCategories + { + get + { + if (_productCategories == null) + { + _productCategories = new LazyMultimap(keys => _funcProductCategories(keys), _categoryIds); + } + return _productCategories; + } + } - public LazyMultimap Files - { - get - { - if (_files == null) - { - _files = new LazyMultimap(keys => _funcFiles(keys).ToMultimap(x => x.Id, x => x), _fileIds); - } - return _files; - } - } - } + public LazyMultimap Files + { + get + { + if (_files == null) + { + _files = new LazyMultimap(keys => _funcFiles(keys).ToMultimap(x => x.Id, x => x), _fileIds); + } + return _files; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs index 918cb3e110..bf2dc56700 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs @@ -7,48 +7,48 @@ namespace SmartStore.Services.DataExchange.Export.Internal { - public class CustomerExportContext - { - protected List _customerIds; - - private Func> _funcGenericAttributes; - - private LazyMultimap _genericAttributes; - - public CustomerExportContext( - IEnumerable customers, - Func> genericAttributes) - { - if (customers == null) - { - _customerIds = new List(); - } - else - { - _customerIds = new List(customers.Select(x => x.Id)); - } - - _funcGenericAttributes = genericAttributes; - } - - public void Clear() - { - if (_genericAttributes != null) - _genericAttributes.Clear(); - - _customerIds.Clear(); - } - - public LazyMultimap GenericAttributes - { - get - { - if (_genericAttributes == null) - { - _genericAttributes = new LazyMultimap(keys => _funcGenericAttributes(keys), _customerIds); - } - return _genericAttributes; - } - } - } + public class CustomerExportContext + { + protected List _customerIds; + + private Func> _funcGenericAttributes; + + private LazyMultimap _genericAttributes; + + public CustomerExportContext( + IEnumerable customers, + Func> genericAttributes) + { + if (customers == null) + { + _customerIds = new List(); + } + else + { + _customerIds = new List(customers.Select(x => x.Id)); + } + + _funcGenericAttributes = genericAttributes; + } + + public void Clear() + { + if (_genericAttributes != null) + _genericAttributes.Clear(); + + _customerIds.Clear(); + } + + public LazyMultimap GenericAttributes + { + get + { + if (_genericAttributes == null) + { + _genericAttributes = new LazyMultimap(keys => _funcGenericAttributes(keys), _customerIds); + } + return _genericAttributes; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index 317d4400e3..be7aa0d58b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -13,74 +13,74 @@ namespace SmartStore.Services.DataExchange.Export.Internal { internal class DataExporterContext - { - public DataExporterContext( - DataExportRequest request, - CancellationToken cancellationToken, - bool isPreview = false) - { - Request = request; - CancellationToken = cancellationToken; - Filter = XmlHelper.Deserialize(request.Profile.Filtering); - Projection = XmlHelper.Deserialize(request.Profile.Projection); - IsPreview = isPreview; - - if (request.Profile.Projection.IsEmpty()) - { - Projection.DescriptionMergingId = (int)ExportDescriptionMerging.Description; - } - - FolderContent = request.Profile.GetExportFolder(true, true); - - DeliveryTimes = new Dictionary(); - QuantityUnits = new Dictionary(); - Stores = new Dictionary(); - Languages = new Dictionary(); - Countries = new Dictionary(); - ProductTemplates = new Dictionary(); - CategoryTemplates = new Dictionary(); - NewsletterSubscriptions = new HashSet(); + { + public DataExporterContext( + DataExportRequest request, + CancellationToken cancellationToken, + bool isPreview = false) + { + Request = request; + CancellationToken = cancellationToken; + Filter = XmlHelper.Deserialize(request.Profile.Filtering); + Projection = XmlHelper.Deserialize(request.Profile.Projection); + IsPreview = isPreview; + + if (request.Profile.Projection.IsEmpty()) + { + Projection.DescriptionMergingId = (int)ExportDescriptionMerging.Description; + } + + FolderContent = request.Profile.GetExportFolder(true, true); + + DeliveryTimes = new Dictionary(); + QuantityUnits = new Dictionary(); + Stores = new Dictionary(); + Languages = new Dictionary(); + Countries = new Dictionary(); + ProductTemplates = new Dictionary(); + CategoryTemplates = new Dictionary(); + NewsletterSubscriptions = new HashSet(); Translations = new Dictionary(); TranslationsPerPage = new Dictionary(); UrlRecords = new Dictionary(); UrlRecordsPerPage = new Dictionary(); StatsPerStore = new Dictionary(); - EntityIdsLoaded = new List(); - EntityIdsPerSegment = new HashSet(); - - Result = new DataExportResult - { - FileFolder = IsFileBasedExport ? FolderContent : null - }; - - ExecuteContext = new ExportExecuteContext(Result, CancellationToken, FolderContent); - ExecuteContext.Filter = Filter; - ExecuteContext.Projection = Projection; - ExecuteContext.ProfileId = request.Profile.Id; - - if (!IsPreview) - { - ExecuteContext.ProgressValueSetter = Request.ProgressValueSetter; - } - } - - /// - /// All entity identifiers per export. - /// - public List EntityIdsLoaded { get; set; } - public void SetLoadedEntityIds(IEnumerable ids) - { - EntityIdsLoaded = EntityIdsLoaded - .Union(ids) - .Distinct() - .ToList(); - } - - /// - /// All entity identifiers per segment (to avoid exporting products multiple times). - /// - public HashSet EntityIdsPerSegment { get; set; } + EntityIdsLoaded = new List(); + EntityIdsPerSegment = new HashSet(); + + Result = new DataExportResult + { + FileFolder = IsFileBasedExport ? FolderContent : null + }; + + ExecuteContext = new ExportExecuteContext(Result, CancellationToken, FolderContent); + ExecuteContext.Filter = Filter; + ExecuteContext.Projection = Projection; + ExecuteContext.ProfileId = request.Profile.Id; + + if (!IsPreview) + { + ExecuteContext.ProgressValueSetter = Request.ProgressValueSetter; + } + } + + /// + /// All entity identifiers per export. + /// + public List EntityIdsLoaded { get; set; } + public void SetLoadedEntityIds(IEnumerable ids) + { + EntityIdsLoaded = EntityIdsLoaded + .Union(ids) + .Distinct() + .ToList(); + } + + /// + /// All entity identifiers per segment (to avoid exporting products multiple times). + /// + public HashSet EntityIdsPerSegment { get; set; } public int LastId { get; set; } public string ProgressInfo { get; set; } @@ -88,40 +88,37 @@ public void SetLoadedEntityIds(IEnumerable ids) public Dictionary StatsPerStore { get; set; } public DataExportRequest Request { get; private set; } - public CancellationToken CancellationToken { get; private set; } - public bool IsPreview { get; private set; } - - public bool Supports(ExportFeatures feature) - { - return !IsPreview && Request.Provider.Metadata.ExportFeatures.HasFlag(feature); - } - - public ExportFilter Filter { get; private set; } - public ExportProjection Projection { get; private set; } - public Currency ContextCurrency { get; set; } - public Customer ContextCustomer { get; set; } - public Language ContextLanguage { get; set; } + public CancellationToken CancellationToken { get; private set; } + public bool IsPreview { get; private set; } + + public bool Supports(ExportFeatures feature) + { + return !IsPreview && Request.Provider.Metadata.ExportFeatures.HasFlag(feature); + } + + public ExportFilter Filter { get; private set; } + public ExportProjection Projection { get; private set; } + public Currency ContextCurrency { get; set; } + public Customer ContextCustomer { get; set; } + public Language ContextLanguage { get; set; } public int LanguageId => Projection.LanguageId ?? 0; public TraceLogger Log { get; set; } - public Store Store { get; set; } + public Store Store { get; set; } - public string FolderContent { get; private set; } + public string FolderContent { get; private set; } - public bool IsFileBasedExport - { - get { return Request.Provider == null || Request.Provider.Value == null || Request.Provider.Value.FileExtension.HasValue(); } - } + public bool IsFileBasedExport => Request.Provider == null || Request.Provider.Value == null || Request.Provider.Value.FileExtension.HasValue(); - // Data loaded once per export. - public Dictionary DeliveryTimes { get; set; } - public Dictionary QuantityUnits { get; set; } - public Dictionary Stores { get; set; } - public Dictionary Languages { get; set; } - public Dictionary Countries { get; set; } - public Dictionary ProductTemplates { get; set; } - public Dictionary CategoryTemplates { get; set; } - public HashSet NewsletterSubscriptions { get; set; } + // Data loaded once per export. + public Dictionary DeliveryTimes { get; set; } + public Dictionary QuantityUnits { get; set; } + public Dictionary Stores { get; set; } + public Dictionary Languages { get; set; } + public Dictionary Countries { get; set; } + public Dictionary ProductTemplates { get; set; } + public Dictionary CategoryTemplates { get; set; } + public HashSet NewsletterSubscriptions { get; set; } /// /// All translations for global scopes (like Category, Manufacturer etc.) @@ -133,9 +130,9 @@ public bool IsFileBasedExport public ProductExportContext ProductExportContext { get; set; } public ProductExportContext AssociatedProductContext { get; set; } public OrderExportContext OrderExportContext { get; set; } - public ManufacturerExportContext ManufacturerExportContext { get; set; } - public CategoryExportContext CategoryExportContext { get; set; } - public CustomerExportContext CustomerExportContext { get; set; } + public ManufacturerExportContext ManufacturerExportContext { get; set; } + public CategoryExportContext CategoryExportContext { get; set; } + public CustomerExportContext CustomerExportContext { get; set; } /// /// All per page translations (like ProductVariantAttributeValue etc.) @@ -144,13 +141,12 @@ public bool IsFileBasedExport public Dictionary UrlRecordsPerPage { get; set; } public ExportExecuteContext ExecuteContext { get; set; } - public DataExportResult Result { get; set; } - } + public DataExportResult Result { get; set; } + } internal class RecordStats { public int TotalRecords { get; set; } public int MaxId { get; set; } - public int OffsetId { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs index 874d43e7e3..db0b6ed93c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs @@ -3,37 +3,37 @@ namespace SmartStore.Services.DataExchange.Export.Internal { - internal class DynamicEntity : HybridExpando - { - public DynamicEntity(DynamicEntity dynamicEntity) - : this(dynamicEntity.WrappedObject) - { - MergeRange(dynamicEntity); - } + internal class DynamicEntity : HybridExpando + { + public DynamicEntity(DynamicEntity dynamicEntity) + : this(dynamicEntity.WrappedObject) + { + MergeRange(dynamicEntity); + } - public DynamicEntity(object entity) - : base(entity) - { - base.Properties["Entity"] = entity; - } + public DynamicEntity(object entity) + : base(entity) + { + base.Properties["Entity"] = entity; + } - public void Merge(string name, object value) - { - Properties[name] = value; - } + public void Merge(string name, object value) + { + Properties[name] = value; + } - public void MergeRange(IDictionary other) - { - foreach (var kvp in other) - { - Properties[kvp.Key] = kvp.Value; - } - } + public void MergeRange(IDictionary other) + { + foreach (var kvp in other) + { + Properties[kvp.Key] = kvp.Value; + } + } - protected override bool TrySetMemberCore(string name, object value) - { - Properties[name] = value; - return true; - } - } + protected override bool TrySetMemberCore(string name, object value) + { + Properties[name] = value; + return true; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs index 484f2fc6f3..9cddbe30cd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs @@ -8,71 +8,71 @@ namespace SmartStore.Services.DataExchange.Export.Internal { internal class ManufacturerExportContext - { - protected List _manufacturerIds; - protected List _fileIds; + { + protected List _manufacturerIds; + protected List _fileIds; - private Func> _funcProductManufacturers; - private Func> _funcFiles; + private Func> _funcProductManufacturers; + private Func> _funcFiles; - private LazyMultimap _productManufacturers; - private LazyMultimap _files; + private LazyMultimap _productManufacturers; + private LazyMultimap _files; - public ManufacturerExportContext( - IEnumerable manufacturers, - Func> productManufacturers, - Func> files) - { - if (manufacturers == null) - { - _manufacturerIds = new List(); - _fileIds = new List(); - } - else - { - _manufacturerIds = new List(manufacturers.Select(x => x.Id)); + public ManufacturerExportContext( + IEnumerable manufacturers, + Func> productManufacturers, + Func> files) + { + if (manufacturers == null) + { + _manufacturerIds = new List(); + _fileIds = new List(); + } + else + { + _manufacturerIds = new List(manufacturers.Select(x => x.Id)); _fileIds = manufacturers .Select(x => x.MediaFileId ?? 0) .Where(x => x != 0) .Distinct() .ToList(); - } + } - _funcProductManufacturers = productManufacturers; - _funcFiles = files; - } + _funcProductManufacturers = productManufacturers; + _funcFiles = files; + } - public void Clear() - { - _productManufacturers?.Clear(); - _files?.Clear(); + public void Clear() + { + _productManufacturers?.Clear(); + _files?.Clear(); - _manufacturerIds?.Clear(); - _fileIds?.Clear(); - } + _manufacturerIds?.Clear(); + _fileIds?.Clear(); + } - public LazyMultimap ProductManufacturers - { - get - { - if (_productManufacturers == null) - { - _productManufacturers = new LazyMultimap(keys => _funcProductManufacturers(keys), _manufacturerIds); - } - return _productManufacturers; - } - } + public LazyMultimap ProductManufacturers + { + get + { + if (_productManufacturers == null) + { + _productManufacturers = new LazyMultimap(keys => _funcProductManufacturers(keys), _manufacturerIds); + } + return _productManufacturers; + } + } - public LazyMultimap Files - { - get - { - if (_files == null) - { - _files = new LazyMultimap(keys => _funcFiles(keys).ToMultimap(x => x.Id, x => x), _fileIds); - } - return _files; - } - } - } + public LazyMultimap Files + { + get + { + if (_files == null) + { + _files = new LazyMultimap(keys => _funcFiles(keys).ToMultimap(x => x.Id, x => x), _fileIds); + } + return _files; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs index 02f56914fb..f3eb5075b1 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs @@ -9,150 +9,150 @@ namespace SmartStore.Services.DataExchange.Export.Internal { - internal class OrderExportContext - { - protected List _orderIds; - protected List _customerIds; - protected List _addressIds; + internal class OrderExportContext + { + protected List _orderIds; + protected List _customerIds; + protected List _addressIds; - private Func> _funcCustomers; - private Func> _funcCustomerGenericAttributes; - private Func> _funcRewardPointsHistories; - private Func> _funcAddresses; - private Func> _funcOrderItems; - private Func> _funcShipments; + private Func> _funcCustomers; + private Func> _funcCustomerGenericAttributes; + private Func> _funcRewardPointsHistories; + private Func> _funcAddresses; + private Func> _funcOrderItems; + private Func> _funcShipments; - private LazyMultimap _customers; - private LazyMultimap _customerGenericAttributes; - private LazyMultimap _rewardPointsHistories; - private LazyMultimap
_addresses; - private LazyMultimap _orderItems; - private LazyMultimap _shipments; + private LazyMultimap _customers; + private LazyMultimap _customerGenericAttributes; + private LazyMultimap _rewardPointsHistories; + private LazyMultimap
_addresses; + private LazyMultimap _orderItems; + private LazyMultimap _shipments; - public OrderExportContext(IEnumerable orders, - Func> customers, - Func> customerGenericAttributes, - Func> rewardPointsHistory, - Func> addresses, - Func> orderItems, - Func> shipments) - { - if (orders == null) - { - _orderIds = new List(); - _customerIds = new List(); - _addressIds = new List(); - } - else - { - _orderIds = new List(orders.Select(x => x.Id)); - _customerIds = new List(orders.Select(x => x.CustomerId)); + public OrderExportContext(IEnumerable orders, + Func> customers, + Func> customerGenericAttributes, + Func> rewardPointsHistory, + Func> addresses, + Func> orderItems, + Func> shipments) + { + if (orders == null) + { + _orderIds = new List(); + _customerIds = new List(); + _addressIds = new List(); + } + else + { + _orderIds = new List(orders.Select(x => x.Id)); + _customerIds = new List(orders.Select(x => x.CustomerId)); - _addressIds = orders.Select(x => x.BillingAddressId) - .Union(orders.Select(x => x.ShippingAddressId ?? 0)) - .Where(x => x != 0) - .Distinct() - .ToList(); - } + _addressIds = orders.Select(x => x.BillingAddressId) + .Union(orders.Select(x => x.ShippingAddressId ?? 0)) + .Where(x => x != 0) + .Distinct() + .ToList(); + } - _funcCustomers = customers; - _funcCustomerGenericAttributes = customerGenericAttributes; - _funcRewardPointsHistories = rewardPointsHistory; - _funcAddresses = addresses; - _funcOrderItems = orderItems; - _funcShipments = shipments; - } + _funcCustomers = customers; + _funcCustomerGenericAttributes = customerGenericAttributes; + _funcRewardPointsHistories = rewardPointsHistory; + _funcAddresses = addresses; + _funcOrderItems = orderItems; + _funcShipments = shipments; + } - public void Clear() - { - if (_customers != null) - _customers.Clear(); - if (_customerGenericAttributes != null) - _customerGenericAttributes.Clear(); - if (_rewardPointsHistories != null) - _rewardPointsHistories.Clear(); - if (_addresses != null) - _addresses.Clear(); - if (_orderItems != null) - _orderItems.Clear(); - if (_shipments != null) - _shipments.Clear(); + public void Clear() + { + if (_customers != null) + _customers.Clear(); + if (_customerGenericAttributes != null) + _customerGenericAttributes.Clear(); + if (_rewardPointsHistories != null) + _rewardPointsHistories.Clear(); + if (_addresses != null) + _addresses.Clear(); + if (_orderItems != null) + _orderItems.Clear(); + if (_shipments != null) + _shipments.Clear(); - _orderIds.Clear(); - _customerIds.Clear(); - _addressIds.Clear(); - } + _orderIds.Clear(); + _customerIds.Clear(); + _addressIds.Clear(); + } - public LazyMultimap Customers - { - get - { - if (_customers == null) - { - _customers = new LazyMultimap(keys => _funcCustomers(keys).ToMultimap(x => x.Id, x => x), _customerIds); - } - return _customers; - } - } + public LazyMultimap Customers + { + get + { + if (_customers == null) + { + _customers = new LazyMultimap(keys => _funcCustomers(keys).ToMultimap(x => x.Id, x => x), _customerIds); + } + return _customers; + } + } - public LazyMultimap CustomerGenericAttributes - { - get - { - if (_customerGenericAttributes == null) - { - _customerGenericAttributes = new LazyMultimap(keys => _funcCustomerGenericAttributes(keys), _customerIds); - } - return _customerGenericAttributes; - } - } + public LazyMultimap CustomerGenericAttributes + { + get + { + if (_customerGenericAttributes == null) + { + _customerGenericAttributes = new LazyMultimap(keys => _funcCustomerGenericAttributes(keys), _customerIds); + } + return _customerGenericAttributes; + } + } - public LazyMultimap RewardPointsHistories - { - get - { - if (_rewardPointsHistories == null) - { - _rewardPointsHistories = new LazyMultimap(keys => _funcRewardPointsHistories(keys), _customerIds); - } - return _rewardPointsHistories; - } - } + public LazyMultimap RewardPointsHistories + { + get + { + if (_rewardPointsHistories == null) + { + _rewardPointsHistories = new LazyMultimap(keys => _funcRewardPointsHistories(keys), _customerIds); + } + return _rewardPointsHistories; + } + } - public LazyMultimap
Addresses - { - get - { - if (_addresses == null) - { - _addresses = new LazyMultimap
(keys => _funcAddresses(keys).ToMultimap(x => x.Id, x => x), _addressIds); - } - return _addresses; - } - } + public LazyMultimap
Addresses + { + get + { + if (_addresses == null) + { + _addresses = new LazyMultimap
(keys => _funcAddresses(keys).ToMultimap(x => x.Id, x => x), _addressIds); + } + return _addresses; + } + } - public LazyMultimap OrderItems - { - get - { - if (_orderItems == null) - { - _orderItems = new LazyMultimap(keys => _funcOrderItems(keys), _orderIds); - } - return _orderItems; - } - } + public LazyMultimap OrderItems + { + get + { + if (_orderItems == null) + { + _orderItems = new LazyMultimap(keys => _funcOrderItems(keys), _orderIds); + } + return _orderItems; + } + } - public LazyMultimap Shipments - { - get - { - if (_shipments == null) - { - _shipments = new LazyMultimap(keys => _funcShipments(keys), _orderIds); - } - return _shipments; - } - } - } + public LazyMultimap Shipments + { + get + { + if (_shipments == null) + { + _shipments = new LazyMultimap(keys => _funcShipments(keys), _orderIds); + } + return _shipments; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ProductExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ProductExportContext.cs index b3019975a0..bf8562da11 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ProductExportContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ProductExportContext.cs @@ -12,92 +12,92 @@ namespace SmartStore.Services.DataExchange.Export /// Cargo data to reduce database round trips during work with product batches (export, list model creation etc.) ///
public class ProductExportContext : PriceCalculationContext - { - private Func> _funcProductPictures; - private Func> _funcProductTags; - private Func> _funcSpecificationAttributes; + { + private Func> _funcProductPictures; + private Func> _funcProductTags; + private Func> _funcSpecificationAttributes; private Func> _funcDownloads; private LazyMultimap _productPictures; - private LazyMultimap _productTags; - private LazyMultimap _specificationAttributes; + private LazyMultimap _productTags; + private LazyMultimap _specificationAttributes; private LazyMultimap _downloads; public ProductExportContext( - IEnumerable products, - Func> attributes, - Func> attributeCombinations, - Func> specificationAttributes, - Func> tierPrices, - Func> productCategories, - Func> productManufacturers, - Func> appliedDiscounts, - Func> productBundleItems, + IEnumerable products, + Func> attributes, + Func> attributeCombinations, + Func> specificationAttributes, + Func> tierPrices, + Func> productCategories, + Func> productManufacturers, + Func> appliedDiscounts, + Func> productBundleItems, Func> associatedProducts, - Func> productPictures, - Func> productTags, + Func> productPictures, + Func> productTags, Func> downloads) - : base(products, - attributes, - attributeCombinations, - tierPrices, - productCategories, - productManufacturers, - appliedDiscounts, - productBundleItems, + : base(products, + attributes, + attributeCombinations, + tierPrices, + productCategories, + productManufacturers, + appliedDiscounts, + productBundleItems, associatedProducts) - { - _funcProductPictures = productPictures; - _funcProductTags = productTags; - _funcSpecificationAttributes = specificationAttributes; + { + _funcProductPictures = productPictures; + _funcProductTags = productTags; + _funcSpecificationAttributes = specificationAttributes; _funcDownloads = downloads; - } + } - public new void Clear() - { + public new void Clear() + { _productPictures?.Clear(); _productTags?.Clear(); _specificationAttributes?.Clear(); _downloads?.Clear(); - base.Clear(); - } + base.Clear(); + } - public LazyMultimap ProductPictures - { - get - { - if (_productPictures == null) - { - _productPictures = new LazyMultimap(keys => _funcProductPictures(keys), _productIds); - } - return _productPictures; - } - } + public LazyMultimap ProductPictures + { + get + { + if (_productPictures == null) + { + _productPictures = new LazyMultimap(keys => _funcProductPictures(keys), _productIds); + } + return _productPictures; + } + } - public LazyMultimap ProductTags - { - get - { - if (_productTags == null) - { - _productTags = new LazyMultimap(keys => _funcProductTags(keys), _productIds); - } - return _productTags; - } - } + public LazyMultimap ProductTags + { + get + { + if (_productTags == null) + { + _productTags = new LazyMultimap(keys => _funcProductTags(keys), _productIds); + } + return _productTags; + } + } - public LazyMultimap SpecificationAttributes - { - get - { - if (_specificationAttributes == null) - { - _specificationAttributes = new LazyMultimap(keys => _funcSpecificationAttributes(keys), _productIds); - } - return _specificationAttributes; - } - } + public LazyMultimap SpecificationAttributes + { + get + { + if (_specificationAttributes == null) + { + _specificationAttributes = new LazyMultimap(keys => _funcSpecificationAttributes(keys), _productIds); + } + return _specificationAttributes; + } + } public LazyMultimap Downloads { @@ -110,5 +110,5 @@ public LazyMultimap Downloads return _downloads; } } - } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs index 9bdbc6d790..8828902596 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs @@ -5,169 +5,169 @@ namespace SmartStore.Services.DataExchange { - public partial interface ISyncMappingService - { - /// - /// Inserts a sync mapping entity - /// - /// Sync mapping - void InsertSyncMapping(SyncMapping mapping); - - /// - /// Inserts a range of sync mapping entities - /// - /// A sequence of sync mappings - void InsertSyncMappings(IEnumerable mappings); - - /// - /// Gets all sync mappings - /// - /// The context (external application) name. Leave null to load all records regardless of context. - /// The entity name. Leave null to load all records regardless of entity name. - /// Array of entity identifiers - /// SyncMappings - IList GetAllSyncMappings(string contextName = null, string entityName = null, int[] entityIds = null); - - /// - /// Gets a sync mapping record by (target) entity id, name and context name. - /// - /// Entity nd - /// Entity name - /// Context name - /// SyncMapping - SyncMapping GetSyncMappingByEntity(int entityId, string entityName, string contextName); - - /// - /// Gets a sync mapping record by (external) source key, entity name and context name. - /// - /// Source key - /// Entity name - /// Context name - /// SyncMappings - SyncMapping GetSyncMappingBySource(string sourceKey, string entityName, string contextName); - - /// - /// Deletes a sync mapping entity - /// - /// Sync mapping - void DeleteSyncMapping(SyncMapping mapping); - - /// - /// Deletes a range of sync mapping entities - /// - /// Sync mappings - void DeleteSyncMappings(IEnumerable mappings); - - /// - /// Deletes all sync mapping entities referencing the specified entity - /// - /// The entity - void DeleteSyncMappingsFor(T entity) where T : BaseEntity; - - /// - /// Deletes a range of sync mapping entities - /// - /// The context (external application) name. - /// The entity name. Leave null to delete all context specific mappings regardless of entity name. - /// SyncMappings - void DeleteSyncMappings(string contextName, string entityName = null); - - /// - /// Updates a sync mapping entity - /// - /// Sync mapping - void UpdateSyncMapping(SyncMapping mapping); - } - - - public static class ISyncMappingServiceExtensions - { - - public static SyncMapping InsertSyncMapping(this ISyncMappingService svc, T entity, string contextName, string sourceKey) where T : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotEmpty(contextName, nameof(contextName)); - Guard.NotEmpty(sourceKey, nameof(sourceKey)); - - if (entity is SyncMapping) - { - throw Error.InvalidOperation("Cannot insert a sync mapping record for a SyncMapping entity"); - } - - if (entity.IsTransientRecord()) - { - throw Error.InvalidOperation("Cannot insert a sync mapping record for a transient (unsaved) entity"); - } - - var mapping = new SyncMapping - { - ContextName = contextName, - EntityId = entity.Id, - EntityName = entity.GetEntityName(), - SourceKey = sourceKey - }; - - return mapping; - } - - public static SyncMapping GetSyncMappingByEntity(this ISyncMappingService svc, T entity, string contextName) where T : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotEmpty(contextName, nameof(contextName)); - - if (entity is SyncMapping) - { - throw Error.InvalidOperation("Cannot get a sync mapping record for a SyncMapping entity"); - } - - if (entity.IsTransientRecord()) - { - throw Error.InvalidOperation("Cannot get a sync mapping record for a transient (unsaved) entity"); - } - - return svc.GetSyncMappingByEntity(entity.Id, entity.GetEntityName(), contextName); - } - - /// - /// Inserts a range of sync mapping entities by specifying two equal sized sequences for entity ids and source keys. - /// - /// Context name for all mappings - /// Entity name for all mappings - /// A sequence of entity ids - /// A sequence of source keys - /// List of persisted sync mappings - /// Both sequences must contain at least one element and must be of equal size. - public static IList InsertSyncMappings(this ISyncMappingService svc, string contextName, string entityName, IEnumerable entityIds, IEnumerable sourceKeys) - { - Guard.NotEmpty(contextName, nameof(contextName)); - Guard.NotEmpty(entityName, nameof(entityName)); - Guard.NotNull(entityIds, nameof(entityIds)); - Guard.NotNull(sourceKeys, nameof(sourceKeys)); - - if (!entityIds.Any() || !sourceKeys.Any() || entityIds.Count() != sourceKeys.Count()) - { - throw Error.InvalidOperation("Both sequences must contain at least one element and must be of equal size."); - } - - var mappings = new List(); - var arrIds = entityIds.ToArray(); - var arrKeys = sourceKeys.ToArray(); - - for (int i = 0; i < arrIds.Length; i++) - { - mappings.Add(new SyncMapping - { - ContextName = contextName, - EntityName = entityName, - EntityId = arrIds[i], - SourceKey = arrKeys[i] - }); - } - - svc.InsertSyncMappings(mappings); - - return mappings; - } - - } + public partial interface ISyncMappingService + { + /// + /// Inserts a sync mapping entity + /// + /// Sync mapping + void InsertSyncMapping(SyncMapping mapping); + + /// + /// Inserts a range of sync mapping entities + /// + /// A sequence of sync mappings + void InsertSyncMappings(IEnumerable mappings); + + /// + /// Gets all sync mappings + /// + /// The context (external application) name. Leave null to load all records regardless of context. + /// The entity name. Leave null to load all records regardless of entity name. + /// Array of entity identifiers + /// SyncMappings + IList GetAllSyncMappings(string contextName = null, string entityName = null, int[] entityIds = null); + + /// + /// Gets a sync mapping record by (target) entity id, name and context name. + /// + /// Entity nd + /// Entity name + /// Context name + /// SyncMapping + SyncMapping GetSyncMappingByEntity(int entityId, string entityName, string contextName); + + /// + /// Gets a sync mapping record by (external) source key, entity name and context name. + /// + /// Source key + /// Entity name + /// Context name + /// SyncMappings + SyncMapping GetSyncMappingBySource(string sourceKey, string entityName, string contextName); + + /// + /// Deletes a sync mapping entity + /// + /// Sync mapping + void DeleteSyncMapping(SyncMapping mapping); + + /// + /// Deletes a range of sync mapping entities + /// + /// Sync mappings + void DeleteSyncMappings(IEnumerable mappings); + + /// + /// Deletes all sync mapping entities referencing the specified entity + /// + /// The entity + void DeleteSyncMappingsFor(T entity) where T : BaseEntity; + + /// + /// Deletes a range of sync mapping entities + /// + /// The context (external application) name. + /// The entity name. Leave null to delete all context specific mappings regardless of entity name. + /// SyncMappings + void DeleteSyncMappings(string contextName, string entityName = null); + + /// + /// Updates a sync mapping entity + /// + /// Sync mapping + void UpdateSyncMapping(SyncMapping mapping); + } + + + public static class ISyncMappingServiceExtensions + { + + public static SyncMapping InsertSyncMapping(this ISyncMappingService svc, T entity, string contextName, string sourceKey) where T : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotEmpty(contextName, nameof(contextName)); + Guard.NotEmpty(sourceKey, nameof(sourceKey)); + + if (entity is SyncMapping) + { + throw Error.InvalidOperation("Cannot insert a sync mapping record for a SyncMapping entity"); + } + + if (entity.IsTransientRecord()) + { + throw Error.InvalidOperation("Cannot insert a sync mapping record for a transient (unsaved) entity"); + } + + var mapping = new SyncMapping + { + ContextName = contextName, + EntityId = entity.Id, + EntityName = entity.GetEntityName(), + SourceKey = sourceKey + }; + + return mapping; + } + + public static SyncMapping GetSyncMappingByEntity(this ISyncMappingService svc, T entity, string contextName) where T : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + Guard.NotEmpty(contextName, nameof(contextName)); + + if (entity is SyncMapping) + { + throw Error.InvalidOperation("Cannot get a sync mapping record for a SyncMapping entity"); + } + + if (entity.IsTransientRecord()) + { + throw Error.InvalidOperation("Cannot get a sync mapping record for a transient (unsaved) entity"); + } + + return svc.GetSyncMappingByEntity(entity.Id, entity.GetEntityName(), contextName); + } + + /// + /// Inserts a range of sync mapping entities by specifying two equal sized sequences for entity ids and source keys. + /// + /// Context name for all mappings + /// Entity name for all mappings + /// A sequence of entity ids + /// A sequence of source keys + /// List of persisted sync mappings + /// Both sequences must contain at least one element and must be of equal size. + public static IList InsertSyncMappings(this ISyncMappingService svc, string contextName, string entityName, IEnumerable entityIds, IEnumerable sourceKeys) + { + Guard.NotEmpty(contextName, nameof(contextName)); + Guard.NotEmpty(entityName, nameof(entityName)); + Guard.NotNull(entityIds, nameof(entityIds)); + Guard.NotNull(sourceKeys, nameof(sourceKeys)); + + if (!entityIds.Any() || !sourceKeys.Any() || entityIds.Count() != sourceKeys.Count()) + { + throw Error.InvalidOperation("Both sequences must contain at least one element and must be of equal size."); + } + + var mappings = new List(); + var arrIds = entityIds.ToArray(); + var arrKeys = sourceKeys.ToArray(); + + for (int i = 0; i < arrIds.Length; i++) + { + mappings.Add(new SyncMapping + { + ContextName = contextName, + EntityName = entityName, + EntityId = arrIds[i], + SourceKey = arrKeys[i] + }); + } + + svc.InsertSyncMappings(mappings); + + return mappings; + } + + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs index ec1ae4335b..b2a0b9302a 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs @@ -1,147 +1,147 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Newtonsoft.Json; namespace SmartStore.Services.DataExchange.Import { - public class ColumnMap - { - // maps source column to property - private readonly Dictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static bool IsIndexed(string name) - { - return (name.EmptyNull().EndsWith("]") && name.EmptyNull().Contains("[")); - } - - private static string CreateSourceName(string name, string index) - { - if (index.HasValue()) - { - name += String.Concat("[", index, "]"); - } - - return name; - } - - public IReadOnlyDictionary Mappings - { - get { return _map; } - } - - public static bool ParseSourceName(string sourceName, out string nameWithoutIndex, out string index) - { - nameWithoutIndex = sourceName; - index = null; - - var result = true; - - if (sourceName.HasValue() && IsIndexed(sourceName)) - { - var x1 = sourceName.IndexOf('['); - var x2 = sourceName.IndexOf(']', x1); - - if (x1 != -1 && x2 != -1 && x2 > x1) - { - nameWithoutIndex = sourceName.Substring(0, x1); - index = sourceName.Substring(x1 + 1, x2 - x1 - 1); - } - else - { - result = false; - } - } - - return result; - } - - public void AddMapping(string sourceName, string mappedName, string defaultValue = null) - { - AddMapping(sourceName, null, mappedName, defaultValue); + public class ColumnMap + { + // maps source column to property + private readonly Dictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private static bool IsIndexed(string name) + { + return (name.EmptyNull().EndsWith("]") && name.EmptyNull().Contains("[")); } - public void AddMapping(string sourceName, string index, string mappedName, string defaultValue = null) - { - Guard.NotEmpty(sourceName, nameof(sourceName)); - Guard.NotEmpty(mappedName, nameof(mappedName)); - - var key = CreateSourceName(sourceName, index); - - _map[key] = new ColumnMappingItem - { - SoureName = key, - MappedName = mappedName, - Default = defaultValue - }; - } - - /// - /// Gets a mapped column value - /// - /// The name of the column to get a mapped value for. - /// The column index, e.g. a language code (de, en etc.) - /// The mapped column value OR - if the name is unmapped - a value with the passed [] - public ColumnMappingItem GetMapping(string sourceName, string index) - { - return GetMapping(CreateSourceName(sourceName, index)); - } - - /// - /// Gets a mapped column value - /// - /// The name of the column to get a mapped value for. - /// The mapped column value OR - if the name is unmapped - the value of the passed - public ColumnMappingItem GetMapping(string sourceName) - { - ColumnMappingItem result; - - if (_map.TryGetValue(sourceName, out result)) - { - return result; - } - - return new ColumnMappingItem { SoureName = sourceName, MappedName = sourceName }; - } - } - - - [JsonObject(MemberSerialization.OptIn)] - public class ColumnMappingItem - { - private bool? _ignored; - - /// - /// The source name - /// - [JsonIgnore] - public string SoureName { get; set; } - - /// - /// The mapped name - /// - [JsonProperty] - public string MappedName { get; set; } - - /// - /// An optional default value - /// - [JsonProperty] - public string Default { get; set; } - - /// - /// Indicates whether to explicitly ignore this property - /// - public bool IgnoreProperty - { - get - { - if (_ignored == null) - { - _ignored = Default != null && Default == "[IGNOREPROPERTY]"; - } - - return _ignored.Value; - } - } - } + private static string CreateSourceName(string name, string index) + { + if (index.HasValue()) + { + name += String.Concat("[", index, "]"); + } + + return name; + } + + public IReadOnlyDictionary Mappings => _map; + + public static bool ParseSourceName(string sourceName, out string nameWithoutIndex, out string index) + { + nameWithoutIndex = sourceName; + index = null; + + var result = true; + + if (sourceName.HasValue() && IsIndexed(sourceName)) + { + var x1 = sourceName.IndexOf('['); + var x2 = sourceName.IndexOf(']', x1); + + if (x1 != -1 && x2 != -1 && x2 > x1) + { + nameWithoutIndex = sourceName.Substring(0, x1); + index = sourceName.Substring(x1 + 1, x2 - x1 - 1); + } + else + { + result = false; + } + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddMapping(string sourceName, string mappedName, string defaultValue = null) + { + AddMapping(sourceName, null, mappedName, defaultValue); + } + + public void AddMapping(string sourceName, string index, string mappedName, string defaultValue = null) + { + Guard.NotEmpty(sourceName, nameof(sourceName)); + Guard.NotEmpty(mappedName, nameof(mappedName)); + + var key = CreateSourceName(sourceName, index); + + _map[key] = new ColumnMappingItem + { + SoureName = key, + MappedName = mappedName, + Default = defaultValue + }; + } + + /// + /// Gets a mapped column value + /// + /// The name of the column to get a mapped value for. + /// The column index, e.g. a language code (de, en etc.) + /// The mapped column value OR - if the name is unmapped - a value with the passed [] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ColumnMappingItem GetMapping(string sourceName, string index) + { + return GetMapping(CreateSourceName(sourceName, index)); + } + + /// + /// Gets a mapped column value + /// + /// The name of the column to get a mapped value for. + /// The mapped column value OR - if the name is unmapped - the value of the passed + public ColumnMappingItem GetMapping(string sourceName) + { + ColumnMappingItem result; + + if (_map.TryGetValue(sourceName, out result)) + { + return result; + } + + return new ColumnMappingItem { SoureName = sourceName, MappedName = sourceName }; + } + } + + + [JsonObject(MemberSerialization.OptIn)] + public class ColumnMappingItem + { + private bool? _ignored; + + /// + /// The source name + /// + [JsonIgnore] + public string SoureName { get; set; } + + /// + /// The mapped name + /// + [JsonProperty] + public string MappedName { get; set; } + + /// + /// An optional default value + /// + [JsonProperty] + public string Default { get; set; } + + /// + /// Indicates whether to explicitly ignore this property + /// + public bool IgnoreProperty + { + get + { + if (_ignored == null) + { + _ignored = Default != null && Default == "[IGNOREPROPERTY]"; + } + + return _ignored.Value; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs index 46c7cfb6ac..4cd4cb5684 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs @@ -6,69 +6,69 @@ namespace SmartStore.Services.DataExchange.Import { - public class ColumnMapConverter : DefaultTypeConverter - { - public ColumnMapConverter() - : base(typeof(object)) - { - } + public class ColumnMapConverter : DefaultTypeConverter + { + public ColumnMapConverter() + : base(typeof(object)) + { + } - public override bool CanConvertFrom(Type type) - { - return type == typeof(string); - } + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } - public override bool CanConvertTo(Type type) - { - return type == typeof(string); - } + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } - public override object ConvertFrom(CultureInfo culture, object value) - { - if (value is string) - { - var dict = JsonConvert.DeserializeObject>((string)value); - var map = new ColumnMap(); + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + var dict = JsonConvert.DeserializeObject>((string)value); + var map = new ColumnMap(); - foreach (var kvp in dict) - { - map.AddMapping(kvp.Key, null, kvp.Value.MappedName, kvp.Value.Default); - } + foreach (var kvp in dict) + { + map.AddMapping(kvp.Key, null, kvp.Value.MappedName, kvp.Value.Default); + } - return map; + return map; } - return base.ConvertFrom(culture, value); - } + return base.ConvertFrom(culture, value); + } - public T ConvertFrom(string value) - { - if (value.HasValue()) - return (T)ConvertFrom(CultureInfo.InvariantCulture, value); + public T ConvertFrom(string value) + { + if (value.HasValue()) + return (T)ConvertFrom(CultureInfo.InvariantCulture, value); - return default(T); - } + return default(T); + } - public override object ConvertTo(CultureInfo culture, string format, object value, Type to) - { - if (to == typeof(string)) - { - if (value is ColumnMap) - { - return JsonConvert.SerializeObject(((ColumnMap)value).Mappings); - } - else - { - return string.Empty; - } - } + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value is ColumnMap) + { + return JsonConvert.SerializeObject(((ColumnMap)value).Mappings); + } + else + { + return string.Empty; + } + } - return base.ConvertTo(culture, format, value, to); - } + return base.ConvertTo(culture, format, value, to); + } - public string ConvertTo(object value) - { - return (string)ConvertTo(CultureInfo.InvariantCulture, null, value, typeof(string)); - } - } + public string ConvertTo(object value) + { + return (string)ConvertTo(CultureInfo.InvariantCulture, null, value, typeof(string)); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs index 541850340f..0f813ba6ab 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs @@ -2,33 +2,33 @@ namespace SmartStore.Services.DataExchange.Import { - // note: namespace persisted in ScheduleTask.Type - public partial class DataImportTask : ITask - { - private readonly IDataImporter _importer; - private readonly IImportProfileService _importProfileService; + // note: namespace persisted in ScheduleTask.Type + public partial class DataImportTask : ITask + { + private readonly IDataImporter _importer; + private readonly IImportProfileService _importProfileService; - public DataImportTask( - IDataImporter importer, - IImportProfileService importProfileService) - { - _importer = importer; - _importProfileService = importProfileService; - } + public DataImportTask( + IDataImporter importer, + IImportProfileService importProfileService) + { + _importer = importer; + _importProfileService = importProfileService; + } - public void Execute(TaskExecutionContext ctx) - { - var profileId = ctx.ScheduleTaskHistory.ScheduleTask.Alias.ToInt(); - var profile = _importProfileService.GetImportProfileById(profileId); + public void Execute(TaskExecutionContext ctx) + { + var profileId = ctx.ScheduleTaskHistory.ScheduleTask.Alias.ToInt(); + var profile = _importProfileService.GetImportProfileById(profileId); - var request = new DataImportRequest(profile); + var request = new DataImportRequest(profile); - request.ProgressValueSetter = delegate (int val, int max, string msg) - { - ctx.SetProgress(val, max, msg, true); - }; + request.ProgressValueSetter = delegate (int val, int max, string msg) + { + ctx.SetProgress(val, max, msg, true); + }; - _importer.Import(request, ctx.CancellationToken); - } - } + _importer.Import(request, ctx.CancellationToken); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs index b6c6cd1703..8939212ee5 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs @@ -24,57 +24,57 @@ namespace SmartStore.Services.DataExchange.Import { public partial class DataImporter : IDataImporter - { - private readonly ICommonServices _services; - private readonly IImportProfileService _importProfileService; - private readonly ILanguageService _languageService; - private readonly Func _importerFactory; - private readonly Lazy _emailAccountService; - private readonly Lazy _emailSender; - private readonly Lazy _contactDataSettings; - private readonly Lazy _dataExchangeSettings; - private readonly IDbCache _dbCache; - private readonly IUrlRecordService _urlRecordService; - private readonly ILocalizedEntityService _localizedEntityService; - - public DataImporter( - ICommonServices services, - IImportProfileService importProfileService, - ILanguageService languageService, - Func importerFactory, - Lazy emailAccountService, - Lazy emailSender, - Lazy contactDataSettings, - Lazy dataExchangeSettings, - IDbCache dbCache, - IUrlRecordService urlRecordService, - ILocalizedEntityService localizedEntityService) - { - _services = services; - _importProfileService = importProfileService; - _languageService = languageService; - _importerFactory = importerFactory; - _emailAccountService = emailAccountService; - _emailSender = emailSender; - _contactDataSettings = contactDataSettings; - _dataExchangeSettings = dataExchangeSettings; - _dbCache = dbCache; - _urlRecordService = urlRecordService; - _localizedEntityService = localizedEntityService; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - private bool HasPermission(DataImporterContext ctx) - { + { + private readonly ICommonServices _services; + private readonly IImportProfileService _importProfileService; + private readonly ILanguageService _languageService; + private readonly Func _importerFactory; + private readonly Lazy _emailAccountService; + private readonly Lazy _emailSender; + private readonly Lazy _contactDataSettings; + private readonly Lazy _dataExchangeSettings; + private readonly IDbCache _dbCache; + private readonly IUrlRecordService _urlRecordService; + private readonly ILocalizedEntityService _localizedEntityService; + + public DataImporter( + ICommonServices services, + IImportProfileService importProfileService, + ILanguageService languageService, + Func importerFactory, + Lazy emailAccountService, + Lazy emailSender, + Lazy contactDataSettings, + Lazy dataExchangeSettings, + IDbCache dbCache, + IUrlRecordService urlRecordService, + ILocalizedEntityService localizedEntityService) + { + _services = services; + _importProfileService = importProfileService; + _languageService = languageService; + _importerFactory = importerFactory; + _emailAccountService = emailAccountService; + _emailSender = emailSender; + _contactDataSettings = contactDataSettings; + _dataExchangeSettings = dataExchangeSettings; + _dbCache = dbCache; + _urlRecordService = urlRecordService; + _localizedEntityService = localizedEntityService; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + private bool HasPermission(DataImporterContext ctx) + { if (ctx.Request.HasPermission) { return true; } - var customer = _services.WorkContext.CurrentCustomer; + var customer = _services.WorkContext.CurrentCustomer; if (customer.SystemName == SystemCustomerNames.BackgroundTask) { @@ -84,8 +84,8 @@ private bool HasPermission(DataImporterContext ctx) return _services.Permissions.Authorize(Permissions.Configuration.Import.Execute); } - private void LogResults(DataImporterContext ctx) - { + private void LogResults(DataImporterContext ctx) + { var sb = new StringBuilder(); foreach (var item in ctx.Results) @@ -124,74 +124,74 @@ private void LogResults(DataImporterContext ctx) } } } - } + } - private void SendCompletionEmail(DataImporterContext ctx) - { - var emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); - var smtpContext = new SmtpContext(emailAccount); - var message = new EmailMessage(); + private void SendCompletionEmail(DataImporterContext ctx) + { + var emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); + var smtpContext = new SmtpContext(emailAccount); + var message = new EmailMessage(); - var store = _services.StoreContext.CurrentStore; - var storeInfo = "{0} ({1})".FormatInvariant(store.Name, store.Url); - var intro = _services.Localization.GetResource("Admin.DataExchange.Import.CompletedEmail.Body").FormatInvariant(storeInfo); - var body = new StringBuilder(intro); - var result = ctx.ExecuteContext.Result; + var store = _services.StoreContext.CurrentStore; + var storeInfo = "{0} ({1})".FormatInvariant(store.Name, store.Url); + var intro = _services.Localization.GetResource("Admin.DataExchange.Import.CompletedEmail.Body").FormatInvariant(storeInfo); + var body = new StringBuilder(intro); + var result = ctx.ExecuteContext.Result; - if (result.LastError.HasValue()) - { - body.AppendFormat("

{0}

", result.LastError); - } + if (result.LastError.HasValue()) + { + body.AppendFormat("

{0}

", result.LastError); + } - body.Append("

"); + body.Append("

"); - body.AppendFormat("

{0}: {1} · {2}: {3}
", - T("Admin.Common.TotalRows"), result.TotalRecords, - T("Admin.Common.Skipped"), result.SkippedRecords); + body.AppendFormat("
{0}: {1} · {2}: {3}
", + T("Admin.Common.TotalRows"), result.TotalRecords, + T("Admin.Common.Skipped"), result.SkippedRecords); - body.AppendFormat("
{0}: {1} · {2}: {3}
", - T("Admin.Common.NewRecords"), result.NewRecords, - T("Admin.Common.Updated"), result.ModifiedRecords); + body.AppendFormat("
{0}: {1} · {2}: {3}
", + T("Admin.Common.NewRecords"), result.NewRecords, + T("Admin.Common.Updated"), result.ModifiedRecords); - body.AppendFormat("
{0}: {1} · {2}: {3}
", - T("Admin.Common.Errors"), result.Errors, - T("Admin.Common.Warnings"), result.Warnings); + body.AppendFormat("
{0}: {1} · {2}: {3}
", + T("Admin.Common.Errors"), result.Errors, + T("Admin.Common.Warnings"), result.Warnings); - body.Append("

"); + body.Append("

"); - message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); - if (_contactDataSettings.Value.WebmasterEmailAddress.HasValue()) - message.To.Add(new EmailAddress(_contactDataSettings.Value.WebmasterEmailAddress)); + if (_contactDataSettings.Value.WebmasterEmailAddress.HasValue()) + message.To.Add(new EmailAddress(_contactDataSettings.Value.WebmasterEmailAddress)); - if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) - message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); + if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) + message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); - if (message.To.Count == 0) - message.To.Add(new EmailAddress(emailAccount.Email, emailAccount.DisplayName)); + if (message.To.Count == 0) + message.To.Add(new EmailAddress(emailAccount.Email, emailAccount.DisplayName)); - message.Subject = T("Admin.DataExchange.Import.CompletedEmail.Subject").Text.FormatInvariant(ctx.Request.Profile.Name); + message.Subject = T("Admin.DataExchange.Import.CompletedEmail.Subject").Text.FormatInvariant(ctx.Request.Profile.Name); - message.Body = body.ToString(); + message.Body = body.ToString(); - _emailSender.Value.SendEmail(smtpContext, message); + _emailSender.Value.SendEmail(smtpContext, message); - //Core.Infrastructure.EngineContext.Current.Resolve().InsertQueuedEmail(new QueuedEmail - //{ - // From = emailAccount.Email, - // FromName = emailAccount.DisplayName, - // To = message.To.First().Address, - // Subject = message.Subject, - // Body = message.Body, - // CreatedOnUtc = DateTime.UtcNow, - // EmailAccountId = emailAccount.Id, - // SendManually = true - //}); - //_services.DbContext.SaveChanges(); - } + //Core.Infrastructure.EngineContext.Current.Resolve().InsertQueuedEmail(new QueuedEmail + //{ + // From = emailAccount.Email, + // FromName = emailAccount.DisplayName, + // To = message.To.First().Address, + // Subject = message.Subject, + // Body = message.Body, + // CreatedOnUtc = DateTime.UtcNow, + // EmailAccountId = emailAccount.Id, + // SendManually = true + //}); + //_services.DbContext.SaveChanges(); + } - private void ImportCoreInner(DataImporterContext ctx, ImportFile file) - { + private void ImportCoreInner(DataImporterContext ctx, ImportFile file) + { var context = ctx.ExecuteContext; var profile = ctx.Request.Profile; @@ -200,24 +200,24 @@ private void ImportCoreInner(DataImporterContext ctx, ImportFile file) return; } - if (!File.Exists(file.Path)) - { - throw new SmartException($"File does not exist {file.Path}."); - } + if (!File.Exists(file.Path)) + { + throw new SmartException($"File does not exist {file.Path}."); + } - using (var stream = new FileStream(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { + using (var stream = new FileStream(file.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { var csvConfiguration = file.IsCsv ? (new CsvConfigurationConverter().ConvertFrom(profile.FileTypeConfiguration) ?? CsvConfiguration.ExcelFriendlyConfiguration) : CsvConfiguration.ExcelFriendlyConfiguration; context.DataTable = LightweightDataTable.FromFile( file.Name, - stream, - stream.Length, + stream, + stream.Length, csvConfiguration, - profile.Skip, - profile.Take > 0 ? profile.Take : int.MaxValue); + profile.Skip, + profile.Take > 0 ? profile.Take : int.MaxValue); context.ColumnMap = file.RelatedType.HasValue ? new ColumnMap() : ctx.ColumnMap; context.File = file; @@ -244,22 +244,22 @@ private void ImportCoreInner(DataImporterContext ctx, ImportFile file) context.Result.AddWarning("Import aborted. A cancellation has been requested."); } } - } - } + } + } - private void ImportCoreOuter(DataImporterContext ctx) - { + private void ImportCoreOuter(DataImporterContext ctx) + { var customer = _services.WorkContext.CurrentCustomer; var profile = ctx.Request.Profile; var logPath = profile.GetImportLogPath(); - FileSystemHelper.DeleteFile(logPath); + FileSystemHelper.DeleteFile(logPath); - using (var logger = new TraceLogger(logPath)) - { + using (var logger = new TraceLogger(logPath)) + { var scopes = new List(); try - { + { var files = profile.GetImportFiles(profile.ImportRelatedData); var groupedFiles = files.GroupBy(x => x.RelatedType); @@ -270,26 +270,27 @@ private void ImportCoreOuter(DataImporterContext ctx) throw new SmartException("You do not have permission to perform the selected import."); _dbCache.Enabled = false; - scopes.Add(_localizedEntityService.BeginScope()); - scopes.Add(_urlRecordService.BeginScope()); + _services.MediaService.ImagePostProcessingEnabled = false; + scopes.Add(_localizedEntityService.BeginScope()); + scopes.Add(_urlRecordService.BeginScope()); - ctx.Log = logger; + ctx.Log = logger; ctx.Importer = _importerFactory(profile.EntityType); ctx.ExecuteContext.Request = ctx.Request; - ctx.ExecuteContext.DataExchangeSettings = _dataExchangeSettings.Value; - ctx.ExecuteContext.Services = _services; - ctx.ExecuteContext.Log = logger; - ctx.ExecuteContext.Languages = _languageService.GetAllLanguages(true); - ctx.ExecuteContext.UpdateOnly = profile.UpdateOnly; - ctx.ExecuteContext.KeyFieldNames = profile.KeyFieldNames.SplitSafe(","); - ctx.ExecuteContext.ImportFolder = profile.GetImportFolder(); - ctx.ExecuteContext.ExtraData = XmlHelper.Deserialize(profile.ExtraData); + ctx.ExecuteContext.DataExchangeSettings = _dataExchangeSettings.Value; + ctx.ExecuteContext.Services = _services; + ctx.ExecuteContext.Log = logger; + ctx.ExecuteContext.Languages = _languageService.GetAllLanguages(true); + ctx.ExecuteContext.UpdateOnly = profile.UpdateOnly; + ctx.ExecuteContext.KeyFieldNames = profile.KeyFieldNames.SplitSafe(","); + ctx.ExecuteContext.ImportFolder = profile.GetImportFolder(); + ctx.ExecuteContext.ExtraData = XmlHelper.Deserialize(profile.ExtraData); var sb = new StringBuilder(); sb.AppendLine(); sb.AppendLine(new string('-', 40)); - sb.AppendLine("SmartStore.NET: v." + SmartStoreVersion.CurrentFullVersion); + sb.AppendLine("Smartstore: v." + SmartStoreVersion.CurrentFullVersion); sb.AppendLine("Import profile: {0} {1}".FormatInvariant(profile.Name, profile.Id == 0 ? " (volatile)" : $" (Id {profile.Id})")); foreach (var fileGroup in groupedFiles) { @@ -308,87 +309,88 @@ private void ImportCoreOuter(DataImporterContext ctx) fileGroup.Each(x => ImportCoreInner(ctx, x)); } - } - catch (Exception ex) - { + } + catch (Exception ex) + { logger.ErrorsAll(ex); - } - finally - { - try - { - _dbCache.Enabled = true; - scopes.Each(x => x.Dispose()); - - _services.EventPublisher.Publish(new ImportExecutedEvent(ctx.ExecuteContext)); - } - catch (Exception ex) - { + } + finally + { + try + { + _dbCache.Enabled = true; + _services.MediaService.ImagePostProcessingEnabled = true; + scopes.Each(x => x.Dispose()); + + _services.EventPublisher.Publish(new ImportExecutedEvent(ctx.ExecuteContext)); + } + catch (Exception ex) + { logger.ErrorsAll(ex); } - try - { - // Database context sharing problem: if there are entities in modified state left by the provider due to SaveChanges failure, - // then all subsequent SaveChanges would fail too (e.g. IImportProfileService.UpdateImportProfile, IScheduledTaskService.UpdateTask...). - // so whatever it is, detach\dispose all what the tracker still has tracked. + try + { + // Database context sharing problem: if there are entities in modified state left by the provider due to SaveChanges failure, + // then all subsequent SaveChanges would fail too (e.g. IImportProfileService.UpdateImportProfile, IScheduledTaskService.UpdateTask...). + // so whatever it is, detach\dispose all what the tracker still has tracked. - _services.DbContext.DetachAll(false); - } - catch (Exception ex) - { + _services.DbContext.DetachAll(false); + } + catch (Exception ex) + { logger.ErrorsAll(ex); } - try - { - SendCompletionEmail(ctx); - } - catch (Exception ex) - { + try + { + SendCompletionEmail(ctx); + } + catch (Exception ex) + { logger.ErrorsAll(ex); } - try - { - LogResults(ctx); - } - catch (Exception ex) - { - logger.ErrorsAll(ex); - } - - try - { + try + { + LogResults(ctx); + } + catch (Exception ex) + { + logger.ErrorsAll(ex); + } + + try + { if (ctx.Results.TryGetValue(string.Empty, out var result)) { profile.ResultInfo = XmlHelper.Serialize(result.Clone()); _importProfileService.UpdateImportProfile(profile); } - } - catch (Exception ex) - { - logger.ErrorsAll(ex); - } - - try - { - ctx.Request.CustomData.Clear(); + } + catch (Exception ex) + { + logger.ErrorsAll(ex); + } + + try + { + ctx.Request.CustomData.Clear(); ctx.Results.Clear(); ctx.Log = null; - } - catch (Exception ex) - { - logger.ErrorsAll(ex); - } - } - } - } - - public void Import(DataImportRequest request, CancellationToken cancellationToken) - { - Guard.NotNull(request, nameof(request)); - Guard.NotNull(cancellationToken, nameof(cancellationToken)); + } + catch (Exception ex) + { + logger.ErrorsAll(ex); + } + } + } + } + + public void Import(DataImportRequest request, CancellationToken cancellationToken) + { + Guard.NotNull(request, nameof(request)); + Guard.NotNull(cancellationToken, nameof(cancellationToken)); if (request.Profile != null && request.Profile.Enabled) { @@ -397,6 +399,6 @@ public void Import(DataImportRequest request, CancellationToken cancellationToke } cancellationToken.ThrowIfCancellationRequested(); - } - } + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs index f35c328fb6..251afcbe6c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs @@ -3,71 +3,71 @@ namespace SmartStore.Services.DataExchange.Import { - public interface IDataColumn - { - string Name { get; } - Type Type { get; } - } + public interface IDataColumn + { + string Name { get; } + Type Type { get; } + } - public interface IDataRow - { - object[] Values { get; } - object this[int index] { get; set; } - object this[string name] { get; set; } + public interface IDataRow + { + object[] Values { get; } + object this[int index] { get; set; } + object this[string name] { get; set; } - IDataTable Table { get; } - } + IDataTable Table { get; } + } - public interface IDataTable - { - bool HasColumn(string name); - int GetColumnIndex(string name); + public interface IDataTable + { + bool HasColumn(string name); + int GetColumnIndex(string name); IList Columns { get; } - IList Rows { get; } - } + IList Rows { get; } + } - public static class IDataRowExtensions - { - public static object GetValue(this IDataRow row, int index) - { - return row[index]; - } + public static class IDataRowExtensions + { + public static object GetValue(this IDataRow row, int index) + { + return row[index]; + } - public static object GetValue(this IDataRow row, string name) - { - return row[name]; - } + public static object GetValue(this IDataRow row, string name) + { + return row[name]; + } - public static void SetValue(this IDataRow row, int index, object value) - { - row[index] = value; - } + public static void SetValue(this IDataRow row, int index, object value) + { + row[index] = value; + } - public static void SetValue(this IDataRow row, string name, object value) - { - row[name] = value; - } + public static void SetValue(this IDataRow row, string name, object value) + { + row[name] = value; + } - public static bool TryGetValue(this IDataRow row, string name, out object value) - { - value = null; + public static bool TryGetValue(this IDataRow row, string name, out object value) + { + value = null; - var index = row.Table.GetColumnIndex(name); - if (index < 0) - return false; + var index = row.Table.GetColumnIndex(name); + if (index < 0) + return false; - value = row[index]; - return true; - } + value = row[index]; + return true; + } - public static bool TrySetValue(this IDataRow row, string name, object value) - { - var index = row.Table.GetColumnIndex(name); - if (index < 0) - return false; + public static bool TrySetValue(this IDataRow row, string name, object value) + { + var index = row.Table.GetColumnIndex(name); + if (index < 0) + return false; - row[index] = value; - return true; - } - } + row[index] = value; + return true; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs index f1a33eb808..628dc0894c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs @@ -12,395 +12,377 @@ namespace SmartStore.Services.DataExchange.Import { - public class LightweightDataTable : IDataTable - { - private readonly IList _columns; - private readonly IList _rows; - private readonly IDictionary _columnIndexes; - private readonly IDictionary _alternativeColumnIndexes; - - public LightweightDataTable(IList columns, IList data) - { - Guard.NotNull(columns, nameof(columns)); - Guard.NotNull(data, nameof(data)); - - if (columns.Select(x => x.Name.ToLower()).Distinct().ToArray().Length != columns.Count) - { - throw Error.Argument("columns", "The columns collection cannot contain duplicate column names."); - } - - _columns = new ReadOnlyCollection(columns); - - TrimData(data); - - var rows = data.Select(x => new LightweightDataRow(this, x)).Cast().ToList(); - _rows = new ReadOnlyCollection(rows); - - _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); - _alternativeColumnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); - - for (var i = 0; i < columns.Count; i++) - { - var name = columns[i].Name; - var alternativeName = GetAlternativeColumnNameFor(name); - - _columnIndexes[name] = i; - - if (!alternativeName.IsCaseInsensitiveEqual(name)) - _alternativeColumnIndexes[alternativeName] = i; - } - } - - private static void TrimData(IList data) - { - // When a user deletes content instead of whole rows from an excel sheet, - // our data table contains completely empty rows at the end. - // Here we get rid of them as they are absolutely useless. - for (int i = data.Count - 1; i >= 0; i--) - { - var allColumnsEmpty = data[i].All(x => x == null || x == DBNull.Value); - if (allColumnsEmpty) - { - data.RemoveAt(i); - //i--; - } - else - { - // get out here on the first occurence of a NON-empty row - break; - } - } - } - - public bool HasColumn(string name) - { - if (name.HasValue()) - { - return (_columnIndexes.ContainsKey(name) || _alternativeColumnIndexes.ContainsKey(name)); - } - - return false; - } - - public int GetColumnIndex(string name) - { - int index; - - if (name.HasValue()) - { - if (_columnIndexes.TryGetValue(name, out index)) - return index; - - if (_alternativeColumnIndexes.TryGetValue(name, out index)) - return index; - } - - return -1; - } - - public IList Columns - { - get - { - return _columns; - } - } - - public IList Rows - { - get - { - return _rows; - } - } - - public static string GetAlternativeColumnNameFor(string name) - { - if (name.IsEmpty()) - return name; - - return name - .Replace(" ", "") - .Replace("-", "") - .Replace("_", ""); - } - - public static IDataTable FromPostedFile( - HttpPostedFileBase file, - int skip = 0, - int take = int.MaxValue) - { - Guard.NotNull(file, nameof(file)); - - return FromFile(file.FileName, file.InputStream, file.ContentLength, new CsvConfiguration(), skip, take); - } - - public static IDataTable FromPostedFile( - HttpPostedFileBase file, - CsvConfiguration configuration, - int skip = 0, - int take = int.MaxValue) - { - Guard.NotNull(file, nameof(file)); - - return FromFile(file.FileName, file.InputStream, file.ContentLength, configuration, skip, take); - } - - public static IDataTable FromFile( - string fileName, - Stream stream, - long contentLength, - CsvConfiguration configuration, - int skip = 0, - int take = int.MaxValue) - { - Guard.NotEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - Guard.NotNull(configuration, nameof(configuration)); - - if (contentLength == 0) - { - throw Error.Argument("fileName", "The posted file '{0}' does not contain any data.".FormatInvariant(fileName)); - } - - IDataReader dataReader = null; - - try - { - var fileExt = System.IO.Path.GetExtension(fileName).ToLowerInvariant(); - - switch (fileExt) - { - case ".xlsx": - dataReader = new ExcelDataReader(stream, true); // TODO: let the user specify if excel file has headers - break; - default: - dataReader = new CsvDataReader(new StreamReader(stream), configuration); - break; - } - - var table = LightweightDataTable.FromDataReader(dataReader, skip, take); - - if (table.Columns.Count == 0 || table.Rows.Count == 0) - { - throw Error.InvalidOperation("The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(fileName)); - } - - return table; - } - catch (Exception ex) - { - throw ex; - } - finally - { - if (dataReader != null) - { - if (!dataReader.IsClosed) - { - dataReader.Dispose(); - } - dataReader = null; - } - } - } - - public static IDataTable FromDataReader( - IDataReader reader, - int skip = 0, - int take = int.MaxValue) - { - Guard.NotNull(reader, nameof(reader)); - - if (reader.IsClosed) - throw new ArgumentException("This operation is invalid when the reader is closed.", "reader"); - - var columns = new List(reader.FieldCount); - var data = new List(); - - var schema = reader.GetSchemaTable(); - - var nameCol = schema.Columns[SchemaTableColumn.ColumnName]; - var typeCol = schema.Columns[SchemaTableColumn.DataType]; - - foreach (DataRow schemaRow in schema.Rows) - { - var column = new LightweightDataColumn((string)schemaRow[nameCol], (Type)schemaRow[typeCol]); - columns.Add(column); - } - - var fieldCount = reader.FieldCount; - - take = Math.Min(take, int.MaxValue - skip); - - int i = -1; - while (reader.Read()) - { - i++; - - if (skip > i) - continue; - - if (i >= skip + take) - break; - - var values = new object[fieldCount]; - reader.GetValues(values); - data.Add(values); - } - - var table = new LightweightDataTable(columns, data); - - return table; - } - } - - internal class LightweightDataRow : DynamicObject, IDataRow - { - private readonly IDataTable _table; - private readonly object[] _values; - - public LightweightDataRow(IDataTable table, object[] values) - { - Guard.NotNull(values, nameof(values)); - - if (table.Columns.Count != values.Length) - { - throw new ArgumentOutOfRangeException( - "values", - "The number of row values must match the number of columns. Expected: {0}, actual: {1}".FormatInvariant(table.Columns.Count, values.Length)); - } - - _table = table; - _values = values; - } - - public IDataTable Table - { - get { return _table; } - } - - public object[] Values - { - get { return _values; } - } - - public object this[string name] - { - get - { - var index = _table.GetColumnIndex(name); - if (index < 0) - throw new KeyNotFoundException(); - - return _values[index]; - } - set - { - var index = _table.GetColumnIndex(name); - if (index < 0) - throw new KeyNotFoundException(); - - _values[index] = value; - } - } - - public object this[int index] - { - get - { - ValidateColumnIndex(index); - return _values[index]; - } - set - { - ValidateColumnIndex(index); - _values[index] = value; - } - } - - private void ValidateColumnIndex(int index) - { - if (index < 0 || index >= _table.Columns.Count) - { - throw new ArgumentOutOfRangeException("index", index, - "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_table.Columns.Count, index)); - } - } - - public override IEnumerable GetDynamicMemberNames() - { - return _table.Columns.Select(x => x.Name); - } - - - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - result = null; - - if (this.TryGetValue(binder.Name, out result)) - { - return true; - } - - return false; - } - - public override bool TrySetMember(SetMemberBinder binder, object value) - { - return this.TrySetValue(binder.Name, value); - } - - public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) - { - result = null; - - try - { - result = _values[(int)indexes[0]]; - return true; - } - catch - { - return false; - } - } - - public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) - { - try - { - _values[(int)indexes[0]] = value; - return true; - } - catch - { - return false; - } - } - } - - internal class LightweightDataColumn : IDataColumn - { - public LightweightDataColumn(string name, Type type) - { - Guard.NotEmpty(name, nameof(name)); - Guard.NotNull(type, nameof(type)); - - this.Name = name; - this.Type = type; - } - - public string Name - { - get; - private set; - } - - public Type Type - { - get; - private set; - } - } + public class LightweightDataTable : IDataTable + { + private readonly IList _columns; + private readonly IList _rows; + private readonly IDictionary _columnIndexes; + private readonly IDictionary _alternativeColumnIndexes; + + public LightweightDataTable(IList columns, IList data) + { + Guard.NotNull(columns, nameof(columns)); + Guard.NotNull(data, nameof(data)); + + if (columns.Select(x => x.Name.ToLower()).Distinct().ToArray().Length != columns.Count) + { + throw Error.Argument("columns", "The columns collection cannot contain duplicate column names."); + } + + _columns = new ReadOnlyCollection(columns); + + TrimData(data); + + var rows = data.Select(x => new LightweightDataRow(this, x)).Cast().ToList(); + _rows = new ReadOnlyCollection(rows); + + _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); + _alternativeColumnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (var i = 0; i < columns.Count; i++) + { + var name = columns[i].Name; + var alternativeName = GetAlternativeColumnNameFor(name); + + _columnIndexes[name] = i; + + if (!alternativeName.IsCaseInsensitiveEqual(name)) + _alternativeColumnIndexes[alternativeName] = i; + } + } + + private static void TrimData(IList data) + { + // When a user deletes content instead of whole rows from an excel sheet, + // our data table contains completely empty rows at the end. + // Here we get rid of them as they are absolutely useless. + for (int i = data.Count - 1; i >= 0; i--) + { + var allColumnsEmpty = data[i].All(x => x == null || x == DBNull.Value); + if (allColumnsEmpty) + { + data.RemoveAt(i); + //i--; + } + else + { + // get out here on the first occurence of a NON-empty row + break; + } + } + } + + public bool HasColumn(string name) + { + if (name.HasValue()) + { + return (_columnIndexes.ContainsKey(name) || _alternativeColumnIndexes.ContainsKey(name)); + } + + return false; + } + + public int GetColumnIndex(string name) + { + int index; + + if (name.HasValue()) + { + if (_columnIndexes.TryGetValue(name, out index)) + return index; + + if (_alternativeColumnIndexes.TryGetValue(name, out index)) + return index; + } + + return -1; + } + + public IList Columns => _columns; + + public IList Rows => _rows; + + public static string GetAlternativeColumnNameFor(string name) + { + if (name.IsEmpty()) + return name; + + return name + .Replace(" ", "") + .Replace("-", "") + .Replace("_", ""); + } + + public static IDataTable FromPostedFile( + HttpPostedFileBase file, + int skip = 0, + int take = int.MaxValue) + { + Guard.NotNull(file, nameof(file)); + + return FromFile(file.FileName, file.InputStream, file.ContentLength, new CsvConfiguration(), skip, take); + } + + public static IDataTable FromPostedFile( + HttpPostedFileBase file, + CsvConfiguration configuration, + int skip = 0, + int take = int.MaxValue) + { + Guard.NotNull(file, nameof(file)); + + return FromFile(file.FileName, file.InputStream, file.ContentLength, configuration, skip, take); + } + + public static IDataTable FromFile( + string fileName, + Stream stream, + long contentLength, + CsvConfiguration configuration, + int skip = 0, + int take = int.MaxValue) + { + Guard.NotEmpty(fileName, nameof(fileName)); + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(configuration, nameof(configuration)); + + if (contentLength == 0) + { + throw Error.Argument("fileName", "The posted file '{0}' does not contain any data.".FormatInvariant(fileName)); + } + + IDataReader dataReader = null; + + try + { + var fileExt = System.IO.Path.GetExtension(fileName).ToLowerInvariant(); + + switch (fileExt) + { + case ".xlsx": + dataReader = new ExcelDataReader(stream, true); // TODO: let the user specify if excel file has headers + break; + default: + dataReader = new CsvDataReader(new StreamReader(stream), configuration); + break; + } + + var table = LightweightDataTable.FromDataReader(dataReader, skip, take); + + if (table.Columns.Count == 0 || table.Rows.Count == 0) + { + throw Error.InvalidOperation("The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(fileName)); + } + + return table; + } + catch (Exception ex) + { + throw ex; + } + finally + { + if (dataReader != null) + { + if (!dataReader.IsClosed) + { + dataReader.Dispose(); + } + dataReader = null; + } + } + } + + public static IDataTable FromDataReader( + IDataReader reader, + int skip = 0, + int take = int.MaxValue) + { + Guard.NotNull(reader, nameof(reader)); + + if (reader.IsClosed) + throw new ArgumentException("This operation is invalid when the reader is closed.", "reader"); + + var columns = new List(reader.FieldCount); + var data = new List(); + + var schema = reader.GetSchemaTable(); + + var nameCol = schema.Columns[SchemaTableColumn.ColumnName]; + var typeCol = schema.Columns[SchemaTableColumn.DataType]; + + foreach (DataRow schemaRow in schema.Rows) + { + var column = new LightweightDataColumn((string)schemaRow[nameCol], (Type)schemaRow[typeCol]); + columns.Add(column); + } + + var fieldCount = reader.FieldCount; + + take = Math.Min(take, int.MaxValue - skip); + + int i = -1; + while (reader.Read()) + { + i++; + + if (skip > i) + continue; + + if (i >= skip + take) + break; + + var values = new object[fieldCount]; + reader.GetValues(values); + data.Add(values); + } + + var table = new LightweightDataTable(columns, data); + + return table; + } + } + + internal class LightweightDataRow : DynamicObject, IDataRow + { + private readonly IDataTable _table; + private readonly object[] _values; + + public LightweightDataRow(IDataTable table, object[] values) + { + Guard.NotNull(values, nameof(values)); + + if (table.Columns.Count != values.Length) + { + throw new ArgumentOutOfRangeException( + "values", + "The number of row values must match the number of columns. Expected: {0}, actual: {1}".FormatInvariant(table.Columns.Count, values.Length)); + } + + _table = table; + _values = values; + } + + public IDataTable Table => _table; + + public object[] Values => _values; + + public object this[string name] + { + get + { + var index = _table.GetColumnIndex(name); + if (index < 0) + throw new KeyNotFoundException(); + + return _values[index]; + } + set + { + var index = _table.GetColumnIndex(name); + if (index < 0) + throw new KeyNotFoundException(); + + _values[index] = value; + } + } + + public object this[int index] + { + get + { + ValidateColumnIndex(index); + return _values[index]; + } + set + { + ValidateColumnIndex(index); + _values[index] = value; + } + } + + private void ValidateColumnIndex(int index) + { + if (index < 0 || index >= _table.Columns.Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_table.Columns.Count, index)); + } + } + + public override IEnumerable GetDynamicMemberNames() + { + return _table.Columns.Select(x => x.Name); + } + + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = null; + + if (this.TryGetValue(binder.Name, out result)) + { + return true; + } + + return false; + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + return this.TrySetValue(binder.Name, value); + } + + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) + { + result = null; + + try + { + result = _values[(int)indexes[0]]; + return true; + } + catch + { + return false; + } + } + + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) + { + try + { + _values[(int)indexes[0]] = value; + return true; + } + catch + { + return false; + } + } + } + + internal class LightweightDataColumn : IDataColumn + { + public LightweightDataColumn(string name, Type type) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(type, nameof(type)); + + this.Name = name; + this.Type = type; + } + + public string Name + { + get; + private set; + } + + public Type Type + { + get; + private set; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs index ad35777b91..e33149ae8e 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs @@ -19,56 +19,56 @@ namespace SmartStore.Services.DataExchange.Import { public abstract class EntityImporterBase : IEntityImporter - { - private const string IMAGE_DOWNLOAD_FOLDER = @"Content\DownloadedImages"; + { + private const string IMAGE_DOWNLOAD_FOLDER = @"Content\DownloadedImages"; - public DateTime UtcNow - { - get; - private set; - } + public DateTime UtcNow + { + get; + private set; + } /// /// URL > file name. To avoid downloading image several times. /// public Dictionary DownloadedItems - { - get; - private set; - } - - public string ImageDownloadFolder - { - get; - private set; - } - - public string ImageFolder - { - get; - private set; - } - - public FileDownloadManagerContext DownloaderContext - { - get; - private set; - } - - public void Execute(ImportExecuteContext context) - { - Import(context); - } - - protected abstract void Import(ImportExecuteContext context); - - protected void Initialize(ImportExecuteContext context) - { - UtcNow = DateTime.UtcNow; - DownloadedItems = new Dictionary(); - ImageDownloadFolder = Path.Combine(context.ImportFolder, IMAGE_DOWNLOAD_FOLDER); - - var settings = context.DataExchangeSettings; + { + get; + private set; + } + + public string ImageDownloadFolder + { + get; + private set; + } + + public string ImageFolder + { + get; + private set; + } + + public FileDownloadManagerContext DownloaderContext + { + get; + private set; + } + + public void Execute(ImportExecuteContext context) + { + Import(context); + } + + protected abstract void Import(ImportExecuteContext context); + + protected void Initialize(ImportExecuteContext context) + { + UtcNow = DateTime.UtcNow; + DownloadedItems = new Dictionary(); + ImageDownloadFolder = Path.Combine(context.ImportFolder, IMAGE_DOWNLOAD_FOLDER); + + var settings = context.DataExchangeSettings; ImageFolder = settings.ImageImportFolder.HasValue() ? Path.Combine(context.ImportFolder, settings.ImageImportFolder) @@ -79,159 +79,168 @@ protected void Initialize(ImportExecuteContext context) System.IO.Directory.CreateDirectory(ImageDownloadFolder); } - DownloaderContext = new FileDownloadManagerContext - { - Timeout = TimeSpan.FromMinutes(settings.ImageDownloadTimeout), - Logger = context.Log, - CancellationToken = context.CancellationToken - }; - - context.Result.TotalRecords = context.DataSegmenter.TotalRows; - } - - public FileDownloadManagerItem CreateDownloadImage(ImportExecuteContext context, string urlOrPath, int displayOrder) - { - var item = new FileDownloadManagerItem + DownloaderContext = new FileDownloadManagerContext { - Id = displayOrder, - DisplayOrder = displayOrder + Timeout = TimeSpan.FromMinutes(settings.ImageDownloadTimeout), + Logger = context.Log, + CancellationToken = context.CancellationToken }; - if (urlOrPath.IsWebUrl()) - { - // We append quality to avoid importing of image duplicates. - item.Url = context.Services.WebHelper.ModifyQueryString(urlOrPath, "q=100", null); + context.Result.TotalRecords = context.DataSegmenter.TotalRows; + } - if (DownloadedItems.ContainsKey(urlOrPath)) + public FileDownloadManagerItem CreateDownloadImage(ImportExecuteContext context, string urlOrPath, int displayOrder) + { + try + { + var item = new FileDownloadManagerItem { - // URL has already been downloaded. - item.Success = true; - item.FileName = DownloadedItems[urlOrPath]; - } - else + Id = displayOrder, + DisplayOrder = displayOrder + }; + + if (urlOrPath.IsWebUrl()) { - var localPath = string.Empty; + // We append quality to avoid importing of image duplicates. + item.Url = context.Services.WebHelper.ModifyQueryString(urlOrPath, "q=100", null); - try + if (DownloadedItems.ContainsKey(urlOrPath)) { - // Exclude query string parts! - localPath = new Uri(urlOrPath).LocalPath; + // URL has already been downloaded. + item.Success = true; + item.FileName = DownloadedItems[urlOrPath]; + } + else + { + var localPath = string.Empty; + + try + { + // Exclude query string parts! + localPath = new Uri(urlOrPath).LocalPath; + } + catch { } + + item.FileName = Path.GetFileName(localPath).ToValidFileName().NullEmpty() ?? Path.GetRandomFileName(); } - catch { } - item.FileName = Path.GetFileName(localPath).ToValidFileName().NullEmpty() ?? Path.GetRandomFileName(); + item.Path = Path.Combine(ImageDownloadFolder, item.FileName); } + else + { + item.Success = true; + item.FileName = Path.GetFileName(urlOrPath).ToValidFileName().NullEmpty() ?? Path.GetRandomFileName(); + + item.Path = Path.IsPathRooted(urlOrPath) + ? urlOrPath + : Path.Combine(ImageFolder, urlOrPath); + } + + item.MimeType = MimeTypes.MapNameToMimeType(item.FileName); + + return item; + } + catch + { + context.Result.AddWarning($"Failed to prepare image download for '{urlOrPath.NaIfEmpty()}'. Skipping file."); + + return null; + } + } + + public void Succeeded(FileDownloadManagerItem item) + { + if ((item.Success ?? false) && item.Url.HasValue() && !DownloadedItems.ContainsKey(item.Url)) + { + DownloadedItems.Add(item.Url, Path.GetFileName(item.Path)); + } + } - item.Path = Path.Combine(ImageDownloadFolder, item.FileName); + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + IDictionary>> localizableProperties) where TEntity : BaseEntity, ILocalizedEntity + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(batch, nameof(batch)); + Guard.NotNull(localizableProperties, nameof(localizableProperties)); + + // Perf: determine whether our localizable properties actually have + // counterparts in the source BEFORE import batch begins. This way we spare ourself + // to query over and over for values. + var localizedProps = (from kvp in localizableProperties + where context.DataSegmenter.GetColumnIndexes(kvp.Key).Length > 0 + select kvp.Key).ToArray(); + + if (localizedProps.Length == 0) + { + return 0; } - else + + var localizedEntityService = context.Services.Resolve(); + + bool shouldSave = false; + + foreach (var row in batch) { - item.Success = true; - item.FileName = Path.GetFileName(urlOrPath).ToValidFileName().NullEmpty() ?? Path.GetRandomFileName(); + foreach (var prop in localizedProps) + { + var lambda = localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } - item.Path = Path.IsPathRooted(urlOrPath) - ? urlOrPath - : Path.Combine(ImageFolder, urlOrPath); + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); } - item.MimeType = MimeTypes.MapNameToMimeType(item.FileName); + return 0; + } + + protected virtual int ProcessStoreMappings( + ImportExecuteContext context, + IEnumerable> batch) where TEntity : BaseEntity, IStoreMappingSupported + { + var storeMappingService = context.Services.Resolve(); + var storeMappingRepository = context.Services.Resolve>(); + + storeMappingRepository.AutoCommitEnabled = false; - return item; + foreach (var row in batch) + { + var storeIds = row.GetDataValue>("StoreIds"); + if (!storeIds.IsNullOrEmpty()) + { + storeMappingService.SaveStoreMappings(row.Entity, storeIds.ToArray()); + } + } + + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); } - public void Succeeded(FileDownloadManagerItem item) - { - if ((item.Success ?? false) && item.Url.HasValue() && !DownloadedItems.ContainsKey(item.Url)) - { - DownloadedItems.Add(item.Url, Path.GetFileName(item.Path)); - } - } - - protected virtual int ProcessLocalizations( - ImportExecuteContext context, - IEnumerable> batch, - IDictionary>> localizableProperties) where TEntity : BaseEntity, ILocalizedEntity - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(batch, nameof(batch)); - Guard.NotNull(localizableProperties, nameof(localizableProperties)); - - // Perf: determine whether our localizable properties actually have - // counterparts in the source BEFORE import batch begins. This way we spare ourself - // to query over and over for values. - var localizedProps = (from kvp in localizableProperties - where context.DataSegmenter.GetColumnIndexes(kvp.Key).Length > 0 - select kvp.Key).ToArray(); - - if (localizedProps.Length == 0) - { - return 0; - } - - var localizedEntityService = context.Services.Resolve(); - - bool shouldSave = false; - - foreach (var row in batch) - { - foreach (var prop in localizedProps) - { - var lambda = localizableProperties[prop]; - foreach (var lang in context.Languages) - { - var code = lang.UniqueSeoCode; - string value; - - if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) - { - localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); - shouldSave = true; - } - } - } - } - - if (shouldSave) - { - // commit whole batch at once - return context.Services.DbContext.SaveChanges(); - } - - return 0; - } - - protected virtual int ProcessStoreMappings( - ImportExecuteContext context, - IEnumerable> batch) where TEntity : BaseEntity, IStoreMappingSupported - { - var storeMappingService = context.Services.Resolve(); - var storeMappingRepository = context.Services.Resolve>(); - - storeMappingRepository.AutoCommitEnabled = false; - - foreach (var row in batch) - { - var storeIds = row.GetDataValue>("StoreIds"); - if (!storeIds.IsNullOrEmpty()) - { - storeMappingService.SaveStoreMappings(row.Entity, storeIds.ToArray()); - } - } - - // commit whole batch at once - return context.Services.DbContext.SaveChanges(); - } - - protected virtual int ProcessSlugs( - ImportExecuteContext context, - IEnumerable> batch, - string entityName) where TEntity : BaseEntity, ISlugSupported - { - var slugMap = new Dictionary(); - UrlRecord urlRecord = null; - - var urlRecordService = context.Services.Resolve(); - var seoSettings = context.Services.Resolve(); + protected virtual int ProcessSlugs( + ImportExecuteContext context, + IEnumerable> batch, + string entityName) where TEntity : BaseEntity, ISlugSupported + { + var slugMap = new Dictionary(); + UrlRecord urlRecord = null; + + var urlRecordService = context.Services.Resolve(); + var seoSettings = context.Services.Resolve(); UrlRecord slugLookup(string s) { @@ -239,65 +248,65 @@ UrlRecord slugLookup(string s) } foreach (var row in batch) - { - try - { + { + try + { string localizedName = null; if (row.TryGetDataValue("SeName", out string seName) || row.IsNew || row.NameChanged) - { - seName = row.Entity.ValidateSeName(seName, row.EntityDisplayName, true, urlRecordService, seoSettings, extraSlugLookup: slugLookup); - - if (row.IsNew) - { - // Don't bother validating SeName for new entities. - urlRecord = new UrlRecord - { - EntityId = row.Entity.Id, - EntityName = entityName, - Slug = seName, - LanguageId = 0, - IsActive = true, - }; + { + seName = row.Entity.ValidateSeName(seName, row.EntityDisplayName, true, urlRecordService, seoSettings, extraSlugLookup: slugLookup); + + if (row.IsNew) + { + // Don't bother validating SeName for new entities. + urlRecord = new UrlRecord + { + EntityId = row.Entity.Id, + EntityName = entityName, + Slug = seName, + LanguageId = 0, + IsActive = true, + }; urlRecordService.InsertUrlRecord(urlRecord); - } - else - { - urlRecord = urlRecordService.SaveSlug(row.Entity, seName, 0); - } - - if (urlRecord != null) - { - // A new record was inserted to the store: keep track of it for this batch. - slugMap[seName] = urlRecord; - } - } - - // Process localized SeNames. - foreach (var lang in context.Languages) - { - var hasSeName = row.TryGetDataValue("SeName", lang.UniqueSeoCode, out seName); - var hasLocalizedName = row.TryGetDataValue("Name", lang.UniqueSeoCode, out localizedName); - - if (hasSeName || hasLocalizedName) - { - seName = row.Entity.ValidateSeName(seName, localizedName, false, urlRecordService, seoSettings, lang.Id, slugLookup); - urlRecord = urlRecordService.SaveSlug(row.Entity, seName, lang.Id); - if (urlRecord != null) - { - slugMap[seName] = urlRecord; - } - } - } - } - catch (Exception ex) - { - context.Result.AddWarning(ex.Message, row.GetRowInfo(), "SeName"); - } - } - - // Commit whole batch at once. - return context.Services.DbContext.SaveChanges(); - } - } + } + else + { + urlRecord = urlRecordService.SaveSlug(row.Entity, seName, 0); + } + + if (urlRecord != null) + { + // A new record was inserted to the store: keep track of it for this batch. + slugMap[seName] = urlRecord; + } + } + + // Process localized SeNames. + foreach (var lang in context.Languages) + { + var hasSeName = row.TryGetDataValue("SeName", lang.UniqueSeoCode, out seName); + var hasLocalizedName = row.TryGetDataValue("Name", lang.UniqueSeoCode, out localizedName); + + if (hasSeName || hasLocalizedName) + { + seName = row.Entity.ValidateSeName(seName, localizedName, false, urlRecordService, seoSettings, lang.Id, slugLookup); + urlRecord = urlRecordService.SaveSlug(row.Entity, seName, lang.Id); + if (urlRecord != null) + { + slugMap[seName] = urlRecord; + } + } + } + } + catch (Exception ex) + { + context.Result.AddWarning(ex.Message, row.GetRowInfo(), "SeName"); + } + } + + // Commit whole batch at once. + return context.Services.DbContext.SaveChanges(); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutedEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutedEvent.cs index c59979e207..073a74ba18 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutedEvent.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutedEvent.cs @@ -2,19 +2,19 @@ namespace SmartStore.Services.DataExchange.Import.Events { - public class ImportExecutedEvent - { - public ImportExecutedEvent(ImportExecuteContext context) - { - Guard.NotNull(context, nameof(context)); + public class ImportExecutedEvent + { + public ImportExecutedEvent(ImportExecuteContext context) + { + Guard.NotNull(context, nameof(context)); - Context = context; - } + Context = context; + } - public ImportExecuteContext Context - { - get; - private set; - } - } + public ImportExecuteContext Context + { + get; + private set; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutingEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutingEvent.cs index 3c0160b306..537606f9c2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutingEvent.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportExecutingEvent.cs @@ -2,19 +2,19 @@ namespace SmartStore.Services.DataExchange.Import.Events { - public class ImportExecutingEvent - { - public ImportExecutingEvent(ImportExecuteContext context) - { - Guard.NotNull(context, nameof(context)); + public class ImportExecutingEvent + { + public ImportExecutingEvent(ImportExecuteContext context) + { + Guard.NotNull(context, nameof(context)); - Context = context; - } + Context = context; + } - public ImportExecuteContext Context - { - get; - private set; - } - } + public ImportExecuteContext Context + { + get; + private set; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs index 9eee0876b3..dfd6d6cd5c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs @@ -6,41 +6,41 @@ namespace SmartStore.Services.DataExchange.Import { - public interface IDataImporter - { - void Import(DataImportRequest request, CancellationToken cancellationToken); - } + public interface IDataImporter + { + void Import(DataImportRequest request, CancellationToken cancellationToken); + } - public class DataImportRequest - { - private readonly static ProgressValueSetter _voidProgressValueSetter = DataImportRequest.SetProgress; + public class DataImportRequest + { + private readonly static ProgressValueSetter _voidProgressValueSetter = DataImportRequest.SetProgress; - public DataImportRequest(ImportProfile profile) - { - Guard.NotNull(profile, nameof(profile)); + public DataImportRequest(ImportProfile profile) + { + Guard.NotNull(profile, nameof(profile)); - Profile = profile; - ProgressValueSetter = _voidProgressValueSetter; + Profile = profile; + ProgressValueSetter = _voidProgressValueSetter; - EntitiesToImport = new List(); - CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + EntitiesToImport = new List(); + CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - public ImportProfile Profile { get; private set; } + public ImportProfile Profile { get; private set; } - public ProgressValueSetter ProgressValueSetter { get; set; } + public ProgressValueSetter ProgressValueSetter { get; set; } - public bool HasPermission { get; set; } + public bool HasPermission { get; set; } - public IList EntitiesToImport { get; set; } + public IList EntitiesToImport { get; set; } - public IDictionary CustomData { get; private set; } + public IDictionary CustomData { get; private set; } - private static void SetProgress(int val, int max, string msg) - { - // do nothing - } - } + private static void SetProgress(int val, int max, string msg) + { + // do nothing + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs index bc64b63531..f4b69ac913 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs @@ -1,7 +1,7 @@ namespace SmartStore.Services.DataExchange.Import { - public partial interface IEntityImporter - { - void Execute(ImportExecuteContext context); - } + public partial interface IEntityImporter + { + void Execute(ImportExecuteContext context); + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs index 6f7b7d4f8e..4da8a9e535 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs @@ -5,62 +5,62 @@ namespace SmartStore.Services.DataExchange.Import { - public interface IImportProfileService - { - /// - /// Gets a new profile name - /// - /// Entity type - /// Suggestion for a new profile name - string GetNewProfileName(ImportEntityType entityType); + public interface IImportProfileService + { + /// + /// Gets a new profile name + /// + /// Entity type + /// Suggestion for a new profile name + string GetNewProfileName(ImportEntityType entityType); - /// - /// Inserts an import profile - /// - /// Name of the import file - /// Profile name - /// Entity type - /// Inserted import profile - ImportProfile InsertImportProfile(string fileName, string name, ImportEntityType entityType); + /// + /// Inserts an import profile + /// + /// Name of the import file + /// Profile name + /// Entity type + /// Inserted import profile + ImportProfile InsertImportProfile(string fileName, string name, ImportEntityType entityType); - /// - /// Updates an import profile - /// - /// Import profile - void UpdateImportProfile(ImportProfile profile); + /// + /// Updates an import profile + /// + /// Import profile + void UpdateImportProfile(ImportProfile profile); - /// - /// Deletes an import profile - /// - /// Import profile - void DeleteImportProfile(ImportProfile profile); + /// + /// Deletes an import profile + /// + /// Import profile + void DeleteImportProfile(ImportProfile profile); - /// - /// Get queryable import profiles - /// - /// Whether to filter enabled or disabled profiles - /// Import profiles - IQueryable GetImportProfiles(bool? enabled = null); + /// + /// Get queryable import profiles + /// + /// Whether to filter enabled or disabled profiles + /// Import profiles + IQueryable GetImportProfiles(bool? enabled = null); - /// - /// Gets an import profile by identifier - /// - /// Import profile identifier - /// Import profile - ImportProfile GetImportProfileById(int id); + /// + /// Gets an import profile by identifier + /// + /// Import profile identifier + /// Import profile + ImportProfile GetImportProfileById(int id); - /// - /// Gets an import profile by name - /// - /// Name of the import profile - /// Import profile - ImportProfile GetImportProfileByName(string name); + /// + /// Gets an import profile by name + /// + /// Name of the import profile + /// Import profile + ImportProfile GetImportProfileByName(string name); - /// - /// Get all importable entity properties and their localized values - /// - /// Import entity type - /// Importable entity properties - Dictionary GetImportableEntityProperties(ImportEntityType entityType); - } + /// + /// Get all importable entity properties and their localized values + /// + /// Import entity type + /// Importable entity properties + Dictionary GetImportableEntityProperties(ImportEntityType entityType); + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs index 656c68cefb..ba2df75006 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs @@ -8,111 +8,81 @@ namespace SmartStore.Services.DataExchange.Import { - public class ImportDataSegmenter - { - private const int BATCHSIZE = 100; - - private readonly IDataTable _table; - private object[] _currentBatch; - private readonly IPageable _pageable; - private bool _bof; - private CultureInfo _culture; - private ColumnMap _columnMap; - - private readonly IDictionary _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public ImportDataSegmenter(IDataTable table, ColumnMap map) - { - Guard.NotNull(table, nameof(table)); - Guard.NotNull(map, nameof(map)); - - _table = table; - _columnMap = map; - - _bof = true; - _pageable = new PagedList(0, BATCHSIZE, table.Rows.Count); - _culture = CultureInfo.InvariantCulture; + public class ImportDataSegmenter + { + private const int BATCHSIZE = 100; + + private readonly IDataTable _table; + private object[] _currentBatch; + private readonly IPageable _pageable; + private bool _bof; + private CultureInfo _culture; + private ColumnMap _columnMap; + + private readonly IDictionary _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public ImportDataSegmenter(IDataTable table, ColumnMap map) + { + Guard.NotNull(table, nameof(table)); + Guard.NotNull(map, nameof(map)); + + _table = table; + _columnMap = map; + + _bof = true; + _pageable = new PagedList(0, BATCHSIZE, table.Rows.Count); + _culture = CultureInfo.InvariantCulture; } - public CultureInfo Culture - { - get - { - return _culture; - } - set - { - _culture = value ?? CultureInfo.InvariantCulture; - } - } - - public ColumnMap ColumnMap - { - get - { - return _columnMap; - } - set - { - _columnMap = value ?? new ColumnMap(); - } - } - - public int TotalRows - { - get { return _table.Rows.Count; } - } - - public int TotalColumns - { - get { return _table.Columns.Count; } - } - - public int CurrentSegment - { - get { return _bof ? 0 : _pageable.PageNumber; } - } - - public int CurrentSegmentFirstRowIndex - { - get { return _pageable.FirstItemIndex; } - } - - public int TotalSegments - { - get { return _pageable.TotalPages; } - } - - public int BatchSize - { - get { return BATCHSIZE; } - } - - /// - /// Determines whether a specific column exists in the underlying data table. - /// - /// The name of the column to find - /// - /// If true and a column with the passed does not exist, - /// this method tests for the existence of any indexed column with the same name. - /// - /// true if the column exists, false otherwise - /// - /// This method takes mapped column names into account. - /// - public bool HasColumn(string name, bool withAnyIndex = false) - { - var result = HasColumn(name, null); - - if (!result && withAnyIndex) - { - // Column does not exist, but withAnyIndex is true: - // Test for existence of any indexed column. - result = GetColumnIndexes(name).Length > 0; - } - - return result; - } + public CultureInfo Culture + { + get => _culture; + set => _culture = value ?? CultureInfo.InvariantCulture; + } + + public ColumnMap ColumnMap + { + get => _columnMap; + set => _columnMap = value ?? new ColumnMap(); + } + + public int TotalRows => _table.Rows.Count; + + public int TotalColumns => _table.Columns.Count; + + public int CurrentSegment => _bof ? 0 : _pageable.PageNumber; + + public int CurrentSegmentFirstRowIndex => _pageable.FirstItemIndex; + + public int TotalSegments => _pageable.TotalPages; + + public int BatchSize => BATCHSIZE; + + /// + /// Determines whether a specific column exists in the underlying data table. + /// + /// The name of the column to find + /// + /// If true and a column with the passed does not exist, + /// this method tests for the existence of any indexed column with the same name. + /// + /// true if the column exists, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasColumn(string name, bool withAnyIndex = false) + { + var result = HasColumn(name, null); + + if (!result && withAnyIndex) + { + // Column does not exist, but withAnyIndex is true: + // Test for existence of any indexed column. + result = GetColumnIndexes(name).Length > 0; + } + + return result; + } /// /// Determines whether the column name[index] exists in the underlying data table. @@ -125,9 +95,9 @@ public bool HasColumn(string name, bool withAnyIndex = false) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasColumn(string name, string index) - { - return _table.HasColumn(_columnMap.GetMapping(name, index).MappedName); - } + { + return _table.HasColumn(_columnMap.GetMapping(name, index).MappedName); + } /// /// Indicates whether to ignore the property that is mapped to columnName @@ -136,9 +106,9 @@ public bool HasColumn(string name, string index) /// true ignore, false do not ignore [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsIgnored(string columnName) - { - return IsIgnored(columnName, null); - } + { + return IsIgnored(columnName, null); + } /// /// Indicates whether to ignore the property that is mapped to columnName @@ -148,100 +118,100 @@ public bool IsIgnored(string columnName) /// true ignore, false do not ignore [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsIgnored(string columnName, string index) - { - var mapping = _columnMap.GetMapping(columnName, index); - - return mapping.IgnoreProperty; - } - - /// - /// Returns an array of exisiting index names for a column - /// - /// The name of the columns without index qualification - /// An array of index names - /// - /// If following columns exist in source: Attr[Color], Attr[Size] - /// This method returns: string[] { "Color", "Size" } - /// - public string[] GetColumnIndexes(string name) - { - string[] indexes; - - if (!_columnIndexes.TryGetValue(name, out indexes)) - { - var startsWith = name + "["; - - var columns1 = _columnMap.Mappings - .Where(x => x.Key.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Key); - - var columns2 = _table.Columns - .Where(x => x.Name.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Name); - - indexes = columns1.Concat(columns2) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(x => x.Substring(x.IndexOf("[", StringComparison.OrdinalIgnoreCase) + 1).TrimEnd(']')) - .ToArray(); - - _columnIndexes[name] = indexes; - } - - return indexes; - } - - public void Reset() - { - if (_pageable.PageIndex != 0 && _currentBatch != null) - { - _currentBatch = null; - } - _bof = true; - _pageable.PageIndex = 0; - } - - public bool ReadNextBatch() - { - if (_currentBatch != null) - { - _currentBatch = null; - } - - if (_bof) - { - _bof = false; - return _pageable.TotalCount > 0; - } - - if (_pageable.HasNextPage) - { - _pageable.PageIndex++; - return true; - } - - Reset(); - return false; - } - - public IEnumerable> GetCurrentBatch() where T : BaseEntity - { - if (_currentBatch == null) - { - int start = _pageable.FirstItemIndex - 1; - int end = _pageable.LastItemIndex - 1; - - _currentBatch = new ImportRow[(end - start) + 1]; - - // Determine values per row - int i = 0; - for (int r = start; r <= end; r++) - { - _currentBatch[i] = new ImportRow(this, _table.Rows[r], r); - i++; - } - } - - return _currentBatch.Cast>(); - } - } + { + var mapping = _columnMap.GetMapping(columnName, index); + + return mapping.IgnoreProperty; + } + + /// + /// Returns an array of exisiting index names for a column + /// + /// The name of the columns without index qualification + /// An array of index names + /// + /// If following columns exist in source: Attr[Color], Attr[Size] + /// This method returns: string[] { "Color", "Size" } + /// + public string[] GetColumnIndexes(string name) + { + string[] indexes; + + if (!_columnIndexes.TryGetValue(name, out indexes)) + { + var startsWith = name + "["; + + var columns1 = _columnMap.Mappings + .Where(x => x.Key.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Key); + + var columns2 = _table.Columns + .Where(x => x.Name.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Name); + + indexes = columns1.Concat(columns2) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(x => x.Substring(x.IndexOf("[", StringComparison.OrdinalIgnoreCase) + 1).TrimEnd(']')) + .ToArray(); + + _columnIndexes[name] = indexes; + } + + return indexes; + } + + public void Reset() + { + if (_pageable.PageIndex != 0 && _currentBatch != null) + { + _currentBatch = null; + } + _bof = true; + _pageable.PageIndex = 0; + } + + public bool ReadNextBatch() + { + if (_currentBatch != null) + { + _currentBatch = null; + } + + if (_bof) + { + _bof = false; + return _pageable.TotalCount > 0; + } + + if (_pageable.HasNextPage) + { + _pageable.PageIndex++; + return true; + } + + Reset(); + return false; + } + + public IEnumerable> GetCurrentBatch() where T : BaseEntity + { + if (_currentBatch == null) + { + int start = _pageable.FirstItemIndex - 1; + int end = _pageable.LastItemIndex - 1; + + _currentBatch = new ImportRow[(end - start) + 1]; + + // Determine values per row + int i = 0; + for (int r = start; r <= end; r++) + { + _currentBatch[i] = new ImportRow(this, _table.Rows[r], r); + i++; + } + } + + return _currentBatch.Cast>(); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs index d5b3ec8692..840f8dcd3c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs @@ -7,65 +7,62 @@ namespace SmartStore.Services.DataExchange.Import { public class ImportExecuteContext - { - private DataExchangeAbortion _abortion; - private ProgressValueSetter _progressValueSetter; - private string _progressInfo; - - private IDataTable _dataTable; - private ImportDataSegmenter _segmenter; - - public ImportExecuteContext( - CancellationToken cancellation, - ProgressValueSetter progressValueSetter, - string progressInfo) - { - _progressValueSetter = progressValueSetter; - _progressInfo = progressInfo; - - CancellationToken = cancellation; - CustomProperties = new Dictionary(); - } - - public DataImportRequest Request - { - get; - internal set; - } - - /// - /// Import settings - /// - public DataExchangeSettings DataExchangeSettings - { - get; - internal set; - } - - /// - /// The data source (CSV, Excel etc.) - /// - public IDataTable DataTable - { - get - { - return _dataTable; - } - internal set - { - _dataTable = value; - _segmenter = null; - } - } - - /// - /// Mapping information between database and data source - /// - public ColumnMap ColumnMap - { - get; - internal set; - } + { + private DataExchangeAbortion _abortion; + private ProgressValueSetter _progressValueSetter; + private string _progressInfo; + + private IDataTable _dataTable; + private ImportDataSegmenter _segmenter; + + public ImportExecuteContext( + CancellationToken cancellation, + ProgressValueSetter progressValueSetter, + string progressInfo) + { + _progressValueSetter = progressValueSetter; + _progressInfo = progressInfo; + + CancellationToken = cancellation; + CustomProperties = new Dictionary(); + } + + public DataImportRequest Request + { + get; + internal set; + } + + /// + /// Import settings + /// + public DataExchangeSettings DataExchangeSettings + { + get; + internal set; + } + + /// + /// The data source (CSV, Excel etc.) + /// + public IDataTable DataTable + { + get => _dataTable; + internal set + { + _dataTable = value; + _segmenter = null; + } + } + + /// + /// Mapping information between database and data source + /// + public ColumnMap ColumnMap + { + get; + internal set; + } /// /// Infos about the import file. @@ -80,148 +77,139 @@ public ImportFile File /// Whether to only update existing records /// public bool UpdateOnly - { - get; - internal set; - } - - /// - /// Name of key fields to identify existing records for updating - /// - public string[] KeyFieldNames - { - get; - internal set; - } - - /// - /// All active languages - /// - public IList Languages - { - get; - internal set; - } - - /// - /// To log information into the import log file - /// - public ILogger Log - { - get; - internal set; - } - - /// - /// Common Services - /// - public ICommonServices Services - { - get; - internal set; - } - - /// - /// Cancellation token - /// - public CancellationToken CancellationToken - { - get; - private set; - } - - /// - /// The import folder - /// - public string ImportFolder - { - get; - internal set; - } - - /// - /// Use this dictionary for any custom data required along the import - /// - public Dictionary CustomProperties - { - get; - set; - } - - /// - /// Result of the import - /// - public ImportResult Result - { - get; - set; - } - - /// - /// Extra import configuration data - /// - public ImportExtraData ExtraData - { - get; - internal set; - } - - /// - /// Indicates whether and how to abort the import - /// - public DataExchangeAbortion Abort - { - get - { - if (CancellationToken.IsCancellationRequested || IsMaxFailures) - return DataExchangeAbortion.Hard; - - return _abortion; - } - set - { - _abortion = value; - } - } - - public bool IsMaxFailures - { - get - { - return Result.Errors > 11; - } - } - - public ImportDataSegmenter DataSegmenter - { - get - { - if (_segmenter == null) - { - if (this.DataTable == null || this.ColumnMap == null) - { - throw new SmartException("A DataTable and a ColumnMap must be specified before accessing the DataSegmenter property."); - } - _segmenter = new ImportDataSegmenter(DataTable, ColumnMap); - } - - return _segmenter; - } - } - - /// - /// Allows to set a progress message - /// - /// Progress value - /// /// Progress maximum - public void SetProgress(int value, int maximum) - { - try - { - _progressValueSetter?.Invoke(value, maximum, _progressInfo.FormatInvariant(value, maximum)); - } - catch { } - } + { + get; + internal set; + } + + /// + /// Name of key fields to identify existing records for updating + /// + public string[] KeyFieldNames + { + get; + internal set; + } + + /// + /// All active languages + /// + public IList Languages + { + get; + internal set; + } + + /// + /// To log information into the import log file + /// + public ILogger Log + { + get; + set; + } + + /// + /// Common Services + /// + public ICommonServices Services + { + get; + internal set; + } + + /// + /// Cancellation token + /// + public CancellationToken CancellationToken + { + get; + private set; + } + + /// + /// The import folder + /// + public string ImportFolder + { + get; + internal set; + } + + /// + /// Use this dictionary for any custom data required along the import + /// + public Dictionary CustomProperties + { + get; + set; + } + + /// + /// Result of the import + /// + public ImportResult Result + { + get; + set; + } + + /// + /// Extra import configuration data + /// + public ImportExtraData ExtraData + { + get; + internal set; + } + + /// + /// Indicates whether and how to abort the import + /// + public DataExchangeAbortion Abort + { + get + { + if (CancellationToken.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + + return _abortion; + } + set => _abortion = value; + } + + public bool IsMaxFailures => Result.Errors > 11; + + public ImportDataSegmenter DataSegmenter + { + get + { + if (_segmenter == null) + { + if (this.DataTable == null || this.ColumnMap == null) + { + throw new SmartException("A DataTable and a ColumnMap must be specified before accessing the DataSegmenter property."); + } + _segmenter = new ImportDataSegmenter(DataTable, ColumnMap); + } + + return _segmenter; + } + } + + /// + /// Allows to set a progress message + /// + /// Progress value + /// /// Progress maximum + public void SetProgress(int value, int maximum) + { + try + { + _progressValueSetter?.Invoke(value, maximum, _progressInfo.FormatInvariant(value, maximum)); + } + catch { } + } /// /// Allows to set a message @@ -236,5 +224,5 @@ public void SetProgress(string message) } catch { } } - } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs index 9b7918fa18..9d7bd8f02b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs @@ -8,36 +8,36 @@ namespace SmartStore.Services.DataExchange.Import { public static class ImportExtensions - { - /// - /// Get folder for import files - /// - /// Import profile - /// Folder path - public static string GetImportFolder( - this ImportProfile profile, - bool content = false, - bool create = false, - bool absolutePath = true) - { - var path = string.Concat( - DataSettings.Current.TenantPath, - "/ImportProfiles/", - profile.FolderName, - content ? "/Content" : ""); + { + /// + /// Get folder for import files + /// + /// Import profile + /// Folder path + public static string GetImportFolder( + this ImportProfile profile, + bool content = false, + bool create = false, + bool absolutePath = true) + { + var path = string.Concat( + DataSettings.Current.TenantPath, + "/ImportProfiles/", + profile.FolderName, + content ? "/Content" : ""); - if (absolutePath) - { - path = CommonHelper.MapPath(path); + if (absolutePath) + { + path = CommonHelper.MapPath(path); - if (create && !System.IO.Directory.Exists(path)) - { - System.IO.Directory.CreateDirectory(path); - } - } + if (create && !System.IO.Directory.Exists(path)) + { + System.IO.Directory.CreateDirectory(path); + } + } - return path; - } + return path; + } /// /// Gets import files for an import profile. @@ -78,8 +78,8 @@ public static List GetImportFiles(this ImportProfile profile, bool i /// Import profile /// Log file path public static string GetImportLogPath(this ImportProfile profile) - { - return Path.Combine(profile.GetImportFolder(), "log.txt"); - } - } + { + return Path.Combine(profile.GetImportFolder(), "log.txt"); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs index ecbe710677..24fd3984c0 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs @@ -2,12 +2,12 @@ namespace SmartStore.Services.DataExchange.Import { - [Serializable] - public class ImportExtraData - { - /// - /// Number of images per object to be imported - /// - public int? NumberOfPictures { get; set; } - } + [Serializable] + public class ImportExtraData + { + /// + /// Number of images per object to be imported + /// + public int? NumberOfPictures { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportFile.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportFile.cs index 6b0af36ef2..ac09239070 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportFile.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportFile.cs @@ -43,13 +43,7 @@ public ImportFile(string path) /// /// Indicates whether the file has an CSV file extension. /// - internal bool IsCsv - { - get - { - return (new string[] { ".csv", ".txt", ".tab" }).Contains(Extension, StringComparer.OrdinalIgnoreCase); - } - } + internal bool IsCsv => (new string[] { ".csv", ".txt", ".tab" }).Contains(Extension, StringComparer.OrdinalIgnoreCase); /// /// Related entity type. diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs index b21728d75b..47f3c16a08 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs @@ -2,89 +2,83 @@ namespace SmartStore.Services.DataExchange.Import { - - public class ImportMessage - { - public ImportMessage(string message, ImportMessageType messageType = ImportMessageType.Info) - { - Guard.NotEmpty(message, nameof(message)); - - this.Message = message; - this.MessageType = messageType; - } - - public ImportRowInfo AffectedItem - { - get; - set; - } - - public string AffectedField - { - get; - set; - } - - public ImportMessageType MessageType - { - get; - set; - } - - public string Message - { - get; - set; - } - - public string FullMessage - { - get; - set; - } - - public override string ToString() - { - var result = Message.NaIfEmpty(); - - string appendix = null; - - if (AffectedItem != null) - appendix = appendix.Grow("Pos: " + (AffectedItem.Position + 1).ToString(), ", "); - - if (AffectedField.HasValue()) - appendix = appendix.Grow("Field: " + AffectedField, ", "); - - if (appendix.HasValue()) - result = "{0} [{1}]".FormatInvariant(result, appendix); - - return result; - } - } - - public class ImportRowInfo : Tuple - { - public ImportRowInfo(int position, string entityName) - : base(position, entityName) - { - } - - public int Position - { - get { return base.Item1; } - } - - public string EntityName - { - get { return base.Item2; } - } - } - - public enum ImportMessageType - { - Info = 0, - Warning = 5, - Error = 10 - } + + public class ImportMessage + { + public ImportMessage(string message, ImportMessageType messageType = ImportMessageType.Info) + { + Guard.NotEmpty(message, nameof(message)); + + this.Message = message; + this.MessageType = messageType; + } + + public ImportRowInfo AffectedItem + { + get; + set; + } + + public string AffectedField + { + get; + set; + } + + public ImportMessageType MessageType + { + get; + set; + } + + public string Message + { + get; + set; + } + + public string FullMessage + { + get; + set; + } + + public override string ToString() + { + var result = Message.NaIfEmpty(); + + string appendix = null; + + if (AffectedItem != null) + appendix = appendix.Grow("Pos: " + (AffectedItem.Position + 1).ToString(), ", "); + + if (AffectedField.HasValue()) + appendix = appendix.Grow("Field: " + AffectedField, ", "); + + if (appendix.HasValue()) + result = "{0} [{1}]".FormatInvariant(result, appendix); + + return result; + } + } + + public class ImportRowInfo : Tuple + { + public ImportRowInfo(int position, string entityName) + : base(position, entityName) + { + } + + public int Position => base.Item1; + + public string EntityName => base.Item2; + } + + public enum ImportMessageType + { + Info = 0, + Warning = 5, + Error = 10 + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs index f83b4a9281..ab3fb9563b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs @@ -20,40 +20,40 @@ namespace SmartStore.Services.DataExchange.Import { public partial class ImportProfileService : IImportProfileService - { - private static object _lock = new object(); - private static Dictionary> _entityProperties = null; - - private readonly IRepository _importProfileRepository; - private readonly IScheduleTaskService _scheduleTaskService; - private readonly ILocalizationService _localizationService; - private readonly ILanguageService _languageService; - private readonly DataExchangeSettings _dataExchangeSettings; - - public ImportProfileService( - IRepository importProfileRepository, - IScheduleTaskService scheduleTaskService, - ILocalizationService localizationService, - ILanguageService languageService, - DataExchangeSettings dataExchangeSettings) - { - _importProfileRepository = importProfileRepository; - _scheduleTaskService = scheduleTaskService; - _localizationService = localizationService; - _languageService = languageService; - _dataExchangeSettings = dataExchangeSettings; - } - - private string GetLocalizedPropertyName(ImportEntityType type, string property) - { + { + private static object _lock = new object(); + private static Dictionary> _entityProperties = null; + + private readonly IRepository _importProfileRepository; + private readonly IScheduleTaskService _scheduleTaskService; + private readonly ILocalizationService _localizationService; + private readonly ILanguageService _languageService; + private readonly DataExchangeSettings _dataExchangeSettings; + + public ImportProfileService( + IRepository importProfileRepository, + IScheduleTaskService scheduleTaskService, + ILocalizationService localizationService, + ILanguageService languageService, + DataExchangeSettings dataExchangeSettings) + { + _importProfileRepository = importProfileRepository; + _scheduleTaskService = scheduleTaskService; + _localizationService = localizationService; + _languageService = languageService; + _dataExchangeSettings = dataExchangeSettings; + } + + private string GetLocalizedPropertyName(ImportEntityType type, string property) + { // TODO: find a better solution for this. if (property.IsEmpty()) { return string.Empty; } - string key = null; - string prefixKey = null; + string key = null; + string prefixKey = null; if (property.StartsWith("BillingAddress.")) { @@ -64,40 +64,40 @@ private string GetLocalizedPropertyName(ImportEntityType type, string property) prefixKey = "Admin.Orders.Fields.ShippingAddress"; } - #region Get resource key + #region Get resource key - switch (property) - { - case "Id": - key = "Admin.Common.Entity.Fields.Id"; - break; - case "DisplayOrder": + switch (property) + { + case "Id": + key = "Admin.Common.Entity.Fields.Id"; + break; + case "DisplayOrder": case "HomePageDisplayOrder": key = "Common.DisplayOrder"; - break; - case "Deleted": - key = "Admin.Common.Deleted"; - break; + break; + case "Deleted": + key = "Admin.Common.Deleted"; + break; case "WorkingLanguageId": key = "Common.Language"; break; - case "CreatedOnUtc": - case "BillingAddress.CreatedOnUtc": - case "ShippingAddress.CreatedOnUtc": - key = "Common.CreatedOn"; - break; + case "CreatedOnUtc": + case "BillingAddress.CreatedOnUtc": + case "ShippingAddress.CreatedOnUtc": + key = "Common.CreatedOn"; + break; case "UpdatedOnUtc": - key = "Common.UpdatedOn"; - break; - case "HasDiscountsApplied": - key = "Admin.Catalog.Products.Fields.HasDiscountsApplied"; - break; - case "DefaultViewMode": - key = "Admin.Configuration.Settings.Catalog.DefaultViewMode"; - break; - case "StoreId": - key = "Admin.Common.Store"; - break; + key = "Common.UpdatedOn"; + break; + case "HasDiscountsApplied": + key = "Admin.Catalog.Products.Fields.HasDiscountsApplied"; + break; + case "DefaultViewMode": + key = "Admin.Configuration.Settings.Catalog.DefaultViewMode"; + break; + case "StoreId": + key = "Admin.Common.Store"; + break; case "LimitedToStores": key = "Admin.Common.Store.LimitedTo"; break; @@ -105,14 +105,14 @@ private string GetLocalizedPropertyName(ImportEntityType type, string property) key = "Admin.Common.CustomerRole.LimitedTo"; break; case "ParentGroupedProductId": - key = "Admin.Catalog.Products.Fields.AssociatedToProductName"; - break; - case "PasswordFormatId": - key = "Admin.Configuration.Settings.CustomerUser.DefaultPasswordFormat"; - break; - case "LastIpAddress": - key = "Admin.Customers.Customers.Fields.IPAddress"; - break; + key = "Admin.Catalog.Products.Fields.AssociatedToProductName"; + break; + case "PasswordFormatId": + key = "Admin.Configuration.Settings.CustomerUser.DefaultPasswordFormat"; + break; + case "LastIpAddress": + key = "Admin.Customers.Customers.Fields.IPAddress"; + break; case "CustomerNumber": key = "Account.Fields.CustomerNumber"; break; @@ -126,26 +126,26 @@ private string GetLocalizedPropertyName(ImportEntityType type, string property) key = "Admin.Catalog.Products.Fields.Visibility"; break; default: - switch (type) - { - case ImportEntityType.Product: - key = "Admin.Catalog.Products.Fields." + property; - break; - case ImportEntityType.Category: - key = "Admin.Catalog.Categories.Fields." + property; - break; - case ImportEntityType.Customer: - if (property.StartsWith("BillingAddress.") || property.StartsWith("ShippingAddress.")) - key = "Address.Fields." + property.Substring(property.IndexOf('.') + 1); - else - key = "Admin.Customers.Customers.Fields." + property; - break; - case ImportEntityType.NewsLetterSubscription: - key = "Admin.Promotions.NewsLetterSubscriptions.Fields." + property; - break; - } - break; - } + switch (type) + { + case ImportEntityType.Product: + key = "Admin.Catalog.Products.Fields." + property; + break; + case ImportEntityType.Category: + key = "Admin.Catalog.Categories.Fields." + property; + break; + case ImportEntityType.Customer: + if (property.StartsWith("BillingAddress.") || property.StartsWith("ShippingAddress.")) + key = "Address.Fields." + property.Substring(property.IndexOf('.') + 1); + else + key = "Admin.Customers.Customers.Fields." + property; + break; + case ImportEntityType.NewsLetterSubscription: + key = "Admin.Promotions.NewsLetterSubscriptions.Fields." + property; + break; + } + break; + } #endregion @@ -154,10 +154,10 @@ private string GetLocalizedPropertyName(ImportEntityType type, string property) return string.Empty; } - var result = _localizationService.GetResource(key, 0, false, "", true); + var result = _localizationService.GetResource(key, 0, false, "", true); - if (result.IsEmpty()) - { + if (result.IsEmpty()) + { if (key.EndsWith("Id")) { result = _localizationService.GetResource(key.Substring(0, key.Length - 2), 0, false, "", true); @@ -166,232 +166,232 @@ private string GetLocalizedPropertyName(ImportEntityType type, string property) { result = _localizationService.GetResource(key.Substring(0, key.Length - 3), 0, false, "", true); } - } + } - if (result.IsEmpty()) - { - Debug.WriteLine("Missing string resource mapping for {0} - {1}".FormatInvariant(type.ToString(), property)); - result = property.SplitPascalCase(); - } + if (result.IsEmpty()) + { + Debug.WriteLine("Missing string resource mapping for {0} - {1}".FormatInvariant(type.ToString(), property)); + result = property.SplitPascalCase(); + } - if (prefixKey.HasValue()) - { - result = string.Concat(_localizationService.GetResource(prefixKey, 0, false, "", true), " - ", result); - } + if (prefixKey.HasValue()) + { + result = string.Concat(_localizationService.GetResource(prefixKey, 0, false, "", true), " - ", result); + } - return result; - } + return result; + } - public string GetNewProfileName(ImportEntityType entityType) - { - var defaultNames = _localizationService.GetResource("Admin.DataExchange.Import.DefaultProfileNames").SplitSafe(";"); + public string GetNewProfileName(ImportEntityType entityType) + { + var defaultNames = _localizationService.GetResource("Admin.DataExchange.Import.DefaultProfileNames").SplitSafe(";"); - var result = defaultNames.SafeGet((int)entityType); + var result = defaultNames.SafeGet((int)entityType); - if (result.IsEmpty()) - result = entityType.ToString(); + if (result.IsEmpty()) + result = entityType.ToString(); - var profileCount = _importProfileRepository.Table.Count(x => x.EntityTypeId == (int)entityType); + var profileCount = _importProfileRepository.Table.Count(x => x.EntityTypeId == (int)entityType); - result = string.Concat(result, " ", profileCount + 1); + result = string.Concat(result, " ", profileCount + 1); - return result; - } + return result; + } - public virtual ImportProfile InsertImportProfile(string fileName, string name, ImportEntityType entityType) - { - Guard.NotEmpty(fileName, nameof(fileName)); - - if (name.IsEmpty()) - name = GetNewProfileName(entityType); + public virtual ImportProfile InsertImportProfile(string fileName, string name, ImportEntityType entityType) + { + Guard.NotEmpty(fileName, nameof(fileName)); - var task = new ScheduleTask - { - CronExpression = "0 */24 * * *", - Type = typeof(DataImportTask).AssemblyQualifiedNameWithoutVersion(), - Enabled = false, - StopOnError = false, - IsHidden = true - }; - - task.Name = string.Concat(name, " Task"); + if (name.IsEmpty()) + name = GetNewProfileName(entityType); - _scheduleTaskService.InsertTask(task); - - var profile = new ImportProfile - { - Name = name, - EntityType = entityType, - Enabled = true, - SchedulingTaskId = task.Id - }; - - if (Path.GetExtension(fileName).IsCaseInsensitiveEqual(".xlsx")) - profile.FileType = ImportFileType.XLSX; - else - profile.FileType = ImportFileType.CSV; + var task = new ScheduleTask + { + CronExpression = "0 */24 * * *", + Type = typeof(DataImportTask).AssemblyQualifiedNameWithoutVersion(), + Enabled = false, + StopOnError = false, + IsHidden = true + }; - string[] keyFieldNames = null; + task.Name = string.Concat(name, " Task"); - switch (entityType) - { - case ImportEntityType.Product: - keyFieldNames = ProductImporter.DefaultKeyFields; - break; - case ImportEntityType.Category: - keyFieldNames = CategoryImporter.DefaultKeyFields; - break; - case ImportEntityType.Customer: - keyFieldNames = CustomerImporter.DefaultKeyFields; - break; - case ImportEntityType.NewsLetterSubscription: - keyFieldNames = NewsLetterSubscriptionImporter.DefaultKeyFields; - break; - } - - profile.KeyFieldNames = string.Join(",", keyFieldNames); - - profile.FolderName = SeoHelper.GetSeName(name, true, false, false) - .ToValidPath() - .Truncate(_dataExchangeSettings.MaxFileNameLength); - - var path = DataSettings.Current.TenantPath + "/ImportProfiles"; - profile.FolderName = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath(path), profile.FolderName); - - _importProfileRepository.Insert(profile); - - task.Alias = profile.Id.ToString(); - _scheduleTaskService.UpdateTask(task); - - return profile; - } - - public virtual void UpdateImportProfile(ImportProfile profile) - { - if (profile == null) - throw new ArgumentNullException("profile"); - - _importProfileRepository.Update(profile); - } + _scheduleTaskService.InsertTask(task); - public virtual void DeleteImportProfile(ImportProfile profile) - { - if (profile == null) - throw new ArgumentNullException("profile"); + var profile = new ImportProfile + { + Name = name, + EntityType = entityType, + Enabled = true, + SchedulingTaskId = task.Id + }; + + if (Path.GetExtension(fileName).IsCaseInsensitiveEqual(".xlsx")) + profile.FileType = ImportFileType.XLSX; + else + profile.FileType = ImportFileType.CSV; + + string[] keyFieldNames = null; + + switch (entityType) + { + case ImportEntityType.Product: + keyFieldNames = ProductImporter.DefaultKeyFields; + break; + case ImportEntityType.Category: + keyFieldNames = CategoryImporter.DefaultKeyFields; + break; + case ImportEntityType.Customer: + keyFieldNames = CustomerImporter.DefaultKeyFields; + break; + case ImportEntityType.NewsLetterSubscription: + keyFieldNames = NewsLetterSubscriptionImporter.DefaultKeyFields; + break; + } - var scheduleTaskId = profile.SchedulingTaskId; - var folder = profile.GetImportFolder(); + profile.KeyFieldNames = string.Join(",", keyFieldNames); - _importProfileRepository.Delete(profile); - - var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); - _scheduleTaskService.DeleteTask(scheduleTask); - - if (System.IO.Directory.Exists(folder)) - { - FileSystemHelper.ClearDirectory(folder, true); - } - } - - public virtual IQueryable GetImportProfiles(bool? enabled = null) - { - var query = _importProfileRepository.Table - .Expand(x => x.ScheduleTask); - - if (enabled.HasValue) - { - query = query.Where(x => x.Enabled == enabled.Value); - } - - query = query - .OrderBy(x => x.EntityTypeId) - .ThenBy(x => x.Name); - - return query; - } - - public virtual ImportProfile GetImportProfileById(int id) - { - if (id == 0) - return null; - - var profile = _importProfileRepository.Table - .Expand(x => x.ScheduleTask) - .FirstOrDefault(x => x.Id == id); - - return profile; - } - - public virtual ImportProfile GetImportProfileByName(string name) - { - if (name.IsEmpty()) - return null; - - var profile = _importProfileRepository.Table - .Expand(x => x.ScheduleTask) - .FirstOrDefault(x => x.Name == name); - - return profile; - } - - public virtual Dictionary GetImportableEntityProperties(ImportEntityType entityType) - { - if (_entityProperties == null) - { - lock (_lock) - { - if (_entityProperties == null) - { - _entityProperties = new Dictionary>(); + profile.FolderName = SeoHelper.GetSeName(name, true, false, false) + .ToValidPath() + .Truncate(_dataExchangeSettings.MaxFileNameLength); - var context = ((IObjectContextAdapter)_importProfileRepository.Context).ObjectContext; - var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace); + var path = DataSettings.Current.TenantPath + "/ImportProfiles"; + profile.FolderName = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath(path), profile.FolderName); + + _importProfileRepository.Insert(profile); + + task.Alias = profile.Id.ToString(); + _scheduleTaskService.UpdateTask(task); + + return profile; + } + + public virtual void UpdateImportProfile(ImportProfile profile) + { + if (profile == null) + throw new ArgumentNullException("profile"); + + _importProfileRepository.Update(profile); + } + + public virtual void DeleteImportProfile(ImportProfile profile) + { + if (profile == null) + throw new ArgumentNullException("profile"); + + var scheduleTaskId = profile.SchedulingTaskId; + var folder = profile.GetImportFolder(); + + _importProfileRepository.Delete(profile); + + var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); + _scheduleTaskService.DeleteTask(scheduleTask); + + if (System.IO.Directory.Exists(folder)) + { + FileSystemHelper.ClearDirectory(folder, true); + } + } + + public virtual IQueryable GetImportProfiles(bool? enabled = null) + { + var query = _importProfileRepository.Table + .Expand(x => x.ScheduleTask); + + if (enabled.HasValue) + { + query = query.Where(x => x.Enabled == enabled.Value); + } + + query = query + .OrderBy(x => x.EntityTypeId) + .ThenBy(x => x.Name); + + return query; + } + + public virtual ImportProfile GetImportProfileById(int id) + { + if (id == 0) + return null; + + var profile = _importProfileRepository.Table + .Expand(x => x.ScheduleTask) + .FirstOrDefault(x => x.Id == id); + + return profile; + } + + public virtual ImportProfile GetImportProfileByName(string name) + { + if (name.IsEmpty()) + return null; + + var profile = _importProfileRepository.Table + .Expand(x => x.ScheduleTask) + .FirstOrDefault(x => x.Name == name); + + return profile; + } + + public virtual Dictionary GetImportableEntityProperties(ImportEntityType entityType) + { + if (_entityProperties == null) + { + lock (_lock) + { + if (_entityProperties == null) + { + _entityProperties = new Dictionary>(); + + var context = ((IObjectContextAdapter)_importProfileRepository.Context).ObjectContext; + var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace); var allLanguages = _languageService.GetAllLanguages(true); - var allLanguageNames = allLanguages.ToDictionarySafe(x => x.UniqueSeoCode, x => LocalizationHelper.GetLanguageNativeName(x.LanguageCulture) ?? x.Name); - - var localizableProperties = new Dictionary - { - { ImportEntityType.Product, new string[] { "Name", "ShortDescription", "FullDescription", "MetaKeywords", "MetaDescription", "MetaTitle", "SeName" } }, - { ImportEntityType.Category, new string[] { "Name", "FullName", "Description", "BottomDescription", "MetaKeywords", "MetaDescription", "MetaTitle", "SeName" } }, - { ImportEntityType.Customer, new string[] { } }, - { ImportEntityType.NewsLetterSubscription, new string[] { } } - }; - - var addressSet = container.GetEntitySetByName("Addresses", true); - - var addressProperties = addressSet.ElementType.Members - .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) - .Select(x => x.Name) - .ToList(); - - - foreach (ImportEntityType type in Enum.GetValues(typeof(ImportEntityType))) - { - EntitySet entitySet = null; - - try - { - if (type == ImportEntityType.Category) - entitySet = container.GetEntitySetByName("Categories", true); - else - entitySet = container.GetEntitySetByName(type.ToString() + "s", true); - } - catch (Exception) - { - throw new SmartException("There is no entity set for ImportEntityType {0}. Note, the enum value must equal the entity name.".FormatInvariant(type.ToString())); - } - - var dic = entitySet.ElementType.Members - .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) - .Select(x => x.Name) - .ToDictionarySafe(x => x, x => "", StringComparer.OrdinalIgnoreCase); - - // lack of abstractness? - if ((type == ImportEntityType.Product || type == ImportEntityType.Category) && !dic.ContainsKey("SeName")) - { - dic["SeName"] = ""; - } + var allLanguageNames = allLanguages.ToDictionarySafe(x => x.UniqueSeoCode, x => LocalizationHelper.GetLanguageNativeName(x.LanguageCulture) ?? x.Name); + + var localizableProperties = new Dictionary + { + { ImportEntityType.Product, new string[] { "Name", "ShortDescription", "FullDescription", "MetaKeywords", "MetaDescription", "MetaTitle", "SeName" } }, + { ImportEntityType.Category, new string[] { "Name", "FullName", "Description", "BottomDescription", "MetaKeywords", "MetaDescription", "MetaTitle", "SeName" } }, + { ImportEntityType.Customer, new string[] { } }, + { ImportEntityType.NewsLetterSubscription, new string[] { } } + }; + + var addressSet = container.GetEntitySetByName("Addresses", true); + + var addressProperties = addressSet.ElementType.Members + .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + .Select(x => x.Name) + .ToList(); + + + foreach (ImportEntityType type in Enum.GetValues(typeof(ImportEntityType))) + { + EntitySet entitySet = null; + + try + { + if (type == ImportEntityType.Category) + entitySet = container.GetEntitySetByName("Categories", true); + else + entitySet = container.GetEntitySetByName(type.ToString() + "s", true); + } + catch (Exception) + { + throw new SmartException("There is no entity set for ImportEntityType {0}. Note, the enum value must equal the entity name.".FormatInvariant(type.ToString())); + } + + var dic = entitySet.ElementType.Members + .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + .Select(x => x.Name) + .ToDictionarySafe(x => x, x => "", StringComparer.OrdinalIgnoreCase); + + // lack of abstractness? + if ((type == ImportEntityType.Product || type == ImportEntityType.Category) && !dic.ContainsKey("SeName")) + { + dic["SeName"] = ""; + } if (type == ImportEntityType.Product) { @@ -400,38 +400,38 @@ public virtual Dictionary GetImportableEntityProperties(ImportEn // shipping and billing address if (type == ImportEntityType.Customer) - { - foreach (var property in addressProperties) - { + { + foreach (var property in addressProperties) + { dic["BillingAddress." + property] = ""; dic["ShippingAddress." + property] = ""; - } - } + } + } - // add localized property names - foreach (var key in dic.Keys.ToList()) - { - var localizedValue = GetLocalizedPropertyName(type, key); + // add localized property names + foreach (var key in dic.Keys.ToList()) + { + var localizedValue = GetLocalizedPropertyName(type, key); - dic[key] = localizedValue.NaIfEmpty(); + dic[key] = localizedValue.NaIfEmpty(); - if (localizableProperties[type].Contains(key)) - { - foreach (var language in allLanguages) - { + if (localizableProperties[type].Contains(key)) + { + foreach (var language in allLanguages) + { dic["{0}[{1}]".FormatInvariant(key, language.UniqueSeoCode.EmptyNull().ToLower())] = "{0} {1}".FormatInvariant(localizedValue.NaIfEmpty(), allLanguageNames[language.UniqueSeoCode]); - } - } - } - - _entityProperties[type] = dic; - } - } - } - } - - return _entityProperties.ContainsKey(entityType) ? _entityProperties[entityType] : null; - } - } + } + } + } + + _entityProperties[type] = dic; + } + } + } + } + + return _entityProperties.ContainsKey(entityType) ? _entityProperties[entityType] : null; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs index a0de84eae2..8f45413f35 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs @@ -5,177 +5,162 @@ namespace SmartStore.Services.DataExchange.Import { - public class ImportResult : ICloneable - { - public ImportResult() - { - this.Messages = new List(); - Clear(); - } - - public DateTime StartDateUtc - { - get; - set; - } - - public DateTime EndDateUtc - { - get; - set; - } - - public int TotalRecords - { - get; - set; - } - - public int SkippedRecords - { - get; - set; - } - - public int NewRecords - { - get; - set; - } - - public int ModifiedRecords - { - get; - set; - } - - public int AffectedRecords - { - get { return NewRecords + ModifiedRecords; } - } - - public bool Cancelled - { - get; - set; - } - - public void Clear() - { - Messages.Clear(); - StartDateUtc = EndDateUtc = DateTime.UtcNow; - TotalRecords = 0; - SkippedRecords = 0; - NewRecords = 0; - ModifiedRecords = 0; - Cancelled = false; - } - - public ImportMessage AddInfo(string message, ImportRowInfo affectedRow = null, string affectedField = null) - { - return this.AddMessage(message, ImportMessageType.Info, affectedRow, affectedField); - } - - public ImportMessage AddWarning(string message, ImportRowInfo affectedRow = null, string affectedField = null) - { - return this.AddMessage(message, ImportMessageType.Warning, affectedRow, affectedField); - } - - public ImportMessage AddError(string message, ImportRowInfo affectedRow = null, string affectedField = null) - { - return this.AddMessage(message, ImportMessageType.Error, affectedRow, affectedField); - } - - public ImportMessage AddError(Exception exception, int? affectedBatch = null, string stage = null) - { - var prefix = new List(); - if (affectedBatch.HasValue) - { - prefix.Add("Batch: " + affectedBatch.Value); - } - if (stage.HasValue()) - { - prefix.Add("Stage: " + stage); - } - - string msg = string.Empty; - if (prefix.Any()) - { - msg = "[{0}] ".FormatCurrent(String.Join(", ", prefix)); - } - - msg += exception.ToAllMessages(); - - return this.AddMessage(msg, ImportMessageType.Error, fullMessage: exception.StackTrace); - } - - public ImportMessage AddError(Exception exception, string message) - { - return AddMessage( - message ?? exception.ToAllMessages(), - ImportMessageType.Error, - null, - null, - exception.StackTrace); - } - - public ImportMessage AddMessage(string message, ImportMessageType severity, ImportRowInfo affectedRow = null, string affectedField = null, string fullMessage = null) - { - var msg = new ImportMessage(message, severity); - - msg.AffectedItem = affectedRow; - msg.AffectedField = affectedField; - msg.FullMessage = fullMessage; - - this.Messages.Add(msg); - return msg; - } - - public IList Messages - { - get; - private set; - } - - public bool HasWarnings - { - get { return this.Messages.Any(x => x.MessageType == ImportMessageType.Warning); } - } - - public int Warnings - { - get { return Messages.Count(x => x.MessageType == ImportMessageType.Warning); } - } - - public bool HasErrors - { - get { return this.Messages.Any(x => x.MessageType == ImportMessageType.Error); } - } - - public int Errors - { - get { return Messages.Count(x => x.MessageType == ImportMessageType.Error); } - } - - public string LastError - { - get - { - var lastError = Messages.LastOrDefault(x => x.MessageType == ImportMessageType.Error); - if (lastError != null) - return lastError.Message; - - return null; - } - } - - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } - - public SerializableImportResult Clone() - { + public class ImportResult : ICloneable + { + public ImportResult() + { + this.Messages = new List(); + Clear(); + } + + public DateTime StartDateUtc + { + get; + set; + } + + public DateTime EndDateUtc + { + get; + set; + } + + public int TotalRecords + { + get; + set; + } + + public int SkippedRecords + { + get; + set; + } + + public int NewRecords + { + get; + set; + } + + public int ModifiedRecords + { + get; + set; + } + + public int AffectedRecords => NewRecords + ModifiedRecords; + + public bool Cancelled + { + get; + set; + } + + public void Clear() + { + Messages.Clear(); + StartDateUtc = EndDateUtc = DateTime.UtcNow; + TotalRecords = 0; + SkippedRecords = 0; + NewRecords = 0; + ModifiedRecords = 0; + Cancelled = false; + } + + public ImportMessage AddInfo(string message, ImportRowInfo affectedRow = null, string affectedField = null) + { + return this.AddMessage(message, ImportMessageType.Info, affectedRow, affectedField); + } + + public ImportMessage AddWarning(string message, ImportRowInfo affectedRow = null, string affectedField = null) + { + return this.AddMessage(message, ImportMessageType.Warning, affectedRow, affectedField); + } + + public ImportMessage AddError(string message, ImportRowInfo affectedRow = null, string affectedField = null) + { + return this.AddMessage(message, ImportMessageType.Error, affectedRow, affectedField); + } + + public ImportMessage AddError(Exception exception, int? affectedBatch = null, string stage = null) + { + var prefix = new List(); + if (affectedBatch.HasValue) + { + prefix.Add("Batch: " + affectedBatch.Value); + } + if (stage.HasValue()) + { + prefix.Add("Stage: " + stage); + } + + string msg = string.Empty; + if (prefix.Any()) + { + msg = "[{0}] ".FormatCurrent(String.Join(", ", prefix)); + } + + msg += exception.ToAllMessages(); + + return this.AddMessage(msg, ImportMessageType.Error, fullMessage: exception.StackTrace); + } + + public ImportMessage AddError(Exception exception, string message) + { + return AddMessage( + message ?? exception.ToAllMessages(), + ImportMessageType.Error, + null, + null, + exception.StackTrace); + } + + public ImportMessage AddMessage(string message, ImportMessageType severity, ImportRowInfo affectedRow = null, string affectedField = null, string fullMessage = null) + { + var msg = new ImportMessage(message, severity); + + msg.AffectedItem = affectedRow; + msg.AffectedField = affectedField; + msg.FullMessage = fullMessage; + + this.Messages.Add(msg); + return msg; + } + + public IList Messages + { + get; + private set; + } + + public bool HasWarnings => this.Messages.Any(x => x.MessageType == ImportMessageType.Warning); + + public int Warnings => Messages.Count(x => x.MessageType == ImportMessageType.Warning); + + public bool HasErrors => this.Messages.Any(x => x.MessageType == ImportMessageType.Error); + + public int Errors => Messages.Count(x => x.MessageType == ImportMessageType.Error); + + public string LastError + { + get + { + var lastError = Messages.LastOrDefault(x => x.MessageType == ImportMessageType.Error); + if (lastError != null) + return lastError.Message; + + return null; + } + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + + public SerializableImportResult Clone() + { var result = new SerializableImportResult(); result.StartDateUtc = StartDateUtc; result.EndDateUtc = EndDateUtc; @@ -191,22 +176,22 @@ public SerializableImportResult Clone() return result; } - } - - - [Serializable] - public partial class SerializableImportResult - { - public DateTime StartDateUtc { get; set; } - public DateTime EndDateUtc { get; set; } - public int TotalRecords { get; set; } - public int SkippedRecords { get; set; } - public int NewRecords { get; set; } - public int ModifiedRecords { get; set; } - public int AffectedRecords { get; set; } - public bool Cancelled { get; set; } - public int Warnings { get; set; } - public int Errors { get; set; } - public string LastError { get; set; } - } + } + + + [Serializable] + public partial class SerializableImportResult + { + public DateTime StartDateUtc { get; set; } + public DateTime EndDateUtc { get; set; } + public int TotalRecords { get; set; } + public int SkippedRecords { get; set; } + public int NewRecords { get; set; } + public int ModifiedRecords { get; set; } + public int AffectedRecords { get; set; } + public bool Cancelled { get; set; } + public int Warnings { get; set; } + public int Errors { get; set; } + public string LastError { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs index fc84dba90a..35cbf0b5bd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs @@ -7,327 +7,302 @@ namespace SmartStore.Services.DataExchange.Import { - public class ImportRow where T : BaseEntity - { - private const string ExplicitNull = "[NULL]"; - private const string ExplicitIgnore = "[IGNORE]"; - - private bool _initialized = false; - private T _entity; - private string _entityDisplayName; - private readonly int _position; - private bool _isNew; - private bool _isDirty; - private ImportRowInfo _rowInfo; - - private readonly ImportDataSegmenter _segmenter; - private readonly IDataRow _row; - - public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) - { - _segmenter = parent; - _row = row; - _position = position; - } - - public void Initialize(T entity, string entityDisplayName) - { - _entity = entity; - _entityDisplayName = entityDisplayName; - _isNew = _entity.Id == 0; - - _initialized = true; - } - - private void CheckInitialized() - { - if (_initialized) - { - throw Error.InvalidOperation("A row must be initialized before interacting with the entity or the data store"); - } - } - - private TProp GetDefaultValue(ColumnMappingItem mapping, TProp defaultValue, ImportResult result = null) - { - if (mapping != null && mapping.Default.HasValue()) - { - try - { - return mapping.Default.Convert(_segmenter.Culture); - } - catch (Exception exception) - { - if (result != null) - { - var msg = "Failed to convert default value '{0}'. Please specify a convertable default value. Column: {1}"; - result.AddWarning(msg.FormatInvariant(mapping.Default, exception.Message), this.GetRowInfo(), mapping.SoureName); - } - } - } - - return defaultValue; - } - - public bool IsTransient - { - get { return _entity.Id == 0; } - } - - public bool IsNew - { - get { return _isNew; } - } - - public bool IsDirty - { - get { return _isDirty; } - } - - public ImportDataSegmenter Segmenter - { - get { return _segmenter; } - } - - public T Entity - { - get { return _entity; } - } - - public IDataRow DataRow - { - get { return _row; } - } - - public string EntityDisplayName - { - get { return _entityDisplayName; } - } - - public bool NameChanged - { - get; - set; - } - - public int Position - { - get { return _position; } - } - - /// - /// Determines whether a specific column exists in the underlying data table - /// and contains a non-null, convertible value. - /// - /// The name of the column - /// - /// If true and a column with the passed does not exist, - /// this method seeks for any indexed column with the same name. - /// - /// true if the column exists and contains a value, false otherwise - /// - /// This method takes mapped column names into account. - /// - public bool HasDataValue(string columnName, bool withAnyIndex = false) - { - var result = HasDataValue(columnName, null); - - if (!result && withAnyIndex) - { - // Column does not have a value, but withAnyIndex is true: - // Test for values in any indexed column. - var indexes = _segmenter.GetColumnIndexes(columnName); - foreach (var idx in indexes) - { - result = HasDataValue(columnName, idx); - if (result) - break; - } - } - - return result; - } - - /// - /// Determines whether the column name[index] exists in the underlying data table - /// and contains a non-null, convertible value. - /// - /// The name of the column - /// The index of the column - /// true if the column exists and contains a value, false otherwise - /// - /// This method takes mapped column names into account. - /// - public bool HasDataValue(string columnName, string index) - { - var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); - return (_row.TryGetValue(mapping.MappedName, out var value) && value != null && value != DBNull.Value); - } + public class ImportRow where T : BaseEntity + { + private const string ExplicitNull = "[NULL]"; + private const string ExplicitIgnore = "[IGNORE]"; + + private bool _initialized = false; + private T _entity; + private string _entityDisplayName; + private readonly int _position; + private bool _isNew; + private bool _isDirty; + private ImportRowInfo _rowInfo; + + private readonly ImportDataSegmenter _segmenter; + private readonly IDataRow _row; + + public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) + { + _segmenter = parent; + _row = row; + _position = position; + } + + public void Initialize(T entity, string entityDisplayName) + { + _entity = entity; + _entityDisplayName = entityDisplayName; + _isNew = _entity.Id == 0; + + _initialized = true; + } + + private void CheckInitialized() + { + if (_initialized) + { + throw Error.InvalidOperation("A row must be initialized before interacting with the entity or the data store"); + } + } + + private TProp GetDefaultValue(ColumnMappingItem mapping, TProp defaultValue, ImportResult result = null) + { + if (mapping != null && mapping.Default.HasValue()) + { + try + { + return mapping.Default.Convert(_segmenter.Culture); + } + catch (Exception exception) + { + if (result != null) + { + var msg = "Failed to convert default value '{0}'. Please specify a convertable default value. Column: {1}"; + result.AddWarning(msg.FormatInvariant(mapping.Default, exception.Message), this.GetRowInfo(), mapping.SoureName); + } + } + } + + return defaultValue; + } + + public bool IsTransient => _entity.Id == 0; + + public bool IsNew => _isNew; + + public bool IsDirty => _isDirty; + + public ImportDataSegmenter Segmenter => _segmenter; + + public T Entity => _entity; + + public IDataRow DataRow => _row; + + public string EntityDisplayName => _entityDisplayName; + + public bool NameChanged + { + get; + set; + } + + public int Position => _position; + + /// + /// Determines whether a specific column exists in the underlying data table + /// and contains a non-null, convertible value. + /// + /// The name of the column + /// + /// If true and a column with the passed does not exist, + /// this method seeks for any indexed column with the same name. + /// + /// true if the column exists and contains a value, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasDataValue(string columnName, bool withAnyIndex = false) + { + var result = HasDataValue(columnName, null); + + if (!result && withAnyIndex) + { + // Column does not have a value, but withAnyIndex is true: + // Test for values in any indexed column. + var indexes = _segmenter.GetColumnIndexes(columnName); + foreach (var idx in indexes) + { + result = HasDataValue(columnName, idx); + if (result) + break; + } + } + + return result; + } + + /// + /// Determines whether the column name[index] exists in the underlying data table + /// and contains a non-null, convertible value. + /// + /// The name of the column + /// The index of the column + /// true if the column exists and contains a value, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasDataValue(string columnName, string index) + { + var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); + return (_row.TryGetValue(mapping.MappedName, out var value) && value != null && value != DBNull.Value); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TProp GetDataValue(string columnName, bool force = false) - { - TryGetDataValue(columnName, null, out var value, force); - return value; - } + { + TryGetDataValue(columnName, null, out var value, force); + return value; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TProp GetDataValue(string columnName, string index, bool force = false) - { - TryGetDataValue(columnName, index, out var value, force); - return value; - } + { + TryGetDataValue(columnName, index, out var value, force); + return value; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetDataValue(string columnName, out TProp value, bool force = false) - { - return TryGetDataValue(columnName, null, out value, force); - } - - public bool TryGetDataValue(string columnName, string index, out TProp value, bool force = false) - { - var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); - - if (!force && mapping.IgnoreProperty) - { - value = default(TProp); - return false; - } - - object rawValue; - if (_row.TryGetValue(mapping.MappedName, out rawValue) && rawValue != null && rawValue != DBNull.Value && !rawValue.ToString().IsCaseInsensitiveEqual(ExplicitIgnore)) - { - value = rawValue.ToString().IsCaseInsensitiveEqual(ExplicitNull) - ? default(TProp) - : rawValue.Convert(_segmenter.Culture); - return true; - } - - if (IsNew) - { - // only transient/new entities should fallback to possible defaults. - value = GetDefaultValue(mapping, default(TProp)); - return true; - } - - value = default(TProp); - return false; - } + { + return TryGetDataValue(columnName, null, out value, force); + } + + public bool TryGetDataValue(string columnName, string index, out TProp value, bool force = false) + { + var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); + + if (!force && mapping.IgnoreProperty) + { + value = default(TProp); + return false; + } + + if (_row.TryGetValue(mapping.MappedName, out var rawValue) && rawValue != null && rawValue != DBNull.Value && !rawValue.ToString().IsCaseInsensitiveEqual(ExplicitIgnore)) + { + value = rawValue.ToString().IsCaseInsensitiveEqual(ExplicitNull) + ? default(TProp) + : rawValue.Convert(_segmenter.Culture); + return true; + } + + if (IsNew) + { + // only transient/new entities should fallback to possible defaults. + value = GetDefaultValue(mapping, default(TProp)); + return true; + } + + value = default(TProp); + return false; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool SetProperty( - ImportResult result, - Expression> prop, - TProp defaultValue = default(TProp), - Func converter = null) - { - return SetProperty( - result, - null, // columnName - prop, - defaultValue, - converter); - } - - public bool SetProperty( - ImportResult result, - string columnName, - Expression> prop, - TProp defaultValue = default(TProp), - Func converter = null) - { - // TBD: (MC) do not check or validate for perf reason? - //CheckInitialized(); - - var isPropertySet = false; - var pi = prop.ExtractPropertyInfo(); - var propName = pi.Name; - var target = _entity; - - columnName = columnName ?? propName; - - try - { - object value; - var mapping = _segmenter.ColumnMap.GetMapping(columnName); - - if (mapping.IgnoreProperty) - { - // explicitly ignore this property - } - else if (_row.TryGetValue(mapping.MappedName, out value) && value != null && value != DBNull.Value && !value.ToString().IsCaseInsensitiveEqual(ExplicitIgnore)) - { - // source contains field value. Set it. - TProp converted; - if (converter != null) - { - converted = converter(value, _segmenter.Culture); - } - else if (value.ToString().IsCaseInsensitiveEqual(ExplicitNull)) - { - // prop is "explicitly" set to null. Don't fallback to any default! - converted = default(TProp); - } - else - { - converted = value.Convert(_segmenter.Culture); - } - - var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); - fastProp.SetValue(target, converted); - isPropertySet = true; - } - else - { - // source field value does not exist or is null/empty - if (IsNew) - { - // if entity is new and source field value is null, determine default value in this particular order: - // 2.) Default value in field mapping table - // 3.) passed default value argument - defaultValue = GetDefaultValue(mapping, defaultValue, result); - - // source does not contain field data or is empty... - if (defaultValue != null) - { - // ...but the entity is new. In this case set the default value if given. - var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); - fastProp.SetValue(target, defaultValue); - isPropertySet = true; - } - } - } - } - catch (Exception exception) - { - result.AddWarning("Conversion failed: " + exception.Message, this.GetRowInfo(), propName); - } - - if (isPropertySet && !_isDirty) - { - _isDirty = true; - } - - return isPropertySet; - } - - public ImportRowInfo GetRowInfo() - { - if (_rowInfo == null) - { - _rowInfo = new ImportRowInfo(this.Position, this.EntityDisplayName); - } - - return _rowInfo; - } - - public override string ToString() - { - var str = "Pos: {0} - Name: {1}, IsNew: {2}, IsTransient: {3}".FormatCurrent( - Position, - EntityDisplayName.EmptyNull(), - _initialized ? IsNew.ToString() : "-", - _initialized ? IsTransient.ToString() : "-"); - return str; - } - } + ImportResult result, + Expression> prop, + TProp defaultValue = default(TProp), + Func converter = null) + { + return SetProperty( + result, + null, // columnName + prop, + defaultValue, + converter); + } + + public bool SetProperty( + ImportResult result, + string columnName, + Expression> prop, + TProp defaultValue = default(TProp), + Func converter = null) + { + // TBD: (MC) do not check or validate for perf reason? + //CheckInitialized(); + + var isPropertySet = false; + var pi = prop.ExtractPropertyInfo(); + var propName = pi.Name; + var target = _entity; + + columnName = columnName ?? propName; + + try + { + object value; + var mapping = _segmenter.ColumnMap.GetMapping(columnName); + + if (mapping.IgnoreProperty) + { + // explicitly ignore this property + } + else if (_row.TryGetValue(mapping.MappedName, out value) && value != null && value != DBNull.Value && !value.ToString().IsCaseInsensitiveEqual(ExplicitIgnore)) + { + // source contains field value. Set it. + TProp converted; + if (converter != null) + { + converted = converter(value, _segmenter.Culture); + } + else if (value.ToString().IsCaseInsensitiveEqual(ExplicitNull)) + { + // prop is "explicitly" set to null. Don't fallback to any default! + converted = default(TProp); + } + else + { + converted = value.Convert(_segmenter.Culture); + } + + var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); + fastProp.SetValue(target, converted); + isPropertySet = true; + } + else + { + // source field value does not exist or is null/empty + if (IsNew) + { + // if entity is new and source field value is null, determine default value in this particular order: + // 2.) Default value in field mapping table + // 3.) passed default value argument + defaultValue = GetDefaultValue(mapping, defaultValue, result); + + // source does not contain field data or is empty... + if (defaultValue != null) + { + // ...but the entity is new. In this case set the default value if given. + var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); + fastProp.SetValue(target, defaultValue); + isPropertySet = true; + } + } + } + } + catch (Exception exception) + { + result.AddWarning("Conversion failed: " + exception.Message, this.GetRowInfo(), propName); + } + + if (isPropertySet && !_isDirty) + { + _isDirty = true; + } + + return isPropertySet; + } + + public ImportRowInfo GetRowInfo() + { + if (_rowInfo == null) + { + _rowInfo = new ImportRowInfo(this.Position, this.EntityDisplayName); + } + + return _rowInfo; + } + + public override string ToString() + { + var str = "Pos: {0} - Name: {1}, IsNew: {2}, IsTransient: {3}".FormatCurrent( + Position, + EntityDisplayName.EmptyNull(), + _initialized ? IsNew.ToString() : "-", + _initialized ? IsTransient.ToString() : "-"); + return str; + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs index f9f36594cc..3bb371dc5b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs @@ -4,17 +4,17 @@ namespace SmartStore.Services.DataExchange.Import.Internal { - internal class DataImporterContext - { - public DataImporterContext( - DataImportRequest request, - CancellationToken cancellationToken, - string progressInfo) - { - Request = request; - CancellationToken = cancellationToken; + internal class DataImporterContext + { + public DataImporterContext( + DataImportRequest request, + CancellationToken cancellationToken, + string progressInfo) + { + Request = request; + CancellationToken = cancellationToken; - ExecuteContext = new ImportExecuteContext(CancellationToken, Request.ProgressValueSetter, progressInfo) + ExecuteContext = new ImportExecuteContext(CancellationToken, Request.ProgressValueSetter, progressInfo) { Request = request }; @@ -23,12 +23,12 @@ public DataImporterContext( Results = new Dictionary(); } - public DataImportRequest Request { get; private set; } - public CancellationToken CancellationToken { get; private set; } + public DataImportRequest Request { get; private set; } + public CancellationToken CancellationToken { get; private set; } - public TraceLogger Log { get; set; } - public ImportExecuteContext ExecuteContext { get; set; } - public IEntityImporter Importer { get; set; } + public TraceLogger Log { get; set; } + public ImportExecuteContext ExecuteContext { get; set; } + public IEntityImporter Importer { get; set; } public ColumnMap ColumnMap { get; private set; } public Dictionary Results { get; private set; } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs index 8b455a6955..bd3add028f 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs @@ -8,137 +8,137 @@ namespace SmartStore.Services.DataExchange { - public partial class SyncMappingService : ISyncMappingService - { - private readonly IRepository _syncMappingsRepository; - - public SyncMappingService(IRepository syncMappingsRepository) - { - this._syncMappingsRepository = syncMappingsRepository; - } - - public void InsertSyncMapping(SyncMapping mapping) - { - Guard.NotNull(mapping, nameof(mapping)); - - _syncMappingsRepository.Insert(mapping); - } - - public void InsertSyncMappings(IEnumerable mappings) - { - Guard.NotNull(mappings, nameof(mappings)); - - _syncMappingsRepository.InsertRange(mappings); - } - - public IList GetAllSyncMappings(string contextName = null, string entityName = null, int[] entityIds = null) - { - var query = _syncMappingsRepository.Table; - - if (entityName.HasValue()) - { - query = query.Where(x => x.EntityName == entityName); - } - - if (contextName.HasValue()) - { - query = query.Where(x => x.ContextName == contextName); - } - - if (entityIds != null && entityIds.Any()) - { - query = query.Where(x => entityIds.Contains(x.EntityId)); - } - - return query.ToList(); - } - - public SyncMapping GetSyncMappingByEntity(int entityId, string entityName, string contextName) - { - Guard.IsPositive(entityId, nameof(entityId)); - Guard.NotEmpty(entityName, nameof(entityName)); - Guard.NotEmpty(contextName, nameof(contextName)); - - var query = from x in _syncMappingsRepository.Table - where - x.EntityId == entityId - && x.EntityName == entityName - && x.ContextName == contextName - select x; - - return query.FirstOrDefault(); - } - - public SyncMapping GetSyncMappingBySource(string sourceKey, string entityName, string contextName) - { - Guard.NotEmpty(sourceKey, nameof(sourceKey)); - Guard.NotEmpty(entityName, nameof(entityName)); - Guard.NotEmpty(contextName, nameof(contextName)); - - var query = from x in _syncMappingsRepository.Table - where - x.SourceKey == sourceKey - && x.EntityName == entityName - && x.ContextName == contextName - select x; - - return query.FirstOrDefault(); - } - - public void DeleteSyncMapping(SyncMapping mapping) - { - Guard.NotNull(mapping, nameof(mapping)); - - _syncMappingsRepository.Delete(mapping); - } - - public void DeleteSyncMappings(IEnumerable mappings) - { - Guard.NotNull(mappings, nameof(mappings)); - - _syncMappingsRepository.DeleteRange(mappings); - } - - public void DeleteSyncMappingsFor(T entity) where T : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - - if (entity is SyncMapping) - { - throw Error.InvalidOperation("Cannot delete a sync mapping record for a SyncMapping entity"); - } - - if (entity.IsTransientRecord()) - { - return; - } - - _syncMappingsRepository.DeleteAll(x => x.EntityId == entity.Id && x.EntityName == entity.GetEntityName()); - } - - public void DeleteSyncMappings(string contextName, string entityName = null) - { - Guard.NotEmpty(contextName, nameof(contextName)); - - if (entityName.HasValue()) - { - _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName && x.EntityName == entityName); - } - else - { - _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName); - } - } - - - public void UpdateSyncMapping(SyncMapping mapping) - { - Guard.NotNull(mapping, nameof(mapping)); - - mapping.SyncedOnUtc = DateTime.UtcNow; - _syncMappingsRepository.Update(mapping); - } - - } + public partial class SyncMappingService : ISyncMappingService + { + private readonly IRepository _syncMappingsRepository; + + public SyncMappingService(IRepository syncMappingsRepository) + { + this._syncMappingsRepository = syncMappingsRepository; + } + + public void InsertSyncMapping(SyncMapping mapping) + { + Guard.NotNull(mapping, nameof(mapping)); + + _syncMappingsRepository.Insert(mapping); + } + + public void InsertSyncMappings(IEnumerable mappings) + { + Guard.NotNull(mappings, nameof(mappings)); + + _syncMappingsRepository.InsertRange(mappings); + } + + public IList GetAllSyncMappings(string contextName = null, string entityName = null, int[] entityIds = null) + { + var query = _syncMappingsRepository.Table; + + if (entityName.HasValue()) + { + query = query.Where(x => x.EntityName == entityName); + } + + if (contextName.HasValue()) + { + query = query.Where(x => x.ContextName == contextName); + } + + if (entityIds != null && entityIds.Any()) + { + query = query.Where(x => entityIds.Contains(x.EntityId)); + } + + return query.ToList(); + } + + public SyncMapping GetSyncMappingByEntity(int entityId, string entityName, string contextName) + { + Guard.IsPositive(entityId, nameof(entityId)); + Guard.NotEmpty(entityName, nameof(entityName)); + Guard.NotEmpty(contextName, nameof(contextName)); + + var query = from x in _syncMappingsRepository.Table + where + x.EntityId == entityId + && x.EntityName == entityName + && x.ContextName == contextName + select x; + + return query.FirstOrDefault(); + } + + public SyncMapping GetSyncMappingBySource(string sourceKey, string entityName, string contextName) + { + Guard.NotEmpty(sourceKey, nameof(sourceKey)); + Guard.NotEmpty(entityName, nameof(entityName)); + Guard.NotEmpty(contextName, nameof(contextName)); + + var query = from x in _syncMappingsRepository.Table + where + x.SourceKey == sourceKey + && x.EntityName == entityName + && x.ContextName == contextName + select x; + + return query.FirstOrDefault(); + } + + public void DeleteSyncMapping(SyncMapping mapping) + { + Guard.NotNull(mapping, nameof(mapping)); + + _syncMappingsRepository.Delete(mapping); + } + + public void DeleteSyncMappings(IEnumerable mappings) + { + Guard.NotNull(mappings, nameof(mappings)); + + _syncMappingsRepository.DeleteRange(mappings); + } + + public void DeleteSyncMappingsFor(T entity) where T : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + + if (entity is SyncMapping) + { + throw Error.InvalidOperation("Cannot delete a sync mapping record for a SyncMapping entity"); + } + + if (entity.IsTransientRecord()) + { + return; + } + + _syncMappingsRepository.DeleteAll(x => x.EntityId == entity.Id && x.EntityName == entity.GetEntityName()); + } + + public void DeleteSyncMappings(string contextName, string entityName = null) + { + Guard.NotEmpty(contextName, nameof(contextName)); + + if (entityName.HasValue()) + { + _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName && x.EntityName == entityName); + } + else + { + _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName); + } + } + + + public void UpdateSyncMapping(SyncMapping mapping) + { + Guard.NotNull(mapping, nameof(mapping)); + + mapping.SyncedOnUtc = DateTime.UtcNow; + _syncMappingsRepository.Update(mapping); + } + + } } diff --git a/src/Libraries/SmartStore.Services/Directory/CountryService.cs b/src/Libraries/SmartStore.Services/Directory/CountryService.cs index 13f6b21646..6e05e0e600 100644 --- a/src/Libraries/SmartStore.Services/Directory/CountryService.cs +++ b/src/Libraries/SmartStore.Services/Directory/CountryService.cs @@ -14,28 +14,28 @@ public partial class CountryService : ICountryService private const string COUNTRIES_SHIPPING_KEY = "SmartStore.country.shipping-{0}"; private const string COUNTRIES_PATTERN_KEY = "SmartStore.country.*"; - private readonly ICommonServices _services; - private readonly IRepository _countryRepository; - private readonly IRepository _storeMappingRepository; + private readonly ICommonServices _services; + private readonly IRepository _countryRepository; + private readonly IRepository _storeMappingRepository; - public CountryService( - ICommonServices services, + public CountryService( + ICommonServices services, IRepository countryRepository, - IRepository storeMappingRepository) + IRepository storeMappingRepository) { _countryRepository = countryRepository; - _services = services; - _storeMappingRepository = storeMappingRepository; + _services = services; + _storeMappingRepository = storeMappingRepository; } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } public virtual void DeleteCountry(Country country) { - Guard.NotNull(country, nameof(country)); + Guard.NotNull(country, nameof(country)); + + _countryRepository.Delete(country); - _countryRepository.Delete(country); - _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); } @@ -44,30 +44,30 @@ public virtual IList GetAllCountries(bool showHidden = false) string key = string.Format(COUNTRIES_ALL_KEY, showHidden); return _services.RequestCache.Get(key, () => { - var query = _countryRepository.Table; + var query = _countryRepository.Table; - if (!showHidden) - query = query.Where(c => c.Published); + if (!showHidden) + query = query.Where(c => c.Published); - query = query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Name); + query = query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Name); - if (!showHidden && !QuerySettings.IgnoreMultiStore) - { - var currentStoreId = _services.StoreContext.CurrentStore.Id; - query = from c in query - join sc in _storeMappingRepository.Table - on new { c1 = c.Id, c2 = "Country" } equals new { c1 = sc.EntityId, c2 = sc.EntityName } into c_sm - from sc in c_sm.DefaultIfEmpty() - where !c.LimitedToStores || currentStoreId == sc.StoreId - select c; + if (!showHidden && !QuerySettings.IgnoreMultiStore) + { + var currentStoreId = _services.StoreContext.CurrentStore.Id; + query = from c in query + join sc in _storeMappingRepository.Table + on new { c1 = c.Id, c2 = "Country" } equals new { c1 = sc.EntityId, c2 = sc.EntityName } into c_sm + from sc in c_sm.DefaultIfEmpty() + where !c.LimitedToStores || currentStoreId == sc.StoreId + select c; - query = from c in query - group c by c.Id into cGroup - orderby cGroup.Key - select cGroup.FirstOrDefault(); + query = from c in query + group c by c.Id into cGroup + orderby cGroup.Key + select cGroup.FirstOrDefault(); - query = query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Name); - } + query = query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Name); + } var countries = query.ToList(); return countries; @@ -79,9 +79,9 @@ public virtual IList GetAllCountriesForBilling(bool showHidden = false) string key = string.Format(COUNTRIES_BILLING_KEY, showHidden); return _services.RequestCache.Get(key, () => { - var allCountries = GetAllCountries(showHidden); + var allCountries = GetAllCountries(showHidden); - var countries = allCountries.Where(x => x.AllowsBilling).ToList(); + var countries = allCountries.Where(x => x.AllowsBilling).ToList(); return countries; }); } @@ -90,8 +90,8 @@ public virtual IList GetAllCountriesForShipping(bool showHidden = false { string key = string.Format(COUNTRIES_SHIPPING_KEY, showHidden); return _services.RequestCache.Get(key, () => - { - var allCountries = GetAllCountries(showHidden); + { + var allCountries = GetAllCountries(showHidden); var countries = allCountries.Where(x => x.AllowsShipping).ToList(); return countries; @@ -106,22 +106,22 @@ public virtual Country GetCountryById(int countryId) return _countryRepository.GetById(countryId); } - public virtual Country GetCountryByTwoOrThreeLetterIsoCode(string letterIsoCode) - { - if (letterIsoCode.HasValue()) - { - if (letterIsoCode.Length == 2) - return GetCountryByTwoLetterIsoCode(letterIsoCode); - else if (letterIsoCode.Length == 3) - return GetCountryByThreeLetterIsoCode(letterIsoCode); - } - return null; - } + public virtual Country GetCountryByTwoOrThreeLetterIsoCode(string letterIsoCode) + { + if (letterIsoCode.HasValue()) + { + if (letterIsoCode.Length == 2) + return GetCountryByTwoLetterIsoCode(letterIsoCode); + else if (letterIsoCode.Length == 3) + return GetCountryByThreeLetterIsoCode(letterIsoCode); + } + return null; + } public virtual Country GetCountryByTwoLetterIsoCode(string twoLetterIsoCode) { - if (twoLetterIsoCode.IsEmpty()) - return null; + if (twoLetterIsoCode.IsEmpty()) + return null; var query = from c in _countryRepository.Table where c.TwoLetterIsoCode == twoLetterIsoCode @@ -133,8 +133,8 @@ public virtual Country GetCountryByTwoLetterIsoCode(string twoLetterIsoCode) public virtual Country GetCountryByThreeLetterIsoCode(string threeLetterIsoCode) { - if (threeLetterIsoCode.IsEmpty()) - return null; + if (threeLetterIsoCode.IsEmpty()) + return null; var query = from c in _countryRepository.Table where c.ThreeLetterIsoCode == threeLetterIsoCode @@ -146,20 +146,20 @@ public virtual Country GetCountryByThreeLetterIsoCode(string threeLetterIsoCode) public virtual void InsertCountry(Country country) { - Guard.NotNull(country, nameof(country)); + Guard.NotNull(country, nameof(country)); - _countryRepository.Insert(country); + _countryRepository.Insert(country); - _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); + _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); } public virtual void UpdateCountry(Country country) { - Guard.NotNull(country, nameof(country)); + Guard.NotNull(country, nameof(country)); _countryRepository.Update(country); - _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); + _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); } - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/CurrencyExtensions.cs b/src/Libraries/SmartStore.Services/Directory/CurrencyExtensions.cs index 03a9dfc7ee..a347e8f800 100644 --- a/src/Libraries/SmartStore.Services/Directory/CurrencyExtensions.cs +++ b/src/Libraries/SmartStore.Services/Directory/CurrencyExtensions.cs @@ -5,23 +5,23 @@ namespace SmartStore.Services.Directory { - public static class CurrencyExtensions - { - public static bool HasDomainEnding(this Currency currency, string domain) - { + public static class CurrencyExtensions + { + public static bool HasDomainEnding(this Currency currency, string domain) + { if (currency == null || domain.IsEmpty() || currency.DomainEndings.IsEmpty()) return false; - var endings = currency.DomainEndings.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); + var endings = currency.DomainEndings.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); return endings.Any(x => domain.EndsWith(x, StringComparison.InvariantCultureIgnoreCase)); - } + } - public static Currency GetByDomainEnding(this IEnumerable currencies, string domain) - { + public static Currency GetByDomainEnding(this IEnumerable currencies, string domain) + { if (currencies == null || domain.IsEmpty()) return null; return currencies.FirstOrDefault(x => x.Published && x.HasDomainEnding(domain)); - } - } + } + } } diff --git a/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs b/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs index d417801b19..09243d5246 100644 --- a/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs +++ b/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs @@ -15,45 +15,45 @@ namespace SmartStore.Services.Directory public partial class CurrencyService : ICurrencyService { private readonly IRepository _currencyRepository; - private readonly IStoreMappingService _storeMappingService; + private readonly IStoreMappingService _storeMappingService; private readonly CurrencySettings _currencySettings; private readonly IPluginFinder _pluginFinder; private readonly IEventPublisher _eventPublisher; - private readonly IProviderManager _providerManager; - private readonly IStoreContext _storeContext; + private readonly IProviderManager _providerManager; + private readonly IStoreContext _storeContext; public CurrencyService( IRepository currencyRepository, - IStoreMappingService storeMappingService, + IStoreMappingService storeMappingService, CurrencySettings currencySettings, IPluginFinder pluginFinder, IEventPublisher eventPublisher, - IProviderManager providerManager, - IStoreContext storeContext) + IProviderManager providerManager, + IStoreContext storeContext) { _currencyRepository = currencyRepository; - _storeMappingService = storeMappingService; + _storeMappingService = storeMappingService; _currencySettings = currencySettings; _pluginFinder = pluginFinder; _eventPublisher = eventPublisher; - _providerManager = providerManager; - _storeContext = storeContext; + _providerManager = providerManager; + _storeContext = storeContext; } public virtual IList GetCurrencyLiveRates(string exchangeRateCurrencyCode) { var exchangeRateProvider = LoadActiveExchangeRateProvider(); - if (exchangeRateProvider != null) - { - return exchangeRateProvider.Value.GetCurrencyLiveRates(exchangeRateCurrencyCode); - } - return new List(); + if (exchangeRateProvider != null) + { + return exchangeRateProvider.Value.GetCurrencyLiveRates(exchangeRateCurrencyCode); + } + return new List(); } public virtual void DeleteCurrency(Currency currency) { - Guard.NotNull(currency, nameof(currency)); - + Guard.NotNull(currency, nameof(currency)); + _currencyRepository.Delete(currency); } @@ -62,49 +62,49 @@ public virtual Currency GetCurrencyById(int currencyId) if (currencyId == 0) return null; - return _currencyRepository.GetByIdCached(currencyId, "db.cur.id-" + currencyId); - } + return _currencyRepository.GetByIdCached(currencyId, "db.cur.id-" + currencyId); + } public virtual Currency GetCurrencyByCode(string currencyCode) { - Guard.NotNull(currencyCode, nameof(currencyCode)); + Guard.NotNull(currencyCode, nameof(currencyCode)); - return GetAllCurrencies(true).FirstOrDefault(c => c.CurrencyCode.ToLower() == currencyCode.ToLower()); + return GetAllCurrencies(true).FirstOrDefault(c => c.CurrencyCode.ToLower() == currencyCode.ToLower()); } - public virtual IList GetAllCurrencies(bool showHidden = false, int storeId = 0) + public virtual IList GetAllCurrencies(bool showHidden = false, int storeId = 0) { - var query = _currencyRepository.Table; + var query = _currencyRepository.Table; - if (!showHidden) - query = query.Where(c => c.Published); + if (!showHidden) + query = query.Where(c => c.Published); - query = query.OrderBy(c => c.DisplayOrder); + query = query.OrderBy(c => c.DisplayOrder); - var currencies = query.ToListCached(); + var currencies = query.ToListCached(); - // store mapping - if (storeId > 0) - { - currencies = currencies - .Where(c => _storeMappingService.Authorize(c, storeId)) - .ToList(); - } - return currencies; + // store mapping + if (storeId > 0) + { + currencies = currencies + .Where(c => _storeMappingService.Authorize(c, storeId)) + .ToList(); + } + return currencies; } public virtual void InsertCurrency(Currency currency) { - Guard.NotNull(currency, nameof(currency)); + Guard.NotNull(currency, nameof(currency)); - _currencyRepository.Insert(currency); + _currencyRepository.Insert(currency); } public virtual void UpdateCurrency(Currency currency) { - Guard.NotNull(currency, nameof(currency)); + Guard.NotNull(currency, nameof(currency)); - _currencyRepository.Update(currency); + _currencyRepository.Update(currency); } public virtual decimal ConvertCurrency(decimal amount, decimal exchangeRate) @@ -115,7 +115,7 @@ public virtual decimal ConvertCurrency(decimal amount, decimal exchangeRate) return decimal.Zero; } - public virtual decimal ConvertCurrency(decimal amount, Currency sourceCurrency, Currency targetCurrency, Store store = null) + public virtual decimal ConvertCurrency(decimal amount, Currency sourceCurrency, Currency targetCurrency, Store store = null) { decimal result = amount; if (sourceCurrency.Id == targetCurrency.Id) @@ -130,10 +130,10 @@ public virtual decimal ConvertCurrency(decimal amount, Currency sourceCurrency, return result; } - public virtual decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Currency sourceCurrency, Store store = null) + public virtual decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Currency sourceCurrency, Store store = null) { decimal result = amount; - var primaryExchangeRateCurrency = (store == null ? _storeContext.CurrentStore.PrimaryExchangeRateCurrency : store.PrimaryExchangeRateCurrency); + var primaryExchangeRateCurrency = (store == null ? _storeContext.CurrentStore.PrimaryExchangeRateCurrency : store.PrimaryExchangeRateCurrency); if (result != decimal.Zero && sourceCurrency.Id != primaryExchangeRateCurrency.Id) { @@ -145,7 +145,7 @@ public virtual decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Curr return result; } - public virtual decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Currency targetCurrency, Store store = null) + public virtual decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Currency targetCurrency, Store store = null) { decimal result = amount; var primaryExchangeRateCurrency = (store == null ? _storeContext.CurrentStore.PrimaryExchangeRateCurrency : store.PrimaryExchangeRateCurrency); @@ -163,7 +163,7 @@ public virtual decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Cu public virtual decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency sourceCurrency, Store store = null) { decimal result = amount; - var primaryStoreCurrency = (store == null ? _storeContext.CurrentStore.PrimaryStoreCurrency : store.PrimaryStoreCurrency); + var primaryStoreCurrency = (store == null ? _storeContext.CurrentStore.PrimaryStoreCurrency : store.PrimaryStoreCurrency); if (result != decimal.Zero && sourceCurrency.Id != primaryStoreCurrency.Id) { @@ -175,27 +175,27 @@ public virtual decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency so return result; } - public virtual decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrency, Store store = null) + public virtual decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrency, Store store = null) { decimal result = amount; - var primaryStoreCurrency = (store == null ? _storeContext.CurrentStore.PrimaryStoreCurrency : store.PrimaryStoreCurrency); + var primaryStoreCurrency = (store == null ? _storeContext.CurrentStore.PrimaryStoreCurrency : store.PrimaryStoreCurrency); result = ConvertCurrency(amount, primaryStoreCurrency, targetCurrency, store); return result; } public virtual Provider LoadActiveExchangeRateProvider() { - return LoadExchangeRateProviderBySystemName(_currencySettings.ActiveExchangeRateProviderSystemName) ?? LoadAllExchangeRateProviders().FirstOrDefault(); + return LoadExchangeRateProviderBySystemName(_currencySettings.ActiveExchangeRateProviderSystemName) ?? LoadAllExchangeRateProviders().FirstOrDefault(); } public virtual Provider LoadExchangeRateProviderBySystemName(string systemName) { - return _providerManager.GetProvider(systemName); + return _providerManager.GetProvider(systemName); } public virtual IEnumerable> LoadAllExchangeRateProviders() { - return _providerManager.GetAllProviders(); + return _providerManager.GetAllProviders(); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs b/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs index c9f813d50c..284303d652 100644 --- a/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs +++ b/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs @@ -1,35 +1,50 @@ +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Threading; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Localization; using SmartStore.Data.Caching; using SmartStore.Data.Utilities; using SmartStore.Services.Catalog; +using SmartStore.Services.Helpers; namespace SmartStore.Services.Directory { public partial class DeliveryTimeService : IDeliveryTimeService { + // Two letter ISO code to shortest month-day format pattern. + private readonly static ConcurrentDictionary _monthDayFormats = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly IRepository _deliveryTimeRepository; private readonly IRepository _productRepository; private readonly IRepository _attributeCombinationRepository; - private readonly CatalogSettings _catalogSettings; + private readonly IDateTimeHelper _dateTimeHelper; + private readonly CatalogSettings _catalogSettings; + private readonly ShippingSettings _shippingSettings; public DeliveryTimeService( IRepository deliveryTimeRepository, IRepository productRepository, IRepository attributeCombinationRepository, - CatalogSettings catalogSettings) + IDateTimeHelper dateTimeHelper, + CatalogSettings catalogSettings, + ShippingSettings shippingSettings) { _deliveryTimeRepository = deliveryTimeRepository; _productRepository = productRepository; _attributeCombinationRepository = attributeCombinationRepository; - _catalogSettings = catalogSettings; - } + _dateTimeHelper = dateTimeHelper; + _catalogSettings = catalogSettings; + _shippingSettings = shippingSettings; + } - public Localizer T { get; set; } = NullLocalizer.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; public virtual void DeleteDeliveryTime(DeliveryTime deliveryTime) { @@ -84,10 +99,10 @@ public virtual bool IsAssociated(int deliveryTimeId) if (deliveryTimeId == 0) return false; - var query = - from p in _productRepository.Table - where p.DeliveryTimeId == deliveryTimeId || p.ProductVariantAttributeCombinations.Any(c => c.DeliveryTimeId == deliveryTimeId) - select p.Id; + var query = + from p in _productRepository.Table + where p.DeliveryTimeId == deliveryTimeId || p.ProductVariantAttributeCombinations.Any(c => c.DeliveryTimeId == deliveryTimeId) + select p.Id; return query.Count() > 0; } @@ -105,22 +120,22 @@ public virtual DeliveryTime GetDeliveryTimeById(int deliveryTimeId) return null; } } - - return _deliveryTimeRepository.GetByIdCached(deliveryTimeId, "deliverytime-{0}".FormatInvariant(deliveryTimeId)); + + return _deliveryTimeRepository.GetByIdCached(deliveryTimeId, "deliverytime-{0}".FormatInvariant(deliveryTimeId)); } public virtual DeliveryTime GetDeliveryTime(Product product) - { + { var deliveryTimeId = product.GetDeliveryTimeIdAccordingToStock(_catalogSettings); return GetDeliveryTimeById(deliveryTimeId ?? 0); - } + } public virtual IList GetAllDeliveryTimes() { - var query = _deliveryTimeRepository.Table.OrderBy(c => c.DisplayOrder); - var deliveryTimes = query.ToListCached("db.delivtimes.all"); - return deliveryTimes; - } + var query = _deliveryTimeRepository.Table.OrderBy(c => c.DisplayOrder); + var deliveryTimes = query.ToListCached("db.delivtimes.all"); + return deliveryTimes; + } public virtual void InsertDeliveryTime(DeliveryTime deliveryTime) { @@ -142,16 +157,173 @@ public virtual void SetToDefault(DeliveryTime deliveryTime) var deliveryTimes = GetAllDeliveryTimes(); - foreach(var time in deliveryTimes) + foreach (var time in deliveryTimes) { time.IsDefault = time.Equals(deliveryTime) ? true : false; _deliveryTimeRepository.Update(time); } } - + public virtual DeliveryTime GetDefaultDeliveryTime() { return _deliveryTimeRepository.Table.Where(x => x.IsDefault == true).FirstOrDefault(); } + + public virtual (DateTime? minDate, DateTime? maxDate) GetDeliveryDate(DeliveryTime deliveryTime) + { + var currentDate = TimeZoneInfo.ConvertTime(DateTime.UtcNow, _dateTimeHelper.DefaultStoreTimeZone); + return GetDeliveryDate(deliveryTime, currentDate); + } + + public virtual (DateTime? minDate, DateTime? maxDate) GetDeliveryDate(DeliveryTime deliveryTime, DateTime fromDate) + { + var minDate = deliveryTime?.MinDays != null + ? AddDays(fromDate, deliveryTime.MinDays.Value) + : (DateTime?)null; + + var maxDate = deliveryTime?.MaxDays != null + ? AddDays(fromDate, deliveryTime.MaxDays.Value) + : (DateTime?)null; + + return (minDate, maxDate); + } + + public virtual string GetFormattedDeliveryDate(DeliveryTime deliveryTime, DateTime? fromDate = null, CultureInfo culture = null) + { + if (deliveryTime == null || (!deliveryTime.MinDays.HasValue && !deliveryTime.MaxDays.HasValue)) + { + return null; + } + + if (culture == null) + { + culture = Thread.CurrentThread.CurrentUICulture; + } + + var currentDate = fromDate ?? TimeZoneInfo.ConvertTime(DateTime.UtcNow, _dateTimeHelper.DefaultStoreTimeZone); + var (min, max) = GetDeliveryDate(deliveryTime, currentDate); + + if (min.HasValue) + { + min = _dateTimeHelper.ConvertToUserTime(min.Value); + } + if (max.HasValue) + { + max = _dateTimeHelper.ConvertToUserTime(max.Value); + } + + // Convention: always separate weekday with comma and format month in shortest form. + if (min.HasValue && max.HasValue) + { + if (min == max) + { + return T("DeliveryTimes.Dates.DeliveryOn", Format(min.Value, "dddd, ", false, "DeliveryTimes.Dates.OnTomorrow")); + } + else if (min < max) + { + return T("DeliveryTimes.Dates.Between", + Format(min.Value, "ddd, ", min.Value.Month == max.Value.Month && min.Value.Year == max.Value.Year, null), + Format(max.Value, "ddd, ", false, null)); + } + } + else if (min.HasValue) + { + return T("DeliveryTimes.Dates.NotBefore", Format(min.Value, "dddd, ")); + } + else if (max.HasValue) + { + return T("DeliveryTimes.Dates.Until", Format(max.Value, "dddd, ")); + } + + return null; + + string Format(DateTime date, string patternPrefix, bool noMonth = false, string tomorrowKey = "DeliveryTimes.Dates.Tomorrow") + { + // Offer some way to skip our formatting and to force a custom formatting. + if (!string.IsNullOrEmpty(_shippingSettings.DeliveryTimesDateFormat)) + { + return date.ToString(_shippingSettings.DeliveryTimesDateFormat, culture) ?? "-"; + } + + if (tomorrowKey != null && (date - currentDate).TotalDays == 1) + { + return T(tomorrowKey); + } + + string patternSuffix = null; + + if (noMonth) + { + // MonthDayPattern can contain non-interpreted text like "de", "mh" or even "'d'" (e.g. 21 de septiembre). + patternSuffix = _monthDayFormats.GetOrAdd(culture.TwoLetterISOLanguageName + "-nomonth", _ => + { + var mdp = culture.DateTimeFormat.MonthDayPattern; + return mdp.Contains("d. ") || mdp.Contains("dd. ") ? "d." : "d"; + }); + } + else + { + patternSuffix = _monthDayFormats.GetOrAdd(culture.TwoLetterISOLanguageName, _ => + { + return culture.DateTimeFormat.MonthDayPattern.Replace("MMMM", "MMM").TrimSafe(); + }); + } + + return date.ToString(patternPrefix + patternSuffix, culture) ?? "-"; + } + } + + #region Utilities + + /// + /// + protected virtual DateTime AddDays(DateTime date, int days) + { + Guard.NotNegative(days, nameof(days)); + + // now.Hour: 0-23. TodayDeliveryHour: 1-24. + if (_shippingSettings.TodayShipmentHour.HasValue && date.Hour < _shippingSettings.TodayShipmentHour) + { + if ((date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday) || !_shippingSettings.DeliveryOnWorkweekDaysOnly) + { + days -= 1; + } + } + + // Normalization. Do not support today delivery. + if (days < 1) + { + days = 1; + } + + if (!_shippingSettings.DeliveryOnWorkweekDaysOnly) + { + return date.AddDays(days); + } + + // Add days for non workweek days. + if (date.DayOfWeek == DayOfWeek.Saturday) + { + date = date.AddDays(2); + days -= 1; + } + else if (date.DayOfWeek == DayOfWeek.Sunday) + { + date = date.AddDays(1); + days -= 1; + } + + date = date.AddDays(days / 5 * 7); + int extraDays = days % 5; + + if ((int)date.DayOfWeek + extraDays > 5) + { + extraDays += 2; + } + + return date.AddDays(extraDays); + } + + #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/EcbExchangeRateProvider.cs b/src/Libraries/SmartStore.Services/Directory/EcbExchangeRateProvider.cs index 2e941fadff..6fab09e930 100644 --- a/src/Libraries/SmartStore.Services/Directory/EcbExchangeRateProvider.cs +++ b/src/Libraries/SmartStore.Services/Directory/EcbExchangeRateProvider.cs @@ -9,27 +9,27 @@ namespace SmartStore.Services.Directory { - [SystemName("CurrencyExchange.ECB")] - [FriendlyName("ECB currency exchange rate provider")] - [DisplayOrder(0)] - public class EcbExchangeRateProvider : IExchangeRateProvider + [SystemName("CurrencyExchange.ECB")] + [FriendlyName("ECB currency exchange rate provider")] + [DisplayOrder(0)] + public class EcbExchangeRateProvider : IExchangeRateProvider { - /// - /// Gets live currency exchange rates - /// - /// Exchange rate currency code - /// Currency exchange rates - public IList GetCurrencyLiveRates(string exchangeRateCurrencyCode) + /// + /// Gets live currency exchange rates + /// + /// Exchange rate currency code + /// Currency exchange rates + public IList GetCurrencyLiveRates(string exchangeRateCurrencyCode) { - if (exchangeRateCurrencyCode.IsEmpty()) - return new List(); + if (exchangeRateCurrencyCode.IsEmpty()) + return new List(); - var result = new Dictionary(); - var targetCurrency = exchangeRateCurrencyCode.ToUpper(); - var updateDate = DateTime.UtcNow; + var result = new Dictionary(); + var targetCurrency = exchangeRateCurrencyCode.ToUpper(); + var updateDate = DateTime.UtcNow; - // old url still works too: http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml - var request = WebRequest.Create("http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml") as HttpWebRequest; + // old url still works too: http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml + var request = WebRequest.Create("http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml") as HttpWebRequest; using (var response = request.GetResponse()) { @@ -41,72 +41,72 @@ public IList GetCurrencyLiveRates(string exchangeRateCurrencyCode) nsmgr.AddNamespace("gesmes", "http://www.gesmes.org/xml/2002-08-01"); var node = document.SelectSingleNode("gesmes:Envelope/ns:Cube/ns:Cube", nsmgr); - var ecbDate = DateTime.ParseExact(node.Attributes["time"].Value, "yyyy-MM-dd", null); + var ecbDate = DateTime.ParseExact(node.Attributes["time"].Value, "yyyy-MM-dd", null); - // see https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html - updateDate = new DateTime(ecbDate.Year, ecbDate.Month, ecbDate.Day, 16, 0, 0); + // see https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html + updateDate = new DateTime(ecbDate.Year, ecbDate.Month, ecbDate.Day, 16, 0, 0); - var provider = new NumberFormatInfo(); + var provider = new NumberFormatInfo(); provider.NumberDecimalSeparator = "."; provider.NumberGroupSeparator = ""; foreach (XmlNode childNode in node.ChildNodes) { - // parse ECB rate - var rate = new ExchangeRate - { - CurrencyCode = childNode.Attributes["currency"].Value.EmptyNull().ToUpper(), - Rate = decimal.Parse(childNode.Attributes["rate"].Value, provider), - UpdatedOn = updateDate - }; - - // add it to result dictionary - if (!result.ContainsKey(rate.CurrencyCode) && rate.Rate != decimal.Zero) - { - result.Add(rate.CurrencyCode, rate); - } + // parse ECB rate + var rate = new ExchangeRate + { + CurrencyCode = childNode.Attributes["currency"].Value.EmptyNull().ToUpper(), + Rate = decimal.Parse(childNode.Attributes["rate"].Value, provider), + UpdatedOn = updateDate + }; + + // add it to result dictionary + if (!result.ContainsKey(rate.CurrencyCode) && rate.Rate != decimal.Zero) + { + result.Add(rate.CurrencyCode, rate); + } } } - // if target currency is EUR then we are ready because ECB always returns EUR rates - if (targetCurrency != "EUR") - { - // cross calculation not possible if ECB list does not contain target currency - if (!result.ContainsKey(targetCurrency)) - return new List(); - - // get clone of EUR rates - var euroRates = result.ToDictionary(x => x.Key, x => x.Value.Rate); - - // cross calculate rates for target currency - result.Each(x => - { - if (x.Value.CurrencyCode == "EUR") - { - x.Value.Rate = 1.0M / euroRates[targetCurrency]; - } - else - { - x.Value.Rate = euroRates[x.Value.CurrencyCode] / euroRates[targetCurrency]; - } - }); - - // remove target currency form result because its rate is always 1.0 - result.Remove(targetCurrency); - - // add EUR rate - if (!result.ContainsKey("EUR")) - { - result.Add("EUR", new ExchangeRate - { - CurrencyCode = "EUR", - Rate = 1.0M / euroRates[targetCurrency], - UpdatedOn = updateDate - }); - } - } - - return result.Select(x => x.Value).ToList(); + // if target currency is EUR then we are ready because ECB always returns EUR rates + if (targetCurrency != "EUR") + { + // cross calculation not possible if ECB list does not contain target currency + if (!result.ContainsKey(targetCurrency)) + return new List(); + + // get clone of EUR rates + var euroRates = result.ToDictionary(x => x.Key, x => x.Value.Rate); + + // cross calculate rates for target currency + result.Each(x => + { + if (x.Value.CurrencyCode == "EUR") + { + x.Value.Rate = 1.0M / euroRates[targetCurrency]; + } + else + { + x.Value.Rate = euroRates[x.Value.CurrencyCode] / euroRates[targetCurrency]; + } + }); + + // remove target currency form result because its rate is always 1.0 + result.Remove(targetCurrency); + + // add EUR rate + if (!result.ContainsKey("EUR")) + { + result.Add("EUR", new ExchangeRate + { + CurrencyCode = "EUR", + Rate = 1.0M / euroRates[targetCurrency], + UpdatedOn = updateDate + }); + } + } + + return result.Select(x => x.Value).ToList(); } } } diff --git a/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs b/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs index 71ec189d47..c2dfaeac4e 100644 --- a/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs +++ b/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using MaxMind.Db; using MaxMind.GeoIP2; using SmartStore.Core; using SmartStore.Core.Caching; @@ -11,11 +12,13 @@ namespace SmartStore.Services.Directory public partial class GeoCountryLookup : DisposableObject, IGeoCountryLookup { private readonly DatabaseReader _reader; - private readonly object _lock = new object(); public GeoCountryLookup() { - _reader = new DatabaseReader(CommonHelper.MapPath("~/App_Data/GeoLite2/GeoLite2-Country.mmdb")); + // Old database not usable anymore due to US (Florida?) legal issues from 01 Jan 2020 on. + //_reader = new DatabaseReader(CommonHelper.MapPath("~/App_Data/GeoLite2/GeoLite2-Country.mmdb")); + + _reader = new DatabaseReader(CommonHelper.MapPath("~/App_Data/Geo/dbip-country-lite.mmdb")); } public LookupCountryResponse LookupCountry(string addr) diff --git a/src/Libraries/SmartStore.Services/Directory/ICountryService.cs b/src/Libraries/SmartStore.Services/Directory/ICountryService.cs index 001d2d55fd..fb62e956d6 100644 --- a/src/Libraries/SmartStore.Services/Directory/ICountryService.cs +++ b/src/Libraries/SmartStore.Services/Directory/ICountryService.cs @@ -43,12 +43,12 @@ public partial interface ICountryService /// Country Country GetCountryById(int countryId); - /// - /// Gets a country by two or three letter ISO code - /// - /// Country two or three letter ISO code - /// Country - Country GetCountryByTwoOrThreeLetterIsoCode(string letterIsoCode); + /// + /// Gets a country by two or three letter ISO code + /// + /// Country two or three letter ISO code + /// Country + Country GetCountryByTwoOrThreeLetterIsoCode(string letterIsoCode); /// /// Gets a country by two letter ISO code @@ -75,5 +75,5 @@ public partial interface ICountryService /// /// Country void UpdateCountry(Country country); - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs b/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs index 14be063172..68c2bbbf3e 100644 --- a/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs +++ b/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs @@ -112,14 +112,14 @@ public partial interface ICurrencyService /// Store to get the primary store currency from /// Converted value decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrency, Store store = null); - - + + /// /// Load active exchange rate provider /// /// Active exchange rate provider - Provider LoadActiveExchangeRateProvider(); + Provider LoadActiveExchangeRateProvider(); /// /// Load exchange rate provider by system name diff --git a/src/Libraries/SmartStore.Services/Directory/IDeliveryTimeService.cs b/src/Libraries/SmartStore.Services/Directory/IDeliveryTimeService.cs index 36663e2e6c..1a5e4ef5c3 100644 --- a/src/Libraries/SmartStore.Services/Directory/IDeliveryTimeService.cs +++ b/src/Libraries/SmartStore.Services/Directory/IDeliveryTimeService.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; +using System.Globalization; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Directory; +using SmartStore.Services.Helpers; namespace SmartStore.Services.Directory { @@ -14,7 +17,7 @@ public partial interface IDeliveryTimeService /// at least one dependant entity /// bool IsAssociated(int deliveryTimeId); - + /// /// Deletes delivery time /// @@ -28,12 +31,12 @@ public partial interface IDeliveryTimeService /// DeliveryTime DeliveryTime GetDeliveryTimeById(int deliveryTimeId); - /// - /// Gets the delivery time for a product - /// - /// The product - /// Delivery time - DeliveryTime GetDeliveryTime(Product product); + /// + /// Gets the delivery time for a product + /// + /// The product + /// Delivery time + DeliveryTime GetDeliveryTime(Product product); /// /// Gets all delivery times @@ -65,5 +68,30 @@ public partial interface IDeliveryTimeService /// The product /// Delivery time DeliveryTime GetDefaultDeliveryTime(); + + /// + /// Calculates the delivery date based on current shop date. + /// + /// Delivery time. + /// Calculated minimum and maximum date. + (DateTime? minDate, DateTime? maxDate) GetDeliveryDate(DeliveryTime deliveryTime); + + /// + /// Calculates the delivery date. + /// + /// Delivery time. + /// The date from which the delivery date should be calculated. + /// Calculated minimum and maximum date. + (DateTime? minDate, DateTime? maxDate) GetDeliveryDate(DeliveryTime deliveryTime, DateTime fromDate); + + /// + /// Gets the formatted delivery date. + /// + /// Delivery time. + /// The date from which the delivery date should be calculated. + /// null to use store's local time . + /// Culture to use for formatting. null to use UI culture of current thread. + /// Formatted delivery date. + string GetFormattedDeliveryDate(DeliveryTime deliveryTime, DateTime? fromDate = null, CultureInfo culture = null); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/IMeasureService.cs b/src/Libraries/SmartStore.Services/Directory/IMeasureService.cs index 5c57d3e851..1a56905e7b 100644 --- a/src/Libraries/SmartStore.Services/Directory/IMeasureService.cs +++ b/src/Libraries/SmartStore.Services/Directory/IMeasureService.cs @@ -74,7 +74,7 @@ decimal ConvertToPrimaryMeasureDimension(decimal quantity, /// Converted value decimal ConvertFromPrimaryMeasureDimension(decimal quantity, MeasureDimension targetMeasureDimension); - + /// /// Deletes measure weight diff --git a/src/Libraries/SmartStore.Services/Directory/IQuantityUnitService.cs b/src/Libraries/SmartStore.Services/Directory/IQuantityUnitService.cs index b198394f2e..49c41a7119 100644 --- a/src/Libraries/SmartStore.Services/Directory/IQuantityUnitService.cs +++ b/src/Libraries/SmartStore.Services/Directory/IQuantityUnitService.cs @@ -14,7 +14,7 @@ public partial interface IQuantityUnitService /// at least one dependant entity /// bool IsAssociated(int quantityUnitId); - + /// /// Deletes measure unit /// @@ -28,10 +28,10 @@ public partial interface IQuantityUnitService /// QuantityUnit QuantityUnit GetQuantityUnitById(int? quantityUnitId); - /// + /// /// Gets the measure unit for a product - /// - /// The product + /// + /// The product /// measure unit QuantityUnit GetQuantityUnit(Product product); diff --git a/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs index e62ff53527..3c62eaa12c 100644 --- a/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs @@ -15,19 +15,19 @@ public partial interface IStateProvinceService /// The state/province void DeleteStateProvince(StateProvince stateProvince); - /// - /// Get all states/provinces - /// - /// A value indicating whether to show hidden records - /// - IQueryable GetAllStateProvinces(bool showHidden = false); + /// + /// Get all states/provinces + /// + /// A value indicating whether to show hidden records + /// + IQueryable GetAllStateProvinces(bool showHidden = false); - /// - /// Gets a state/province - /// - /// The state/province identifier - /// State/province - StateProvince GetStateProvinceById(int stateProvinceId); + /// + /// Gets a state/province + /// + /// The state/province identifier + /// State/province + StateProvince GetStateProvinceById(int stateProvinceId); /// /// Gets a state/province @@ -35,7 +35,7 @@ public partial interface IStateProvinceService /// The state/province abbreviation /// State/province StateProvince GetStateProvinceByAbbreviation(string abbreviation); - + /// /// Gets a state/province collection by country identifier /// diff --git a/src/Libraries/SmartStore.Services/Directory/MeasureService.cs b/src/Libraries/SmartStore.Services/Directory/MeasureService.cs index 1ff51e67a9..3d81a13d16 100644 --- a/src/Libraries/SmartStore.Services/Directory/MeasureService.cs +++ b/src/Libraries/SmartStore.Services/Directory/MeasureService.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Events; @@ -15,18 +13,15 @@ public partial class MeasureService : IMeasureService private readonly IRepository _measureDimensionRepository; private readonly IRepository _measureWeightRepository; private readonly MeasureSettings _measureSettings; - private readonly IEventPublisher _eventPublisher; public MeasureService( IRepository measureDimensionRepository, IRepository measureWeightRepository, - MeasureSettings measureSettings, - IEventPublisher eventPublisher) + MeasureSettings measureSettings) { _measureDimensionRepository = measureDimensionRepository; _measureWeightRepository = measureWeightRepository; _measureSettings = measureSettings; - _eventPublisher = eventPublisher; } #region Dimensions @@ -38,14 +33,14 @@ public virtual void DeleteMeasureDimension(MeasureDimension measureDimension) _measureDimensionRepository.Delete(measureDimension); } - + public virtual MeasureDimension GetMeasureDimensionById(int measureDimensionId) { if (measureDimensionId == 0) return null; - return _measureDimensionRepository.GetByIdCached(measureDimensionId, "db.dimension.id-" + measureDimensionId); - } + return _measureDimensionRepository.GetByIdCached(measureDimensionId, "db.dimension.id-" + measureDimensionId); + } public virtual MeasureDimension GetMeasureDimensionBySystemKeyword(string systemKeyword) { @@ -61,12 +56,12 @@ public virtual MeasureDimension GetMeasureDimensionBySystemKeyword(string system public virtual IList GetAllMeasureDimensions() { - var query = from md in _measureDimensionRepository.Table - orderby md.DisplayOrder - select md; - var measureDimensions = query.ToListCached("db.dimension.all"); - return measureDimensions; - } + var query = from md in _measureDimensionRepository.Table + orderby md.DisplayOrder + select md; + var measureDimensions = query.ToListCached("db.dimension.all"); + return measureDimensions; + } public virtual void InsertMeasureDimension(MeasureDimension measure) { @@ -84,8 +79,7 @@ public virtual void UpdateMeasureDimension(MeasureDimension measure) _measureDimensionRepository.Update(measure); } - public virtual decimal ConvertDimension(decimal quantity, - MeasureDimension sourceMeasureDimension, MeasureDimension targetMeasureDimension, bool round = true) + public virtual decimal ConvertDimension(decimal quantity, MeasureDimension sourceMeasureDimension, MeasureDimension targetMeasureDimension, bool round = true) { decimal result = quantity; if (result != decimal.Zero && sourceMeasureDimension.Id != targetMeasureDimension.Id) @@ -98,8 +92,7 @@ public virtual decimal ConvertDimension(decimal quantity, return result; } - public virtual decimal ConvertToPrimaryMeasureDimension(decimal quantity, - MeasureDimension sourceMeasureDimension) + public virtual decimal ConvertToPrimaryMeasureDimension(decimal quantity, MeasureDimension sourceMeasureDimension) { decimal result = quantity; var baseDimensionIn = GetMeasureDimensionById(_measureSettings.BaseDimensionId); @@ -113,8 +106,7 @@ public virtual decimal ConvertToPrimaryMeasureDimension(decimal quantity, return result; } - public virtual decimal ConvertFromPrimaryMeasureDimension(decimal quantity, - MeasureDimension targetMeasureDimension) + public virtual decimal ConvertFromPrimaryMeasureDimension(decimal quantity, MeasureDimension targetMeasureDimension) { decimal result = quantity; var baseDimensionIn = GetMeasureDimensionById(_measureSettings.BaseDimensionId); @@ -145,8 +137,8 @@ public virtual MeasureWeight GetMeasureWeightById(int measureWeightId) if (measureWeightId == 0) return null; - return _measureWeightRepository.GetByIdCached(measureWeightId, "db.weight.id-" + measureWeightId); - } + return _measureWeightRepository.GetByIdCached(measureWeightId, "db.weight.id-" + measureWeightId); + } public virtual MeasureWeight GetMeasureWeightBySystemKeyword(string systemKeyword) { @@ -162,13 +154,13 @@ public virtual MeasureWeight GetMeasureWeightBySystemKeyword(string systemKeywor public virtual IList GetAllMeasureWeights() { - var query = from mw in _measureWeightRepository.Table - orderby mw.DisplayOrder - select mw; + var query = from mw in _measureWeightRepository.Table + orderby mw.DisplayOrder + select mw; - var measureWeights = query.ToListCached("db.weight.all"); - return measureWeights; - } + var measureWeights = query.ToListCached("db.weight.all"); + return measureWeights; + } public virtual void InsertMeasureWeight(MeasureWeight measure) { diff --git a/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs b/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs index 1048718152..a3bb181539 100644 --- a/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs +++ b/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs @@ -15,20 +15,20 @@ public partial class QuantityUnitService : IQuantityUnitService private readonly IRepository _quantityUnitRepository; private readonly IRepository _productRepository; private readonly IEventPublisher _eventPublisher; - private readonly CatalogSettings _catalogSettings; - + private readonly CatalogSettings _catalogSettings; + public QuantityUnitService( IRepository quantityUnitRepository, IRepository productRepository, IRepository attributeCombinationRepository, ICustomerService customerService, IEventPublisher eventPublisher, - CatalogSettings catalogSettings) + CatalogSettings catalogSettings) { this._quantityUnitRepository = quantityUnitRepository; this._eventPublisher = eventPublisher; this._productRepository = productRepository; - this._catalogSettings = catalogSettings; + this._catalogSettings = catalogSettings; } public virtual void DeleteQuantityUnit(QuantityUnit quantityUnit) @@ -47,19 +47,19 @@ public virtual bool IsAssociated(int quantityUnitId) if (quantityUnitId == 0) return false; - var query = - from p in _productRepository.Table + var query = + from p in _productRepository.Table where p.QuantityUnitId == quantityUnitId || p.ProductVariantAttributeCombinations.Any(c => c.QuantityUnitId == quantityUnitId) - select p.Id; + select p.Id; return query.Count() > 0; } public virtual QuantityUnit GetQuantityUnitById(int? quantityUnitId) { - if (quantityUnitId == null || quantityUnitId == 0) + if (quantityUnitId == null || quantityUnitId == 0) { - if(_catalogSettings.ShowDefaultQuantityUnit) + if (_catalogSettings.ShowDefaultQuantityUnit) { return GetAllQuantityUnits().Where(x => x.IsDefault == true).FirstOrDefault(); } @@ -68,25 +68,25 @@ public virtual QuantityUnit GetQuantityUnitById(int? quantityUnitId) return null; } } - + return _quantityUnitRepository.GetById(quantityUnitId); } public virtual QuantityUnit GetQuantityUnit(Product product) - { - if (product == null) - return null; + { + if (product == null) + return null; return GetQuantityUnitById(product.QuantityUnitId ?? 0); - } + } public virtual IList GetAllQuantityUnits() { - var query = _quantityUnitRepository.Table.OrderBy(c => c.DisplayOrder); + var query = _quantityUnitRepository.Table.OrderBy(c => c.DisplayOrder); - var quantityUnits = query.ToListCached("db.qtyunit.all"); - return quantityUnits; - } + var quantityUnits = query.ToListCached("db.qtyunit.all"); + return quantityUnits; + } public virtual void InsertQuantityUnit(QuantityUnit quantityUnit) { @@ -101,7 +101,8 @@ public virtual void UpdateQuantityUnit(QuantityUnit quantityUnit) if (quantityUnit == null) throw new ArgumentNullException("quantityUnit"); - if (quantityUnit.IsDefault == true) { + if (quantityUnit.IsDefault == true) + { var temp = new List(); temp.Add(quantityUnit); @@ -109,8 +110,8 @@ public virtual void UpdateQuantityUnit(QuantityUnit quantityUnit) var query = GetAllQuantityUnits() .Where(x => x.IsDefault == true) .Except(temp); - - foreach(var qu in query) + + foreach (var qu in query) { qu.IsDefault = false; _quantityUnitRepository.Update(qu); diff --git a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs index 925bfdc04f..14e99909ed 100644 --- a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs @@ -8,7 +8,7 @@ namespace SmartStore.Services.Directory { - public partial class StateProvinceService : IStateProvinceService + public partial class StateProvinceService : IStateProvinceService { private readonly IRepository _stateProvinceRepository; private readonly IEventPublisher _eventPublisher; @@ -25,21 +25,21 @@ public virtual void DeleteStateProvince(StateProvince stateProvince) { if (stateProvince == null) throw new ArgumentNullException("stateProvince"); - + _stateProvinceRepository.Delete(stateProvince); } - public virtual IQueryable GetAllStateProvinces(bool showHidden = false) - { - var query = _stateProvinceRepository.Table; + public virtual IQueryable GetAllStateProvinces(bool showHidden = false) + { + var query = _stateProvinceRepository.Table; - if (!showHidden) - query = query.Where(x => x.Published); + if (!showHidden) + query = query.Where(x => x.Published); - return query; - } + return query; + } - public virtual StateProvince GetStateProvinceById(int stateProvinceId) + public virtual StateProvince GetStateProvinceById(int stateProvinceId) { if (stateProvinceId == 0) return null; @@ -55,7 +55,7 @@ public virtual StateProvince GetStateProvinceByAbbreviation(string abbreviation) var stateProvince = query.FirstOrDefault(); return stateProvince; } - + public virtual IList GetStateProvincesByCountryId(int countryId, bool showHidden = false) { if (countryId == 0) @@ -63,15 +63,15 @@ public virtual IList GetStateProvincesByCountryId(int countryId, return new List(); } - var query = from sp in _stateProvinceRepository.Table - orderby sp.DisplayOrder - where sp.CountryId == countryId && - (showHidden || sp.Published) - select sp; + var query = from sp in _stateProvinceRepository.Table + orderby sp.DisplayOrder + where sp.CountryId == countryId && + (showHidden || sp.Published) + select sp; - var stateProvinces = query.ToListCached("db.regions.{0}.{1}".FormatInvariant(countryId, showHidden)); - return stateProvinces; - } + var stateProvinces = query.ToListCached("db.regions.{0}.{1}".FormatInvariant(countryId, showHidden)); + return stateProvinces; + } public virtual void InsertStateProvince(StateProvince stateProvince) { diff --git a/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs b/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs index b96dce8613..6fc51c7256 100644 --- a/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs +++ b/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs @@ -11,16 +11,16 @@ public partial class UpdateExchangeRateTask : ITask { private readonly ICurrencyService _currencyService; private readonly CurrencySettings _currencySettings; - private readonly ICommonServices _services; + private readonly ICommonServices _services; public UpdateExchangeRateTask( - ICurrencyService currencyService, - CurrencySettings currencySettings, - ICommonServices services) + ICurrencyService currencyService, + CurrencySettings currencySettings, + ICommonServices services) { this._currencyService = currencyService; this._currencySettings = currencySettings; - this._services = services; + this._services = services; } /// @@ -45,17 +45,17 @@ public void Execute(TaskExecutionContext ctx) var currency = _currencyService.GetCurrencyByCode(exchageRate.CurrencyCode); if (currency != null) { - if (currency.Rate != exchageRate.Rate) - { - currency.Rate = exchageRate.Rate; - _currencyService.UpdateCurrency(currency); - } + if (currency.Rate != exchageRate.Rate) + { + currency.Rate = exchageRate.Rate; + _currencyService.UpdateCurrency(currency); + } } } // save new update time value _currencySettings.LastUpdateTime = DateTime.UtcNow.ToBinary(); - _services.Settings.SaveSetting(_currencySettings); + _services.Settings.SaveSetting(_currencySettings); } } } diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs index df737a6d23..e6c7a85c9d 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs @@ -3,7 +3,7 @@ namespace SmartStore.Services.Discounts { - public static class DiscountExtentions + public static class DiscountExtentions { /// /// Gets the discount amount for the specified value @@ -13,29 +13,29 @@ public static class DiscountExtentions /// The discount amount public static decimal GetDiscountAmount(this Discount discount, decimal amount) { - Guard.NotNull(discount, nameof(discount)); + Guard.NotNull(discount, nameof(discount)); var result = decimal.Zero; - if (discount.UsePercentage) - { - result = (decimal)((((float)amount) * ((float)discount.DiscountPercentage)) / 100f); - } - else - { - result = discount.DiscountAmount; - } + if (discount.UsePercentage) + { + result = (decimal)((((float)amount) * ((float)discount.DiscountPercentage)) / 100f); + } + else + { + result = discount.DiscountAmount; + } return result; } - /// - /// Get the discount that achieves the highest discount amount other than zero. - /// - /// List of discounts - /// Amount without discount (for percentage discounts) - /// Discount that achieves the highest discount amount other than zero. - public static Discount GetPreferredDiscount(this ICollection discounts, decimal amount) + /// + /// Get the discount that achieves the highest discount amount other than zero. + /// + /// List of discounts + /// Amount without discount (for percentage discounts) + /// Discount that achieves the highest discount amount other than zero. + public static Discount GetPreferredDiscount(this ICollection discounts, decimal amount) { Discount preferredDiscount = null; decimal? maximumDiscountValue = null; @@ -43,14 +43,14 @@ public static Discount GetPreferredDiscount(this ICollection discounts foreach (var discount in discounts) { var currentDiscountValue = discount.GetDiscountAmount(amount); - if (currentDiscountValue != decimal.Zero) - { - if (!maximumDiscountValue.HasValue || currentDiscountValue > maximumDiscountValue) - { - maximumDiscountValue = currentDiscountValue; - preferredDiscount = discount; - } - } + if (currentDiscountValue != decimal.Zero) + { + if (!maximumDiscountValue.HasValue || currentDiscountValue > maximumDiscountValue) + { + maximumDiscountValue = currentDiscountValue; + preferredDiscount = discount; + } + } } return preferredDiscount; diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs index 8e2ff2b59f..5ab5f8a1f9 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs @@ -22,8 +22,8 @@ public partial class DiscountService : IDiscountService private readonly IRepository _discountRepository; private readonly IRepository _discountUsageHistoryRepository; private readonly IRequestCache _requestCache; - private readonly IStoreContext _storeContext; - private readonly IGenericAttributeService _genericAttributeService; + private readonly IStoreContext _storeContext; + private readonly IGenericAttributeService _genericAttributeService; private readonly ICartRuleProvider _cartRuleProvider; private readonly IDictionary _discountValidityCache; @@ -31,28 +31,28 @@ public DiscountService( IRequestCache requestCache, IRepository discountRepository, IRepository discountUsageHistoryRepository, - IStoreContext storeContext, - IGenericAttributeService genericAttributeService, + IStoreContext storeContext, + IGenericAttributeService genericAttributeService, ICartRuleProvider cartRuleProvider) { _requestCache = requestCache; _discountRepository = discountRepository; _discountUsageHistoryRepository = discountUsageHistoryRepository; - _storeContext = storeContext; - _genericAttributeService = genericAttributeService; + _storeContext = storeContext; + _genericAttributeService = genericAttributeService; _cartRuleProvider = cartRuleProvider; - _discountValidityCache = new Dictionary(); - } - - /// - /// Checks discount limitation for customer - /// - /// Discount - /// Customer - /// Value indicating whether discount can be used - protected virtual bool CheckDiscountLimitations(Discount discount, Customer customer) + _discountValidityCache = new Dictionary(); + } + + /// + /// Checks discount limitation for customer + /// + /// Discount + /// Customer + /// Value indicating whether discount can be used + protected virtual bool CheckDiscountLimitations(Discount discount, Customer customer) { - Guard.NotNull(discount, nameof(discount)); + Guard.NotNull(discount, nameof(discount)); switch (discount.DiscountLimitation) { @@ -87,9 +87,9 @@ protected virtual bool CheckDiscountLimitations(Discount discount, Customer cust public virtual void DeleteDiscount(Discount discount) { - Guard.NotNull(discount, nameof(discount)); + Guard.NotNull(discount, nameof(discount)); - _discountRepository.Delete(discount); + _discountRepository.Delete(discount); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); } @@ -103,29 +103,25 @@ public virtual Discount GetDiscountById(int discountId) public virtual IEnumerable GetAllDiscounts(DiscountType? discountType, string couponCode = "", bool showHidden = false) { - int? discountTypeId = null; - if (discountType.HasValue) - discountTypeId = (int)discountType.Value; - - // we load all discounts, and filter them by passed "discountType" parameter later - // we do it because we know that this method is invoked several times per HTTP request with distinct "discountType" parameter - // that's why we access the database only once - string key = string.Format(DISCOUNTS_ALL_KEY, showHidden, couponCode); + var discountTypeId = discountType.HasValue ? (int)discountType.Value : 0; + + // We load all discounts and filter them by passed "discountType" parameter later because + // this method is invoked several times per HTTP request with distinct "discountType" parameter. + var key = string.Format(DISCOUNTS_ALL_KEY, showHidden, couponCode); var result = _requestCache.Get(key, () => { var query = _discountRepository.Table; if (!showHidden) { - // The function 'CurrentUtcDateTime' is not supported by SQL Server Compact. - // That's why we pass the date value - var nowUtc = DateTime.UtcNow.Date; + var utcNow = DateTime.UtcNow; + query = query.Where(d => - (!d.StartDateUtc.HasValue || d.StartDateUtc <= nowUtc) - && (!d.EndDateUtc.HasValue || d.EndDateUtc >= nowUtc)); + (!d.StartDateUtc.HasValue || d.StartDateUtc <= utcNow) && + (!d.EndDateUtc.HasValue || d.EndDateUtc >= utcNow)); } - if (!String.IsNullOrWhiteSpace(couponCode)) + if (!string.IsNullOrWhiteSpace(couponCode)) { couponCode = couponCode.Trim(); query = query.Where(d => d.CouponCode == couponCode); @@ -135,15 +131,15 @@ public virtual IEnumerable GetAllDiscounts(DiscountType? discountType, var discounts = query.ToList(); - var map = new Multimap(); - discounts.Each(x => map.Add(x.DiscountTypeId, x)); + var map = new Multimap(); + discounts.Each(x => map.Add(x.DiscountTypeId, x)); - return map; + return map; }); if (discountTypeId > 0) { - return result[discountTypeId.Value]; + return result[discountTypeId]; } return result.SelectMany(x => x.Value); @@ -151,7 +147,7 @@ public virtual IEnumerable GetAllDiscounts(DiscountType? discountType, public virtual void InsertDiscount(Discount discount) { - Guard.NotNull(discount, nameof(discount)); + Guard.NotNull(discount, nameof(discount)); _discountRepository.Insert(discount); @@ -160,7 +156,7 @@ public virtual void InsertDiscount(Discount discount) public virtual void UpdateDiscount(Discount discount) { - Guard.NotNull(discount, nameof(discount)); + Guard.NotNull(discount, nameof(discount)); _discountRepository.Update(discount); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); @@ -175,30 +171,30 @@ public virtual Discount GetDiscountByCouponCode(string couponCode, bool showHidd return discount; } - public virtual bool IsDiscountValid(Discount discount, Customer customer) + public virtual bool IsDiscountValid(Discount discount, Customer customer) { - var couponCodeToValidate = ""; + var couponCodeToValidate = ""; if (customer != null) - { - couponCodeToValidate = customer.GetAttribute(SystemCustomerAttributeNames.DiscountCouponCode, _genericAttributeService); - } + { + couponCodeToValidate = customer.GetAttribute(SystemCustomerAttributeNames.DiscountCouponCode, _genericAttributeService); + } var valid = IsDiscountValid(discount, customer, couponCodeToValidate); - return valid; + return valid; } public virtual bool IsDiscountValid(Discount discount, Customer customer, string couponCodeToValidate) { - Guard.NotNull(discount, nameof(discount)); + Guard.NotNull(discount, nameof(discount)); - var cacheKey = new DiscountKey(discount, customer, couponCodeToValidate); - if (_discountValidityCache.TryGetValue(cacheKey, out var result)) - { - return result; - } + var cacheKey = new DiscountKey(discount, customer, couponCodeToValidate); + if (_discountValidityCache.TryGetValue(cacheKey, out var result)) + { + return result; + } - // Check coupon code. - if (discount.RequiresCouponCode) + // Check coupon code. + if (discount.RequiresCouponCode) { if (discount.CouponCode.IsEmpty()) return Cached(false); @@ -227,13 +223,13 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string if (!CheckDiscountLimitations(discount, customer)) return Cached(false); - // Better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. - if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) - { - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, _storeContext.CurrentStore.Id); - if (cart.Any(x => x.Item?.Product != null && x.Item.Product.IsGiftCard)) - return Cached(false); - } + // Better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. + if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) + { + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, _storeContext.CurrentStore.Id); + if (cart.Any(x => x.Item?.Product != null && x.Item.Product.IsGiftCard)) + return Cached(false); + } // Rule sets. if (!_cartRuleProvider.RuleMatches(discount)) @@ -243,11 +239,11 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string return Cached(true); - bool Cached(bool value) - { - _discountValidityCache[cacheKey] = value; - return value; - } + bool Cached(bool value) + { + _discountValidityCache[cacheKey] = value; + return value; + } } public virtual DiscountUsageHistory GetDiscountUsageHistoryById(int discountUsageHistoryId) @@ -275,34 +271,34 @@ public virtual IPagedList GetAllDiscountUsageHistory(int? public virtual void InsertDiscountUsageHistory(DiscountUsageHistory discountUsageHistory) { - Guard.NotNull(discountUsageHistory, nameof(discountUsageHistory)); + Guard.NotNull(discountUsageHistory, nameof(discountUsageHistory)); - _discountUsageHistoryRepository.Insert(discountUsageHistory); + _discountUsageHistoryRepository.Insert(discountUsageHistory); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); } public virtual void UpdateDiscountUsageHistory(DiscountUsageHistory discountUsageHistory) { - Guard.NotNull(discountUsageHistory, nameof(discountUsageHistory)); + Guard.NotNull(discountUsageHistory, nameof(discountUsageHistory)); - _discountUsageHistoryRepository.Update(discountUsageHistory); + _discountUsageHistoryRepository.Update(discountUsageHistory); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); } public virtual void DeleteDiscountUsageHistory(DiscountUsageHistory discountUsageHistory) { - Guard.NotNull(discountUsageHistory, nameof(discountUsageHistory)); + Guard.NotNull(discountUsageHistory, nameof(discountUsageHistory)); - _discountUsageHistoryRepository.Delete(discountUsageHistory); + _discountUsageHistoryRepository.Delete(discountUsageHistory); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); } - class DiscountKey : Tuple - { - public DiscountKey(Discount discount, Customer customer, string customerCouponCode) - : base(discount, customer, customerCouponCode) - { - } - } + class DiscountKey : Tuple + { + public DiscountKey(Discount discount, Customer customer, string customerCouponCode) + : base(discount, customer, customerCouponCode) + { + } + } } } diff --git a/src/Libraries/SmartStore.Services/Discounts/IDiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/IDiscountService.cs index 1e5111c762..159328a87e 100644 --- a/src/Libraries/SmartStore.Services/Discounts/IDiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/IDiscountService.cs @@ -23,13 +23,13 @@ public partial interface IDiscountService /// Discount Discount GetDiscountById(int discountId); - /// - /// Gets all discounts - /// - /// Discount type; null to load all discount - /// A value indicating whether to show hidden records - /// Discount collection - IEnumerable GetAllDiscounts(DiscountType? discountType, string couponCode = "", bool showHidden = false); + /// + /// Gets all discounts + /// + /// Discount type; null to load all discount + /// A value indicating whether to show hidden records + /// Discount collection + IEnumerable GetAllDiscounts(DiscountType? discountType, string couponCode = "", bool showHidden = false); /// /// Inserts a discount @@ -74,7 +74,7 @@ public partial interface IDiscountService /// Discount usage history record identifier /// Discount usage history DiscountUsageHistory GetDiscountUsageHistoryById(int discountUsageHistoryId); - + /// /// Gets all discount usage history records /// @@ -83,7 +83,7 @@ public partial interface IDiscountService /// Page index /// Page size /// Discount usage history records - IPagedList GetAllDiscountUsageHistory(int? discountId, + IPagedList GetAllDiscountUsageHistory(int? discountId, int? customerId, int pageIndex, int pageSize); /// @@ -91,7 +91,7 @@ IPagedList GetAllDiscountUsageHistory(int? discountId, /// /// Discount usage history record void InsertDiscountUsageHistory(DiscountUsageHistory discountUsageHistory); - + /// /// Update discount usage history record /// diff --git a/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs b/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs index 3f89138f56..7c03c1b4e1 100644 --- a/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs +++ b/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs @@ -2,125 +2,124 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; using System.Threading.Tasks; using SmartStore.Collections; using SmartStore.Core.Events; namespace SmartStore.Services.Events { - public class ConsumerRegistry : IConsumerRegistry - { - private readonly Multimap _descriptorMap = new Multimap(); - - public ConsumerRegistry(IEnumerable> consumers) - { - foreach (var consumer in consumers) - { - var metadata = consumer.Metadata; - - if (!metadata.IsActive) - continue; - - var methods = FindMethods(metadata); - var messageTypes = new Dictionary(); - - foreach (var method in methods) - { - var descriptor = new ConsumerDescriptor(metadata) - { - IsAsync = method.ReturnType == typeof(Task), - FireForget = method.HasAttribute(false) - }; - - //if (descriptor.IsAsync && descriptor.FireForget) - //{ - // throw new NotSupportedException($"An asynchronous message consumer method cannot be called as fire & forget. Method: '{method}'."); - //} - - if (method.ReturnType != typeof(Task) && method.ReturnType != typeof(void)) - { - throw new NotSupportedException($"A message consumer method's return type must either be 'void' or '${typeof(Task).FullName}'. Method: '{method}'."); - } - - if (method.Name.EndsWith("Async") && !descriptor.IsAsync) - { - throw new NotSupportedException($"A synchronous message consumer method name should not end with 'Async'. Method: '{method}'."); - } - - var parameters = method.GetParameters(); - if (parameters.Length == 0) - { - throw new NotSupportedException($"A message consumer method must have at least one parameter identifying the message to consume. Method: '{method}'."); - } - - if (parameters.Any(x => x.ParameterType.IsByRef || x.IsOut || x.IsOptional)) - { - throw new NotSupportedException($"'out', 'ref' and optional parameters are not allowed in consumer methods. Method: '{method}'."); - } - - var p = parameters[0]; - var messageType = p.ParameterType; - - if (messageType.IsGenericType && messageType.GetGenericTypeDefinition() == typeof(ConsumeContext<>)) - { - messageType = messageType.GetGenericArguments()[0]; - descriptor.WithEnvelope = true; - } - - if (messageTypes.TryGetValue(messageType, out var method2)) - { - // We won't allow methods with different signatures, but same message type: there can only be one! - throw new AmbigousConsumerException(method2, method); - } - - messageTypes.Add(messageType, method); - - if (messageType.IsPublic && (messageType.IsClass || messageType.IsInterface)) - { - // The method signature is valid: add to dictionary. - descriptor.MessageParameter = p; - descriptor.Parameters = parameters.Skip(1).ToArray(); - descriptor.MessageType = messageType; - descriptor.Method = method; - - _descriptorMap.Add(messageType, descriptor); - } - else - { - // TODO: message - throw new NotSupportedException(); - } - } - } - } - - private IEnumerable FindMethods(EventConsumerMetadata metadata) - { - var methods = metadata.ContainerType.GetMethods(BindingFlags.Public | BindingFlags.Instance); - - var validNames = new HashSet(new[] { "Handle", "HandleEvent", "Consume", "HandleAsync", "HandleEventAsync", "ConsumeAsync" }); - - foreach (var method in methods) - { - if (validNames.Contains(method.Name)) - { - yield return method; - } - } - } - - public virtual IEnumerable GetConsumers(object message) - { - Guard.NotNull(message, nameof(message)); - - var type = message.GetType(); - if (_descriptorMap.ContainsKey(type)) - { - return _descriptorMap[type]; - } - - return Enumerable.Empty(); - } - } + public class ConsumerRegistry : IConsumerRegistry + { + private readonly Multimap _descriptorMap = new Multimap(); + + public ConsumerRegistry(IEnumerable> consumers) + { + foreach (var consumer in consumers) + { + var metadata = consumer.Metadata; + + if (!metadata.IsActive) + continue; + + var methods = FindMethods(metadata); + var messageTypes = new Dictionary(); + + foreach (var method in methods) + { + var descriptor = new ConsumerDescriptor(metadata) + { + IsAsync = method.ReturnType == typeof(Task), + FireForget = method.HasAttribute(false) + }; + + //if (descriptor.IsAsync && descriptor.FireForget) + //{ + // throw new NotSupportedException($"An asynchronous message consumer method cannot be called as fire & forget. Method: '{method}'."); + //} + + if (method.ReturnType != typeof(Task) && method.ReturnType != typeof(void)) + { + throw new NotSupportedException($"A message consumer method's return type must either be 'void' or '${typeof(Task).FullName}'. Method: '{method}'."); + } + + if (method.Name.EndsWith("Async") && !descriptor.IsAsync) + { + throw new NotSupportedException($"A synchronous message consumer method name should not end with 'Async'. Method: '{method}'."); + } + + var parameters = method.GetParameters(); + if (parameters.Length == 0) + { + throw new NotSupportedException($"A message consumer method must have at least one parameter identifying the message to consume. Method: '{method}'."); + } + + if (parameters.Any(x => x.ParameterType.IsByRef || x.IsOut || x.IsOptional)) + { + throw new NotSupportedException($"'out', 'ref' and optional parameters are not allowed in consumer methods. Method: '{method}'."); + } + + var p = parameters[0]; + var messageType = p.ParameterType; + + if (messageType.IsGenericType && messageType.GetGenericTypeDefinition() == typeof(ConsumeContext<>)) + { + messageType = messageType.GetGenericArguments()[0]; + descriptor.WithEnvelope = true; + } + + if (messageTypes.TryGetValue(messageType, out var method2)) + { + // We won't allow methods with different signatures, but same message type: there can only be one! + throw new AmbigousConsumerException(method2, method); + } + + messageTypes.Add(messageType, method); + + if (messageType.IsPublic && (messageType.IsClass || messageType.IsInterface)) + { + // The method signature is valid: add to dictionary. + descriptor.MessageParameter = p; + descriptor.Parameters = parameters.Skip(1).ToArray(); + descriptor.MessageType = messageType; + descriptor.Method = method; + + _descriptorMap.Add(messageType, descriptor); + } + else + { + // TODO: message + throw new NotSupportedException(); + } + } + } + } + + private IEnumerable FindMethods(EventConsumerMetadata metadata) + { + var methods = metadata.ContainerType.GetMethods(BindingFlags.Public | BindingFlags.Instance); + + var validNames = new HashSet(new[] { "Handle", "HandleEvent", "Consume", "HandleAsync", "HandleEventAsync", "ConsumeAsync" }); + + foreach (var method in methods) + { + if (validNames.Contains(method.Name)) + { + yield return method; + } + } + } + + public virtual IEnumerable GetConsumers(object message) + { + Guard.NotNull(message, nameof(message)); + + var type = message.GetType(); + if (_descriptorMap.ContainsKey(type)) + { + return _descriptorMap[type]; + } + + return Enumerable.Empty(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs b/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs index ebab9eff71..cecb542b00 100644 --- a/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs +++ b/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Autofac; using SmartStore.Core; using SmartStore.Core.Events; @@ -15,59 +12,59 @@ namespace SmartStore.Services.Events { - public class ConsumerResolver : IConsumerResolver - { - private readonly Work _container; + public class ConsumerResolver : IConsumerResolver + { + private readonly Work _container; - public ConsumerResolver(Work container) - { - _container = container; - } + public ConsumerResolver(Work container) + { + _container = container; + } - public virtual IConsumer Resolve(ConsumerDescriptor descriptor) - { - if (descriptor.PluginDescriptor == null || IsActiveForStore(descriptor.PluginDescriptor)) - { - return _container.Value.ResolveKeyed(descriptor.ContainerType); - } + public virtual IConsumer Resolve(ConsumerDescriptor descriptor) + { + if (descriptor.PluginDescriptor == null || IsActiveForStore(descriptor.PluginDescriptor)) + { + return _container.Value.ResolveKeyed(descriptor.ContainerType); + } - return null; - } + return null; + } - public virtual object ResolveParameter(ParameterInfo p, IComponentContext c = null) - { - return (c ?? _container.Value).Resolve(p.ParameterType); - } + public virtual object ResolveParameter(ParameterInfo p, IComponentContext c = null) + { + return (c ?? _container.Value).Resolve(p.ParameterType); + } - private bool IsActiveForStore(PluginDescriptor plugin) - { - int storeId = 0; - if (EngineContext.Current.IsFullyInitialized) - { - storeId = _container.Value.Resolve().CurrentStore.Id; - } + private bool IsActiveForStore(PluginDescriptor plugin) + { + int storeId = 0; + if (EngineContext.Current.IsFullyInitialized) + { + storeId = _container.Value.Resolve().CurrentStore.Id; + } - if (storeId == 0) - { - return true; - } + if (storeId == 0) + { + return true; + } - var settingService = _container.Value.Resolve(); + var settingService = _container.Value.Resolve(); - var limitedToStoresSetting = settingService.GetSettingByKey(plugin.GetSettingKey("LimitedToStores")); - if (limitedToStoresSetting.IsEmpty()) - { - return true; - } + var limitedToStoresSetting = settingService.GetSettingByKey(plugin.GetSettingKey("LimitedToStores")); + if (limitedToStoresSetting.IsEmpty()) + { + return true; + } - var limitedToStores = limitedToStoresSetting.ToIntArray(); - if (limitedToStores.Length > 0) - { - var flag = limitedToStores.Contains(storeId); - return flag; - } + var limitedToStores = limitedToStoresSetting.ToIntArray(); + if (limitedToStores.Length > 0) + { + var flag = limitedToStores.Contains(storeId); + return flag; + } - return true; - } - } + return true; + } + } } diff --git a/src/Libraries/SmartStore.Services/Events/DefaultMessageBus.cs b/src/Libraries/SmartStore.Services/Events/DefaultMessageBus.cs index f4c72f778b..564d7ddef1 100644 --- a/src/Libraries/SmartStore.Services/Events/DefaultMessageBus.cs +++ b/src/Libraries/SmartStore.Services/Events/DefaultMessageBus.cs @@ -7,29 +7,29 @@ namespace SmartStore.Services.Events { - public class DefaultMessageBus : IMessageBus - { - private readonly IMessageBroker _messageBroker; + public class DefaultMessageBus : IMessageBus + { + private readonly IMessageBroker _messageBroker; - public DefaultMessageBus(IEnumerable messageBrokers) - { - _messageBroker = messageBrokers.FirstOrDefault(); - } + public DefaultMessageBus(IEnumerable messageBrokers) + { + _messageBroker = messageBrokers.FirstOrDefault(); + } - public void Subscribe(string channel, Action handler) - { - if (_messageBroker == null) - return; + public void Subscribe(string channel, Action handler) + { + if (_messageBroker == null) + return; - _messageBroker.Subscribe(channel, handler); - } + _messageBroker.Subscribe(channel, handler); + } - public void Publish(string channel, string message) - { - if (_messageBroker == null) - return; + public void Publish(string channel, string message) + { + if (_messageBroker == null) + return; - _messageBroker.Publish(channel, message); - } - } + _messageBroker.Publish(channel, message); + } + } } diff --git a/src/Libraries/SmartStore.Services/Events/EventPublisher.cs b/src/Libraries/SmartStore.Services/Events/EventPublisher.cs index e6529d9469..026abf2efd 100644 --- a/src/Libraries/SmartStore.Services/Events/EventPublisher.cs +++ b/src/Libraries/SmartStore.Services/Events/EventPublisher.cs @@ -1,51 +1,49 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using SmartStore.ComponentModel; using SmartStore.Core.Events; using SmartStore.Core.Logging; namespace SmartStore.Services.Events { - public class EventPublisher : IEventPublisher - { - private readonly IConsumerRegistry _registry; - private readonly IConsumerResolver _resolver; - private readonly IConsumerInvoker _invoker; - - public EventPublisher(IConsumerRegistry registry, IConsumerResolver resolver, IConsumerInvoker invoker) - { - _registry = registry; - _resolver = resolver; - _invoker = invoker; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - public void Publish(T message) where T : class - { - var descriptors = _registry.GetConsumers(message); - - if (!descriptors.Any()) - { - return; - } - - var envelopeType = typeof(ConsumeContext<>).MakeGenericType(typeof(T)); - var envelope = (ConsumeContext)FastActivator.CreateInstance(envelopeType, message); - - foreach (var descriptor in descriptors) - { - var consumer = _resolver.Resolve(descriptor); - if (consumer != null) - { - _invoker.Invoke(descriptor, consumer, envelope); - } - } - } - } + public class EventPublisher : IEventPublisher + { + private readonly IConsumerRegistry _registry; + private readonly IConsumerResolver _resolver; + private readonly IConsumerInvoker _invoker; + + public EventPublisher(IConsumerRegistry registry, IConsumerResolver resolver, IConsumerInvoker invoker) + { + _registry = registry; + _resolver = resolver; + _invoker = invoker; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void Publish(T message) where T : class + { + var descriptors = _registry.GetConsumers(message); + + if (!descriptors.Any()) + { + return; + } + + var envelopeType = typeof(ConsumeContext<>).MakeGenericType(typeof(T)); + var envelope = (ConsumeContext)FastActivator.CreateInstance(envelopeType, message); + + foreach (var descriptor in descriptors) + { + var consumer = _resolver.Resolve(descriptor); + if (consumer != null) + { + _invoker.Invoke(descriptor, consumer, envelope); + } + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Events/IMessageBroker.cs b/src/Libraries/SmartStore.Services/Events/IMessageBroker.cs index ce54a1965b..dcaf55717f 100644 --- a/src/Libraries/SmartStore.Services/Events/IMessageBroker.cs +++ b/src/Libraries/SmartStore.Services/Events/IMessageBroker.cs @@ -2,9 +2,9 @@ namespace SmartStore.Services.Events { - public interface IMessageBroker - { - void Subscribe(string channel, Action handler); - void Publish(string channel, string message); - } + public interface IMessageBroker + { + void Subscribe(string channel, Action handler); + void Publish(string channel, string message); + } } diff --git a/src/Libraries/SmartStore.Services/Events/IMessageBus.cs b/src/Libraries/SmartStore.Services/Events/IMessageBus.cs index d7245ba7e2..f2d764e4d6 100644 --- a/src/Libraries/SmartStore.Services/Events/IMessageBus.cs +++ b/src/Libraries/SmartStore.Services/Events/IMessageBus.cs @@ -2,9 +2,9 @@ namespace SmartStore.Services.Events { - public interface IMessageBus - { - void Subscribe(string channel, Action handler); - void Publish(string channel, string message); - } + public interface IMessageBus + { + void Subscribe(string channel, Action handler); + void Publish(string channel, string message); + } } diff --git a/src/Libraries/SmartStore.Services/Forums/ForumExtensions.cs b/src/Libraries/SmartStore.Services/Forums/ForumExtensions.cs index 54a67d93b6..1b4de19af4 100644 --- a/src/Libraries/SmartStore.Services/Forums/ForumExtensions.cs +++ b/src/Libraries/SmartStore.Services/Forums/ForumExtensions.cs @@ -23,12 +23,12 @@ public static string FormatPostText(this ForumPost post) return string.Empty; } - text = HtmlUtils.ConvertPlainTextToHtml(text.HtmlEncode()); + text = HtmlUtils.ConvertPlainTextToHtml(text.HtmlEncode()); - if (EngineContext.Current.Resolve().ForumEditor == EditorType.BBCodeEditor) - { - text = BBCodeHelper.ToHtml(text); - } + if (EngineContext.Current.Resolve().ForumEditor == EditorType.BBCodeEditor) + { + text = BBCodeHelper.ToHtml(text); + } return text; } @@ -74,8 +74,8 @@ public static string FormatForumSignatureText(this string text) return string.Empty; } - return HtmlUtils.ConvertPlainTextToHtml(text.HtmlEncode()); - } + return HtmlUtils.ConvertPlainTextToHtml(text.HtmlEncode()); + } /// /// Formats the private message text @@ -92,10 +92,10 @@ public static string FormatPrivateMessageText(this PrivateMessage message) return string.Empty; } - text = HtmlUtils.ConvertPlainTextToHtml(text.HtmlEncode()); + text = HtmlUtils.ConvertPlainTextToHtml(text.HtmlEncode()); return BBCodeHelper.ToHtml(text); } - + /// /// Get forum last topic /// diff --git a/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs index 0e8a1140be..ea779210d8 100644 --- a/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs @@ -6,45 +6,45 @@ namespace SmartStore.Services.Forums { - public static class ForumMessageFactoryExtensions - { - /// - /// Sends a forum subscription message to a customer - /// - public static CreateMessageResult SendNewForumTopicMessage(this IMessageFactory factory, Customer customer, ForumTopic forumTopic, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(forumTopic, nameof(forumTopic)); + public static class ForumMessageFactoryExtensions + { + /// + /// Sends a forum subscription message to a customer + /// + public static CreateMessageResult SendNewForumTopicMessage(this IMessageFactory factory, Customer customer, ForumTopic forumTopic, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(forumTopic, nameof(forumTopic)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewForumTopic, languageId, customer: customer), true, forumTopic, forumTopic.Forum); - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewForumTopic, languageId, customer: customer), true, forumTopic, forumTopic.Forum); + } - /// - /// Sends a forum subscription message to a customer - /// - /// Friendly forum topic page to use for URL generation (1-based) - public static CreateMessageResult SendNewForumPostMessage(this IMessageFactory factory, Customer customer, ForumPost forumPost, int topicPageIndex, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(forumPost, nameof(forumPost)); + /// + /// Sends a forum subscription message to a customer + /// + /// Friendly forum topic page to use for URL generation (1-based) + public static CreateMessageResult SendNewForumPostMessage(this IMessageFactory factory, Customer customer, ForumPost forumPost, int topicPageIndex, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(forumPost, nameof(forumPost)); - var bag = new ModelPart - { - ["TopicPageIndex"] = topicPageIndex - }; + var bag = new ModelPart + { + ["TopicPageIndex"] = topicPageIndex + }; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewForumPost, languageId, customer: customer), true, bag, forumPost, forumPost.ForumTopic, forumPost.ForumTopic.Forum); - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewForumPost, languageId, customer: customer), true, bag, forumPost, forumPost.ForumTopic, forumPost.ForumTopic.Forum); + } - /// - /// Sends a private message notification - /// - public static CreateMessageResult SendPrivateMessageNotification(this IMessageFactory factory, Customer customer, PrivateMessage privateMessage, int languageId = 0) - { - Guard.NotNull(customer, nameof(customer)); - Guard.NotNull(privateMessage, nameof(privateMessage)); + /// + /// Sends a private message notification + /// + public static CreateMessageResult SendPrivateMessageNotification(this IMessageFactory factory, Customer customer, PrivateMessage privateMessage, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(privateMessage, nameof(privateMessage)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewPrivateMessage, languageId, privateMessage.StoreId, customer), true, privateMessage); - } - } + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewPrivateMessage, languageId, privateMessage.StoreId, customer), true, privateMessage); + } + } } diff --git a/src/Libraries/SmartStore.Services/Forums/ForumService.cs b/src/Libraries/SmartStore.Services/Forums/ForumService.cs index 76c9da28a0..6430f209ce 100644 --- a/src/Libraries/SmartStore.Services/Forums/ForumService.cs +++ b/src/Libraries/SmartStore.Services/Forums/ForumService.cs @@ -273,12 +273,12 @@ public virtual void DeleteForum(Forum forum) } // Delete forum subscriptions (topics). - var queryTopicIds = + var queryTopicIds = from ft in _forumTopicRepository.Table where ft.ForumId == forum.Id select ft.Id; - var queryFs1 = + var queryFs1 = from fs in _forumSubscriptionRepository.Table where queryTopicIds.Contains(fs.TopicId) select fs; @@ -289,7 +289,7 @@ where queryTopicIds.Contains(fs.TopicId) } // Delete forum subscriptions (forum). - var queryFs2 = + var queryFs2 = from fs in _forumSubscriptionRepository.Table where fs.ForumId == forum.Id select fs; @@ -715,17 +715,17 @@ public virtual PrivateMessage GetPrivateMessageById(int messageId) return privateMessage; } - public virtual IPagedList GetAllPrivateMessages( - int storeId, - int fromCustomerId, - int toCustomerId, - bool? isRead, - bool? isDeletedByAuthor, - bool? isDeletedByRecipient, - int pageIndex, - int pageSize) + public virtual IPagedList GetAllPrivateMessages( + int storeId, + int fromCustomerId, + int toCustomerId, + bool? isRead, + bool? isDeletedByAuthor, + bool? isDeletedByRecipient, + int pageIndex, + int pageSize) { - var query = _forumPrivateMessageRepository.Table; + var query = _forumPrivateMessageRepository.Table; if (storeId > 0) { @@ -752,9 +752,9 @@ public virtual IPagedList GetAllPrivateMessages( query = query.Where(pm => isDeletedByRecipient.Value == pm.IsDeletedByRecipient); } - query = query.OrderByDescending(pm => pm.CreatedOnUtc); + query = query.OrderByDescending(pm => pm.CreatedOnUtc); - var privateMessages = new PagedList(query, pageIndex, pageSize); + var privateMessages = new PagedList(query, pageIndex, pageSize); return privateMessages; } @@ -770,11 +770,11 @@ public virtual void InsertPrivateMessage(PrivateMessage message) throw new SmartException("Recipient could not be loaded"); } - _genericAttributeService.SaveAttribute(customerTo, SystemCustomerAttributeNames.NotifiedAboutNewPrivateMessages, false, message.StoreId); + _genericAttributeService.SaveAttribute(customerTo, SystemCustomerAttributeNames.NotifiedAboutNewPrivateMessages, false, message.StoreId); if (_forumSettings.NotifyAboutPrivateMessages) { - _services.MessageFactory.SendPrivateMessageNotification(customerTo, message, _services.WorkContext.WorkingLanguage.Id); + _services.MessageFactory.SendPrivateMessageNotification(customerTo, message, _services.WorkContext.WorkingLanguage.Id); } } @@ -797,7 +797,7 @@ public virtual void DeletePrivateMessage(PrivateMessage message) if (message != null) { _forumPrivateMessageRepository.Delete(message); - } + } } #endregion @@ -817,21 +817,21 @@ public virtual ForumSubscription GetSubscriptionById(int subscriptionId) public virtual IPagedList GetAllSubscriptions(int customerId, int forumId, int topicId, int pageIndex, int pageSize) { - var fsQuery = - from fs in _forumSubscriptionRepository.Table - join c in _customerRepository.Table on fs.CustomerId equals c.Id - where - (customerId == 0 || fs.CustomerId == customerId) && - (forumId == 0 || fs.ForumId == forumId) && - (topicId == 0 || fs.TopicId == topicId) && - (c.Active && !c.Deleted) - select fs.SubscriptionGuid; - - var query = - from fs in _forumSubscriptionRepository.Table - where fsQuery.Contains(fs.SubscriptionGuid) - orderby fs.CreatedOnUtc descending, fs.SubscriptionGuid descending - select fs; + var fsQuery = + from fs in _forumSubscriptionRepository.Table + join c in _customerRepository.Table on fs.CustomerId equals c.Id + where + (customerId == 0 || fs.CustomerId == customerId) && + (forumId == 0 || fs.ForumId == forumId) && + (topicId == 0 || fs.TopicId == topicId) && + (c.Active && !c.Deleted) + select fs.SubscriptionGuid; + + var query = + from fs in _forumSubscriptionRepository.Table + where fsQuery.Contains(fs.SubscriptionGuid) + orderby fs.CreatedOnUtc descending, fs.SubscriptionGuid descending + select fs; var forumSubscriptions = new PagedList(query, pageIndex, pageSize); return forumSubscriptions; @@ -1019,7 +1019,7 @@ class ForumXmlSitemapResult : XmlSitemapProvider public ForumXmlSitemapResult( XmlSitemapBuildContext context, - IForumService forumService, + IForumService forumService, UrlHelper urlHelper, IUrlRecordService urlRecordService) { @@ -1085,7 +1085,7 @@ public override IEnumerable Enlist() // Enlist topics var pager = new FastPager(_topicsQuery.AsNoTracking(), _context.MaximumNodeCount); - while (pager.ReadNextPage(x => new { x.Id, x.UpdatedOnUtc, x.Subject }, x => x.Id, out var topics)) + while (pager.ReadNextPage(x => new { x.Id, x.UpdatedOnUtc, x.Subject }, x => x.Id, out var topics)) { if (_context.CancellationToken.IsCancellationRequested) { diff --git a/src/Libraries/SmartStore.Services/Helpers/DateTimeHelper.cs b/src/Libraries/SmartStore.Services/Helpers/DateTimeHelper.cs index 78f34b444d..37a1b86fd3 100644 --- a/src/Libraries/SmartStore.Services/Helpers/DateTimeHelper.cs +++ b/src/Libraries/SmartStore.Services/Helpers/DateTimeHelper.cs @@ -19,8 +19,8 @@ public partial class DateTimeHelper : IDateTimeHelper private TimeZoneInfo _cachedUserTimeZone; public DateTimeHelper( - IWorkContext workContext, - ISettingService settingService, + IWorkContext workContext, + ISettingService settingService, DateTimeSettings dateTimeSettings, ICustomerService customerService) { @@ -52,7 +52,7 @@ public virtual DateTime ConvertToUserTime(DateTime dt) public virtual DateTime ConvertToUserTime(DateTime dt, DateTimeKind sourceDateTimeKind) { return TimeZoneInfo.ConvertTime( - DateTime.SpecifyKind(dt, sourceDateTimeKind), + DateTime.SpecifyKind(dt, sourceDateTimeKind), this.CurrentTimeZone); } @@ -95,21 +95,21 @@ public virtual DateTime ConvertToUtcTime(DateTime dt, TimeZoneInfo sourceTimeZon public virtual TimeZoneInfo GetCustomerTimeZone(Customer customer) { - if (_cachedUserTimeZone != null) - return _cachedUserTimeZone; + if (_cachedUserTimeZone != null) + return _cachedUserTimeZone; - // registered user - TimeZoneInfo timeZone = null; + // registered user + TimeZoneInfo timeZone = null; if (_dateTimeSettings.AllowCustomersToSetTimeZone) { string timeZoneId = string.Empty; if (customer != null) - timeZoneId = customer.TimeZoneId; + timeZoneId = customer.TimeZoneId; try { - if (timeZoneId.HasValue()) - timeZone = FindTimeZoneById(timeZoneId); + if (timeZoneId.HasValue()) + timeZone = FindTimeZoneById(timeZoneId); } catch (Exception ex) { @@ -121,9 +121,9 @@ public virtual TimeZoneInfo GetCustomerTimeZone(Customer customer) if (timeZone == null) timeZone = this.DefaultStoreTimeZone; - _cachedUserTimeZone = timeZone; + _cachedUserTimeZone = timeZone; - return timeZone; + return timeZone; } public virtual TimeZoneInfo DefaultStoreTimeZone @@ -134,7 +134,7 @@ public virtual TimeZoneInfo DefaultStoreTimeZone try { if (_dateTimeSettings.DefaultStoreTimeZoneId.HasValue()) - timeZoneInfo = FindTimeZoneById(_dateTimeSettings.DefaultStoreTimeZoneId); + timeZoneInfo = FindTimeZoneById(_dateTimeSettings.DefaultStoreTimeZoneId); } catch (Exception ex) { @@ -156,17 +156,14 @@ public virtual TimeZoneInfo DefaultStoreTimeZone _dateTimeSettings.DefaultStoreTimeZoneId = defaultTimeZoneId; _settingService.SaveSetting(_dateTimeSettings); - _cachedUserTimeZone = null; + _cachedUserTimeZone = null; - } + } } public virtual TimeZoneInfo CurrentTimeZone { - get - { - return GetCustomerTimeZone(_workContext.CurrentCustomer); - } + get => GetCustomerTimeZone(_workContext.CurrentCustomer); set { if (!_dateTimeSettings.AllowCustomersToSetTimeZone) @@ -181,8 +178,8 @@ public virtual TimeZoneInfo CurrentTimeZone _workContext.CurrentCustomer.TimeZoneId = timeZoneId; _customerService.UpdateCustomer(_workContext.CurrentCustomer); - _cachedUserTimeZone = null; - } + _cachedUserTimeZone = null; + } } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs b/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs index b1518ba862..9ebb286f0b 100644 --- a/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs @@ -3,40 +3,41 @@ using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Security; +using SmartStore.Data; using SmartStore.Services.Security; namespace SmartStore.Services.Common { - public class AclEntityHook : DbSaveHook - { - private readonly Lazy _aclService; - private readonly HashSet _toDelete = new HashSet(); + public class AclEntityHook : DbSaveHook + { + private readonly Lazy _aclService; + private readonly HashSet _toDelete = new HashSet(); - public AclEntityHook(Lazy aclService) - { - _aclService = aclService; - } + public AclEntityHook(Lazy aclService) + { + _aclService = aclService; + } - protected override void OnDeleted(IAclSupported entity, IHookedEntity entry) - { - var entityType = entry.EntityType; + protected override void OnDeleted(IAclSupported entity, IHookedEntity entry) + { + var entityType = entry.EntityType; - var records = _aclService.Value.GetAclRecordsFor(entityType.Name, entry.Entity.Id); - _toDelete.AddRange(records); - } + var records = _aclService.Value.GetAclRecordsFor(entityType.Name, entry.Entity.Id); + _toDelete.AddRange(records); + } - public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; + public override void OnAfterSaveCompleted() + { + if (_toDelete.Count == 0) + return; - using (var scope = new DbContextScope(autoCommit: false)) - { - _toDelete.Each(x => _aclService.Value.DeleteAclRecord(x)); - scope.Commit(); - } + using (var scope = new DbContextScope(autoCommit: false)) + { + _toDelete.Each(x => _aclService.Value.DeleteAclRecord(x)); + scope.Commit(); + } - _toDelete.Clear(); - } - } + _toDelete.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs b/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs index cb22b6a757..8d5a068e62 100644 --- a/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs @@ -1,26 +1,27 @@ using System; using SmartStore.Core; using SmartStore.Core.Data.Hooks; +using SmartStore.Data; namespace SmartStore.Services.Hooks { - [Important] - public class AuditableHook : DbSaveHook - { - protected override void OnInserting(IAuditable entity, IHookedEntity entry) - { - var now = DateTime.UtcNow; + [Important] + public class AuditableHook : DbSaveHook + { + protected override void OnInserting(IAuditable entity, IHookedEntity entry) + { + var now = DateTime.UtcNow; - if (entity.CreatedOnUtc == DateTime.MinValue) - entity.CreatedOnUtc = now; + if (entity.CreatedOnUtc == DateTime.MinValue) + entity.CreatedOnUtc = now; - if (entity.UpdatedOnUtc == DateTime.MinValue) - entity.UpdatedOnUtc = now; - } + if (entity.UpdatedOnUtc == DateTime.MinValue) + entity.UpdatedOnUtc = now; + } - protected override void OnUpdating(IAuditable entity, IHookedEntity entry) - { - entity.UpdatedOnUtc = DateTime.UtcNow; - } - } + protected override void OnUpdating(IAuditable entity, IHookedEntity entry) + { + entity.UpdatedOnUtc = DateTime.UtcNow; + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs b/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs index e5ecbf91b9..8651c5e009 100644 --- a/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs @@ -7,48 +7,48 @@ namespace SmartStore.Services.Common { - public class FixProductMainPictureHook : DbSaveHook - { - private readonly IRepository _rsProduct; - private readonly HashSet _products = new HashSet(); - - public FixProductMainPictureHook(IRepository rsProduct) - { - _rsProduct = rsProduct; - } - - protected override void OnDeleting(ProductMediaFile entity, IHookedEntity entry) - { - Fix(entity); - } - - protected override void OnInserting(ProductMediaFile entity, IHookedEntity entry) - { - Fix(entity); - } - - protected override void OnUpdating(ProductMediaFile entity, IHookedEntity entry) - { - Fix(entity); - } - - private void Fix(ProductMediaFile entity) - { - var product = entity.Product ?? _rsProduct.GetById(entity.ProductId); - if (product != null) - { - _products.Add(product); - } - } - - public override void OnBeforeSaveCompleted() - { - foreach (var product in _products) - { - DataMigrator.FixProductMainPictureId(_rsProduct.Context, product); - } - - _products.Clear(); - } - } + public class FixProductMainPictureHook : DbSaveHook + { + private readonly IRepository _rsProduct; + private readonly HashSet _products = new HashSet(); + + public FixProductMainPictureHook(IRepository rsProduct) + { + _rsProduct = rsProduct; + } + + protected override void OnDeleting(ProductMediaFile entity, IHookedEntity entry) + { + Fix(entity); + } + + protected override void OnInserting(ProductMediaFile entity, IHookedEntity entry) + { + Fix(entity); + } + + protected override void OnUpdating(ProductMediaFile entity, IHookedEntity entry) + { + Fix(entity); + } + + private void Fix(ProductMediaFile entity) + { + var product = entity.Product ?? _rsProduct.GetById(entity.ProductId); + if (product != null) + { + _products.Add(product); + } + } + + public override void OnBeforeSaveCompleted() + { + foreach (var product in _products) + { + DataMigrator.FixProductMainPictureId(_rsProduct.Context, product); + } + + _products.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs index c0f0942078..8622f4a66b 100644 --- a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs @@ -3,42 +3,43 @@ using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Localization; +using SmartStore.Data; using SmartStore.Services.Localization; namespace SmartStore.Services.Hooks { - public class LocalizedEntityHook : DbSaveHook - { - private readonly Lazy _localizedEntityService; - private readonly HashSet _toDelete = new HashSet(); + public class LocalizedEntityHook : DbSaveHook + { + private readonly Lazy _localizedEntityService; + private readonly HashSet _toDelete = new HashSet(); - public LocalizedEntityHook(Lazy localizedEntityService) - { - _localizedEntityService = localizedEntityService; - } + public LocalizedEntityHook(Lazy localizedEntityService) + { + _localizedEntityService = localizedEntityService; + } - protected override void OnDeleted(ILocalizedEntity entity, IHookedEntity entry) - { - var entityType = entry.EntityType; - var localizedEntities = _localizedEntityService.Value.GetLocalizedProperties(entry.Entity.Id, entityType.Name); - _toDelete.AddRange(localizedEntities); - } + protected override void OnDeleted(ILocalizedEntity entity, IHookedEntity entry) + { + var entityType = entry.EntityType; + var localizedEntities = _localizedEntityService.Value.GetLocalizedProperties(entry.Entity.Id, entityType.Name); + _toDelete.AddRange(localizedEntities); + } - public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; + public override void OnAfterSaveCompleted() + { + if (_toDelete.Count == 0) + return; - using (var scope = new DbContextScope(autoCommit: false)) - { - using (_localizedEntityService.Value.BeginScope()) - { - _toDelete.Each(x => _localizedEntityService.Value.DeleteLocalizedProperty(x)); - } + using (var scope = new DbContextScope(autoCommit: false)) + { + using (_localizedEntityService.Value.BeginScope()) + { + _toDelete.Each(x => _localizedEntityService.Value.DeleteLocalizedProperty(x)); + } - scope.Commit(); - _toDelete.Clear(); - } - } - } + scope.Commit(); + _toDelete.Clear(); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs index 6667f53bc4..b20ace7997 100644 --- a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs @@ -11,85 +11,85 @@ namespace SmartStore.Services.Hooks { - /// - /// Deletes localized properties of entities that were deleted by referential integrity. - /// Otherwise these localized properties would remain in the database. - /// - public class LocalizedEntityRelationHook : DbSaveHook - { - private readonly Lazy> _productAttributeRepository; - private readonly Lazy _localizedEntityService; - private readonly Lazy _productAttributeService; - private readonly Lazy _specificationAttributeService; + /// + /// Deletes localized properties of entities that were deleted by referential integrity. + /// Otherwise these localized properties would remain in the database. + /// + public class LocalizedEntityRelationHook : DbSaveHook + { + private readonly Lazy> _productAttributeRepository; + private readonly Lazy _localizedEntityService; + private readonly Lazy _productAttributeService; + private readonly Lazy _specificationAttributeService; - private readonly HashSet _toDelete = new HashSet(); + private readonly HashSet _toDelete = new HashSet(); - private static readonly HashSet _candidateTypes = new HashSet(new Type[] - { - typeof(SpecificationAttribute), - typeof(ProductVariantAttribute), - typeof(ProductAttribute), - typeof(ProductAttributeOptionsSet) - }); + private static readonly HashSet _candidateTypes = new HashSet(new Type[] + { + typeof(SpecificationAttribute), + typeof(ProductVariantAttribute), + typeof(ProductAttribute), + typeof(ProductAttributeOptionsSet) + }); - public LocalizedEntityRelationHook( - Lazy> productAttributeRepository, - Lazy localizedEntityService, - Lazy productAttributeService, - Lazy specificationAttributeService) - { - _productAttributeRepository = productAttributeRepository; - _localizedEntityService = localizedEntityService; - _productAttributeService = productAttributeService; - _specificationAttributeService = specificationAttributeService; - } + public LocalizedEntityRelationHook( + Lazy> productAttributeRepository, + Lazy localizedEntityService, + Lazy productAttributeService, + Lazy specificationAttributeService) + { + _productAttributeRepository = productAttributeRepository; + _localizedEntityService = localizedEntityService; + _productAttributeService = productAttributeService; + _specificationAttributeService = specificationAttributeService; + } - protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) - { - var type = entry.EntityType; + protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) + { + var type = entry.EntityType; - if (!_candidateTypes.Contains(type)) - throw new NotSupportedException(); + if (!_candidateTypes.Contains(type)) + throw new NotSupportedException(); - if (type == typeof(SpecificationAttribute)) - { - var attribute = (SpecificationAttribute)entry.Entity; - var attributeOptions = attribute.SpecificationAttributeOptions.ToList(); - if (!attributeOptions.Any()) - attributeOptions = _specificationAttributeService.Value.GetSpecificationAttributeOptionsBySpecificationAttribute(attribute.Id).ToList(); + if (type == typeof(SpecificationAttribute)) + { + var attribute = (SpecificationAttribute)entry.Entity; + var attributeOptions = attribute.SpecificationAttributeOptions.ToList(); + if (!attributeOptions.Any()) + attributeOptions = _specificationAttributeService.Value.GetSpecificationAttributeOptionsBySpecificationAttribute(attribute.Id).ToList(); - attributeOptions.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x.Id, "SpecificationAttributeOption"))); - } - else if (type == typeof(ProductVariantAttribute)) - { - var attribute = (ProductVariantAttribute)entry.Entity; - var attributeOptions = attribute.ProductVariantAttributeValues.ToList(); - if (!attributeOptions.Any()) - attributeOptions = _productAttributeService.Value.GetProductVariantAttributeValues(attribute.Id).ToList(); + attributeOptions.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x.Id, "SpecificationAttributeOption"))); + } + else if (type == typeof(ProductVariantAttribute)) + { + var attribute = (ProductVariantAttribute)entry.Entity; + var attributeOptions = attribute.ProductVariantAttributeValues.ToList(); + if (!attributeOptions.Any()) + attributeOptions = _productAttributeService.Value.GetProductVariantAttributeValues(attribute.Id).ToList(); - attributeOptions.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x.Id, "ProductVariantAttributeValue"))); - } - else if (type == typeof(ProductAttribute)) - { - var optionIds = ( - from a in _productAttributeRepository.Value.TableUntracked - from os in a.ProductAttributeOptionsSets - from ao in os.ProductAttributeOptions - where a.Id == entry.Entity.Id - select ao.Id).ToList(); + attributeOptions.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x.Id, "ProductVariantAttributeValue"))); + } + else if (type == typeof(ProductAttribute)) + { + var optionIds = ( + from a in _productAttributeRepository.Value.TableUntracked + from os in a.ProductAttributeOptionsSets + from ao in os.ProductAttributeOptions + where a.Id == entry.Entity.Id + select ao.Id).ToList(); - optionIds.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x, "ProductAttributeOption"))); - } - else if (type == typeof(ProductAttributeOptionsSet)) - { - var set = (ProductAttributeOptionsSet)entry.Entity; - var attributeOptions = set.ProductAttributeOptions.ToList(); - if (!attributeOptions.Any()) - attributeOptions = _productAttributeService.Value.GetProductAttributeOptionsByOptionsSetId(set.Id).ToList(); + optionIds.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x, "ProductAttributeOption"))); + } + else if (type == typeof(ProductAttributeOptionsSet)) + { + var set = (ProductAttributeOptionsSet)entry.Entity; + var attributeOptions = set.ProductAttributeOptions.ToList(); + if (!attributeOptions.Any()) + attributeOptions = _productAttributeService.Value.GetProductAttributeOptionsByOptionsSetId(set.Id).ToList(); - attributeOptions.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x.Id, "ProductAttributeOption"))); - } - } + attributeOptions.ForEach(x => _toDelete.AddRange(_localizedEntityService.Value.GetLocalizedProperties(x.Id, "ProductAttributeOption"))); + } + } public override void OnAfterSave(IHookedEntity entry) { @@ -97,20 +97,20 @@ public override void OnAfterSave(IHookedEntity entry) } public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; + { + if (_toDelete.Count == 0) + return; - using (var scope = new DbContextScope(autoCommit: false)) - { - using (_localizedEntityService.Value.BeginScope()) - { - _toDelete.Each(x => _localizedEntityService.Value.DeleteLocalizedProperty(x)); - } + using (var scope = new DbContextScope(autoCommit: false)) + { + using (_localizedEntityService.Value.BeginScope()) + { + _toDelete.Each(x => _localizedEntityService.Value.DeleteLocalizedProperty(x)); + } - scope.Commit(); - _toDelete.Clear(); - } - } - } + scope.Commit(); + _toDelete.Clear(); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs index 3a31edcde0..95e22e27ed 100644 --- a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs @@ -7,33 +7,33 @@ namespace SmartStore.Services.Hooks { - public class ProductVariantAttributeHook : DbSaveHook - { - private readonly Lazy _productAttributeService; - private readonly HashSet _toDelete = new HashSet(); + public class ProductVariantAttributeHook : DbSaveHook + { + private readonly Lazy _productAttributeService; + private readonly HashSet _toDelete = new HashSet(); - public ProductVariantAttributeHook(Lazy productAttributeService) - { - _productAttributeService = productAttributeService; - } + public ProductVariantAttributeHook(Lazy productAttributeService) + { + _productAttributeService = productAttributeService; + } - protected override void OnDeleted(ProductVariantAttribute entity, IHookedEntity entry) - { - _toDelete.Add(entity.Id); - } + protected override void OnDeleted(ProductVariantAttribute entity, IHookedEntity entry) + { + _toDelete.Add(entity.Id); + } - public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; + public override void OnAfterSaveCompleted() + { + if (_toDelete.Count == 0) + return; - using (var scope = new DbContextScope(autoCommit: false)) - { - _toDelete.Each(x => _productAttributeService.Value.DeleteProductBundleItemAttributeFilter(x)); - scope.Commit(); - } + using (var scope = new DbContextScope(autoCommit: false)) + { + _toDelete.Each(x => _productAttributeService.Value.DeleteProductBundleItemAttributeFilter(x)); + scope.Commit(); + } - _toDelete.Clear(); - } - } + _toDelete.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs index a1d4d15db5..6d04b04640 100644 --- a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs @@ -7,33 +7,33 @@ namespace SmartStore.Services.Hooks { - public class ProductVariantAttributeValueHook : DbSaveHook - { - private readonly Lazy _productAttributeService; - private readonly HashSet _toDelete = new HashSet(); + public class ProductVariantAttributeValueHook : DbSaveHook + { + private readonly Lazy _productAttributeService; + private readonly HashSet _toDelete = new HashSet(); - public ProductVariantAttributeValueHook(Lazy productAttributeService) - { - _productAttributeService = productAttributeService; - } + public ProductVariantAttributeValueHook(Lazy productAttributeService) + { + _productAttributeService = productAttributeService; + } - protected override void OnDeleted(ProductVariantAttributeValue entity, IHookedEntity entry) - { - _toDelete.Add(entity); - } + protected override void OnDeleted(ProductVariantAttributeValue entity, IHookedEntity entry) + { + _toDelete.Add(entity); + } - public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; + public override void OnAfterSaveCompleted() + { + if (_toDelete.Count == 0) + return; - using (var scope = new DbContextScope(autoCommit: false)) - { - _toDelete.Each(x => _productAttributeService.Value.DeleteProductBundleItemAttributeFilter(x.ProductVariantAttributeId, x.Id)); - scope.Commit(); - } + using (var scope = new DbContextScope(autoCommit: false)) + { + _toDelete.Each(x => _productAttributeService.Value.DeleteProductBundleItemAttributeFilter(x.ProductVariantAttributeId, x.Id)); + scope.Commit(); + } - _toDelete.Clear(); - } - } + _toDelete.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs b/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs index 3efd100e04..143352aea0 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs @@ -15,421 +15,421 @@ namespace SmartStore.Services.Hooks { - public class SearchQueryAliasHook : DbSaveHook - { - private readonly IComponentContext _ctx; - private readonly Lazy _catalogSearchQueryAliasMapper; - private readonly Lazy> _localizedPropertyRepository; - - private string _errorMessage; - - private static readonly HashSet _candidateTypes = new HashSet(new Type[] - { - typeof(LocalizedProperty), - typeof(SpecificationAttribute), - typeof(SpecificationAttributeOption), - typeof(ProductSpecificationAttribute), - typeof(ProductAttribute), - typeof(ProductAttributeOption), - typeof(ProductVariantAttribute), - typeof(ProductVariantAttributeValue) - }); - - public SearchQueryAliasHook( - IComponentContext ctx, - Lazy catalogSearchQueryAliasMapper, - Lazy> localizedPropertyRepository) - { - _ctx = ctx; - _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; - _localizedPropertyRepository = localizedPropertyRepository; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - #region Utilities - - private bool IsPropertyModified(IHookedEntity entry, string propertyName) - { - var result = false; - - if (entry.State != EntityState.Detached) - { - var prop = entry.Entry.Property(propertyName); - if (prop != null) - { - switch (entry.State) - { - case EntityState.Added: - // OriginalValues cannot be used for entities in the Added state. - result = prop.CurrentValue != null; - break; - case EntityState.Deleted: - // CurrentValues cannot be used for entities in the Deleted state. - result = prop.OriginalValue != null; - break; - default: - result = - (prop.CurrentValue != null && !prop.CurrentValue.Equals(prop.OriginalValue)) || - (prop.OriginalValue != null && !prop.OriginalValue.Equals(prop.CurrentValue)); - break; - } - } - } - - return result; - } - - private void RevertChanges(IHookedEntity entry, string errorMessage) - { - // throw exception in OnBeforeSaveCompleted - _errorMessage = errorMessage; - - // revert changes - if (entry.State == EntityState.Modified) - { - entry.State = EntityState.Unchanged; - } - else if (entry.State == EntityState.Added) - { - entry.State = EntityState.Detached; - } - } - - private bool HasAliasDuplicate( - IHookedEntity entry, - BaseEntity baseEntity, - Func, TEntity, bool> hasDuplicate = null) - where TEntity : BaseEntity - { - if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) - { - var entity = baseEntity as ISearchAlias; - if (entity != null) - { - entity.Alias = SeoExtensions.GetSeName(entity.Alias); - if (entity.Alias.HasValue()) - { - var repository = _ctx.Resolve>(); - var allEntities = repository.TableUntracked as IQueryable; - - //if (allEntities != null && allEntities.Any(x => x.Id != entity.Id && x.Alias == entity.Alias)) - if (allEntities != null) - { - var duplicateExists = hasDuplicate == null - ? allEntities.Any(x => x.Id != entity.Id && x.Alias == entity.Alias) - : hasDuplicate(repository.TableUntracked, (TEntity)entity); - - if (duplicateExists) - { - RevertChanges(entry, string.Concat(T("Common.Error.AliasAlreadyExists", entity.Alias), " ", T("Common.Error.ChooseDifferentValue"))); - return true; - } - } - } - } - } - - return false; - } - - private bool HasAliasDuplicate(IHookedEntity entry, BaseEntity baseEntity) where T1 : BaseEntity where T2 : BaseEntity - { - if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) - { - var entity = baseEntity as ISearchAlias; - if (entity != null) - { - entity.Alias = SeoExtensions.GetSeName(entity.Alias); - if (entity.Alias.HasValue()) - { - var entities1 = _ctx.Resolve>().TableUntracked as IQueryable; - var entities2 = _ctx.Resolve>().TableUntracked as IQueryable; - - var duplicate1 = entities1.FirstOrDefault(x => x.Alias == entity.Alias); - var duplicate2 = entities2.FirstOrDefault(x => x.Alias == entity.Alias); - - if (duplicate1 != null || duplicate2 != null) - { - var type = entry.EntityType; - - if (duplicate1 != null && duplicate1.Id == entity.Id && type == typeof(T1)) - return false; - if (duplicate2 != null && duplicate2.Id == entity.Id && type == typeof(T2)) - return false; - - RevertChanges(entry, string.Concat(T("Common.Error.AliasAlreadyExists", entity.Alias), " ", T("Common.Error.ChooseDifferentValue"))); - return true; - } - } - } - } - - return false; - } - - private bool HasAliasDuplicate(LocalizedProperty property) - { - var existingProperties = _localizedPropertyRepository.Value.Table.Where(x => - x.Id != property.Id && - x.LocaleKey == "Alias" && - x.LocaleKeyGroup == property.LocaleKeyGroup && - x.LanguageId == property.LanguageId && - x.LocaleValue == property.LocaleValue).ToList(); - - if (existingProperties.Count == 0) - { - // Check cases where alias has to be globally unique. - string otherKeyGroup = null; - - if (property.LocaleKeyGroup.IsCaseInsensitiveEqual("SpecificationAttribute")) - otherKeyGroup = "ProductAttribute"; - else if (property.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductAttribute")) - otherKeyGroup = "SpecificationAttribute"; - - if (otherKeyGroup.HasValue()) - { - existingProperties = _localizedPropertyRepository.Value.Table.Where(x => - x.LocaleKey == "Alias" && - x.LocaleKeyGroup == otherKeyGroup && - x.LanguageId == property.LanguageId && - x.LocaleValue == property.LocaleValue).ToList(); - } - - if (existingProperties.Count == 0) - return false; - } - - var toDelete = new List(); - - foreach (var prop in existingProperties) - { - // Check if the related entity exists. The user would not be able to solve an invalidated alias when the related entity does not exist anymore. - var relatedEntityExists = true; - - if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("SpecificationAttribute")) - { - relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; - } - else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("SpecificationAttributeOption")) - { - relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; - } - else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductAttribute")) - { - relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; - } - else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductAttributeOption")) - { - relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; - } - //else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductVariantAttributeValue")) - //{ - // relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; - //} - - if (relatedEntityExists) - { - // We cannot delete any localized property because we are going to throw duplicate alias exception in OnBeforeSaveCompleted. - return true; - } - else - { - // Delete accidentally dead localized properties. - toDelete.Add(prop); - } - } - - if (toDelete.Any()) - { - try - { - _localizedPropertyRepository.Value.DeleteRange(toDelete); - } - catch (Exception exception) - { - exception.Dump(); - } - } - - return false; - } - - private bool HasEntityDuplicate( - IHookedEntity entry, - BaseEntity baseEntity, - Func getName, - Expression> getDuplicate) where TEntity : BaseEntity - { - if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) - { - var repository = _ctx.Resolve>(); - var allEntities = repository.TableUntracked as IQueryable; - - var existingEntity = allEntities.FirstOrDefault(getDuplicate); - if (existingEntity != null && existingEntity.Id != baseEntity.Id) - { - RevertChanges(entry, string.Concat(T("Common.Error.OptionAlreadyExists", getName(existingEntity).NaIfEmpty()), " ", T("Common.Error.ChooseDifferentValue"))); - return true; - } - } - - return false; - } - - #endregion - - protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) - { - HookObject(entity, entry); - } - - protected override void OnInserting(BaseEntity entity, IHookedEntity entry) - { - HookObject(entity, entry); - } - - protected override void OnUpdating(BaseEntity entity, IHookedEntity entry) - { - HookObject(entity, entry); - } - - public override void OnBeforeSaveCompleted() - { - if (_errorMessage.HasValue()) - { - var message = string.Copy(_errorMessage); - _errorMessage = null; - - throw new SmartException(message); - } - } - - private void HookObject(BaseEntity baseEntity, IHookedEntity entry) - { - var type = entry.EntityType; - - if (!_candidateTypes.Contains(type)) - throw new NotSupportedException(); - - if (type == typeof(SpecificationAttribute)) - { - if (HasAliasDuplicate(entry, baseEntity)) - return; - - if (IsPropertyModified(entry, "Alias")) - _catalogSearchQueryAliasMapper.Value.ClearAttributeCache(); - } - else if (type == typeof(SpecificationAttributeOption)) - { - var entity = (SpecificationAttributeOption)baseEntity; - - if (HasEntityDuplicate(entry, baseEntity, x => x.Name, - x => x.SpecificationAttributeId == entity.SpecificationAttributeId && x.Name == entity.Name)) - return; - - if (HasAliasDuplicate(entry, baseEntity)) - return; - - if (IsPropertyModified(entry, "Alias")) - _catalogSearchQueryAliasMapper.Value.ClearAttributeCache(); - } - else if (type == typeof(ProductSpecificationAttribute)) - { - var entity = (ProductSpecificationAttribute)baseEntity; - - if (HasEntityDuplicate(entry, baseEntity, x => x.SpecificationAttributeOption?.Name, - x => x.ProductId == entity.ProductId && x.SpecificationAttributeOptionId == entity.SpecificationAttributeOptionId)) - return; - } - else if (type == typeof(ProductAttribute)) - { - if (HasAliasDuplicate(entry, baseEntity)) - return; - - if (IsPropertyModified(entry, "Alias")) - _catalogSearchQueryAliasMapper.Value.ClearVariantCache(); - } - else if (type == typeof(ProductAttributeOption)) - { - var entity = (ProductAttributeOption)baseEntity; - - if (HasEntityDuplicate(entry, baseEntity, x => x.Name, - x => x.ProductAttributeOptionsSetId == entity.ProductAttributeOptionsSetId && x.Name == entity.Name)) - return; - - // ClearVariantCache() not necessary - if (HasAliasDuplicate(entry, baseEntity)) - return; - } - else if (type == typeof(ProductVariantAttribute)) - { - var entity = (ProductVariantAttribute)baseEntity; - - if (HasEntityDuplicate(entry, baseEntity, x => x.ProductAttribute?.Name, - x => x.ProductId == entity.ProductId && x.ProductAttributeId == entity.ProductAttributeId)) - return; - } - else if (type == typeof(ProductVariantAttributeValue)) - { - var entity = (ProductVariantAttributeValue)baseEntity; - - if (HasEntityDuplicate(entry, baseEntity, x => x.Name, - x => x.ProductVariantAttributeId == entity.ProductVariantAttributeId && x.Name == entity.Name)) - return; - - if (HasAliasDuplicate(entry, baseEntity, - (all, e) => all.Any(x => x.Id != e.Id && x.ProductVariantAttributeId == e.ProductVariantAttributeId && x.Alias == e.Alias))) - return; - - if (IsPropertyModified(entry, "Alias")) - _catalogSearchQueryAliasMapper.Value.ClearVariantCache(); - } - else if (type == typeof(LocalizedProperty)) - { - // note: not fired when SpecificationAttribute or SpecificationAttributeOption deleted. - // not necessary anyway because cache cleared by above code. - var prop = (LocalizedProperty)baseEntity; - var keyGroup = prop.LocaleKeyGroup; - - if (!prop.LocaleKey.IsCaseInsensitiveEqual("Alias")) - return; - - // validating ProductVariantAttributeValue goes too far here. - if (!keyGroup.IsCaseInsensitiveEqual("SpecificationAttribute") && - !keyGroup.IsCaseInsensitiveEqual("SpecificationAttributeOption") && - !keyGroup.IsCaseInsensitiveEqual("ProductAttribute") && - !keyGroup.IsCaseInsensitiveEqual("ProductAttributeOption")) - { - return; - } - - // check alias duplicate - if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) - { - prop.LocaleValue = SeoExtensions.GetSeName(prop.LocaleValue); - if (prop.LocaleValue.HasValue() && HasAliasDuplicate(prop)) - { - RevertChanges(entry, string.Concat(T("Common.Error.AliasAlreadyExists", prop.LocaleValue), " ", T("Common.Error.ChooseDifferentValue"))); - return; - } - } - - if (IsPropertyModified(entry, "LocaleValue")) - { - if (keyGroup.IsCaseInsensitiveEqual("SpecificationAttribute") || keyGroup.IsCaseInsensitiveEqual("SpecificationAttributeOption")) - { - _catalogSearchQueryAliasMapper.Value.ClearAttributeCache(); - } - else if (keyGroup.IsCaseInsensitiveEqual("ProductAttribute") || keyGroup.IsCaseInsensitiveEqual("ProductVariantAttributeValue")) - { - // not necessary for ProductAttributeOption - _catalogSearchQueryAliasMapper.Value.ClearVariantCache(); - } - } - } - } - } + public class SearchQueryAliasHook : DbSaveHook + { + private readonly IComponentContext _ctx; + private readonly Lazy _catalogSearchQueryAliasMapper; + private readonly Lazy> _localizedPropertyRepository; + + private string _errorMessage; + + private static readonly HashSet _candidateTypes = new HashSet(new Type[] + { + typeof(LocalizedProperty), + typeof(SpecificationAttribute), + typeof(SpecificationAttributeOption), + typeof(ProductSpecificationAttribute), + typeof(ProductAttribute), + typeof(ProductAttributeOption), + typeof(ProductVariantAttribute), + typeof(ProductVariantAttributeValue) + }); + + public SearchQueryAliasHook( + IComponentContext ctx, + Lazy catalogSearchQueryAliasMapper, + Lazy> localizedPropertyRepository) + { + _ctx = ctx; + _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; + _localizedPropertyRepository = localizedPropertyRepository; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #region Utilities + + private bool IsPropertyModified(IHookedEntity entry, string propertyName) + { + var result = false; + + if (entry.State != EntityState.Detached) + { + var prop = entry.Entry.Property(propertyName); + if (prop != null) + { + switch (entry.State) + { + case EntityState.Added: + // OriginalValues cannot be used for entities in the Added state. + result = prop.CurrentValue != null; + break; + case EntityState.Deleted: + // CurrentValues cannot be used for entities in the Deleted state. + result = prop.OriginalValue != null; + break; + default: + result = + (prop.CurrentValue != null && !prop.CurrentValue.Equals(prop.OriginalValue)) || + (prop.OriginalValue != null && !prop.OriginalValue.Equals(prop.CurrentValue)); + break; + } + } + } + + return result; + } + + private void RevertChanges(IHookedEntity entry, string errorMessage) + { + // throw exception in OnBeforeSaveCompleted + _errorMessage = errorMessage; + + // revert changes + if (entry.State == EntityState.Modified) + { + entry.State = EntityState.Unchanged; + } + else if (entry.State == EntityState.Added) + { + entry.State = EntityState.Detached; + } + } + + private bool HasAliasDuplicate( + IHookedEntity entry, + BaseEntity baseEntity, + Func, TEntity, bool> hasDuplicate = null) + where TEntity : BaseEntity + { + if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) + { + var entity = baseEntity as ISearchAlias; + if (entity != null) + { + entity.Alias = SeoExtensions.GetSeName(entity.Alias); + if (entity.Alias.HasValue()) + { + var repository = _ctx.Resolve>(); + var allEntities = repository.TableUntracked as IQueryable; + + //if (allEntities != null && allEntities.Any(x => x.Id != entity.Id && x.Alias == entity.Alias)) + if (allEntities != null) + { + var duplicateExists = hasDuplicate == null + ? allEntities.Any(x => x.Id != entity.Id && x.Alias == entity.Alias) + : hasDuplicate(repository.TableUntracked, (TEntity)entity); + + if (duplicateExists) + { + RevertChanges(entry, string.Concat(T("Common.Error.AliasAlreadyExists", entity.Alias), " ", T("Common.Error.ChooseDifferentValue"))); + return true; + } + } + } + } + } + + return false; + } + + private bool HasAliasDuplicate(IHookedEntity entry, BaseEntity baseEntity) where T1 : BaseEntity where T2 : BaseEntity + { + if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) + { + var entity = baseEntity as ISearchAlias; + if (entity != null) + { + entity.Alias = SeoExtensions.GetSeName(entity.Alias); + if (entity.Alias.HasValue()) + { + var entities1 = _ctx.Resolve>().TableUntracked as IQueryable; + var entities2 = _ctx.Resolve>().TableUntracked as IQueryable; + + var duplicate1 = entities1.FirstOrDefault(x => x.Alias == entity.Alias); + var duplicate2 = entities2.FirstOrDefault(x => x.Alias == entity.Alias); + + if (duplicate1 != null || duplicate2 != null) + { + var type = entry.EntityType; + + if (duplicate1 != null && duplicate1.Id == entity.Id && type == typeof(T1)) + return false; + if (duplicate2 != null && duplicate2.Id == entity.Id && type == typeof(T2)) + return false; + + RevertChanges(entry, string.Concat(T("Common.Error.AliasAlreadyExists", entity.Alias), " ", T("Common.Error.ChooseDifferentValue"))); + return true; + } + } + } + } + + return false; + } + + private bool HasAliasDuplicate(LocalizedProperty property) + { + var existingProperties = _localizedPropertyRepository.Value.Table.Where(x => + x.Id != property.Id && + x.LocaleKey == "Alias" && + x.LocaleKeyGroup == property.LocaleKeyGroup && + x.LanguageId == property.LanguageId && + x.LocaleValue == property.LocaleValue).ToList(); + + if (existingProperties.Count == 0) + { + // Check cases where alias has to be globally unique. + string otherKeyGroup = null; + + if (property.LocaleKeyGroup.IsCaseInsensitiveEqual("SpecificationAttribute")) + otherKeyGroup = "ProductAttribute"; + else if (property.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductAttribute")) + otherKeyGroup = "SpecificationAttribute"; + + if (otherKeyGroup.HasValue()) + { + existingProperties = _localizedPropertyRepository.Value.Table.Where(x => + x.LocaleKey == "Alias" && + x.LocaleKeyGroup == otherKeyGroup && + x.LanguageId == property.LanguageId && + x.LocaleValue == property.LocaleValue).ToList(); + } + + if (existingProperties.Count == 0) + return false; + } + + var toDelete = new List(); + + foreach (var prop in existingProperties) + { + // Check if the related entity exists. The user would not be able to solve an invalidated alias when the related entity does not exist anymore. + var relatedEntityExists = true; + + if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("SpecificationAttribute")) + { + relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; + } + else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("SpecificationAttributeOption")) + { + relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; + } + else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductAttribute")) + { + relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; + } + else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductAttributeOption")) + { + relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; + } + //else if (prop.LocaleKeyGroup.IsCaseInsensitiveEqual("ProductVariantAttributeValue")) + //{ + // relatedEntityExists = _ctx.Resolve>().GetById(prop.EntityId) != null; + //} + + if (relatedEntityExists) + { + // We cannot delete any localized property because we are going to throw duplicate alias exception in OnBeforeSaveCompleted. + return true; + } + else + { + // Delete accidentally dead localized properties. + toDelete.Add(prop); + } + } + + if (toDelete.Any()) + { + try + { + _localizedPropertyRepository.Value.DeleteRange(toDelete); + } + catch (Exception exception) + { + exception.Dump(); + } + } + + return false; + } + + private bool HasEntityDuplicate( + IHookedEntity entry, + BaseEntity baseEntity, + Func getName, + Expression> getDuplicate) where TEntity : BaseEntity + { + if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) + { + var repository = _ctx.Resolve>(); + var allEntities = repository.TableUntracked as IQueryable; + + var existingEntity = allEntities.FirstOrDefault(getDuplicate); + if (existingEntity != null && existingEntity.Id != baseEntity.Id) + { + RevertChanges(entry, string.Concat(T("Common.Error.OptionAlreadyExists", getName(existingEntity).NaIfEmpty()), " ", T("Common.Error.ChooseDifferentValue"))); + return true; + } + } + + return false; + } + + #endregion + + protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) + { + HookObject(entity, entry); + } + + protected override void OnInserting(BaseEntity entity, IHookedEntity entry) + { + HookObject(entity, entry); + } + + protected override void OnUpdating(BaseEntity entity, IHookedEntity entry) + { + HookObject(entity, entry); + } + + public override void OnBeforeSaveCompleted() + { + if (_errorMessage.HasValue()) + { + var message = string.Copy(_errorMessage); + _errorMessage = null; + + throw new SmartException(message); + } + } + + private void HookObject(BaseEntity baseEntity, IHookedEntity entry) + { + var type = entry.EntityType; + + if (!_candidateTypes.Contains(type)) + throw new NotSupportedException(); + + if (type == typeof(SpecificationAttribute)) + { + if (HasAliasDuplicate(entry, baseEntity)) + return; + + if (IsPropertyModified(entry, "Alias")) + _catalogSearchQueryAliasMapper.Value.ClearAttributeCache(); + } + else if (type == typeof(SpecificationAttributeOption)) + { + var entity = (SpecificationAttributeOption)baseEntity; + + if (HasEntityDuplicate(entry, baseEntity, x => x.Name, + x => x.SpecificationAttributeId == entity.SpecificationAttributeId && x.Name == entity.Name)) + return; + + if (HasAliasDuplicate(entry, baseEntity)) + return; + + if (IsPropertyModified(entry, "Alias")) + _catalogSearchQueryAliasMapper.Value.ClearAttributeCache(); + } + else if (type == typeof(ProductSpecificationAttribute)) + { + var entity = (ProductSpecificationAttribute)baseEntity; + + if (HasEntityDuplicate(entry, baseEntity, x => x.SpecificationAttributeOption?.Name, + x => x.ProductId == entity.ProductId && x.SpecificationAttributeOptionId == entity.SpecificationAttributeOptionId)) + return; + } + else if (type == typeof(ProductAttribute)) + { + if (HasAliasDuplicate(entry, baseEntity)) + return; + + if (IsPropertyModified(entry, "Alias")) + _catalogSearchQueryAliasMapper.Value.ClearVariantCache(); + } + else if (type == typeof(ProductAttributeOption)) + { + var entity = (ProductAttributeOption)baseEntity; + + if (HasEntityDuplicate(entry, baseEntity, x => x.Name, + x => x.ProductAttributeOptionsSetId == entity.ProductAttributeOptionsSetId && x.Name == entity.Name)) + return; + + // ClearVariantCache() not necessary + if (HasAliasDuplicate(entry, baseEntity)) + return; + } + else if (type == typeof(ProductVariantAttribute)) + { + var entity = (ProductVariantAttribute)baseEntity; + + if (HasEntityDuplicate(entry, baseEntity, x => x.ProductAttribute?.Name, + x => x.ProductId == entity.ProductId && x.ProductAttributeId == entity.ProductAttributeId)) + return; + } + else if (type == typeof(ProductVariantAttributeValue)) + { + var entity = (ProductVariantAttributeValue)baseEntity; + + if (HasEntityDuplicate(entry, baseEntity, x => x.Name, + x => x.ProductVariantAttributeId == entity.ProductVariantAttributeId && x.Name == entity.Name)) + return; + + if (HasAliasDuplicate(entry, baseEntity, + (all, e) => all.Any(x => x.Id != e.Id && x.ProductVariantAttributeId == e.ProductVariantAttributeId && x.Alias == e.Alias))) + return; + + if (IsPropertyModified(entry, "Alias")) + _catalogSearchQueryAliasMapper.Value.ClearVariantCache(); + } + else if (type == typeof(LocalizedProperty)) + { + // note: not fired when SpecificationAttribute or SpecificationAttributeOption deleted. + // not necessary anyway because cache cleared by above code. + var prop = (LocalizedProperty)baseEntity; + var keyGroup = prop.LocaleKeyGroup; + + if (!prop.LocaleKey.IsCaseInsensitiveEqual("Alias")) + return; + + // validating ProductVariantAttributeValue goes too far here. + if (!keyGroup.IsCaseInsensitiveEqual("SpecificationAttribute") && + !keyGroup.IsCaseInsensitiveEqual("SpecificationAttributeOption") && + !keyGroup.IsCaseInsensitiveEqual("ProductAttribute") && + !keyGroup.IsCaseInsensitiveEqual("ProductAttributeOption")) + { + return; + } + + // check alias duplicate + if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) + { + prop.LocaleValue = SeoExtensions.GetSeName(prop.LocaleValue); + if (prop.LocaleValue.HasValue() && HasAliasDuplicate(prop)) + { + RevertChanges(entry, string.Concat(T("Common.Error.AliasAlreadyExists", prop.LocaleValue), " ", T("Common.Error.ChooseDifferentValue"))); + return; + } + } + + if (IsPropertyModified(entry, "LocaleValue")) + { + if (keyGroup.IsCaseInsensitiveEqual("SpecificationAttribute") || keyGroup.IsCaseInsensitiveEqual("SpecificationAttributeOption")) + { + _catalogSearchQueryAliasMapper.Value.ClearAttributeCache(); + } + else if (keyGroup.IsCaseInsensitiveEqual("ProductAttribute") || keyGroup.IsCaseInsensitiveEqual("ProductVariantAttributeValue")) + { + // not necessary for ProductAttributeOption + _catalogSearchQueryAliasMapper.Value.ClearVariantCache(); + } + } + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/SettingSaveHook.cs b/src/Libraries/SmartStore.Services/Hooks/SettingSaveHook.cs index 4e1e9fea79..8099c8c115 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SettingSaveHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SettingSaveHook.cs @@ -5,21 +5,20 @@ namespace SmartStore.Services.Hooks { - public class SettingSaveHook : DbSaveHook - { - private readonly ICustomerActivityService _customerActivityService; - private readonly ILocalizationService _localizationService; + public class SettingSaveHook : DbSaveHook + { + private readonly ICustomerActivityService _customerActivityService; + private readonly ILocalizationService _localizationService; - public SettingSaveHook(ICustomerActivityService customerActivityService, - ILocalizationService localizationService) - { - _customerActivityService = customerActivityService; - _localizationService = localizationService; - } + public SettingSaveHook(ICustomerActivityService customerActivityService, ILocalizationService localizationService) + { + _customerActivityService = customerActivityService; + _localizationService = localizationService; + } - protected override void OnUpdated(Setting entity, IHookedEntity entry) - { - _customerActivityService.InsertActivity("EditSettings", _localizationService.GetResource("ActivityLog.EditSettings"), entity.Name, entity.Value); - } - } + protected override void OnUpdated(Setting entity, IHookedEntity entry) + { + _customerActivityService.InsertActivity("EditSettings", _localizationService.GetResource("ActivityLog.EditSettings"), entity.Name, entity.Value); + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs b/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs index e744e41f2d..d8591ba072 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs @@ -3,42 +3,43 @@ using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Seo; +using SmartStore.Data; using SmartStore.Services.Seo; namespace SmartStore.Services.Common { - public class SlugSupportedHook : DbSaveHook - { - private readonly Lazy _urlRecordService; - private readonly HashSet _toDelete = new HashSet(); + public class SlugSupportedHook : DbSaveHook + { + private readonly Lazy _urlRecordService; + private readonly HashSet _toDelete = new HashSet(); - public SlugSupportedHook(Lazy urlRecordService) - { - _urlRecordService = urlRecordService; - } + public SlugSupportedHook(Lazy urlRecordService) + { + _urlRecordService = urlRecordService; + } - protected override void OnDeleted(ISlugSupported entity, IHookedEntity entry) - { - var entityType = entry.EntityType; - var records = _urlRecordService.Value.GetUrlRecordsFor(entityType.Name, entry.Entity.Id); - _toDelete.AddRange(records); - } + protected override void OnDeleted(ISlugSupported entity, IHookedEntity entry) + { + var entityType = entry.EntityType; + var records = _urlRecordService.Value.GetUrlRecordsFor(entityType.Name, entry.Entity.Id); + _toDelete.AddRange(records); + } - public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; + public override void OnAfterSaveCompleted() + { + if (_toDelete.Count == 0) + return; - using (var scope = new DbContextScope(autoCommit: false)) - { - using (_urlRecordService.Value.BeginScope()) - { - _toDelete.Each(x => _urlRecordService.Value.DeleteUrlRecord(x)); - } + using (var scope = new DbContextScope(autoCommit: false)) + { + using (_urlRecordService.Value.BeginScope()) + { + _toDelete.Each(x => _urlRecordService.Value.DeleteUrlRecord(x)); + } - scope.Commit(); - _toDelete.Clear(); - } - } - } + scope.Commit(); + _toDelete.Clear(); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs b/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs index a7fed44987..0bd7d36ded 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs @@ -1,4 +1,6 @@ -using Autofac; +using System; +using System.Linq; +using Autofac; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; @@ -7,65 +9,108 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Data; +using SmartStore.Core.Domain.Catalog; +using System.Collections.Generic; +using SmartStore.Services.Catalog; +using SmartStore.Services.Media; +using SmartStore.Core.Domain.Media; namespace SmartStore.Services.Hooks { - public class SoftDeletablePreUpdateHook : DbSaveHook - { - private readonly IComponentContext _ctx; - - public SoftDeletablePreUpdateHook(IComponentContext ctx) - { - _ctx = ctx; - } - - protected override void OnUpdating(ISoftDeletable entity, IHookedEntity entry) - { - var baseEntity = entry.Entity; - - var prop = entry.Entry.Property("Deleted"); - var deletedModified = !prop.CurrentValue.Equals(prop.OriginalValue); - if (!deletedModified) - return; - - var entityType = entry.EntityType; - - // mark orphaned ACL records as idle - var aclSupported = baseEntity as IAclSupported; - if (aclSupported != null && aclSupported.SubjectToAcl) - { - var shouldSetIdle = entity.Deleted; - var rsAclRecord = _ctx.Resolve>(); - - var aclService = _ctx.Resolve(); - var records = aclService.GetAclRecordsFor(entityType.Name, baseEntity.Id); - foreach (var record in records) - { - record.IsIdle = shouldSetIdle; - aclService.UpdateAclRecord(record); - } - } - - // Delete orphaned inactive UrlRecords. - // We keep the active ones on purpose in order to be able to fully restore a soft deletable entity once we implemented the "recycle bin" feature - var slugSupported = baseEntity as ISlugSupported; - if (slugSupported != null && entity.Deleted) - { - var rsUrlRecord = _ctx.Resolve>(); - - var urlRecordService = _ctx.Resolve(); - var activeRecords = urlRecordService.GetUrlRecordsFor(entityType.Name, baseEntity.Id); - using (urlRecordService.BeginScope()) - { - foreach (var record in activeRecords) - { - if (!record.IsActive) - { - urlRecordService.DeleteUrlRecord(record); - } - } - } - } - } - } + public class SoftDeletablePreUpdateHook : DbSaveHook + { + private readonly IComponentContext _ctx; + + public SoftDeletablePreUpdateHook(IComponentContext ctx) + { + _ctx = ctx; + } + + protected override void OnUpdating(ISoftDeletable entity, IHookedEntity entry) + { + var baseEntity = entry.Entity; + + var prop = entry.Entry.Property(nameof(ISoftDeletable.Deleted)); + var deletedModified = !prop.CurrentValue.Equals(prop.OriginalValue); + if (!deletedModified) + return; + + var entityType = entry.EntityType; + + // Mark orphaned ACL records as idle + if (baseEntity is IAclSupported aclSupported && aclSupported.SubjectToAcl) + { + var shouldSetIdle = entity.Deleted; + var aclService = _ctx.Resolve(); + var records = aclService.GetAclRecordsFor(entityType.Name, baseEntity.Id); + foreach (var record in records) + { + record.IsIdle = shouldSetIdle; + aclService.UpdateAclRecord(record); + } + } + + if (!entity.Deleted) + return; // Nothing to process from here on. + + // Delete orphaned inactive UrlRecords. + // We keep the active ones on purpose in order to be able to fully restore a soft deletable entity once we implemented the "recycle bin" feature + if (baseEntity is ISlugSupported) + { + var urlRecordService = _ctx.Resolve(); + var activeRecords = urlRecordService.GetUrlRecordsFor(entityType.Name, baseEntity.Id); + using (urlRecordService.BeginScope()) + { + foreach (var record in activeRecords) + { + if (!record.IsActive) + { + urlRecordService.DeleteUrlRecord(record); + } + } + } + } + + // Unassign any media file and let the media tracker reliably clean up orphaned files later. + // TODO: Once we implement "recycle bin" feature for catalog entities, don't unassign anymore. + if (baseEntity is Product product) + { + product.MainPictureId = null; + + var productService = _ctx.Resolve(); + var mediaTracker = _ctx.Resolve(); + var tracks = new List(); + + foreach (var pm in product.ProductPictures.ToList()) + { + productService.DeleteProductPicture(pm); + + tracks.Add(new MediaTrack + { + EntityId = pm.Id, + EntityName = nameof(ProductMediaFile), + MediaFileId = pm.MediaFileId, + Property = nameof(pm.MediaFileId), + Operation = MediaTrackOperation.Untrack + }); + } + + // We have to untrack manually, because the tracker hook will not run in the scope of another hook. + mediaTracker.TrackMany(SystemAlbumProvider.Catalog, tracks); + } + + // Although nested hooks do not run, the following stuff will still trigger the tracker hook, + // because this hook runs before the tracker hook. + + if (baseEntity is Category category) + { + category.MediaFileId = null; + } + + if (baseEntity is Manufacturer manufacturer) + { + manufacturer.MediaFileId = null; + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs b/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs index 422ba4c281..7bb0e65992 100644 --- a/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs @@ -4,43 +4,44 @@ using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Stores; +using SmartStore.Data; using SmartStore.Services.Stores; namespace SmartStore.Services.Common { - public class StoreMappingEntityHook : DbSaveHook - { - private readonly Lazy _storeMappingService; - private readonly HashSet _toDelete = new HashSet(); - - public StoreMappingEntityHook(Lazy storeMappingService) - { - _storeMappingService = storeMappingService; - } - - protected override void OnDeleted(IStoreMappingSupported entity, IHookedEntity entry) - { - var entityType = entry.EntityType; - - var records = _storeMappingService.Value - .GetStoreMappingsFor(entityType.Name, entry.Entity.Id) - .ToList(); - - _toDelete.AddRange(records); - } - - public override void OnAfterSaveCompleted() - { - if (_toDelete.Count == 0) - return; - - using (var scope = new DbContextScope(autoCommit: false)) - { - _toDelete.Each(x => _storeMappingService.Value.DeleteStoreMapping(x)); - scope.Commit(); - } - - _toDelete.Clear(); - } - } + public class StoreMappingEntityHook : DbSaveHook + { + private readonly Lazy _storeMappingService; + private readonly HashSet _toDelete = new HashSet(); + + public StoreMappingEntityHook(Lazy storeMappingService) + { + _storeMappingService = storeMappingService; + } + + protected override void OnDeleted(IStoreMappingSupported entity, IHookedEntity entry) + { + var entityType = entry.EntityType; + + var records = _storeMappingService.Value + .GetStoreMappingsFor(entityType.Name, entry.Entity.Id) + .ToList(); + + _toDelete.AddRange(records); + } + + public override void OnAfterSaveCompleted() + { + if (_toDelete.Count == 0) + return; + + using (var scope = new DbContextScope(autoCommit: false)) + { + _toDelete.Each(x => _storeMappingService.Value.DeleteStoreMapping(x)); + scope.Commit(); + } + + _toDelete.Clear(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs b/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs index 16ad944332..dafd38a099 100644 --- a/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs @@ -8,43 +8,43 @@ namespace SmartStore.Services.Hooks { public class StoreSaveHook : DbSaveHook - { - private readonly ITaskScheduler _taskScheduler; - private readonly IStoreService _storeService; - private readonly HttpContextBase _httpContext; + { + private readonly ITaskScheduler _taskScheduler; + private readonly IStoreService _storeService; + private readonly HttpContextBase _httpContext; - public StoreSaveHook(ITaskScheduler taskScheduler, IStoreService storeService, HttpContextBase httpContext) - { - _taskScheduler = taskScheduler; - _storeService = storeService; - _httpContext = httpContext; - } + public StoreSaveHook(ITaskScheduler taskScheduler, IStoreService storeService, HttpContextBase httpContext) + { + _taskScheduler = taskScheduler; + _storeService = storeService; + _httpContext = httpContext; + } - protected override void OnUpdating(Store entity, IHookedEntity entry) - { - } + protected override void OnUpdating(Store entity, IHookedEntity entry) + { + } - protected override void OnInserted(Store entity, IHookedEntity entry) - { - TryChangeSchedulerBaseUrl(); - } + protected override void OnInserted(Store entity, IHookedEntity entry) + { + TryChangeSchedulerBaseUrl(); + } - protected override void OnUpdated(Store entity, IHookedEntity entry) - { - TryChangeSchedulerBaseUrl(); - } + protected override void OnUpdated(Store entity, IHookedEntity entry) + { + TryChangeSchedulerBaseUrl(); + } - protected override void OnDeleted(Store entity, IHookedEntity entry) - { - TryChangeSchedulerBaseUrl(); - } + protected override void OnDeleted(Store entity, IHookedEntity entry) + { + TryChangeSchedulerBaseUrl(); + } - private void TryChangeSchedulerBaseUrl() - { - if (CommonHelper.GetAppSetting("sm:TaskSchedulerBaseUrl").IsWebUrl() == false) - { - _taskScheduler.SetBaseUrl(_storeService, _httpContext); - } - } - } + private void TryChangeSchedulerBaseUrl() + { + if (CommonHelper.GetAppSetting("sm:TaskSchedulerBaseUrl").IsWebUrl() == false) + { + _taskScheduler.SetBaseUrl(_storeService, _httpContext); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Hooks/UpdateCustomerFullNameHook.cs b/src/Libraries/SmartStore.Services/Hooks/UpdateCustomerFullNameHook.cs index 3604142ffa..d47e38523e 100644 --- a/src/Libraries/SmartStore.Services/Hooks/UpdateCustomerFullNameHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/UpdateCustomerFullNameHook.cs @@ -8,36 +8,36 @@ namespace SmartStore.Services.Hooks { [Important] - public class UpdateCustomerFullNameHook : DbSaveHook - { - private static readonly HashSet _candidateProps = new HashSet(new string[] - { - nameof(Customer.Title), - nameof(Customer.Salutation), - nameof(Customer.FirstName), - nameof(Customer.LastName) - }); + public class UpdateCustomerFullNameHook : DbSaveHook + { + private static readonly HashSet _candidateProps = new HashSet(new string[] + { + nameof(Customer.Title), + nameof(Customer.Salutation), + nameof(Customer.FirstName), + nameof(Customer.LastName) + }); - private readonly IComponentContext _ctx; + private readonly IComponentContext _ctx; - public UpdateCustomerFullNameHook(IComponentContext ctx) - { - _ctx = ctx; - } + public UpdateCustomerFullNameHook(IComponentContext ctx) + { + _ctx = ctx; + } - protected override void OnUpdating(Customer entity, IHookedEntity entry) - { - UpdateFullName(entity, entry); - } + protected override void OnUpdating(Customer entity, IHookedEntity entry) + { + UpdateFullName(entity, entry); + } - protected override void OnInserting(Customer entity, IHookedEntity entry) - { - UpdateFullName(entity, entry); - } + protected override void OnInserting(Customer entity, IHookedEntity entry) + { + UpdateFullName(entity, entry); + } - private void UpdateFullName(Customer entity, IHookedEntity entry) - { - var shouldUpdate = entity.IsTransientRecord(); + private void UpdateFullName(Customer entity, IHookedEntity entry) + { + var shouldUpdate = entity.IsTransientRecord(); if (!shouldUpdate) { @@ -45,23 +45,23 @@ private void UpdateFullName(Customer entity, IHookedEntity entry) } if (!shouldUpdate) - { - var modProps = _ctx.Resolve().GetModifiedProperties(entity); - shouldUpdate = _candidateProps.Any(x => modProps.ContainsKey(x)); - } + { + var modProps = _ctx.Resolve().GetModifiedProperties(entity); + shouldUpdate = _candidateProps.Any(x => modProps.ContainsKey(x)); + } if (shouldUpdate) - { - var parts = new[] - { - entity.Salutation, - entity.Title, - entity.FirstName, - entity.LastName - }; + { + var parts = new[] + { + entity.Salutation, + entity.Title, + entity.FirstName, + entity.LastName + }; - entity.FullName = string.Join(" ", parts.Where(x => x.HasValue())).NullEmpty(); - } - } - } + entity.FullName = string.Join(" ", parts.Where(x => x.HasValue())).NullEmpty(); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/ICommonServices.cs b/src/Libraries/SmartStore.Services/ICommonServices.cs index a68e73ca06..8b9e8b0baa 100644 --- a/src/Libraries/SmartStore.Services/ICommonServices.cs +++ b/src/Libraries/SmartStore.Services/ICommonServices.cs @@ -16,59 +16,59 @@ namespace SmartStore.Services { public interface ICommonServices - { - IComponentContext Container { get; } - IApplicationEnvironment ApplicationEnvironment { get; } - ICacheManager Cache { get; } - IRequestCache RequestCache { get; } - IDisplayControl DisplayControl { get; } - IDbContext DbContext { get; } - IStoreContext StoreContext { get; } - IWebHelper WebHelper { get; } - IWorkContext WorkContext { get; } - IEventPublisher EventPublisher { get; } - ILocalizationService Localization { get; } - ICustomerActivityService CustomerActivity { get; } - IMediaService MediaService { get; } - INotifier Notifier { get; } + { + IComponentContext Container { get; } + IApplicationEnvironment ApplicationEnvironment { get; } + ICacheManager Cache { get; } + IRequestCache RequestCache { get; } + IDisplayControl DisplayControl { get; } + IDbContext DbContext { get; } + IStoreContext StoreContext { get; } + IWebHelper WebHelper { get; } + IWorkContext WorkContext { get; } + IEventPublisher EventPublisher { get; } + ILocalizationService Localization { get; } + ICustomerActivityService CustomerActivity { get; } + IMediaService MediaService { get; } + INotifier Notifier { get; } IPermissionService Permissions { get; } ISettingService Settings { get; } - IStoreService StoreService { get; } - IDateTimeHelper DateTimeHelper { get; } - IChronometer Chronometer { get; } - IMessageFactory MessageFactory { get; } - } + IStoreService StoreService { get; } + IDateTimeHelper DateTimeHelper { get; } + IChronometer Chronometer { get; } + IMessageFactory MessageFactory { get; } + } - public static class ICommonServicesExtensions - { - public static TService Resolve(this ICommonServices services) - { - return services.Container.Resolve(); - } + public static class ICommonServicesExtensions + { + public static TService Resolve(this ICommonServices services) + { + return services.Container.Resolve(); + } - public static TService Resolve(this ICommonServices services, object serviceKey) - { - return services.Container.ResolveKeyed(serviceKey); - } + public static TService Resolve(this ICommonServices services, object serviceKey) + { + return services.Container.ResolveKeyed(serviceKey); + } - public static TService ResolveNamed(this ICommonServices services, string serviceName) - { - return services.Container.ResolveNamed(serviceName); - } + public static TService ResolveNamed(this ICommonServices services, string serviceName) + { + return services.Container.ResolveNamed(serviceName); + } - public static object Resolve(this ICommonServices services, Type serviceType) - { - return services.Resolve(null, serviceType); - } + public static object Resolve(this ICommonServices services, Type serviceType) + { + return services.Resolve(null, serviceType); + } - public static object Resolve(this ICommonServices services, object serviceKey, Type serviceType) - { - return services.Container.ResolveKeyed(serviceKey, serviceType); - } + public static object Resolve(this ICommonServices services, object serviceKey, Type serviceType) + { + return services.Container.ResolveKeyed(serviceKey, serviceType); + } - public static object ResolveNamed(this ICommonServices services, string serviceName, Type serviceType) - { - return services.Container.ResolveNamed(serviceName, serviceType); - } - } + public static object ResolveNamed(this ICommonServices services, string serviceName, Type serviceType) + { + return services.Container.ResolveNamed(serviceName, serviceType); + } + } } diff --git a/src/Libraries/SmartStore.Services/IScopedService.cs b/src/Libraries/SmartStore.Services/IScopedService.cs index 3c081b953f..9494016f64 100644 --- a/src/Libraries/SmartStore.Services/IScopedService.cs +++ b/src/Libraries/SmartStore.Services/IScopedService.cs @@ -2,23 +2,23 @@ namespace SmartStore.Services { - public interface IScopedService - { - /// - /// Creates a long running unit of work in which cache eviction is suppressed - /// - /// Specifies whether the cache should be evicted completely on batch disposal - /// A disposable unit of work - IDisposable BeginScope(bool clearCache = true); + public interface IScopedService + { + /// + /// Creates a long running unit of work in which cache eviction is suppressed + /// + /// Specifies whether the cache should be evicted completely on batch disposal + /// A disposable unit of work + IDisposable BeginScope(bool clearCache = true); - /// - /// Gets a value indicating whether data has changed during a request, making cache eviction necessary. - /// - /// - /// Cache eviction sets this member to false - /// - bool HasChanges { get; } + /// + /// Gets a value indicating whether data has changed during a request, making cache eviction necessary. + /// + /// + /// Cache eviction sets this member to false + /// + bool HasChanges { get; } - void ClearCache(); - } + void ClearCache(); + } } diff --git a/src/Libraries/SmartStore.Services/Localization/ILanguageService.cs b/src/Libraries/SmartStore.Services/Localization/ILanguageService.cs index a717f199ca..a0e7e7009e 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILanguageService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILanguageService.cs @@ -62,34 +62,34 @@ public partial interface ILanguageService /// Language void UpdateLanguage(Language language); - /// - /// Determines whether a language is active/published - /// - /// The SEO code of the language to check - /// The store id - /// true when the language is published, false otherwise - bool IsPublishedLanguage(string seoCode, int storeId = 0); + /// + /// Determines whether a language is active/published + /// + /// The SEO code of the language to check + /// The store id + /// true when the language is published, false otherwise + bool IsPublishedLanguage(string seoCode, int storeId = 0); - /// - /// Determines whether a language is active/published - /// - /// The id of the language to check - /// The store id - /// true when the language is published, false otherwise - bool IsPublishedLanguage(int languageId, int storeId = 0); + /// + /// Determines whether a language is active/published + /// + /// The id of the language to check + /// The store id + /// true when the language is published, false otherwise + bool IsPublishedLanguage(int languageId, int storeId = 0); - /// - /// Gets the seo code of the default (first) active language - /// - /// The store id - /// The seo code - string GetDefaultLanguageSeoCode(int storeId = 0); + /// + /// Gets the seo code of the default (first) active language + /// + /// The store id + /// The seo code + string GetDefaultLanguageSeoCode(int storeId = 0); - /// - /// Gets the id of the default (first) active language - /// - /// The store id - /// The language id - int GetDefaultLanguageId(int storeId = 0); + /// + /// Gets the id of the default (first) active language + /// + /// The store id + /// The language id + int GetDefaultLanguageId(int storeId = 0); } } diff --git a/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs index 09f2d9bf1a..00ac0273ee 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs @@ -5,14 +5,15 @@ using SmartStore.Core; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.IO; using SmartStore.Core.Plugins; namespace SmartStore.Services.Localization { - /// - /// Localization manager interface - /// - public partial interface ILocalizationService + /// + /// Localization manager interface + /// + public partial interface ILocalizationService { /// /// Deletes a locale string resource @@ -20,12 +21,12 @@ public partial interface ILocalizationService /// Locale string resource void DeleteLocaleStringResource(LocaleStringResource localeStringResource); - /// - /// Deletes all string resources with its key beginning with rootKey. - /// - /// e.g. Plugins.Import.Biz - /// Number of deleted string resources - int DeleteLocaleStringResources(string key, bool keyIsRootKey = true); + /// + /// Deletes all string resources with its key beginning with rootKey. + /// + /// e.g. Plugins.Import.Biz + /// Number of deleted string resources + int DeleteLocaleStringResources(string key, bool keyIsRootKey = true); /// /// Gets a locale string resource @@ -57,19 +58,19 @@ public partial interface ILocalizationService /// Locale string resources IQueryable All(int languageId); - /// - /// Gets all locale string resources by language identifier starting with a prefix - /// - /// Pattern - /// Language identifier - /// Locale string resources matching language id and starting with - IList GetResourcesByPattern(string pattern, int languageId); + /// + /// Gets all locale string resources by language identifier starting with a prefix + /// + /// Pattern + /// Language identifier + /// Locale string resources matching language id and starting with + IList GetResourcesByPattern(string pattern, int languageId); - /// - /// Inserts a locale string resource - /// - /// Locale string resource - void InsertLocaleStringResource(LocaleStringResource localeStringResource); + /// + /// Inserts a locale string resource + /// + /// Locale string resource + void InsertLocaleStringResource(LocaleStringResource localeStringResource); /// /// Updates the locale string resource @@ -96,36 +97,41 @@ public partial interface ILocalizationService /// Result in XML format string ExportResourcesToXml(Language language); - /// - /// Import language resources from XML file - /// - /// codehint: sm-edit - /// Language - /// XML document - /// Prefix for resource key name - /// Specifies whether resource should be inserted or updated (or both) - /// Specifies whether user touched resources should also be updated - /// The number of processed (added or updated) resource entries - int ImportResourcesFromXml(Language language, + /// + /// Import language resources from XML file + /// + /// Language + /// XML document + /// Prefix for resource key name + /// Specifies whether resource should be inserted or updated (or both) + /// Specifies whether user touched resources should also be updated + /// The number of processed (added or updated) resource entries + int ImportResourcesFromXml(Language language, XmlDocument xmlDocument, string rootKey = null, bool sourceIsPlugin = false, ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, bool updateTouchedResources = false); - /// - /// Import plugin resources from xml files in plugin's localization directory. Notes: Deletes existing resources before importing. - /// - /// codehint: sm-add - /// Descriptor of the plugin - /// Load them into the passed list rather than into database - /// Specifies whether user touched resources should also be updated - /// Import only files for particular languages - void ImportPluginResourcesFromXml( - PluginDescriptor pluginDescriptor, - IList targetList = null, - bool updateTouchedResources = true, - IList filterLanguages = null); + /// + /// Creates a directory hasher used to determine plugin localization changes across app startups. + /// + /// Descriptor of the plugin + /// The hasher impl + DirectoryHasher CreatePluginResourcesHasher(PluginDescriptor pluginDescriptor); + + /// + /// Import plugin resources from xml files in plugin's localization directory. Notes: Deletes existing resources before importing. + /// + /// Descriptor of the plugin + /// Load them into the passed list rather than into database + /// Specifies whether user touched resources should also be updated + /// Import only files for particular languages + void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null); /// /// Flattens all nested LocaleResource child nodes into a new document diff --git a/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs index 979ef91ca5..c07ffe0766 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using SmartStore.Core; +using SmartStore.Core.Configuration; using SmartStore.Core.Domain.Localization; namespace SmartStore.Services.Localization @@ -10,7 +11,7 @@ namespace SmartStore.Services.Localization /// Localized entity service interface /// public partial interface ILocalizedEntityService : IScopedService - { + { /// /// Deletes a localized property /// @@ -42,44 +43,44 @@ public partial interface ILocalizedEntityService : IScopedService /// Localized properties IList GetLocalizedProperties(int entityId, string localeKeyGroup); - /// - /// Prefetches a collection of localized properties for a range of entities in one go - /// and caches them for the duration of the current request. - /// - /// Locale key group (scope) - /// - /// The entity ids to prefetch translations for. Can be null, - /// in which case all translations for the requested scope are loaded. - /// - /// Whether represents a range of ids (perf). - /// Whether is already sorted (perf). - /// Localized property collection - /// - /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). - /// - void PrefetchLocalizedProperties(string localeKeyGroup, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false); + /// + /// Prefetches a collection of localized properties for a range of entities in one go + /// and caches them for the duration of the current request. + /// + /// Locale key group (scope) + /// + /// The entity ids to prefetch translations for. Can be null, + /// in which case all translations for the requested scope are loaded. + /// + /// Whether represents a range of ids (perf). + /// Whether is already sorted (perf). + /// Localized property collection + /// + /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). + /// + void PrefetchLocalizedProperties(string localeKeyGroup, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false); - /// - /// Gets a collection of localized properties for a range of entities in one go. - /// - /// Locale key group (scope) - /// - /// The entity ids to load translations for. Can be null, - /// in which case all translations for the requested scope are loaded. - /// - /// Whether represents a range of ids (perf). - /// Whether is already sorted (perf). - /// Localized property collection - /// - /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). - /// - LocalizedPropertyCollection GetLocalizedPropertyCollection(string localeKeyGroup, int[] entityIds, bool isRange = false, bool isSorted = false); + /// + /// Gets a collection of localized properties for a range of entities in one go. + /// + /// Locale key group (scope) + /// + /// The entity ids to load translations for. Can be null, + /// in which case all translations for the requested scope are loaded. + /// + /// Whether represents a range of ids (perf). + /// Whether is already sorted (perf). + /// Localized property collection + /// + /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). + /// + LocalizedPropertyCollection GetLocalizedPropertyCollection(string localeKeyGroup, int[] entityIds, bool isRange = false, bool isSorted = false); - /// - /// Inserts a localized property - /// - /// Localized property - void InsertLocalizedProperty(LocalizedProperty localizedProperty); + /// + /// Inserts a localized property + /// + /// Localized property + void InsertLocalizedProperty(LocalizedProperty localizedProperty); /// /// Updates the localized property @@ -93,18 +94,45 @@ public partial interface ILocalizedEntityService : IScopedService /// Type /// Entity /// Key selector - /// Locale value + /// Locale value /// Language ID void SaveLocalizedValue( - T entity, + T entity, Expression> keySelector, - string localeValue, + string value, int languageId) where T : BaseEntity, ILocalizedEntity; + + /// + /// Save localized value + /// + /// Type + /// Property type + /// Entity + /// Key selector + /// Locale value + /// Language ID void SaveLocalizedValue( - T entity, + T entity, Expression> keySelector, - TPropType localeValue, + TPropType value, int languageId) where T : BaseEntity, ILocalizedEntity; + + /// + /// Save localized setting value + /// + /// Setting impl type + /// Property type + /// Settings instance + /// Key selector + /// Locale value + /// Language ID + /// Store ID + void SaveLocalizedSetting( + TSetting settings, + Expression> keySelector, + TPropType value, + int languageId, + int storeId = 0) where TSetting : class, ISettings; } } diff --git a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs index 4ac2289f38..65f58ab09a 100644 --- a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs @@ -19,40 +19,38 @@ public partial class LanguageService : ILanguageService private const string LANGUAGES_PATTERN_KEY = "SmartStore.language.*"; private readonly IRepository _languageRepository; - private readonly IStoreMappingService _storeMappingService; - private readonly IStoreService _storeService; - private readonly IStoreContext _storeContext; + private readonly IStoreMappingService _storeMappingService; + private readonly IStoreService _storeService; + private readonly IStoreContext _storeContext; private readonly IRequestCache _requestCache; - private readonly ICacheManager _cache; - private readonly ISettingService _settingService; + private readonly ICacheManager _cache; + private readonly ISettingService _settingService; private readonly LocalizationSettings _localizationSettings; - private readonly IEventPublisher _eventPublisher; public LanguageService( - IRequestCache requestCache, - ICacheManager cache, - IRepository languageRepository, + IRequestCache requestCache, + ICacheManager cache, + IRepository languageRepository, ISettingService settingService, LocalizationSettings localizationSettings, IEventPublisher eventPublisher, - IStoreMappingService storeMappingService, - IStoreService storeService, - IStoreContext storeContext) + IStoreMappingService storeMappingService, + IStoreService storeService, + IStoreContext storeContext) { _requestCache = requestCache; - _cache = cache; + _cache = cache; _languageRepository = languageRepository; _settingService = settingService; _localizationSettings = localizationSettings; - _eventPublisher = eventPublisher; - _storeMappingService = storeMappingService; - _storeService = storeService; - _storeContext = storeContext; + _storeMappingService = storeMappingService; + _storeService = storeService; + _storeContext = storeContext; } public virtual void DeleteLanguage(Language language) { - Guard.NotNull(language, nameof(language)); + Guard.NotNull(language, nameof(language)); // Update default admin area language (if required) if (_localizationSettings.DefaultAdminLanguageId == language.Id) @@ -67,35 +65,35 @@ public virtual void DeleteLanguage(Language language) } } } - + _languageRepository.Delete(language); // cache - _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); + _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); } - public virtual IList GetAllLanguages(bool showHidden = false, int storeId = 0) + public virtual IList GetAllLanguages(bool showHidden = false, int storeId = 0) { - var query = _languageRepository.Table; + var query = _languageRepository.Table; - if (!showHidden) - { - query = query.Where(x => x.Published); - } + if (!showHidden) + { + query = query.Where(x => x.Published); + } - query = query.OrderBy(x => x.DisplayOrder); + query = query.OrderBy(x => x.DisplayOrder); - var languages = query.ToListCached("db.lang.all.{0}".FormatInvariant(showHidden)); + var languages = query.ToListCached("db.lang.all.{0}".FormatInvariant(showHidden)); - // store mapping - if (storeId > 0) - { - languages = languages - .Where(l => _storeMappingService.Authorize(l, storeId)) - .ToList(); - } + // store mapping + if (storeId > 0) + { + languages = languages + .Where(l => _storeMappingService.Authorize(l, storeId)) + .ToList(); + } - return languages; + return languages; } public virtual int GetLanguagesCount(bool showHidden = false) @@ -115,28 +113,28 @@ public virtual Language GetLanguageById(int languageId) if (languageId == 0) return null; - return _languageRepository.GetByIdCached(languageId, "db.lang.id-" + languageId); - } + return _languageRepository.GetByIdCached(languageId, "db.lang.id-" + languageId); + } public virtual Language GetLanguageByCulture(string culture) { if (!culture.HasValue()) return null; - return _languageRepository.Table - .Where(x => culture.Equals(x.LanguageCulture, StringComparison.InvariantCultureIgnoreCase)) - .FirstOrDefaultCached("db.lang.culture-" + culture); - } + return _languageRepository.Table + .Where(x => culture.Equals(x.LanguageCulture, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefaultCached("db.lang.culture-" + culture); + } public virtual Language GetLanguageBySeoCode(string seoCode) { if (!seoCode.HasValue()) return null; - return _languageRepository.Table - .Where(x => seoCode.Equals(x.UniqueSeoCode, StringComparison.InvariantCultureIgnoreCase)) - .FirstOrDefaultCached("db.lang.seo-" + seoCode); - } + return _languageRepository.Table + .Where(x => seoCode.Equals(x.UniqueSeoCode, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefaultCached("db.lang.seo-" + seoCode); + } public virtual void InsertLanguage(Language language) { @@ -153,7 +151,7 @@ public virtual void UpdateLanguage(Language language) { if (language == null) throw new ArgumentNullException("language"); - + //update language _languageRepository.Update(language); @@ -161,109 +159,109 @@ public virtual void UpdateLanguage(Language language) _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); } - public virtual bool IsPublishedLanguage(string seoCode, int storeId = 0) - { - if (storeId <= 0) - storeId = _storeContext.CurrentStore.Id; - - var map = this.GetStoreLanguageMap(); - if (map.ContainsKey(storeId)) - { - return map[storeId].Any(x => x.UniqueSeoCode == seoCode); - } - - return false; - } - - public virtual bool IsPublishedLanguage(int languageId, int storeId = 0) - { - if (languageId <= 0) - return false; - - if (storeId <= 0) - storeId = _storeContext.CurrentStore.Id; - - var map = this.GetStoreLanguageMap(); - if (map.ContainsKey(storeId)) - { - return map[storeId].Any(x => x.Id == languageId); - } - - return false; - } - - public virtual string GetDefaultLanguageSeoCode(int storeId = 0) - { - if (storeId <= 0) - storeId = _storeContext.CurrentStore.Id; - - var map = this.GetStoreLanguageMap(); - if (map.ContainsKey(storeId)) - { - return map[storeId].FirstOrDefault().UniqueSeoCode; - } - - return null; - } - - public virtual int GetDefaultLanguageId(int storeId = 0) - { - if (storeId <= 0) - storeId = _storeContext.CurrentStore.Id; - - var map = this.GetStoreLanguageMap(); - if (map.ContainsKey(storeId)) - { - return map[storeId].FirstOrDefault().Id; - } - - return 0; - } - - /// - /// Gets a map of active/published store languages - /// - /// A map of store languages where key is the store id and values are tuples of language ids and seo codes - protected virtual Multimap GetStoreLanguageMap() - { - var result = _cache.Get(ServiceCacheBuster.STORE_LANGUAGE_MAP_KEY, () => - { - var map = new Multimap(); - - var allStores = _storeService.GetAllStores(); - foreach (var store in allStores) - { - var languages = GetAllLanguages(false, store.Id); - if (!languages.Any()) - { - // language-less stores aren't allowed but could exist accidentally. Correct this. - var firstStoreLang = GetAllLanguages(true, store.Id).FirstOrDefault(); - if (firstStoreLang == null) - { - // absolute fallback - firstStoreLang = GetAllLanguages(true).FirstOrDefault(); - } - map.Add(store.Id, new MinifiedLanguage { Id = firstStoreLang.Id, UniqueSeoCode = firstStoreLang.UniqueSeoCode }); - } - else - { - foreach (var lang in languages) - { - map.Add(store.Id, new MinifiedLanguage { Id = lang.Id, UniqueSeoCode = lang.UniqueSeoCode }); - } - } - } - - return map; - }, TimeSpan.FromDays(1)); - - return result; - } - - public class MinifiedLanguage - { - public int Id { get; set; } - public string UniqueSeoCode { get; set; } - } + public virtual bool IsPublishedLanguage(string seoCode, int storeId = 0) + { + if (storeId <= 0) + storeId = _storeContext.CurrentStore.Id; + + var map = this.GetStoreLanguageMap(); + if (map.ContainsKey(storeId)) + { + return map[storeId].Any(x => x.UniqueSeoCode == seoCode); + } + + return false; + } + + public virtual bool IsPublishedLanguage(int languageId, int storeId = 0) + { + if (languageId <= 0) + return false; + + if (storeId <= 0) + storeId = _storeContext.CurrentStore.Id; + + var map = this.GetStoreLanguageMap(); + if (map.ContainsKey(storeId)) + { + return map[storeId].Any(x => x.Id == languageId); + } + + return false; + } + + public virtual string GetDefaultLanguageSeoCode(int storeId = 0) + { + if (storeId <= 0) + storeId = _storeContext.CurrentStore.Id; + + var map = this.GetStoreLanguageMap(); + if (map.ContainsKey(storeId)) + { + return map[storeId].FirstOrDefault().UniqueSeoCode; + } + + return null; + } + + public virtual int GetDefaultLanguageId(int storeId = 0) + { + if (storeId <= 0) + storeId = _storeContext.CurrentStore.Id; + + var map = this.GetStoreLanguageMap(); + if (map.ContainsKey(storeId)) + { + return map[storeId].FirstOrDefault().Id; + } + + return 0; + } + + /// + /// Gets a map of active/published store languages + /// + /// A map of store languages where key is the store id and values are tuples of language ids and seo codes + protected virtual Multimap GetStoreLanguageMap() + { + var result = _cache.Get(ServiceCacheBuster.STORE_LANGUAGE_MAP_KEY, () => + { + var map = new Multimap(); + + var allStores = _storeService.GetAllStores(); + foreach (var store in allStores) + { + var languages = GetAllLanguages(false, store.Id); + if (!languages.Any()) + { + // language-less stores aren't allowed but could exist accidentally. Correct this. + var firstStoreLang = GetAllLanguages(true, store.Id).FirstOrDefault(); + if (firstStoreLang == null) + { + // absolute fallback + firstStoreLang = GetAllLanguages(true).FirstOrDefault(); + } + map.Add(store.Id, new MinifiedLanguage { Id = firstStoreLang.Id, UniqueSeoCode = firstStoreLang.UniqueSeoCode }); + } + else + { + foreach (var lang in languages) + { + map.Add(store.Id, new MinifiedLanguage { Id = lang.Id, UniqueSeoCode = lang.UniqueSeoCode }); + } + } + } + + return map; + }, TimeSpan.FromDays(1)); + + return result; + } + + public class MinifiedLanguage + { + public int Id { get; set; } + public string UniqueSeoCode { get; set; } + } } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs index f6ea40c2b1..34accb3b46 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Linq.Expressions; +using System.Runtime.CompilerServices; using System.Xml; using SmartStore.ComponentModel; using SmartStore.Core; +using SmartStore.Core.Configuration; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; @@ -12,8 +14,10 @@ namespace SmartStore.Services.Localization { - public static class LocalizationExtensions + public static class LocalizationExtensions { + #region GetLocalized BaseEntity + /// /// Get localized property of an entity /// @@ -28,6 +32,7 @@ public static LocalizedValue GetLocalized(this T entity, Expression().GetLocalizedValue( entity, + entity.Id, entity.GetEntityName(), invoker.Property.Name, (Func)invoker, @@ -46,17 +51,18 @@ public static LocalizedValue GetLocalized(this T entity, ExpressionA value indicating whether to ensure that we have at least two published languages; otherwise, load only default value /// When true, additionally checks whether the localized value contains empty HTML only and falls back to the default value if so. /// Localized property - public static LocalizedValue GetLocalized(this T entity, - Expression> keySelector, - int languageId, - bool returnDefaultValue = true, + public static LocalizedValue GetLocalized(this T entity, + Expression> keySelector, + int languageId, + bool returnDefaultValue = true, bool ensureTwoPublishedLanguages = true, - bool detectEmptyHtml = false) + bool detectEmptyHtml = false) where T : BaseEntity, ILocalizedEntity { var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( entity, + entity.Id, entity.GetEntityName(), invoker.Property.Name, invoker, @@ -88,6 +94,7 @@ public static LocalizedValue GetLocalized(this T entity, { return EngineContext.Current.Resolve().GetLocalizedValue( entity, + entity.Id, entity.GetEntityName(), localeKey, x => fallback, @@ -119,6 +126,7 @@ public static LocalizedValue GetLocalized(this T entity, var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( entity, + entity.Id, entity.GetEntityName(), invoker.Property.Name, invoker, @@ -151,11 +159,12 @@ public static LocalizedValue GetLocalized(this T entity, var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( entity, + entity.Id, entity.GetEntityName(), invoker.Property.Name, (Func)invoker, languageId, - returnDefaultValue, + returnDefaultValue, ensureTwoPublishedLanguages, detectEmptyHtml); } @@ -183,6 +192,7 @@ public static LocalizedValue GetLocalized(this T entity, var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( entity, + entity.Id, entity.GetEntityName(), invoker.Property.Name, (Func)invoker, @@ -192,22 +202,27 @@ public static LocalizedValue GetLocalized(this T entity, detectEmptyHtml); } - /// - /// Get localized property of an instance - /// - /// Node - /// Key selector - /// Localized property - public static LocalizedValue GetLocalized(this ICategoryNode node, Expression> keySelector) - { + #endregion + + #region GetLocalized ICategoryNode + + /// + /// Get localized property of an instance + /// + /// Node + /// Key selector + /// Localized property + public static LocalizedValue GetLocalized(this ICategoryNode node, Expression> keySelector) + { var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( - node, - "Category", + node, + node.Id, + "Category", invoker.Property.Name, (Func)invoker, - EngineContext.Current.Resolve().WorkingLanguage); - } + null); + } /// /// Get localized property of an instance @@ -220,11 +235,12 @@ public static LocalizedValue GetLocalized(this ICategoryNode node, Expre { var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( - node, - "Category", + node, + node.Id, + nameof(Category), invoker.Property.Name, (Func)invoker, - languageId); + languageId); } /// @@ -238,13 +254,89 @@ public static LocalizedValue GetLocalized(this ICategoryNode node, Expre { var invoker = keySelector.CompileFast(); return EngineContext.Current.Resolve().GetLocalizedValue( - node, - "Category", + node, + node.Id, + nameof(Category), invoker.Property.Name, (Func)invoker, - language); + language); + } + + #endregion + + #region GetLocalized ISettings + + /// + /// Get localized property of an implementation + /// + /// The settings instance + /// Key selector + /// Localized property + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LocalizedValue GetLocalizedSetting(this TSetting settings, + Expression> keySelector, + int? storeId = null, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true, + bool detectEmptyHtml = false) + where TSetting : class, ISettings + { + return GetLocalizedSetting(settings, keySelector, null, storeId, returnDefaultValue, ensureTwoPublishedLanguages, detectEmptyHtml); + } + + /// + /// Get localized property of an implementation + /// + /// The settings instance + /// Key selector + /// Language id, object instance or null + /// Localized property + public static LocalizedValue GetLocalizedSetting(this TSetting settings, + Expression> keySelector, + object requestLanguageIdOrObj, // Id or Language + int? storeId = null, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true, + bool detectEmptyHtml = false) + where TSetting : class, ISettings + { + var helper = EngineContext.Current.Resolve(); + var invoker = keySelector.CompileFast(); + + if (storeId == null) + { + storeId = EngineContext.Current.Resolve().CurrentStore.Id; + } + + // Make fallback only when storeId is 0 and the paramter says so. + var localizedValue = GetValue(storeId.Value, storeId == 0 && returnDefaultValue); + + if (storeId > 0 && string.IsNullOrEmpty(localizedValue.Value)) + { + localizedValue = GetValue(0, returnDefaultValue); + } + + return localizedValue; + + LocalizedValue GetValue(int id /* storeId */, bool doFallback) + { + return helper.GetLocalizedValue( + settings, + id, + typeof(TSetting).Name, + invoker.Property.Name, + (Func)invoker, + requestLanguageIdOrObj, + doFallback, + ensureTwoPublishedLanguages, + detectEmptyHtml); + } } + #endregion + + #region GetLocalizedEnum + /// /// Get localized value of enum /// @@ -252,14 +344,14 @@ public static LocalizedValue GetLocalized(this ICategoryNode node, Expre /// Enum value /// Localization service /// Work context + /// Whether to load the hint. /// Localized value - public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, IWorkContext workContext) + public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, IWorkContext workContext, bool hint = false) where T : struct { - if (workContext == null) - throw new ArgumentNullException(nameof(workContext)); + Guard.NotNull(workContext, nameof(workContext)); - return GetLocalizedEnum(enumValue, localizationService, workContext.WorkingLanguage.Id); + return GetLocalizedEnum(enumValue, localizationService, workContext.WorkingLanguage.Id, hint); } /// @@ -269,30 +361,42 @@ public static string GetLocalizedEnum(this T enumValue, ILocalizationService /// Enum value /// Localization service /// Language identifier + /// Whether to load the hint. /// Localized value - public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, int languageId = 0) + public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, int languageId = 0, bool hint = false) where T : struct { - if (localizationService == null) - throw new ArgumentNullException(nameof(localizationService)); + Guard.NotNull(localizationService, nameof(localizationService)); - if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type"); + if (!typeof(T).IsEnum) + { + throw new ArgumentException("T must be an enumerated type."); + } - //localized value - string resourceName = string.Format("Enums.{0}.{1}", - typeof(T).ToString(), - //Convert.ToInt32(enumValue) + var resourceName = string.Format("Enums.{0}.{1}", + typeof(T).ToString(), enumValue.ToString()); - string result = localizationService.GetResource(resourceName, languageId, false, "", true); + if (hint) + { + resourceName += ".Hint"; + } - // Set default value if required - if (String.IsNullOrEmpty(result)) + var result = localizationService.GetResource(resourceName, languageId, false, "", true); + + // Set default value if required. + if (string.IsNullOrEmpty(result)) + { result = Inflector.Titleize(enumValue.ToString()); + } return result; } + #endregion + + #region Plugin Localization + /// /// Delete a locale resource /// @@ -312,9 +416,9 @@ public static void DeletePluginLocaleResource(this BasePlugin plugin, string res /// Localization service /// Language service /// Resource name - public static void DeletePluginLocaleResource(this BasePlugin plugin, - ILocalizationService localizationService, - ILanguageService languageService, + public static void DeletePluginLocaleResource(this BasePlugin plugin, + ILocalizationService localizationService, + ILanguageService languageService, string resourceName) { // actually plugin instance is not required @@ -346,8 +450,8 @@ public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, { var localizationService = EngineContext.Current.Resolve(); var languageService = EngineContext.Current.Resolve(); - AddOrUpdatePluginLocaleResource(plugin, localizationService, - languageService, resourceName, resourceValue); + AddOrUpdatePluginLocaleResource(plugin, localizationService, + languageService, resourceName, resourceValue); } /// @@ -358,11 +462,11 @@ public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, /// Language service /// Resource name /// Resource value - public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, - ILocalizationService localizationService, - ILanguageService languageService, - string resourceName, - string resourceValue) + public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, + ILocalizationService localizationService, + ILanguageService languageService, + string resourceName, + string resourceValue) { // Actually plugin instance is not required if (plugin == null) @@ -371,7 +475,7 @@ public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, throw new ArgumentNullException("localizationService"); if (languageService == null) throw new ArgumentNullException("languageService"); - + foreach (var lang in languageService.GetAllLanguages(true)) { var lsr = localizationService.GetLocaleStringResourceByName(resourceName, lang.Id, false); @@ -408,63 +512,63 @@ public static void AddPluginLocaleResource(this BasePlugin plugin, ILocalization } } - /// - /// Get localized property value of a plugin - /// - /// Plugin - /// Plugin - /// Localization service - /// Name of the property - /// Language identifier - /// A value indicating whether to return default value (if localized is not found) - /// Localized value - public static string GetLocalizedValue(this T plugin, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) - where T : IPlugin - { - if (plugin == null) - throw new ArgumentNullException(nameof(plugin)); - - if (plugin.PluginDescriptor == null) - throw new ArgumentNullException("PluginDescriptor cannot be loaded"); - - return plugin.PluginDescriptor.GetLocalizedValue(localizationService, propertyName, languageId, returnDefaultValue); - } - - /// - /// Get localized property value of a plugin - /// - /// Plugin descriptor - /// Localization service - /// Name of the property - /// Language identifier - /// A value indicating whether to return default value (if localized is not found) - /// Localized value - public static string GetLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) - { - if (localizationService == null) - throw new ArgumentNullException(nameof(localizationService)); - - if (descriptor == null) - throw new ArgumentNullException(nameof(descriptor)); - - if (propertyName == null) - throw new ArgumentNullException(nameof(propertyName)); - - string systemName = descriptor.SystemName; - string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); - string result = localizationService.GetResource(resourceName, languageId, false, "", true); - - if (String.IsNullOrEmpty(result) && returnDefaultValue) - { - var fastProp = FastProperty.GetProperty(descriptor.GetType(), propertyName); - if (fastProp != null) - { - result = fastProp.GetValue(descriptor) as string; - } - } - - return result; - } + /// + /// Get localized property value of a plugin + /// + /// Plugin + /// Plugin + /// Localization service + /// Name of the property + /// Language identifier + /// A value indicating whether to return default value (if localized is not found) + /// Localized value + public static string GetLocalizedValue(this T plugin, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) + where T : IPlugin + { + if (plugin == null) + throw new ArgumentNullException(nameof(plugin)); + + if (plugin.PluginDescriptor == null) + throw new ArgumentNullException("PluginDescriptor cannot be loaded"); + + return plugin.PluginDescriptor.GetLocalizedValue(localizationService, propertyName, languageId, returnDefaultValue); + } + + /// + /// Get localized property value of a plugin + /// + /// Plugin descriptor + /// Localization service + /// Name of the property + /// Language identifier + /// A value indicating whether to return default value (if localized is not found) + /// Localized value + public static string GetLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) + { + if (localizationService == null) + throw new ArgumentNullException(nameof(localizationService)); + + if (descriptor == null) + throw new ArgumentNullException(nameof(descriptor)); + + if (propertyName == null) + throw new ArgumentNullException(nameof(propertyName)); + + string systemName = descriptor.SystemName; + string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); + string result = localizationService.GetResource(resourceName, languageId, false, "", true); + + if (String.IsNullOrEmpty(result) && returnDefaultValue) + { + var fastProp = FastProperty.GetProperty(descriptor.GetType(), propertyName); + if (fastProp != null) + { + result = fastProp.GetValue(descriptor) as string; + } + } + + return result; + } /// /// Save localized plugin descriptor value @@ -476,7 +580,7 @@ public static string GetLocalizedValue(this PluginDescriptor descriptor, ILocali /// Name of the property /// Localized value public static void SaveLocalizedValue(this T plugin, ILocalizationService localizationService, int languageId, - string propertyName, string value) where T : IPlugin + string propertyName, string value) where T : IPlugin { if (plugin == null) throw new ArgumentNullException("plugin"); @@ -484,7 +588,7 @@ public static void SaveLocalizedValue(this T plugin, ILocalizationService loc if (plugin.PluginDescriptor == null) throw new ArgumentNullException("PluginDescriptor cannot be loaded"); - plugin.PluginDescriptor.SaveLocalizedValue(localizationService, languageId, propertyName, value); + plugin.PluginDescriptor.SaveLocalizedValue(localizationService, languageId, propertyName, value); } /// @@ -496,7 +600,7 @@ public static void SaveLocalizedValue(this T plugin, ILocalizationService loc /// Name of the property /// Localized value public static void SaveLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, int languageId, - string propertyName, string value) + string propertyName, string value) { if (localizationService == null) throw new ArgumentNullException(nameof(localizationService)); @@ -507,8 +611,8 @@ public static void SaveLocalizedValue(this PluginDescriptor descriptor, ILocaliz if (descriptor == null) throw new ArgumentNullException(nameof(descriptor)); - if (propertyName == null) - throw new ArgumentNullException(nameof(propertyName)); + if (propertyName == null) + throw new ArgumentNullException(nameof(propertyName)); string systemName = descriptor.SystemName; string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); @@ -549,8 +653,8 @@ public static void SaveLocalizedValue(this PluginDescriptor descriptor, ILocaliz /// /// Language /// XML - public static void ImportResourcesFromXml(this ILocalizationService service, - Language language, + public static void ImportResourcesFromXml(this ILocalizationService service, + Language language, string xml, string rootKey = null, bool sourceIsPlugin = false, @@ -568,5 +672,7 @@ public static void ImportResourcesFromXml(this ILocalizationService service, service.ImportResourcesFromXml(language, xmlDoc, rootKey, sourceIsPlugin, mode, updateTouchedResources); } + + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs index d85eafb668..abe7e3a8a9 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs @@ -17,83 +17,82 @@ using SmartStore.Core.Plugins; using SmartStore.Core.Localization; using System.Globalization; +using SmartStore.Core.IO; namespace SmartStore.Services.Localization { - public partial class LocalizationService : ILocalizationService + public partial class LocalizationService : ILocalizationService { - /// - /// 0 = segment (first 3 chars of key), 1 = language id - /// - const string LOCALESTRINGRESOURCES_SEGMENT_KEY = "localization:{0}-lang-{1}"; - const string LOCALESTRINGRESOURCES_SEGMENT_PATTERN = "localization:{0}*"; + /// + /// 0 = language id + /// + const string LOCALESTRINGRESOURCES_SEGMENT_KEY = "localization:{0}"; + const string LOCALESTRINGRESOURCES_SEGMENT_PATTERN = "localization:*"; - private readonly IRepository _lsrRepository; + private readonly IRepository _lsrRepo; private readonly IWorkContext _workContext; private readonly ILogger _logger; private readonly ILanguageService _languageService; private readonly ICacheManager _cacheManager; private readonly IDbContext _dbContext; - private readonly IEventPublisher _eventPublisher; - private int _notFoundLogCount = 0; - private int? _defaultLanguageId; + private int _notFoundLogCount = 0; + private int? _defaultLanguageId; public LocalizationService( - ICacheManager cacheManager, - ILogger logger, - IWorkContext workContext, - IRepository lsrRepository, - ILanguageService languageService, - IEventPublisher eventPublisher) + ICacheManager cacheManager, + ILogger logger, + IWorkContext workContext, + IRepository lsrRepository, + ILanguageService languageService) { _cacheManager = cacheManager; _logger = logger; _workContext = workContext; - _lsrRepository = lsrRepository; + _lsrRepo = lsrRepository; _languageService = languageService; - _dbContext = lsrRepository.Context; - _eventPublisher = eventPublisher; + _dbContext = lsrRepository.Context; } public virtual void DeleteLocaleStringResource(LocaleStringResource resource) { - Guard.NotNull(resource, nameof(resource)); + Guard.NotNull(resource, nameof(resource)); // cache - ClearCacheSegment(resource.ResourceName, resource.LanguageId); - + ClearCacheSegment(resource.LanguageId); + // db - _lsrRepository.Delete(resource); + _lsrRepo.Delete(resource); } - public virtual int DeleteLocaleStringResources(string key, bool keyIsRootKey = true) { - int result = 0; + public virtual int DeleteLocaleStringResources(string key, bool keyIsRootKey = true) + { + int result = 0; - if (key.HasValue()) + if (key.HasValue()) { - try + try { - var sqlDelete = "Delete From LocaleStringResource Where ResourceName Like '{0}%'".FormatWith(key.EndsWith(".") || !keyIsRootKey ? key : key + "."); - result = _dbContext.ExecuteSqlCommand(sqlDelete); + var sqlDelete = "Delete From LocaleStringResource Where ResourceName Like '{0}%'".FormatWith(key.EndsWith(".") || !keyIsRootKey ? key : key + "."); + result = _dbContext.ExecuteSqlCommand(sqlDelete); - ClearCacheSegment(key); - } - catch (Exception exc) + ClearCacheSegment(null); + } + catch (Exception exc) { - exc.Dump(); - } - } + exc.Dump(); + } + } - return result; - } + return result; + } public virtual LocaleStringResource GetLocaleStringResourceById(int localeStringResourceId) { if (localeStringResourceId == 0) return null; - var localeStringResource = _lsrRepository.GetById(localeStringResourceId); + var localeStringResource = _lsrRepo.GetById(localeStringResourceId); return localeStringResource; } @@ -107,7 +106,7 @@ public virtual LocaleStringResource GetLocaleStringResourceByName(string resourc public virtual LocaleStringResource GetLocaleStringResourceByName(string resourceName, int languageId, bool logIfNotFound = true) { - var query = from lsr in _lsrRepository.Table + var query = from lsr in _lsrRepo.Table orderby lsr.ResourceName where lsr.LanguageId == languageId && lsr.ResourceName == resourceName select lsr; @@ -121,160 +120,150 @@ orderby lsr.ResourceName public virtual IQueryable All(int languageId) { - var query = from lsr in _lsrRepository.Table + var query = from lsr in _lsrRepo.Table orderby lsr.ResourceName where lsr.LanguageId == languageId select lsr; - return query; + return query; } - public virtual IList GetResourcesByPattern(string pattern, int languageId) - { - Guard.NotEmpty(pattern, nameof(pattern)); + public virtual IList GetResourcesByPattern(string pattern, int languageId) + { + Guard.NotEmpty(pattern, nameof(pattern)); - var query = from l in _lsrRepository.Table - where l.ResourceName.StartsWith(pattern) && l.LanguageId == languageId - select l; + var query = from l in _lsrRepo.Table + where l.ResourceName.StartsWith(pattern) && l.LanguageId == languageId + select l; - var resources = query.ToList(); - return resources; - } + var resources = query.ToList(); + return resources; + } - public virtual void InsertLocaleStringResource(LocaleStringResource resource) + public virtual void InsertLocaleStringResource(LocaleStringResource resource) { - Guard.NotNull(resource, nameof(resource)); + Guard.NotNull(resource, nameof(resource)); - _lsrRepository.Insert(resource); + _lsrRepo.Insert(resource); - // cache - ClearCacheSegment(resource.ResourceName, resource.LanguageId); + // cache + ClearCacheSegment(resource.LanguageId); } public virtual void UpdateLocaleStringResource(LocaleStringResource resource) { - Guard.NotNull(resource, nameof(resource)); + Guard.NotNull(resource, nameof(resource)); // cache - object origKey = null; - if (_dbContext.TryGetModifiedProperty(resource, "ResourceName", out origKey)) - { - ClearCacheSegment((string)origKey, resource.LanguageId); - } - ClearCacheSegment(resource.ResourceName, resource.LanguageId); - - _lsrRepository.Update(resource); + ClearCacheSegment(resource.LanguageId); + + _lsrRepo.Update(resource); } - protected virtual IDictionary GetCacheSegment(string forKey, int languageId) - { - Guard.NotEmpty(forKey, nameof(forKey)); - - var segmentKey = GetSegmentKeyPart(forKey); - var cacheKey = BuildCacheSegmentKey(segmentKey, languageId); - - return _cacheManager.Get(cacheKey, () => - { - var resources = _lsrRepository.TableUntracked - .Where(x => x.ResourceName.StartsWith(segmentKey) && x.LanguageId == languageId) - //.OrderBy(x => x.ResourceName) - .ToList(); - - var dict = new Dictionary(resources.Count); - - foreach (var res in resources) - { - dict[res.ResourceName.ToLowerInvariant()] = res.ResourceValue; - } - - return dict; - }); - } - - /// - /// Clears the cached resource segment from the cache - /// - /// The resource key for which a segment key should be created - /// Language Id. If null, segments for all cached languages will be invalidated - protected virtual void ClearCacheSegment(string forKey, int? languageId = null) - { - var segmentKey = GetSegmentKeyPart(forKey); - - if (languageId.HasValue && languageId.Value > 0) - { - _cacheManager.Remove(BuildCacheSegmentKey(segmentKey, languageId.Value)); - } - else - { - _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_SEGMENT_PATTERN.FormatInvariant(segmentKey)); - } - } + protected virtual IDictionary GetCacheSegment(int languageId) + { + var cacheKey = BuildCacheSegmentKey(languageId); + + return _cacheManager.Get(cacheKey, () => + { + var resources = _lsrRepo.TableUntracked + .Where(x => x.LanguageId == languageId) + .Select(x => new { x.ResourceName, x.ResourceValue }) + //.OrderBy(x => x.ResourceName) + .ToList(); + + var dict = new Dictionary(resources.Count); + + foreach (var res in resources) + { + dict[res.ResourceName.ToLowerInvariant()] = res.ResourceValue; + } + + return dict; + }); + } + + /// + /// Clears the cached resource segment from the cache + /// + /// Language Id. If null, segments for all cached languages will be invalidated + protected virtual void ClearCacheSegment(int? languageId = null) + { + if (languageId.HasValue && languageId.Value > 0) + { + _cacheManager.Remove(BuildCacheSegmentKey(languageId.Value)); + } + else + { + _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_SEGMENT_PATTERN); + } + } public virtual string GetResource( - string resourceKey, - int languageId = 0, - bool logIfNotFound = true, - string defaultValue = "", - bool returnEmptyIfNotFound = false) + string resourceKey, + int languageId = 0, + bool logIfNotFound = true, + string defaultValue = "", + bool returnEmptyIfNotFound = false) { if (languageId <= 0) { if (_workContext.WorkingLanguage == null) - { - return defaultValue; - } - + { + return defaultValue; + } + languageId = _workContext.WorkingLanguage.Id; } - - string result = string.Empty; + + string result = string.Empty; resourceKey = resourceKey.EmptyNull().Trim().ToLowerInvariant(); - var cachedSegment = GetCacheSegment(resourceKey, languageId); + var cachedSegment = GetCacheSegment(languageId); if (!cachedSegment.TryGetValue(resourceKey, out result)) { - if (logIfNotFound) - { - if (_notFoundLogCount < 50) - { - _logger.Warn(string.Format("Resource string ({0}) does not exist. Language ID = {1}", resourceKey, languageId)); - } - else if (_notFoundLogCount == 50) - { - _logger.Warn("Too many language resources do not exist (> 50). Stopped logging missing resources to prevent performance drop."); - } - - _notFoundLogCount++; - } - + if (logIfNotFound) + { + if (_notFoundLogCount < 50) + { + _logger.Warn(string.Format("Resource string ({0}) does not exist. Language ID = {1}", resourceKey, languageId)); + } + else if (_notFoundLogCount == 50) + { + _logger.Warn("Too many language resources do not exist (> 50). Stopped logging missing resources to prevent performance drop."); + } + + _notFoundLogCount++; + } + if (!String.IsNullOrEmpty(defaultValue)) { result = defaultValue; } else { - // try fallback to default language - if (!_defaultLanguageId.HasValue) - { - _defaultLanguageId = _languageService.GetDefaultLanguageId(); - } - - var defaultLangId = _defaultLanguageId.Value; - if (defaultLangId > 0 && defaultLangId != languageId) - { - var fallbackResult = GetResource(resourceKey, defaultLangId, false, resourceKey); - if (fallbackResult != resourceKey) - { - result = fallbackResult; - } - } - - if (!returnEmptyIfNotFound && result.IsEmpty()) - { - result = resourceKey; - } + // try fallback to default language + if (!_defaultLanguageId.HasValue) + { + _defaultLanguageId = _languageService.GetDefaultLanguageId(); + } + + var defaultLangId = _defaultLanguageId.Value; + if (defaultLangId > 0 && defaultLangId != languageId) + { + var fallbackResult = GetResource(resourceKey, defaultLangId, false, resourceKey); + if (fallbackResult != resourceKey) + { + result = fallbackResult; + } + } + + if (!returnEmptyIfNotFound && result.IsEmpty()) + { + result = resourceKey; + } } } @@ -283,7 +272,7 @@ public virtual string GetResource( public virtual string ExportResourcesToXml(Language language) { - Guard.NotNull(language, nameof(language)); + Guard.NotNull(language, nameof(language)); var sb = new StringBuilder(); var stringWriter = new StringWriter(sb); @@ -292,20 +281,20 @@ public virtual string ExportResourcesToXml(Language language) xmlWriter.WriteStartElement("Language"); xmlWriter.WriteAttributeString("Name", language.Name); - using (var scope = new DbContextScope(forceNoTracking: true)) - { - var resources = All(language.Id).ToList(); - foreach (var resource in resources) - { - if (resource.IsFromPlugin.GetValueOrDefault() == false) - { - xmlWriter.WriteStartElement("LocaleResource"); - xmlWriter.WriteAttributeString("Name", resource.ResourceName); - xmlWriter.WriteElementString("Value", null, resource.ResourceValue); - xmlWriter.WriteEndElement(); - } - } - } + using (var scope = new DbContextScope(forceNoTracking: true)) + { + var resources = All(language.Id).ToList(); + foreach (var resource in resources) + { + if (resource.IsFromPlugin.GetValueOrDefault() == false) + { + xmlWriter.WriteStartElement("LocaleResource"); + xmlWriter.WriteAttributeString("Name", resource.ResourceName); + xmlWriter.WriteElementString("Value", null, resource.ResourceValue); + xmlWriter.WriteEndElement(); + } + } + } xmlWriter.WriteEndElement(); xmlWriter.WriteEndDocument(); @@ -313,277 +302,283 @@ public virtual string ExportResourcesToXml(Language language) return stringWriter.ToString(); } - public virtual void ImportPluginResourcesFromXml( - PluginDescriptor pluginDescriptor, - IList targetList = null, - bool updateTouchedResources = true, - IList filterLanguages = null) - { - var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.Assembly.OriginalFile.Directory.FullName, "Localization")); - - if (!directory.Exists) - return; - - if (targetList == null && updateTouchedResources) - { - DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); - } - - var unprocessedLanguages = new List(); - - var defaultLanguageId = _languageService.GetDefaultLanguageId(); - var languages = filterLanguages ?? _languageService.GetAllLanguages(true); - - string code = null; - foreach (var language in languages) - { - code = ImportPluginResourcesForLanguage( - language, - null, - directory, - pluginDescriptor.ResourceRootKey, - targetList, - updateTouchedResources, - false); - - if (code == null) - { - unprocessedLanguages.Add(language); - } - } - - if (filterLanguages == null && unprocessedLanguages.Count > 0) - { - // There were unprocessed languages (no corresponding resource file could be found). - // In order for GetResource() to be able to gracefully fallback to the default language's resources, - // we need to import resources for the current default language.... - var processedLanguages = languages.Except(unprocessedLanguages).ToList(); - if (!processedLanguages.Any(x => x.Id == defaultLanguageId)) - { - // ...but only if no resource file could be mapped to the default language before, - // namely because in this case the following operation would be redundant. - var defaultLanguage = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId()); - if (defaultLanguage != null) - { - ImportPluginResourcesForLanguage( - defaultLanguage, - "en-us", - directory, - pluginDescriptor.ResourceRootKey, - targetList, - updateTouchedResources, - true); - } - } - } - } - - private string ImportPluginResourcesForLanguage( - Language language, - string fileCode, - DirectoryInfo directory, - string resourceRootKey, - IList targetList, - bool updateTouchedResources, - bool canFallBackToAnyResourceFile) - { - var fileNamePattern = "resources.{0}.xml"; - - var codeCandidates = GetResourceFileCodeCandidates( - fileCode ?? language.LanguageCulture, - directory, - canFallBackToAnyResourceFile); - - string path = null; - string code = null; - - foreach (var candidate in codeCandidates) - { - var pathCandidate = Path.Combine(directory.FullName, fileNamePattern.FormatInvariant(candidate)); - if (File.Exists(pathCandidate)) - { - code = candidate; - path = pathCandidate; - break; - } - } - - if (code != null) - { - var doc = new XmlDocument(); - - doc.Load(path); - doc = FlattenResourceFile(doc); - - if (targetList == null) - { - ImportResourcesFromXml(language, doc, resourceRootKey, true, updateTouchedResources: updateTouchedResources); - } - else - { - var nodes = doc.SelectNodes(@"//Language/LocaleResource"); - foreach (XmlNode node in nodes) - { - var valueNode = node.SelectSingleNode("Value"); - var res = new LocaleStringResource - { - ResourceName = node.Attributes["Name"].InnerText.Trim(), - ResourceValue = (valueNode == null ? "" : valueNode.InnerText), - LanguageId = language.Id, - IsFromPlugin = true - }; - - if (res.ResourceName.HasValue()) - { - targetList.Add(res); - } - } - } - } - - return code; - } - - private IEnumerable GetResourceFileCodeCandidates(string code, DirectoryInfo directory, bool canFallBackToAnyResourceFile) - { - // exact match (de-DE) - yield return code; - - // neutral culture (de) - var ci = CultureInfo.GetCultureInfo(code); - if (ci.Parent != null && !ci.IsNeutralCulture) - { - code = ci.Parent.Name; - yield return code; - } - - var rgFileName = new Regex("^resources.(.+?).xml$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // any other region with same language (de-*) - foreach (var fi in directory.EnumerateFiles("resources.{0}-*.xml".FormatInvariant(code), SearchOption.TopDirectoryOnly)) - { - code = rgFileName.Match(fi.Name).Groups[1].Value; - if (LocalizationHelper.IsValidCultureCode(code)) - { - yield return code; - yield break; - } - } - - if (canFallBackToAnyResourceFile) - { - foreach (var fi in directory.EnumerateFiles("resources.*.xml", SearchOption.TopDirectoryOnly)) - { - code = rgFileName.Match(fi.Name).Groups[1].Value; - if (LocalizationHelper.IsValidCultureCode(code)) - { - yield return code; - yield break; - } - } - } - } - - public virtual int ImportResourcesFromXml( - Language language, - XmlDocument xmlDocument, - string rootKey = null, - bool sourceIsPlugin = false, + public DirectoryHasher CreatePluginResourcesHasher(PluginDescriptor pluginDescriptor) + { + return new DirectoryHasher(Path.Combine(pluginDescriptor.PhysicalPath, "Localization"), "resources.*.xml"); + } + + public virtual void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null) + { + var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.PhysicalPath, "Localization")); + + if (!directory.Exists) + return; + + if (targetList == null && updateTouchedResources) + { + DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + } + + var unprocessedLanguages = new List(); + + var defaultLanguageId = _languageService.GetDefaultLanguageId(); + var languages = filterLanguages ?? _languageService.GetAllLanguages(true); + + string code = null; + foreach (var language in languages) + { + code = ImportPluginResourcesForLanguage( + language, + null, + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + false); + + if (code == null) + { + unprocessedLanguages.Add(language); + } + } + + if (filterLanguages == null && unprocessedLanguages.Count > 0) + { + // There were unprocessed languages (no corresponding resource file could be found). + // In order for GetResource() to be able to gracefully fallback to the default language's resources, + // we need to import resources for the current default language.... + var processedLanguages = languages.Except(unprocessedLanguages).ToList(); + if (!processedLanguages.Any(x => x.Id == defaultLanguageId)) + { + // ...but only if no resource file could be mapped to the default language before, + // namely because in this case the following operation would be redundant. + var defaultLanguage = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId()); + if (defaultLanguage != null) + { + ImportPluginResourcesForLanguage( + defaultLanguage, + "en-us", + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + true); + } + } + } + + try + { + var hasher = CreatePluginResourcesHasher(pluginDescriptor); + hasher.Persist(); + } + catch { } + } + + private string ImportPluginResourcesForLanguage( + Language language, + string fileCode, + DirectoryInfo directory, + string resourceRootKey, + IList targetList, + bool updateTouchedResources, + bool canFallBackToAnyResourceFile) + { + var fileNamePattern = "resources.{0}.xml"; + + var codeCandidates = GetResourceFileCodeCandidates( + fileCode ?? language.LanguageCulture, + directory, + canFallBackToAnyResourceFile); + + string path = null; + string code = null; + + foreach (var candidate in codeCandidates) + { + var pathCandidate = Path.Combine(directory.FullName, fileNamePattern.FormatInvariant(candidate)); + if (File.Exists(pathCandidate)) + { + code = candidate; + path = pathCandidate; + break; + } + } + + if (code != null) + { + var doc = new XmlDocument(); + + doc.Load(path); + doc = FlattenResourceFile(doc); + + if (targetList == null) + { + ImportResourcesFromXml(language, doc, resourceRootKey, true, updateTouchedResources: updateTouchedResources); + } + else + { + var nodes = doc.SelectNodes(@"//Language/LocaleResource"); + foreach (XmlNode node in nodes) + { + var valueNode = node.SelectSingleNode("Value"); + var res = new LocaleStringResource + { + ResourceName = node.Attributes["Name"].InnerText.Trim(), + ResourceValue = (valueNode == null ? "" : valueNode.InnerText), + LanguageId = language.Id, + IsFromPlugin = true + }; + + if (res.ResourceName.HasValue()) + { + targetList.Add(res); + } + } + } + } + + return code; + } + + private IEnumerable GetResourceFileCodeCandidates(string code, DirectoryInfo directory, bool canFallBackToAnyResourceFile) + { + // exact match (de-DE) + yield return code; + + // neutral culture (de) + var ci = CultureInfo.GetCultureInfo(code); + if (ci.Parent != null && !ci.IsNeutralCulture) + { + code = ci.Parent.Name; + yield return code; + } + + var rgFileName = new Regex("^resources.(.+?).xml$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // any other region with same language (de-*) + foreach (var fi in directory.EnumerateFiles("resources.{0}-*.xml".FormatInvariant(code), SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + + if (canFallBackToAnyResourceFile) + { + foreach (var fi in directory.EnumerateFiles("resources.*.xml", SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + } + } + + public virtual int ImportResourcesFromXml( + Language language, + XmlDocument xmlDocument, + string rootKey = null, + bool sourceIsPlugin = false, ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, bool updateTouchedResources = false) - { - using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false, forceNoTracking: true, hooksEnabled: false)) - { - var toAdd = new List(); - var toUpdate = new List(); - var nodes = xmlDocument.SelectNodes(@"//Language/LocaleResource"); - - var resources = language.LocaleStringResources.ToDictionarySafe(x => x.ResourceName, StringComparer.OrdinalIgnoreCase); - - LocaleStringResource resource; - - foreach (var xel in nodes.Cast()) - { - string name = xel.GetAttribute("Name").TrimSafe(); - string value = ""; - var valueNode = xel.SelectSingleNode("Value"); - if (valueNode != null) - value = valueNode.InnerText; - - if (String.IsNullOrEmpty(name)) - continue; - - if (rootKey.HasValue()) - { - if (!xel.GetAttributeText("AppendRootKey").IsCaseInsensitiveEqual("false")) - name = "{0}.{1}".FormatWith(rootKey, name); - } - - resource = null; - - // do not use "Insert"/"Update" methods because they clear cache - // let's bulk insert - //var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (resources.TryGetValue(name, out resource)) - { - if (mode.HasFlag(ImportModeFlags.Update)) - { - if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) - { - if (value != resource.ResourceValue) - { - resource.ResourceValue = value; - resource.IsTouched = null; - toUpdate.Add(resource); - } - } - } - } - else - { - if (mode.HasFlag(ImportModeFlags.Insert)) - { - toAdd.Add( - new LocaleStringResource - { - LanguageId = language.Id, - ResourceName = name, - ResourceValue = value, - IsFromPlugin = sourceIsPlugin - }); - } - } - } - - //_lsrRepository.AutoCommitEnabled = true; - - if (toAdd.Any() || toUpdate.Any()) - { - var segmentKeys = new HashSet(); - - toAdd.Each(x => segmentKeys.Add(GetSegmentKeyPart(x.ResourceName))); - toUpdate.Each(x => segmentKeys.Add(GetSegmentKeyPart(x.ResourceName))); - - _lsrRepository.InsertRange(toAdd); - toAdd.Clear(); - - _lsrRepository.UpdateRange(toUpdate); - toUpdate.Clear(); - - int num = _lsrRepository.Context.SaveChanges(); - - // clear cache - foreach (var segmentKey in segmentKeys) - { - ClearCacheSegment(segmentKey, language.Id); - } - - return num; - } - - return 0; - } - } + { + using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false, forceNoTracking: true, hooksEnabled: false)) + { + var toAdd = new List(); + var toUpdate = new List(); + var nodes = xmlDocument.SelectNodes(@"//Language/LocaleResource"); + + var resources = language.LocaleStringResources.ToDictionarySafe(x => x.ResourceName, StringComparer.OrdinalIgnoreCase); + + LocaleStringResource resource; + + foreach (var xel in nodes.Cast()) + { + string name = xel.GetAttribute("Name").TrimSafe(); + string value = ""; + var valueNode = xel.SelectSingleNode("Value"); + if (valueNode != null) + value = valueNode.InnerText; + + if (String.IsNullOrEmpty(name)) + continue; + + if (rootKey.HasValue()) + { + if (!xel.GetAttributeText("AppendRootKey").IsCaseInsensitiveEqual("false")) + name = "{0}.{1}".FormatWith(rootKey, name); + } + + resource = null; + + // do not use "Insert"/"Update" methods because they clear cache + // let's bulk insert + //var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (resources.TryGetValue(name, out resource)) + { + if (mode.HasFlag(ImportModeFlags.Update)) + { + if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) + { + if (value != resource.ResourceValue) + { + resource.ResourceValue = value; + resource.IsTouched = null; + toUpdate.Add(resource); + } + } + } + } + else + { + if (mode.HasFlag(ImportModeFlags.Insert)) + { + toAdd.Add( + new LocaleStringResource + { + LanguageId = language.Id, + ResourceName = name, + ResourceValue = value, + IsFromPlugin = sourceIsPlugin + }); + } + } + } + + //_lsrRepository.AutoCommitEnabled = true; + + if (toAdd.Any() || toUpdate.Any()) + { + var segmentKeys = new HashSet(); + + _lsrRepo.InsertRange(toAdd); + toAdd.Clear(); + + _lsrRepo.UpdateRange(toUpdate); + toUpdate.Clear(); + + int num = _lsrRepo.Context.SaveChanges(); + + // clear cache + ClearCacheSegment(language.Id); + + return num; + } + + return 0; + } + } public virtual XmlDocument FlattenResourceFile(XmlDocument source) { @@ -647,19 +642,19 @@ private void RecursivelyWriteResource(LocaleStringResourceParent resource, XmlWr writer.WriteString(resource.NameWithNamespace); writer.WriteEndAttribute(); - if (resource.AppendRootKey.HasValue) - { - writer.WriteStartAttribute("AppendRootKey", ""); - writer.WriteString(resource.AppendRootKey.Value ? "true" : "false"); - writer.WriteEndAttribute(); - parentAppendRootKey = resource.AppendRootKey; - } - else if (parentAppendRootKey.HasValue) - { - writer.WriteStartAttribute("AppendRootKey", ""); - writer.WriteString(parentAppendRootKey.Value ? "true" : "false"); - writer.WriteEndAttribute(); - } + if (resource.AppendRootKey.HasValue) + { + writer.WriteStartAttribute("AppendRootKey", ""); + writer.WriteString(resource.AppendRootKey.Value ? "true" : "false"); + writer.WriteEndAttribute(); + parentAppendRootKey = resource.AppendRootKey; + } + else if (parentAppendRootKey.HasValue) + { + writer.WriteStartAttribute("AppendRootKey", ""); + writer.WriteString(parentAppendRootKey.Value ? "true" : "false"); + writer.WriteEndAttribute(); + } writer.WriteStartElement("Value", ""); writer.WriteString(resource.ResourceValue); @@ -670,7 +665,7 @@ private void RecursivelyWriteResource(LocaleStringResourceParent resource, XmlWr foreach (var child in resource.ChildLocaleStringResources) { - RecursivelyWriteResource(child, writer, resource.AppendRootKey ?? parentAppendRootKey); + RecursivelyWriteResource(child, writer, resource.AppendRootKey ?? parentAppendRootKey); } } @@ -685,16 +680,10 @@ private void RecursivelySortChildrenResource(LocaleStringResourceParent resource } } - private string BuildCacheSegmentKey(string segment, int languageId) - { - return String.Format(LOCALESTRINGRESOURCES_SEGMENT_KEY, segment, languageId); - } - - private string GetSegmentKeyPart(string forKey) - { - return forKey.Substring(0, Math.Min(forKey.Length, 3)).ToLowerInvariant(); - } - + private string BuildCacheSegmentKey(int languageId) + { + return String.Format(LOCALESTRINGRESOURCES_SEGMENT_KEY, languageId); + } private class LocaleStringResourceParent : LocaleStringResource { @@ -715,11 +704,11 @@ public LocaleStringResourceParent(XmlNode localStringResource, string nameSpace } ResourceName = resName; - var appendRootKeyAttribute = localStringResource.Attributes["AppendRootKey"]; - if (appendRootKeyAttribute != null) - { - AppendRootKey = appendRootKeyAttribute.Value.ToBool(true); - } + var appendRootKeyAttribute = localStringResource.Attributes["AppendRootKey"]; + if (appendRootKeyAttribute != null) + { + AppendRootKey = appendRootKeyAttribute.Value.ToBool(true); + } if (resValueNode == null || string.IsNullOrEmpty(resValueNode.InnerText.Trim())) { @@ -743,7 +732,7 @@ public LocaleStringResourceParent(XmlNode localStringResource, string nameSpace public bool IsPersistable { get; set; } - public bool? AppendRootKey { get; set; } + public bool? AppendRootKey { get; set; } public string NameWithNamespace { diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityHelper.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityHelper.cs index d5a19dc09a..2dd34267c7 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityHelper.cs @@ -3,6 +3,7 @@ using System.Globalization; using SmartStore.Core; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Html; using SmartStore.Services.Seo; namespace SmartStore.Services.Localization @@ -18,7 +19,7 @@ public class LocalizedEntityHelper private readonly Language _defaultLanguage; public LocalizedEntityHelper( - ILanguageService languageService, + ILanguageService languageService, ILocalizedEntityService localizedEntityService, IUrlRecordService urlRecordService, IWorkContext workContext) @@ -32,7 +33,8 @@ public LocalizedEntityHelper( _defaultLanguage = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId()); } - public LocalizedValue GetLocalizedValue(T entity, + public LocalizedValue GetLocalizedValue(T obj, + int id, // T is BaseEntity = EntityId, T is ISetting = StoreId string localeKeyGroup, string localeKey, Func fallback, @@ -40,10 +42,10 @@ public LocalizedValue GetLocalizedValue(T entity, bool returnDefaultValue = true, bool ensureTwoPublishedLanguages = true, bool detectEmptyHtml = false) - where T : ILocalizedEntity + where T : class { - if (entity == null) - throw new ArgumentNullException(nameof(entity)); + if (obj == null) + throw new ArgumentNullException(nameof(obj)); TProp result = default; var str = string.Empty; @@ -78,14 +80,14 @@ public LocalizedValue GetLocalizedValue(T entity, // Localized value if (loadLocalizedValue) { - str = _localizedEntityService.GetLocalizedValue(requestLanguage.Id, entity.Id, localeKeyGroup, localeKey); + str = _localizedEntityService.GetLocalizedValue(requestLanguage.Id, id, localeKeyGroup, localeKey); - if (detectEmptyHtml && str.HasValue() && str.RemoveHtml().IsEmpty()) + if (detectEmptyHtml && HtmlUtils.IsEmptyHtml(str)) { str = string.Empty; } - if (str.HasValue()) + if (!string.IsNullOrEmpty(str)) { currentLanguage = requestLanguage; result = str.Convert(CultureInfo.InvariantCulture); @@ -93,10 +95,10 @@ public LocalizedValue GetLocalizedValue(T entity, } // Set default value if required - if (returnDefaultValue && str.IsEmpty()) + if (returnDefaultValue && string.IsNullOrEmpty(str)) { currentLanguage = _defaultLanguage; - result = fallback(entity); + result = fallback(obj); } if (currentLanguage == null) diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index bdd52f1977..2371b71bc7 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -3,174 +3,176 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using SmartStore.Core; using SmartStore.Core.Caching; +using SmartStore.Core.Configuration; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Localization; namespace SmartStore.Services.Localization { - /// - /// Provides information about localizable entities - /// - public partial class LocalizedEntityService : ScopedServiceBase, ILocalizedEntityService + /// + /// Provides information about localizable entities + /// + public partial class LocalizedEntityService : ScopedServiceBase, ILocalizedEntityService { - /// - /// 0 = segment (keygroup.key.idrange), 1 = language id - /// - const string LOCALIZEDPROPERTY_SEGMENT_KEY = "localizedproperty:{0}-lang-{1}"; - const string LOCALIZEDPROPERTY_SEGMENT_PATTERN = "localizedproperty:{0}*"; - const string LOCALIZEDPROPERTY_ALLSEGMENTS_PATTERN = "localizedproperty:*"; - - private readonly IRepository _localizedPropertyRepository; + /// + /// 0 = segment (keygroup.key.idrange), 1 = language id + /// + const string LOCALIZEDPROPERTY_SEGMENT_KEY = "localizedproperty:{0}-lang-{1}"; + const string LOCALIZEDPROPERTY_SEGMENT_PATTERN = "localizedproperty:{0}*"; + const string LOCALIZEDPROPERTY_ALLSEGMENTS_PATTERN = "localizedproperty:*"; + + private readonly IRepository _localizedPropertyRepository; private readonly ICacheManager _cacheManager; - private readonly PerformanceSettings _performanceSettings; + private readonly PerformanceSettings _performanceSettings; - private readonly IDictionary _prefetchedCollections; - private static int _lastCacheSegmentSize = -1; + private readonly IDictionary _prefetchedCollections; + private static int _lastCacheSegmentSize = -1; public LocalizedEntityService( - ICacheManager cacheManager, - IRepository localizedPropertyRepository, - PerformanceSettings performanceSettings) + ICacheManager cacheManager, + IRepository localizedPropertyRepository, + PerformanceSettings performanceSettings) { _cacheManager = cacheManager; _localizedPropertyRepository = localizedPropertyRepository; - _performanceSettings = performanceSettings; - - _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); - - ValidateCacheState(); - } - - private void ValidateCacheState() - { - // Ensure that after a segment size change the cache segments are invalidated. - var size = _performanceSettings.CacheSegmentSize; - var changed = _lastCacheSegmentSize == -1; - - if (size <= 0) - { - _performanceSettings.CacheSegmentSize = size = 1; - } - - if (_lastCacheSegmentSize > 0 && _lastCacheSegmentSize != size) - { - OnClearCache(); - changed = true; - } - - if (changed) - { - Interlocked.Exchange(ref _lastCacheSegmentSize, size); - } - } - - protected override void OnClearCache() - { - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_ALLSEGMENTS_PATTERN); - } - - protected virtual IDictionary GetCacheSegment(string localeKeyGroup, string localeKey, int entityId, int languageId) - { - Guard.NotEmpty(localeKeyGroup, nameof(localeKeyGroup)); - Guard.NotEmpty(localeKey, nameof(localeKey)); - - var segmentKey = GetSegmentKeyPart(localeKeyGroup, localeKey, entityId, out var minEntityId, out var maxEntityId); - var cacheKey = BuildCacheSegmentKey(segmentKey, languageId); - - // TODO: (MC) skip caching product.fulldescription (?), OR - // ...additionally segment by entity id ranges. - - return _cacheManager.Get(cacheKey, () => - { - var properties = _localizedPropertyRepository.TableUntracked - .Where(x => x.EntityId >= minEntityId && x.EntityId <= maxEntityId && x.LocaleKey == localeKey && x.LocaleKeyGroup == localeKeyGroup && x.LanguageId == languageId) - .ToList(); - - var dict = new Dictionary(properties.Count); - - foreach (var prop in properties) - { - dict[prop.EntityId] = prop.LocaleValue.EmptyNull(); - } - - return dict; - }); - } - - /// - /// Clears the cached segment from the cache - /// - protected virtual void ClearCacheSegment(string localeKeyGroup, string localeKey, int entityId, int? languageId = null) - { - try - { - if (IsInScope) - return; - - var segmentKey = GetSegmentKeyPart(localeKeyGroup, localeKey, entityId); - - if (languageId.HasValue && languageId.Value > 0) - { - _cacheManager.Remove(BuildCacheSegmentKey(segmentKey, languageId.Value)); - } - else - { - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_SEGMENT_PATTERN.FormatInvariant(segmentKey)); - } - } - catch { } - } - - public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) - { - if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var collection)) - { - var cachedItem = collection.Find(languageId, entityId, localeKey); - if (cachedItem != null) - { - return cachedItem.LocaleValue; - } - } - - if (IsInScope) - { - return GetLocalizedValueUncached(languageId, entityId, localeKeyGroup, localeKey); - } - - if (languageId <= 0) - return string.Empty; - - var props = GetCacheSegment(localeKeyGroup, localeKey, entityId, languageId); - - if (!props.TryGetValue(entityId, out var val)) - { - return string.Empty; - } - - return val; - } - - protected string GetLocalizedValueUncached(int languageId, int entityId, string localeKeyGroup, string localeKey) - { - if (languageId <= 0) - return string.Empty; - - var query = from lp in _localizedPropertyRepository.TableUntracked - where - lp.EntityId == entityId && - lp.LocaleKey == localeKey && - lp.LocaleKeyGroup == localeKeyGroup && - lp.LanguageId == languageId - select lp.LocaleValue; - - return query.FirstOrDefault().EmptyNull(); - } - - public virtual IList GetLocalizedProperties(int entityId, string localeKeyGroup) + _performanceSettings = performanceSettings; + + _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ValidateCacheState(); + } + + private void ValidateCacheState() + { + // Ensure that after a segment size change the cache segments are invalidated. + var size = _performanceSettings.CacheSegmentSize; + var changed = _lastCacheSegmentSize == -1; + + if (size <= 0) + { + _performanceSettings.CacheSegmentSize = size = 1; + } + + if (_lastCacheSegmentSize > 0 && _lastCacheSegmentSize != size) + { + OnClearCache(); + changed = true; + } + + if (changed) + { + Interlocked.Exchange(ref _lastCacheSegmentSize, size); + } + } + + protected override void OnClearCache() + { + _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_ALLSEGMENTS_PATTERN); + } + + protected virtual IDictionary GetCacheSegment(string localeKeyGroup, string localeKey, int entityId, int languageId) + { + Guard.NotEmpty(localeKeyGroup, nameof(localeKeyGroup)); + Guard.NotEmpty(localeKey, nameof(localeKey)); + + var segmentKey = GetSegmentKeyPart(localeKeyGroup, localeKey, entityId, out var minEntityId, out var maxEntityId); + var cacheKey = BuildCacheSegmentKey(segmentKey, languageId); + + // TODO: (MC) skip caching product.fulldescription (?), OR + // ...additionally segment by entity id ranges. + + return _cacheManager.Get(cacheKey, () => + { + var properties = _localizedPropertyRepository.TableUntracked + .Where(x => x.EntityId >= minEntityId && x.EntityId <= maxEntityId && x.LocaleKey == localeKey && x.LocaleKeyGroup == localeKeyGroup && x.LanguageId == languageId) + .ToList(); + + var dict = new Dictionary(properties.Count); + + foreach (var prop in properties) + { + dict[prop.EntityId] = prop.LocaleValue ?? string.Empty; + } + + return dict; + }); + } + + /// + /// Clears the cached segment from the cache + /// + protected virtual void ClearCacheSegment(string localeKeyGroup, string localeKey, int entityId, int? languageId = null) + { + try + { + if (IsInScope) + return; + + var segmentKey = GetSegmentKeyPart(localeKeyGroup, localeKey, entityId); + + if (languageId.HasValue && languageId.Value > 0) + { + _cacheManager.Remove(BuildCacheSegmentKey(segmentKey, languageId.Value)); + } + else + { + _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_SEGMENT_PATTERN.FormatInvariant(segmentKey)); + } + } + catch { } + } + + public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var collection)) + { + var cachedItem = collection.Find(languageId, entityId, localeKey); + if (cachedItem != null) + { + return cachedItem.LocaleValue; + } + } + + if (IsInScope) + { + return GetLocalizedValueUncached(languageId, entityId, localeKeyGroup, localeKey); + } + + if (languageId <= 0) + return string.Empty; + + var props = GetCacheSegment(localeKeyGroup, localeKey, entityId, languageId); + + if (!props.TryGetValue(entityId, out var val)) + { + return string.Empty; + } + + return val; + } + + protected string GetLocalizedValueUncached(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + if (languageId <= 0) + return string.Empty; + + var query = from lp in _localizedPropertyRepository.TableUntracked + where + lp.EntityId == entityId && + lp.LocaleKey == localeKey && + lp.LocaleKeyGroup == localeKeyGroup && + lp.LanguageId == languageId + select lp.LocaleValue; + + return query.FirstOrDefault().EmptyNull(); + } + + public virtual IList GetLocalizedProperties(int entityId, string localeKeyGroup) { if (localeKeyGroup.IsEmpty()) return new List(); @@ -184,151 +186,199 @@ orderby x.Id return props; } - public virtual void PrefetchLocalizedProperties(string localeKeyGroup, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false) - { - if (languageId == 0) - return; - - var collection = GetLocalizedPropertyCollectionInternal(localeKeyGroup, languageId, entityIds, isRange, isSorted); - - if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var existing)) - { - collection.MergeWith(existing); - } - else - { - _prefetchedCollections[localeKeyGroup] = collection; - } - } - - public virtual LocalizedPropertyCollection GetLocalizedPropertyCollection(string localeKeyGroup, int[] entityIds, bool isRange = false, bool isSorted = false) - { - return GetLocalizedPropertyCollectionInternal(localeKeyGroup, 0, entityIds, isRange, isSorted); - } - - public virtual LocalizedPropertyCollection GetLocalizedPropertyCollectionInternal(string localeKeyGroup, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false) - { - Guard.NotEmpty(localeKeyGroup, nameof(localeKeyGroup)); - - using (new DbContextScope(proxyCreation: false, lazyLoading: false)) - { - var query = from x in _localizedPropertyRepository.TableUntracked - where x.LocaleKeyGroup == localeKeyGroup - select x; - - var requestedSet = entityIds; - - if (entityIds != null && entityIds.Length > 0) - { - if (isRange) - { - if (!isSorted) - { - Array.Sort(entityIds); - } - - var min = entityIds[0]; - var max = entityIds[entityIds.Length - 1]; - - if (entityIds.Length == 2 && max > min + 1) - { - // Only min & max were passed, create the range sequence. - requestedSet = Enumerable.Range(min, max - min + 1).ToArray(); - } - - query = query.Where(x => x.EntityId >= min && x.EntityId <= max); - } - else - { - requestedSet = entityIds; - query = query.Where(x => entityIds.Contains(x.EntityId)); - } - } - - if (languageId > 0) - { - query = query.Where(x => x.LanguageId == languageId); - } - - return new LocalizedPropertyCollection(localeKeyGroup, requestedSet, query.ToList()); - } - } - - protected virtual LocalizedProperty GetLocalizedProperty(int languageId, int entityId, string localeKeyGroup, string localeKey) - { - var query = from lp in _localizedPropertyRepository.Table - where - lp.EntityId == entityId && - lp.LocaleKey == localeKey && - lp.LocaleKeyGroup == localeKeyGroup && - lp.LanguageId == languageId - select lp; - - return query.FirstOrDefault(); - } - - public virtual void InsertLocalizedProperty(LocalizedProperty property) + public virtual void PrefetchLocalizedProperties(string localeKeyGroup, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false) { - Guard.NotNull(property, nameof(property)); + if (languageId == 0) + return; + + var collection = GetLocalizedPropertyCollectionInternal(localeKeyGroup, languageId, entityIds, isRange, isSorted); - // db - _localizedPropertyRepository.Insert(property); - HasChanges = true; + if (_prefetchedCollections.TryGetValue(localeKeyGroup, out var existing)) + { + collection.MergeWith(existing); + } + else + { + _prefetchedCollections[localeKeyGroup] = collection; + } + } - // cache - ClearCacheSegment(property.LocaleKeyGroup, property.LocaleKey, property.EntityId, property.LanguageId); + public virtual LocalizedPropertyCollection GetLocalizedPropertyCollection(string localeKeyGroup, int[] entityIds, bool isRange = false, bool isSorted = false) + { + return GetLocalizedPropertyCollectionInternal(localeKeyGroup, 0, entityIds, isRange, isSorted); + } + + protected virtual LocalizedPropertyCollection GetLocalizedPropertyCollectionInternal(string localeKeyGroup, int languageId, int[] entityIds, bool isRange = false, bool isSorted = false) + { + Guard.NotEmpty(localeKeyGroup, nameof(localeKeyGroup)); + + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) + { + var query = from x in _localizedPropertyRepository.TableUntracked + where x.LocaleKeyGroup == localeKeyGroup + select x; + + var splitEntityIds = false; + var requestedSet = entityIds; + + if (entityIds != null && entityIds.Length > 0) + { + if (isRange) + { + if (!isSorted) + { + Array.Sort(entityIds); + } + + var min = entityIds[0]; + var max = entityIds[entityIds.Length - 1]; + + if (entityIds.Length == 2 && max > min + 1) + { + // Only min & max were passed, create the range sequence. + requestedSet = Enumerable.Range(min, max - min + 1).ToArray(); + } + + query = query.Where(x => x.EntityId >= min && x.EntityId <= max); + } + else + { + requestedSet = entityIds; + + if (entityIds.Length > 5000) + { + splitEntityIds = true; + } + else + { + query = query.Where(x => entityIds.Contains(x.EntityId)); + } + } + } + + if (languageId > 0) + { + query = query.Where(x => x.LanguageId == languageId); + } + + if (splitEntityIds) + { + var items = new List(); + foreach (var chunk in entityIds.Slice(5000)) + { + items.AddRange(query.Where(x => chunk.Contains(x.EntityId)).ToList()); + } + + return new LocalizedPropertyCollection(localeKeyGroup, requestedSet, items); + } + else + { + return new LocalizedPropertyCollection(localeKeyGroup, requestedSet, query.ToList()); + } + } + } + + protected virtual LocalizedProperty GetLocalizedProperty(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + var query = from lp in _localizedPropertyRepository.Table + where + lp.EntityId == entityId && + lp.LocaleKey == localeKey && + lp.LocaleKeyGroup == localeKeyGroup && + lp.LanguageId == languageId + select lp; + + return query.FirstOrDefault(); + } + + public virtual void InsertLocalizedProperty(LocalizedProperty property) + { + Guard.NotNull(property, nameof(property)); + + // db + _localizedPropertyRepository.Insert(property); + HasChanges = true; + + // cache + ClearCacheSegment(property.LocaleKeyGroup, property.LocaleKey, property.EntityId, property.LanguageId); } public virtual void UpdateLocalizedProperty(LocalizedProperty property) { - Guard.NotNull(property, nameof(property)); + Guard.NotNull(property, nameof(property)); - // db - _localizedPropertyRepository.Update(property); - HasChanges = true; + // db + _localizedPropertyRepository.Update(property); + HasChanges = true; - // cache - ClearCacheSegment(property.LocaleKeyGroup, property.LocaleKey, property.EntityId, property.LanguageId); - } + // cache + ClearCacheSegment(property.LocaleKeyGroup, property.LocaleKey, property.EntityId, property.LanguageId); + } - public virtual void DeleteLocalizedProperty(LocalizedProperty property) - { - Guard.NotNull(property, nameof(property)); + public virtual void DeleteLocalizedProperty(LocalizedProperty property) + { + Guard.NotNull(property, nameof(property)); - // cache - ClearCacheSegment(property.LocaleKeyGroup, property.LocaleKey, property.EntityId, property.LanguageId); + // cache + ClearCacheSegment(property.LocaleKeyGroup, property.LocaleKey, property.EntityId, property.LanguageId); - // db - _localizedPropertyRepository.Delete(property); - HasChanges = true; - } + // db + _localizedPropertyRepository.Delete(property); + HasChanges = true; + } - public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId) - { - if (localizedPropertyId == 0) - return null; + public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId) + { + if (localizedPropertyId == 0) + return null; - var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); - return localizedProperty; - } + var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); + return localizedProperty; + } - public virtual void SaveLocalizedValue( - T entity, + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void SaveLocalizedValue( + T entity, Expression> keySelector, - string localeValue, + string value, int languageId) where T : BaseEntity, ILocalizedEntity { - SaveLocalizedValue(entity, keySelector, localeValue, languageId); + SaveLocalizedValue(entity, entity.Id, entity.GetEntityName(), keySelector, value, languageId); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void SaveLocalizedValue( - T entity, + T entity, Expression> keySelector, - TPropType localeValue, + TPropType value, int languageId) where T : BaseEntity, ILocalizedEntity { - Guard.NotNull(entity, nameof(entity)); - Guard.NotZero(languageId, nameof(languageId)); + SaveLocalizedValue(entity, entity.Id, entity.GetEntityName(), keySelector, value, languageId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void SaveLocalizedSetting( + TSetting settings, + Expression> keySelector, + TPropType value, + int languageId, + int storeId = 0) where TSetting : class, ISettings + { + // INFO: unfortunately we have to misuse the "EntityId" prop and store StoreId instead. + SaveLocalizedValue(settings, storeId, typeof(TSetting).Name, keySelector, value, languageId); + } + + protected virtual void SaveLocalizedValue( + T obj, + int id, // T is BaseEntity = EntityId, T is ISetting = StoreId + string keyGroup, + Expression> keySelector, + TPropType value, + int languageId) where T : class + { + Guard.NotNull(obj, nameof(obj)); + Guard.NotEmpty(keyGroup, nameof(keyGroup)); + Guard.NotZero(languageId, nameof(languageId)); var member = keySelector.Body as MemberExpression; if (member == null) @@ -342,14 +392,13 @@ public virtual void SaveLocalizedValue( throw new ArgumentException($"Expression '{keySelector}' refers to a field, not a property."); } - var keyGroup = entity.GetEntityName(); var key = propInfo.Name; - var valueStr = localeValue.Convert(); - var prop = GetLocalizedProperty(languageId, entity.Id, keyGroup, key); + var valueStr = value.Convert(); + var prop = GetLocalizedProperty(languageId, id, keyGroup, key); if (prop != null) { - if (valueStr.IsEmpty()) + if (string.IsNullOrEmpty(valueStr)) { // Delete DeleteLocalizedProperty(prop); @@ -357,21 +406,21 @@ public virtual void SaveLocalizedValue( else { // Update - if (prop.LocaleValue != valueStr) - { - prop.LocaleValue = valueStr; - UpdateLocalizedProperty(prop); - } + if (prop.LocaleValue != valueStr) + { + prop.LocaleValue = valueStr; + UpdateLocalizedProperty(prop); + } } } else { - if (valueStr.HasValue()) + if (!string.IsNullOrEmpty(valueStr)) { // insert prop = new LocalizedProperty { - EntityId = entity.Id, + EntityId = id, LanguageId = languageId, LocaleKey = key, LocaleKeyGroup = keyGroup, @@ -382,20 +431,20 @@ public virtual void SaveLocalizedValue( } } - private string BuildCacheSegmentKey(string segment, int languageId) - { - return String.Format(LOCALIZEDPROPERTY_SEGMENT_KEY, segment, languageId); - } - - private string GetSegmentKeyPart(string localeKeyGroup, string localeKey, int entityId) - { - return GetSegmentKeyPart(localeKeyGroup, localeKey, entityId, out var minId, out var maxId); - } - - private string GetSegmentKeyPart(string localeKeyGroup, string localeKey, int entityId, out int minId, out int maxId) - { - maxId = entityId.GetRange(_performanceSettings.CacheSegmentSize, out minId); - return (localeKeyGroup + "." + localeKey + "." + maxId.ToString()).ToLowerInvariant(); - } - } + private string BuildCacheSegmentKey(string segment, int languageId) + { + return String.Format(LOCALIZEDPROPERTY_SEGMENT_KEY, segment, languageId); + } + + private string GetSegmentKeyPart(string localeKeyGroup, string localeKey, int entityId) + { + return GetSegmentKeyPart(localeKeyGroup, localeKey, entityId, out _, out _); + } + + private string GetSegmentKeyPart(string localeKeyGroup, string localeKey, int entityId, out int minId, out int maxId) + { + (minId, maxId) = entityId.GetRange(_performanceSettings.CacheSegmentSize); + return (localeKeyGroup + "." + localeKey + "." + minId.ToString()).ToLowerInvariant(); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs index ff9a57690a..ceffa079a2 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs @@ -3,9 +3,9 @@ namespace SmartStore.Services.Localization { - public class LocalizedUrlHelper + public class LocalizedUrlHelper { - public LocalizedUrlHelper(HttpRequestBase httpRequest, bool rawUrl = false) + public LocalizedUrlHelper(HttpRequestBase httpRequest, bool rawUrl = false) : this(httpRequest.ApplicationPath, rawUrl ? httpRequest.RawUrl : httpRequest.AppRelativeCurrentExecutionFilePath, rawUrl) { Guard.NotNull(httpRequest, nameof(httpRequest)); @@ -55,7 +55,7 @@ public bool IsLocalizedUrl(out string seoCode) firstPart = firstPart.Substring(0, firstSlash); } - if (LocalizationHelper.IsValidCultureCode(firstPart)) + if (LocalizationHelper.IsValidCultureCode(firstPart)) { seoCode = firstPart; return true; diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs index 625f53f986..c383f2c992 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedValue.cs @@ -8,154 +8,139 @@ namespace SmartStore.Services.Localization { - public class LocalizedValue - { - // Regex for all types of brackets which need to be "swapped": ({[]}) - private readonly static Regex _rgBrackets = new Regex(@"\(|\{|\[|\]|\}|\)", RegexOptions.Compiled | RegexOptions.CultureInvariant); - - /// - /// Fixes the flow of brackets within a text if the current page language has RTL flow. - /// - /// The test to fix. - /// Current language - /// - public static string FixBrackets(string str, Language currentLanguage) - { - if (!currentLanguage.Rtl || str.IsEmpty()) - { - return str; - } - - var controlChar = "‎"; - return _rgBrackets.Replace(str, m => - { - return controlChar + m.Value + controlChar; - }); - } - } - - [Serializable] - public class LocalizedValue : IHtmlString, IEquatable>, IComparable, IComparable> - { - private T _value; - private readonly Language _requestLanguage; - private readonly Language _currentLanguage; - - public LocalizedValue(T value, Language requestLanguage, Language currentLanguage) - { - _value = value; - _requestLanguage = requestLanguage; - _currentLanguage = currentLanguage; - } - - public T Value - { - get { return _value; } - } - - [JsonIgnore] - public Language RequestLanguage - { - get { return _requestLanguage; } - } - - [JsonIgnore] - public Language CurrentLanguage - { - get { return _currentLanguage; } - } - - public bool IsFallback - { - get { return _requestLanguage != _currentLanguage; } - } - - public bool BidiOverride - { - get { return _requestLanguage != _currentLanguage && _requestLanguage.Rtl != _currentLanguage.Rtl; } - } - - public void ChangeValue(T value) - { - _value = value; - } - - public static implicit operator T(LocalizedValue obj) - { - if (obj == null) - { - return default; - } - - return obj.Value; - } - - public string ToHtmlString() - { - return this.ToString(); - } - - public override string ToString() - { - if (_value == null) - { - return null; - } - - if (typeof(T) == typeof(string)) - { - return _value as string; - } - - return _value.Convert(CultureInfo.GetCultureInfo(_currentLanguage.LanguageCulture)); - } - - public override int GetHashCode() - { - var hashCode = 0; - if (_value != null) - hashCode ^= _value.GetHashCode(); - return hashCode; - } - - public override bool Equals(object other) - { - return this.Equals(other as LocalizedValue); - } - - public bool Equals(LocalizedValue other) - { - if (ReferenceEquals(null, other)) - return false; - if (ReferenceEquals(this, other)) - return true; - - return object.Equals(_value, other._value); - } - - public int CompareTo(object other) - { - return CompareTo(other as LocalizedValue); - } - - public int CompareTo(LocalizedValue other) - { - if (other == null) - { - return 1; - } - - if (Value is IComparable val1) - { - return val1.CompareTo(other.Value); - } - - if (Value is IComparable val2) - { - return val2.CompareTo(other.Value); - } - - return 0; - } - } + public class LocalizedValue + { + // Regex for all types of brackets which need to be "swapped": ({[]}) + private readonly static Regex _rgBrackets = new Regex(@"\(|\{|\[|\]|\}|\)", RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Fixes the flow of brackets within a text if the current page language has RTL flow. + /// + /// The test to fix. + /// Current language + /// + public static string FixBrackets(string str, Language currentLanguage) + { + if (!currentLanguage.Rtl || str.IsEmpty()) + { + return str; + } + + var controlChar = "‎"; + return _rgBrackets.Replace(str, m => + { + return controlChar + m.Value + controlChar; + }); + } + } + + [Serializable] + public class LocalizedValue : IHtmlString, IEquatable>, IComparable, IComparable> + { + private T _value; + private readonly Language _requestLanguage; + private readonly Language _currentLanguage; + + public LocalizedValue(T value, Language requestLanguage, Language currentLanguage) + { + _value = value; + _requestLanguage = requestLanguage; + _currentLanguage = currentLanguage; + } + + public T Value => _value; + + [JsonIgnore] + public Language RequestLanguage => _requestLanguage; + + [JsonIgnore] + public Language CurrentLanguage => _currentLanguage; + + public bool IsFallback => _requestLanguage != _currentLanguage; + + public bool BidiOverride => _requestLanguage != _currentLanguage && _requestLanguage.Rtl != _currentLanguage.Rtl; + + public void ChangeValue(T value) + { + _value = value; + } + + public static implicit operator T(LocalizedValue obj) + { + if (obj == null) + { + return default; + } + + return obj.Value; + } + + public string ToHtmlString() + { + return this.ToString(); + } + + public override string ToString() + { + if (_value == null) + { + return null; + } + + if (typeof(T) == typeof(string)) + { + return _value as string; + } + + return _value.Convert(CultureInfo.GetCultureInfo(_currentLanguage.LanguageCulture)); + } + + public override int GetHashCode() + { + var hashCode = 0; + if (_value != null) + hashCode ^= _value.GetHashCode(); + return hashCode; + } + + public override bool Equals(object other) + { + return this.Equals(other as LocalizedValue); + } + + public bool Equals(LocalizedValue other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return object.Equals(_value, other._value); + } + + public int CompareTo(object other) + { + return CompareTo(other as LocalizedValue); + } + + public int CompareTo(LocalizedValue other) + { + if (other == null) + { + return 1; + } + + if (Value is IComparable val1) + { + return val1.CompareTo(other.Value); + } + + if (Value is IComparable val2) + { + return val2.CompareTo(other.Value); + } + + return 0; + } + } } diff --git a/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs b/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs index 6c45b687c9..447d5e1fc1 100644 --- a/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs +++ b/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs @@ -10,38 +10,38 @@ namespace SmartStore.Services.Logging { - /// - /// Customer activity service - /// - public class CustomerActivityService : ICustomerActivityService + /// + /// Customer activity service + /// + public class CustomerActivityService : ICustomerActivityService { - #region Fields + #region Fields - private const int _deleteNumberOfEntries = 1000; + private const int _deleteNumberOfEntries = 1000; - private readonly IRepository _activityLogRepository; + private readonly IRepository _activityLogRepository; private readonly IRepository _activityLogTypeRepository; - private readonly IRepository _customerRepository; - private readonly IWorkContext _workContext; + private readonly IRepository _customerRepository; + private readonly IWorkContext _workContext; private readonly IDbContext _dbContext; private readonly static object s_lock = new object(); private readonly static ConcurrentDictionary s_logTypes = new ConcurrentDictionary(); - #endregion + #endregion - #region Ctor + #region Ctor - public CustomerActivityService( + public CustomerActivityService( IRepository activityLogRepository, IRepository activityLogTypeRepository, - IRepository customerRepository, - IWorkContext workContext, + IRepository customerRepository, + IWorkContext workContext, IDbContext dbContext) { this._activityLogRepository = activityLogRepository; this._activityLogTypeRepository = activityLogTypeRepository; - this._customerRepository = customerRepository; + this._customerRepository = customerRepository; this._workContext = workContext; this._dbContext = dbContext; } @@ -111,7 +111,8 @@ private void EnsureLogTypesAreLoaded() var query = from alt in _activityLogTypeRepository.Table orderby alt.Name select alt; - query.Each(x => { + query.Each(x => + { s_logTypes[x.SystemKeyword] = x; }); } @@ -176,9 +177,9 @@ public virtual ActivityLog InsertActivity(string systemKeyword, string comment, if (activityType == null || !activityType.Enabled) return null; - comment = comment.EmptyNull(); + comment = comment.EmptyNull(); comment = string.Format(comment, commentParams); - comment = comment.Truncate(4000); + comment = comment.Truncate(4000); var activity = new ActivityLog(); activity.ActivityLogTypeId = activityType.Id; @@ -203,45 +204,45 @@ public virtual void DeleteActivity(ActivityLog activityLog) _activityLogRepository.Delete(activityLog); } - /// - /// Gets all activity log items - /// - /// Log item creation from; null to load all customers - /// Log item creation to; null to load all customers - /// Customer identifier; null to load all customers - /// Activity log type identifier - /// Page index - /// Page size - /// Customer email - /// Customer system name - /// Activity log collection - public virtual IPagedList GetAllActivities( - DateTime? createdOnFrom, + /// + /// Gets all activity log items + /// + /// Log item creation from; null to load all customers + /// Log item creation to; null to load all customers + /// Customer identifier; null to load all customers + /// Activity log type identifier + /// Page index + /// Page size + /// Customer email + /// Customer system name + /// Activity log collection + public virtual IPagedList GetAllActivities( + DateTime? createdOnFrom, DateTime? createdOnTo, - int? customerId, - int activityLogTypeId, + int? customerId, + int activityLogTypeId, int pageIndex, - int pageSize, - string email = null, - bool? customerSystemAccount = null) + int pageSize, + string email = null, + bool? customerSystemAccount = null) { var query = _activityLogRepository.Table; - if (email.HasValue() || customerSystemAccount.HasValue) - { - var queryCustomers = _customerRepository.Table; + if (email.HasValue() || customerSystemAccount.HasValue) + { + var queryCustomers = _customerRepository.Table; - if (email.HasValue()) - queryCustomers = queryCustomers.Where(x => x.Email.Contains(email)); + if (email.HasValue()) + queryCustomers = queryCustomers.Where(x => x.Email.Contains(email)); - if (customerSystemAccount.HasValue) - queryCustomers = queryCustomers.Where(x => x.IsSystemAccount == customerSystemAccount.Value); + if (customerSystemAccount.HasValue) + queryCustomers = queryCustomers.Where(x => x.IsSystemAccount == customerSystemAccount.Value); - query = - from al in _activityLogRepository.Table - join c in queryCustomers on al.CustomerId equals c.Id - select al; - } + query = + from al in _activityLogRepository.Table + join c in queryCustomers on al.CustomerId equals c.Id + select al; + } if (createdOnFrom.HasValue) query = query.Where(al => createdOnFrom.Value <= al.CreatedOnUtc); @@ -279,54 +280,54 @@ public virtual ActivityLog GetActivityById(int activityLogId) return activityLog; } - public virtual IList GetActivityByIds(int[] activityLogIds) - { - if (activityLogIds == null || activityLogIds.Length == 0) - return new List(); + public virtual IList GetActivityByIds(int[] activityLogIds) + { + if (activityLogIds == null || activityLogIds.Length == 0) + return new List(); - var query = _activityLogRepository.Table - .Where(x => activityLogIds.Contains(x.Id)) - .OrderByDescending(x => x.CreatedOnUtc); + var query = _activityLogRepository.Table + .Where(x => activityLogIds.Contains(x.Id)) + .OrderByDescending(x => x.CreatedOnUtc); - return query.ToList(); - } + return query.ToList(); + } - /// - /// Clears activity log - /// - public virtual void ClearAllActivities() + /// + /// Clears activity log + /// + public virtual void ClearAllActivities() { - try - { - _dbContext.ExecuteSqlCommand("TRUNCATE TABLE [ActivityLog]"); - } - catch - { - try - { - for (int i = 0; i < 100000; ++i) - { - if (_dbContext.ExecuteSqlCommand("Delete Top ({0}) From [ActivityLog]", false, null, _deleteNumberOfEntries) < _deleteNumberOfEntries) - break; - } - } - catch { } - - try - { - _dbContext.ExecuteSqlCommand("DBCC CHECKIDENT('ActivityLog', RESEED, 0)"); - } - catch - { - try - { - _dbContext.ExecuteSqlCommand("Alter Table [ActivityLog] Alter Column [Id] Identity(1,1)"); - } - catch { } - } - } - } - - #endregion + try + { + _dbContext.ExecuteSqlCommand("TRUNCATE TABLE [ActivityLog]"); + } + catch + { + try + { + for (int i = 0; i < 100000; ++i) + { + if (_dbContext.ExecuteSqlCommand("Delete Top ({0}) From [ActivityLog]", false, null, _deleteNumberOfEntries) < _deleteNumberOfEntries) + break; + } + } + catch { } + + try + { + _dbContext.ExecuteSqlCommand("DBCC CHECKIDENT('ActivityLog', RESEED, 0)"); + } + catch + { + try + { + _dbContext.ExecuteSqlCommand("Alter Table [ActivityLog] Alter Column [Id] Identity(1,1)"); + } + catch { } + } + } + } + + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Logging/DbLogService.cs b/src/Libraries/SmartStore.Services/Logging/DbLogService.cs index d725fadef6..4efc4187b9 100644 --- a/src/Libraries/SmartStore.Services/Logging/DbLogService.cs +++ b/src/Libraries/SmartStore.Services/Logging/DbLogService.cs @@ -8,141 +8,141 @@ namespace SmartStore.Services.Logging { - public partial class DbLogService : ILogService - { - private const int _deleteNumberOfEntries = 1000; - - private readonly IRepository _logRepository; - private readonly IDbContext _dbContext; - private readonly ILoggerFactory _loggerFactory; - - public DbLogService(IRepository logRepository, ILoggerFactory loggerFactory) - { - _logRepository = logRepository; - _dbContext = logRepository.Context; - _loggerFactory = loggerFactory; - } - - public virtual void DeleteLog(Log log) - { - Guard.NotNull(log, nameof(log)); - - _logRepository.Delete(log); - } - - public virtual void ClearLog() - { - try - { - _dbContext.ExecuteSqlCommand("TRUNCATE TABLE [Log]"); - } - catch - { - try - { - for (int i = 0; i < 100000; ++i) - { - if (_dbContext.ExecuteSqlCommand("Delete Top ({0}) From [Log]", false, null, _deleteNumberOfEntries) < _deleteNumberOfEntries) - break; - } - } - catch { } - - try - { - _dbContext.ExecuteSqlCommand("DBCC CHECKIDENT('Log', RESEED, 0)"); - } - catch - { - try - { - _dbContext.ExecuteSqlCommand("Alter Table [Log] Alter Column [Id] Identity(1,1)"); - } - catch { } - } - } - - _dbContext.ShrinkDatabase(); - } - - public virtual void ClearLog(DateTime toUtc, LogLevel logLevel) - { - try - { - string sqlDelete = "Delete Top ({0}) From [Log] Where LogLevelId < {1} And CreatedOnUtc <= {2}"; - - for (int i = 0; i < 100000; ++i) - { - if (_dbContext.ExecuteSqlCommand(sqlDelete, false, null, _deleteNumberOfEntries, (int)logLevel, toUtc) < _deleteNumberOfEntries) - break; - } - - _dbContext.ShrinkDatabase(); - } - catch { } - } - - public virtual IPagedList GetAllLogs( - DateTime? fromUtc, - DateTime? toUtc, - string logger, - string message, - LogLevel? logLevel, - int pageIndex, - int pageSize) - { - // force flush to get most recent entries - _loggerFactory.FlushAll(); - - var query = _logRepository.Table; - - if (fromUtc.HasValue) - query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc); - - if (toUtc.HasValue) - query = query.Where(l => toUtc.Value >= l.CreatedOnUtc); - - if (logger.HasValue()) - query = query.Where(l => l.Logger.Contains(logger)); - - if (logLevel.HasValue) - { - int logLevelId = (int)logLevel.Value; - query = query.Where(l => logLevelId == l.LogLevelId); - } - - if (message.HasValue()) - query = query.Where(l => l.ShortMessage.Contains(message) || l.FullMessage.Contains(message)); - - query = query.OrderByDescending(l => l.CreatedOnUtc); - - //query = _logRepository.Expand(query, x => x.Customer); - - var log = new PagedList(query, pageIndex, pageSize); - return log; - } - - public virtual Log GetLogById(int logId) - { - if (logId == 0) - return null; - - var log = _logRepository.GetById(logId); - return log; - } - - public virtual IList GetLogByIds(int[] logIds) - { - if (logIds == null || logIds.Length == 0) - return new List(); - - var query = from l in _logRepository.Table - where logIds.Contains(l.Id) - select l; - var logItems = query.ToList(); - - // sort by passed identifier sequence - return logItems.OrderBySequence(logIds).ToList(); - } - } + public partial class DbLogService : ILogService + { + private const int _deleteNumberOfEntries = 1000; + + private readonly IRepository _logRepository; + private readonly IDbContext _dbContext; + private readonly ILoggerFactory _loggerFactory; + + public DbLogService(IRepository logRepository, ILoggerFactory loggerFactory) + { + _logRepository = logRepository; + _dbContext = logRepository.Context; + _loggerFactory = loggerFactory; + } + + public virtual void DeleteLog(Log log) + { + Guard.NotNull(log, nameof(log)); + + _logRepository.Delete(log); + } + + public virtual void ClearLog() + { + try + { + _dbContext.ExecuteSqlCommand("TRUNCATE TABLE [Log]"); + } + catch + { + try + { + for (int i = 0; i < 100000; ++i) + { + if (_dbContext.ExecuteSqlCommand("Delete Top ({0}) From [Log]", false, null, _deleteNumberOfEntries) < _deleteNumberOfEntries) + break; + } + } + catch { } + + try + { + _dbContext.ExecuteSqlCommand("DBCC CHECKIDENT('Log', RESEED, 0)"); + } + catch + { + try + { + _dbContext.ExecuteSqlCommand("Alter Table [Log] Alter Column [Id] Identity(1,1)"); + } + catch { } + } + } + + _dbContext.ShrinkDatabase(); + } + + public virtual void ClearLog(DateTime toUtc, LogLevel logLevel) + { + try + { + string sqlDelete = "Delete Top ({0}) From [Log] Where LogLevelId < {1} And CreatedOnUtc <= {2}"; + + for (int i = 0; i < 100000; ++i) + { + if (_dbContext.ExecuteSqlCommand(sqlDelete, false, null, _deleteNumberOfEntries, (int)logLevel, toUtc) < _deleteNumberOfEntries) + break; + } + + _dbContext.ShrinkDatabase(); + } + catch { } + } + + public virtual IPagedList GetAllLogs( + DateTime? fromUtc, + DateTime? toUtc, + string logger, + string message, + LogLevel? logLevel, + int pageIndex, + int pageSize) + { + // force flush to get most recent entries + _loggerFactory.FlushAll(); + + var query = _logRepository.Table; + + if (fromUtc.HasValue) + query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc); + + if (toUtc.HasValue) + query = query.Where(l => toUtc.Value >= l.CreatedOnUtc); + + if (logger.HasValue()) + query = query.Where(l => l.Logger.Contains(logger)); + + if (logLevel.HasValue) + { + int logLevelId = (int)logLevel.Value; + query = query.Where(l => logLevelId == l.LogLevelId); + } + + if (message.HasValue()) + query = query.Where(l => l.ShortMessage.Contains(message) || l.FullMessage.Contains(message)); + + query = query.OrderByDescending(l => l.CreatedOnUtc); + + //query = _logRepository.Expand(query, x => x.Customer); + + var log = new PagedList(query, pageIndex, pageSize); + return log; + } + + public virtual Log GetLogById(int logId) + { + if (logId == 0) + return null; + + var log = _logRepository.GetById(logId); + return log; + } + + public virtual IList GetLogByIds(int[] logIds) + { + if (logIds == null || logIds.Length == 0) + return new List(); + + var query = from l in _logRepository.Table + where logIds.Contains(l.Id) + select l; + var logItems = query.ToList(); + + // sort by passed identifier sequence + return logItems.OrderBySequence(logIds).ToList(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Logging/DeleteLogsTask.cs b/src/Libraries/SmartStore.Services/Logging/DeleteLogsTask.cs index 1fbf6dbd76..135c819c1f 100644 --- a/src/Libraries/SmartStore.Services/Logging/DeleteLogsTask.cs +++ b/src/Libraries/SmartStore.Services/Logging/DeleteLogsTask.cs @@ -21,11 +21,11 @@ public DeleteLogsTask( _commonSettings = commonSettings; } - public void Execute(TaskExecutionContext ctx) + public void Execute(TaskExecutionContext ctx) { var toUtc = DateTime.UtcNow.AddDays(-_commonSettings.MaxLogAgeInDays); - _logService.ClearLog(toUtc, LogLevel.Error); + _logService.ClearLog(toUtc, LogLevel.Error); } } } diff --git a/src/Libraries/SmartStore.Services/Logging/ILogService.cs b/src/Libraries/SmartStore.Services/Logging/ILogService.cs index 926f87aea0..e2e4131346 100644 --- a/src/Libraries/SmartStore.Services/Logging/ILogService.cs +++ b/src/Libraries/SmartStore.Services/Logging/ILogService.cs @@ -9,52 +9,52 @@ namespace SmartStore.Services.Logging { - public interface ILogService - { - /// - /// Deletes a log item - /// - /// Log item - void DeleteLog(Log log); + public interface ILogService + { + /// + /// Deletes a log item + /// + /// Log item + void DeleteLog(Log log); - /// - /// Clears a log - /// - void ClearLog(); - void ClearLog(DateTime toUtc, LogLevel logLevel); + /// + /// Clears a log + /// + void ClearLog(); + void ClearLog(DateTime toUtc, LogLevel logLevel); - /// - /// Gets all log items - /// - /// Log item creation from; null to load all records - /// Log item creation to; null to load all records - /// Message - /// Logger name - /// Log level; null to load all records - /// Page index - /// Page size - /// Log item collection - IPagedList GetAllLogs( - DateTime? fromUtc, - DateTime? toUtc, - string logger, - string message, - LogLevel? logLevel, - int pageIndex, - int pageSize); + /// + /// Gets all log items + /// + /// Log item creation from; null to load all records + /// Log item creation to; null to load all records + /// Message + /// Logger name + /// Log level; null to load all records + /// Page index + /// Page size + /// Log item collection + IPagedList GetAllLogs( + DateTime? fromUtc, + DateTime? toUtc, + string logger, + string message, + LogLevel? logLevel, + int pageIndex, + int pageSize); - /// - /// Gets a log item - /// - /// Log item identifier - /// Log item - Log GetLogById(int logId); + /// + /// Gets a log item + /// + /// Log item identifier + /// Log item + Log GetLogById(int logId); - /// - /// Get log items by identifiers - /// - /// Log item identifiers - /// Log items - IList GetLogByIds(int[] logIds); - } + /// + /// Get log items by identifiers + /// + /// Log item identifiers + /// Log items + IList GetLogByIds(int[] logIds); + } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/AlbumInfo.cs b/src/Libraries/SmartStore.Services/Media/Album/AlbumInfo.cs similarity index 52% rename from src/Libraries/SmartStore.Services/Media/v4/Album/AlbumInfo.cs rename to src/Libraries/SmartStore.Services/Media/Album/AlbumInfo.cs index 8aa3925c28..4e1422fa65 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/AlbumInfo.cs +++ b/src/Libraries/SmartStore.Services/Media/Album/AlbumInfo.cs @@ -2,14 +2,44 @@ namespace SmartStore.Services.Media { + /// + /// Common info about a media album. + /// public class AlbumInfo { + /// + /// Storage id of the album + /// public int Id { get; set; } + + /// + /// System name of the album + /// public string Name { get; set; } + + /// + /// The type of the concrete implementation. + /// public Type ProviderType { get; set; } + + /// + /// An album created by is always a system album. + /// public bool IsSystemAlbum { get; set; } + + /// + /// true when the provider () also implements . + /// public bool IsTrackDetector { get; set; } + + /// + /// Info about how to display the album by the media manager UI + /// public AlbumDisplayHint DisplayHint { get; set; } + + /// + /// Reflection info about trackable properties. + /// public TrackedMediaProperty[] TrackedProperties { get; set; } = new TrackedMediaProperty[0]; } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/AlbumRegistry.cs b/src/Libraries/SmartStore.Services/Media/Album/AlbumRegistry.cs similarity index 99% rename from src/Libraries/SmartStore.Services/Media/v4/Album/AlbumRegistry.cs rename to src/Libraries/SmartStore.Services/Media/Album/AlbumRegistry.cs index 86b612754c..00b553a95a 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/AlbumRegistry.cs +++ b/src/Libraries/SmartStore.Services/Media/Album/AlbumRegistry.cs @@ -14,7 +14,7 @@ public class AlbumRegistry : IAlbumRegistry private readonly IDbContext _dbContext; private readonly ICacheManager _cache; private readonly IEnumerable> _albumProviders; - + public AlbumRegistry( IDbContext dbContext, ICacheManager cache, @@ -105,7 +105,7 @@ protected virtual IEnumerable LoadAllAlbums() public virtual AlbumInfo GetAlbumByName(string name) { Guard.NotEmpty(name, nameof(name)); - + return GetAlbumDictionary().Get(name); } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/FolderService.cs b/src/Libraries/SmartStore.Services/Media/Album/FolderService.cs similarity index 95% rename from src/Libraries/SmartStore.Services/Media/v4/Album/FolderService.cs rename to src/Libraries/SmartStore.Services/Media/Album/FolderService.cs index a3a97cc970..260e3c0282 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/FolderService.cs +++ b/src/Libraries/SmartStore.Services/Media/Album/FolderService.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Data.Entity; -using System.Threading.Tasks; using SmartStore.Collections; using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Infrastructure; namespace SmartStore.Services.Media { @@ -40,13 +35,13 @@ public TreeNode GetRootNode() { var cacheKey = FolderTreeKey; - var root = _cache.Get(cacheKey, () => + var root = _cache.Get(cacheKey, () => { var query = from x in _folderRepo.TableUntracked orderby x.ParentId, x.Name select x; - var unsortedNodes = query.ToList().Select(x => + var unsortedNodes = query.ToList().Select(x => { var item = new MediaFolderNode { @@ -134,7 +129,7 @@ public TreeNode GetNodeById(int id) { if (id <= 0) return null; - + return GetRootNode().SelectNodeById(id); } @@ -148,7 +143,7 @@ public bool CheckUniqueFolderName(string path, out string newName) { Guard.NotEmpty(path, nameof(path)); - // TODO: (mm) throw when path is not a folder path + // TODO: (mm) (mc) throw when path is not a folder path newName = null; diff --git a/src/Libraries/SmartStore.Services/Media/Album/IAlbumProvider.cs b/src/Libraries/SmartStore.Services/Media/Album/IAlbumProvider.cs new file mode 100644 index 0000000000..3653492cd7 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Album/IAlbumProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.Media +{ + /// + /// Creates album entities and provides media manager UI infos about them. + /// Implementation classes are instantiated once on app startup. Impl does NOT + /// need to be registered in DI. + /// + public interface IAlbumProvider + { + /// + /// Creates a list of album entities the provider is responsible for. + /// A special bootstrapper checks whether an album already exists in the + /// database (by key). The entity is inserted + /// to the database if it does not exist yet, or skipped otherwise. + /// + /// List of entities. + IEnumerable GetAlbums(); + + /// + /// Gets UI display info about an album. + /// + /// Album to get info for. + /// UI display hint object. + AlbumDisplayHint GetDisplayHint(MediaAlbum album); + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/IAlbumRegistry.cs b/src/Libraries/SmartStore.Services/Media/Album/IAlbumRegistry.cs similarity index 54% rename from src/Libraries/SmartStore.Services/Media/v4/Album/IAlbumRegistry.cs rename to src/Libraries/SmartStore.Services/Media/Album/IAlbumRegistry.cs index 5e328c5cab..31d2415a9a 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/IAlbumRegistry.cs +++ b/src/Libraries/SmartStore.Services/Media/Album/IAlbumRegistry.cs @@ -3,11 +3,32 @@ namespace SmartStore.Services.Media { + /// + /// Manages all registered albums. + /// public interface IAlbumRegistry { + /// + /// Enlists all albums. + /// IReadOnlyCollection GetAllAlbums(); + + /// + /// Gets the names of all albums. + /// + /// If true skips all albums without track detection capability. IEnumerable GetAlbumNames(bool withTrackDetectors = false); + + /// + /// Gets an album by name. + /// + /// Name of album to retrieve. AlbumInfo GetAlbumByName(string name); + + /// + /// Gets an album by ID. + /// + /// Storage id of album to retrieve. AlbumInfo GetAlbumById(int id); /// @@ -22,6 +43,10 @@ public interface IAlbumRegistry /// Name of album to delete /// Name of album to move files to void DeleteAlbum(string albumName, string moveFilesToAlbum); + + /// + /// Clears album cache and reloads all entities from database. + /// void ClearCache(); } } diff --git a/src/Libraries/SmartStore.Services/Media/Album/IFolderService.cs b/src/Libraries/SmartStore.Services/Media/Album/IFolderService.cs new file mode 100644 index 0000000000..6d7fe41f33 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Album/IFolderService.cs @@ -0,0 +1,63 @@ +using System; +using SmartStore.Collections; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.Media +{ + /// + /// Reads media folder objects either from cache (as ) or from storage. + /// Methods with in their signature always work against the cache and enable very fast + /// data retrieval. + /// The tree cache is invalidated automatically after any storage action. + /// + public interface IFolderService : IScopedService + { + /// + /// Gets the root folder node from cache. + /// + TreeNode GetRootNode(); + + /// + /// Gets a folder node by storage id. + /// + TreeNode GetNodeById(int id); + + /// + /// Gets a folder node by path, e.g. "catalog/subfolder1/subfolder2". + /// The first token always refers to an album. This method operates very fast + /// because all possible pathes are cached. + /// + TreeNode GetNodeByPath(string path); + + /// + /// Checks whether any given path does already exist and - if true - + /// outputs a unique leaf folder name that can be used to save a folder + /// to the database. + /// + /// The path to check. + /// If method return value is true: the new unique folder name, otherwise: null. + /// true when passed path exists already. + bool CheckUniqueFolderName(string path, out string newName); + + /// + /// Gets a folder entity by storage id from the database (bypassing the cache). + /// + /// Whether all containing files should be eager loaded. + MediaFolder GetFolderById(int id, bool withFiles = false); + + /// + /// Inserts a new folder to the database. + /// + void InsertFolder(MediaFolder folder); + + /// + /// Deletes a folder from the database. + /// + void DeleteFolder(MediaFolder folder); + + /// + /// Updates a folder in the database. + /// + void UpdateFolder(MediaFolder folder); + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/MediaFolderInfo.cs b/src/Libraries/SmartStore.Services/Media/Album/MediaFolderInfo.cs similarity index 93% rename from src/Libraries/SmartStore.Services/Media/v4/Album/MediaFolderInfo.cs rename to src/Libraries/SmartStore.Services/Media/Album/MediaFolderInfo.cs index 4408dde50c..1f51f35926 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/MediaFolderInfo.cs +++ b/src/Libraries/SmartStore.Services/Media/Album/MediaFolderInfo.cs @@ -19,7 +19,10 @@ public MediaFolderInfo(TreeNode node) [JsonProperty("filesCount")] public int FilesCount => Node.Value.FilesCount; - public static implicit operator TreeNode(MediaFolderInfo folderInfo) => folderInfo.Node; + public static implicit operator TreeNode(MediaFolderInfo folderInfo) + { + return folderInfo.Node; + } [JsonProperty("id")] public int Id => Node.Value.Id; diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/MediaFolderNode.cs b/src/Libraries/SmartStore.Services/Media/Album/MediaFolderNode.cs similarity index 100% rename from src/Libraries/SmartStore.Services/Media/v4/Album/MediaFolderNode.cs rename to src/Libraries/SmartStore.Services/Media/Album/MediaFolderNode.cs diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/SystemAlbumProvider.cs b/src/Libraries/SmartStore.Services/Media/Album/SystemAlbumProvider.cs similarity index 66% rename from src/Libraries/SmartStore.Services/Media/v4/Album/SystemAlbumProvider.cs rename to src/Libraries/SmartStore.Services/Media/Album/SystemAlbumProvider.cs index 68662d2f05..e809483acc 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/SystemAlbumProvider.cs +++ b/src/Libraries/SmartStore.Services/Media/Album/SystemAlbumProvider.cs @@ -11,27 +11,25 @@ using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Orders; namespace SmartStore.Services.Media { public partial class SystemAlbumProvider : IAlbumProvider, IMediaTrackDetector { private readonly IDbContext _dbContext; - + public SystemAlbumProvider(IDbContext dbContext) { _dbContext = dbContext; } - public const string Products = "product"; - public const string Categories = "category"; - public const string Brands = "brand"; - public const string Customers = "customer"; - public const string Blog = "blog"; - public const string News = "news"; - public const string Forums = "forum"; + public const string Catalog = "catalog"; + public const string Content = "content"; public const string Downloads = "download"; public const string Messages = "message"; + public const string Customers = "customer"; public const string Files = "file"; #region Album Provider @@ -42,74 +40,45 @@ public IEnumerable GetAlbums() { new MediaAlbum { - Name = Products, - ResKey = "Admin.Catalog.Products", + Name = Catalog, + ResKey = "Admin.Catalog", CanDetectTracks = true, Order = int.MinValue }, new MediaAlbum { - Name = Categories, - ResKey = "Admin.Catalog.Categories", + Name = Content, + ResKey = "Admin.Media.Album.Content", CanDetectTracks = true, Order = int.MinValue + 10 }, new MediaAlbum { - Name = Brands, - ResKey = "Manufacturers", + Name = Downloads, + ResKey = "Common.Downloads", CanDetectTracks = true, - Order = int.MinValue + 20 - }, - new MediaAlbum - { - Name = Customers, - ResKey = "Admin.Customers", - CanDetectTracks = true, // TBD Order = int.MinValue + 30 }, new MediaAlbum { - Name = Blog, - ResKey = "Blog", + Name = Messages, + ResKey = "Admin.Media.Album.Message", CanDetectTracks = true, Order = int.MinValue + 40 }, new MediaAlbum { - Name = News, - ResKey = "News", - CanDetectTracks = true, + Name = Customers, + ResKey = "Admin.Customers", + CanDetectTracks = true, // TBD Order = int.MinValue + 50 }, new MediaAlbum - { - Name = Forums, - ResKey = "Forum.Forum", - CanDetectTracks = false, // TBD - Order = int.MinValue + 60 - }, - new MediaAlbum - { - Name = Downloads, - ResKey = "Common.Downloads", - CanDetectTracks = true, - Order = int.MinValue + 70 - }, - new MediaAlbum - { - Name = Messages, - ResKey = "Admin.Media.Album.Message", - CanDetectTracks = true, - Order = int.MinValue + 80 - }, - new MediaAlbum { Name = Files, ResKey = "Admin.Media.Album.File", CanDetectTracks = false, IncludePath = true, - // Slug = "uploaded", // TBD: hmmmm?? Order = int.MaxValue } }; @@ -117,34 +86,15 @@ public IEnumerable GetAlbums() public AlbumDisplayHint GetDisplayHint(MediaAlbum album) { - if (album.Name == Products) + + if (album.Name == Catalog) { return new AlbumDisplayHint { OverlayIcon = "fa fa-cube" }; } - if (album.Name == Categories) + if (album.Name == Content) { return new AlbumDisplayHint { OverlayIcon = "fa fa-sitemap" }; } - if (album.Name == Brands) - { - return new AlbumDisplayHint { OverlayIcon = "far fa-building" }; - } - if (album.Name == Customers) - { - return new AlbumDisplayHint { OverlayIcon = "fa fa-user" }; - } - if (album.Name == Blog) - { - return new AlbumDisplayHint { OverlayIcon = "fa fa-blog" }; - } - if (album.Name == News) - { - return new AlbumDisplayHint { OverlayIcon = "fa fa-newspaper" }; - } - if (album.Name == Forums) - { - return new AlbumDisplayHint { OverlayIcon = "fa fa-users" }; - } if (album.Name == Downloads) { return new AlbumDisplayHint { OverlayIcon = "fa fa-download" }; @@ -153,6 +103,10 @@ public AlbumDisplayHint GetDisplayHint(MediaAlbum album) { return new AlbumDisplayHint { OverlayIcon = "fa fa-envelope" }; } + if (album.Name == Customers) + { + return new AlbumDisplayHint { OverlayIcon = "fa fa-user" }; + } if (album.Name == Files) { // TODO: var(--success) should be system default. @@ -168,30 +122,27 @@ public AlbumDisplayHint GetDisplayHint(MediaAlbum album) public void ConfigureTracks(string albumName, TrackedMediaPropertyTable table) { - if (albumName == Products) + if (albumName == Catalog) { table.Register(x => x.MediaFileId); table.Register(x => x.MediaFileId); table.Register(x => x.MediaFileId); + table.Register(x => x.MediaFileId); table.Register(x => x.MediaFileId); - } - else if (albumName == Categories) - { table.Register(x => x.MediaFileId); - } - else if (albumName == Brands) - { table.Register(x => x.MediaFileId); } - else if (albumName == Blog) + else if (albumName == Content) { table.Register(x => x.MediaFileId); table.Register(x => x.PreviewMediaFileId); - } - else if (albumName == News) - { table.Register(x => x.MediaFileId); table.Register(x => x.PreviewMediaFileId); + table.Register(x => x.LogoMediaFileId); + table.Register(x => x.FavIconMediaFileId); + table.Register(x => x.PngIconMediaFileId); + table.Register(x => x.AppleTouchIconMediaFileId); + table.Register(x => x.MsTileImageMediaFileId); } else if (albumName == Downloads) { @@ -199,7 +150,7 @@ public void ConfigureTracks(string albumName, TrackedMediaPropertyTable table) } else if (albumName == Messages) { - // TODO: (mm) These props are localizable + // TODO: (mm) (mc) These props are localizable table.Register(x => x.Attachment1FileId); table.Register(x => x.Attachment2FileId); table.Register(x => x.Attachment3FileId); @@ -211,154 +162,164 @@ public IEnumerable DetectAllTracks(string albumName) var ctx = _dbContext; var entityName = string.Empty; - // TODO: Messages, Forums (?), Store (?) - // Products - if (albumName == Products) + if (albumName == Catalog) { // Products { var name = nameof(ProductMediaFile); - var p = new FastPager(ctx.Set().AsNoTracking()); + var p = new FastPager(ctx.Set().AsNoTracking(), 5000); while (p.ReadNextPage(x => new { x.Id, x.ProductId, x.MediaFileId }, x => x.Id, out var list)) { foreach (var x in list) { yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId, Property = nameof(x.MediaFileId) }; } + list.Clear(); } } + GC.Collect(); + // ProductAttributeOption { var name = nameof(ProductAttributeOption); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId > 0)); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId > 0), 5000); while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { foreach (var x in list) { yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId, Property = nameof(x.MediaFileId) }; } + list.Clear(); } } // ProductVariantAttributeValue { var name = nameof(ProductVariantAttributeValue); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId > 0)); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId > 0), 5000); while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { foreach (var x in list) { yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId, Property = nameof(x.MediaFileId) }; } + list.Clear(); } } - // SpecificationAttributeOption + // CheckoutAttributeValue { - var name = nameof(SpecificationAttributeOption); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId > 0)); + var name = nameof(CheckoutAttributeValue); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue), 5000); while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { foreach (var x in list) { - yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId, Property = nameof(x.MediaFileId) }; + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; } + list.Clear(); } } - yield break; - } + GC.Collect(); - // Categories - if (albumName == Categories) - { - var name = nameof(Category); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue)); - while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) + // SpecificationAttributeOption { - foreach (var x in list) + var name = nameof(SpecificationAttributeOption); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId > 0), 5000); + while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { - yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; + foreach (var x in list) + { + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId, Property = nameof(x.MediaFileId) }; + } + list.Clear(); } } - yield break; - } - - // Brands - if (albumName == Brands) - { - var name = nameof(Manufacturer); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue)); - while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) + // Categories { - foreach (var x in list) + var name = nameof(Category); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue), 5000); + while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { - yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; + foreach (var x in list) + { + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; + } + list.Clear(); } } - yield break; - } + GC.Collect(); - // BlogPost - if (albumName == Blog) - { - var name = nameof(BlogPost); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue || x.PreviewMediaFileId.HasValue)); - while (p.ReadNextPage(x => new { x.Id, x.MediaFileId, x.PreviewMediaFileId }, x => x.Id, out var list)) + // Brands { - foreach (var x in list) + var name = nameof(Manufacturer); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue), 5000); + while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { - if (x.MediaFileId.HasValue) + foreach (var x in list) + { yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; - if (x.PreviewMediaFileId.HasValue) - yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.PreviewMediaFileId.Value, Property = nameof(x.PreviewMediaFileId) }; + } + list.Clear(); } } yield break; } - // NewsItem - if (albumName == News) + // Content + if (albumName == Content) { - var name = nameof(NewsItem); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue || x.PreviewMediaFileId.HasValue)); - while (p.ReadNextPage(x => new { x.Id, x.MediaFileId, x.PreviewMediaFileId }, x => x.Id, out var list)) + // Blog { - foreach (var x in list) + var name = nameof(BlogPost); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue || x.PreviewMediaFileId.HasValue)); + while (p.ReadNextPage(x => new { x.Id, x.MediaFileId, x.PreviewMediaFileId }, x => x.Id, out var list)) { - if (x.MediaFileId.HasValue) - yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; - if (x.PreviewMediaFileId.HasValue) - yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.PreviewMediaFileId.Value, Property = nameof(x.PreviewMediaFileId) }; + foreach (var x in list) + { + if (x.MediaFileId.HasValue) + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; + if (x.PreviewMediaFileId.HasValue) + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.PreviewMediaFileId.Value, Property = nameof(x.PreviewMediaFileId) }; + } + list.Clear(); } } - yield break; - } - - // Customer - if (albumName == Customers) - { - var name = nameof(Customer); - var key = SystemCustomerAttributeNames.AvatarPictureId; + // NewsItem + { + var name = nameof(NewsItem); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue || x.PreviewMediaFileId.HasValue)); + while (p.ReadNextPage(x => new { x.Id, x.MediaFileId, x.PreviewMediaFileId }, x => x.Id, out var list)) + { + foreach (var x in list) + { + if (x.MediaFileId.HasValue) + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; + if (x.PreviewMediaFileId.HasValue) + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.PreviewMediaFileId.Value, Property = nameof(x.PreviewMediaFileId) }; + } + list.Clear(); + } + } - // Avatars - var p = new FastPager(ctx.Set().AsNoTracking() - .Where(x => x.KeyGroup == nameof(Customer) && x.Key == key)); - while (p.ReadNextPage(x => new { x.Id, x.EntityId, x.Value }, x => x.Id, out var list)) + // Store { - foreach (var x in list) + var name = nameof(Store); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.LogoMediaFileId > 0)); + while (p.ReadNextPage(x => new { x.Id, x.LogoMediaFileId }, x => x.Id, out var list)) { - var id = x.Value.ToInt(); - if (id > 0) + foreach (var x in list) { - yield return new MediaTrack { EntityId = x.EntityId, EntityName = name, MediaFileId = id, Property = key }; + yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.LogoMediaFileId, Property = nameof(x.LogoMediaFileId) }; } + list.Clear(); } } @@ -369,13 +330,14 @@ public IEnumerable DetectAllTracks(string albumName) if (albumName == Downloads) { var name = nameof(Download); - var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue)); + var p = new FastPager(ctx.Set().AsNoTracking().Where(x => x.MediaFileId.HasValue), 5000); while (p.ReadNextPage(x => new { x.Id, x.MediaFileId }, x => x.Id, out var list)) { foreach (var x in list) { yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.MediaFileId.Value, Property = nameof(x.MediaFileId) }; } + list.Clear(); } yield break; @@ -397,6 +359,32 @@ public IEnumerable DetectAllTracks(string albumName) if (x.Attachment3FileId.HasValue) yield return new MediaTrack { EntityId = x.Id, EntityName = name, MediaFileId = x.Attachment3FileId.Value, Property = nameof(x.Attachment3FileId) }; } + list.Clear(); + } + + yield break; + } + + // Customer + if (albumName == Customers) + { + var name = nameof(Customer); + var key = SystemCustomerAttributeNames.AvatarPictureId; + + // Avatars + var p = new FastPager(ctx.Set().AsNoTracking() + .Where(x => x.KeyGroup == nameof(Customer) && x.Key == key)); + while (p.ReadNextPage(x => new { x.Id, x.EntityId, x.Value }, x => x.Id, out var list)) + { + foreach (var x in list) + { + var id = x.Value.ToInt(); + if (id > 0) + { + yield return new MediaTrack { EntityId = x.EntityId, EntityName = name, MediaFileId = id, Property = key }; + } + } + list.Clear(); } yield break; diff --git a/src/Libraries/SmartStore.Services/Media/CachedImage.cs b/src/Libraries/SmartStore.Services/Media/CachedImage.cs deleted file mode 100644 index e23d763d16..0000000000 --- a/src/Libraries/SmartStore.Services/Media/CachedImage.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using SmartStore.Core.IO; - -namespace SmartStore.Services.Media -{ - /// - /// Contains information about a cached image - /// - /// - /// An instance of this object is always returned, even when - /// the requested image does not physically exists in the storage. - /// - public class CachedImage - { - private bool? _exists; - private string _mimeType; - - public CachedImage(IFile file) - { - Guard.NotNull(file, nameof(file)); - - File = file; - } - - /// - /// The abstracted file object - /// - public IFile File { get; internal set; } - - /// - /// true when the image exists in the cache, false otherwise. - /// - public bool Exists - { - get - { - return _exists ?? (_exists = File.Exists).Value; - } - // For internal use - set - { - _exists = value; - } - } - - /// - /// The name of the file (without path) - /// - public string FileName - { - get { return System.IO.Path.GetFileName(this.Path); } - } - - public long FileSize - { - get { return !Exists ? 0 : File.Size; } - } - - /// - /// The file extension (without 'dot') - /// - public string Extension { get; set; } - - /// - /// The filemime type - /// - public string MimeType - { - get => _mimeType ?? (_mimeType = MimeTypes.MapNameToMimeType(FileName)); - } - - /// - /// The path relative to the cache root folder - /// - public string Path { get; set; } - - /// - /// The last modified date or null if the file does not exist - /// - public DateTime? LastModifiedUtc - { - get { return Exists ? File.LastUpdated : (DateTime?)null; } - } - - /// - /// Checks whether the file is remote (outside the application's physical root) - /// - public bool IsRemote { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs deleted file mode 100644 index e1e8e5e118..0000000000 --- a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Drawing; -using ImageProcessor; -using ImageProcessor.Imaging; -using ImageProcessor.Imaging.Formats; -using SmartStore.Core.Logging; -using ImageProcessor.Configuration; -using SmartStore.Core.Events; -using SmartStore.Utilities; -using SmartStore.Core.IO; - -namespace SmartStore.Services.Media -{ - public partial class DefaultImageProcessor : IImageProcessor - { - private static long _totalProcessingTime; - - private readonly IEventPublisher _eventPublisher; - - public DefaultImageProcessor(IEventPublisher eventPublisher) - { - _eventPublisher = eventPublisher; - } - - public ILogger Logger { get; set; } = NullLogger.Instance; - - public bool IsSupportedImage(string extension) - { - if (extension.IsEmpty()) - { - return false; - } - - if (extension[0] == '.' && extension.Length > 1) - { - extension = extension.Substring(1); - } - - return ImageProcessorBootstrapper.Instance.SupportedImageFormats - .SelectMany(x => x.FileExtensions) - .Any(x => x.Equals(extension, StringComparison.OrdinalIgnoreCase)); - } - - public ProcessImageResult ProcessImage(ProcessImageQuery query, bool disposeOutput = true) - { - Guard.NotNull(query, nameof(query)); - - ValidateQuery(query); - - var watch = new Stopwatch(); - byte[] inBuffer = null; - - try - { - watch.Start(); - - using (var processor = new ImageFactory(preserveExifData: false, fixGamma: false)) - { - var source = query.Source; - - // Load source - if (source is byte[] b) - { - inBuffer = b; - } - else if (source is Stream s) - { - inBuffer = s.ToByteArray(); - } - else if (source is Image img) - { - processor.Load(img); - } - else if (source is string str) - { - var path = NormalizePath(str); - using (var fs = File.OpenRead(path)) - { - inBuffer = fs.ToByteArray(); - } - } - else if (source is IFile file) - { - using (var fs = file.OpenRead()) - { - inBuffer = fs.ToByteArray(); - } - } - else - { - throw new ProcessImageException("Invalid source type '{0}' in query.".FormatInvariant(query.Source.GetType().FullName), query); - } - - if (inBuffer != null) - { - processor.Load(inBuffer); - } - - // Pre-process event - _eventPublisher.Publish(new ImageProcessingEvent(query, processor)); - - var result = new ProcessImageResult - { - Query = query, - SourceWidth = processor.Image.Width, - SourceHeight = processor.Image.Height, - SourceMimeType = processor.CurrentImageFormat.MimeType, - DisposeOutputStream = disposeOutput - }; - - // Core processing - ProcessImageCore(query, processor, out var fxApplied); - - // Create & prepare result - var outStream = new MemoryStream(); - processor.Save(outStream); - - var fmt = processor.CurrentImageFormat; - result.FileExtension = fmt.DefaultExtension == "jpeg" ? "jpg" : fmt.DefaultExtension; - result.MimeType = fmt.MimeType; - - result.HasAppliedVisualEffects = fxApplied; - result.Width = processor.Image.Width; - result.Height = processor.Image.Height; - - if (inBuffer != null) - { - // Check whether it is more beneficial to return the source instead of the result. - // Prefer result only if its size is smaller than the source size. - // Result size may be larger if a high-compressed image has been uploaded. - // During image processing the source compression algorithm gets lost and the image may be larger in size - // after encoding with default encoders. - var compare = - // only when image was not altered visually... - !fxApplied - // ...size has not changed - && result.Width == result.SourceWidth - && result.Height == result.SourceHeight - // ...and format has not changed - && result.MimeType == result.SourceMimeType; - - if (compare && inBuffer.LongLength <= outStream.Length) - { - // Source is smaller. Throw away result and get back to source. - outStream.Dispose(); - result.OutputStream = new MemoryStream(inBuffer, 0, inBuffer.Length, true, true); - } - } - - // Set output stream - if (result.OutputStream == null) - { - result.OutputStream = outStream; - } - - // Post-process event - _eventPublisher.Publish(new ImageProcessedEvent(query, processor, result)); - - result.OutputStream.Position = 0; - - result.ProcessTimeMs = watch.ElapsedMilliseconds; - - return result; - } - } - catch (Exception ex) - { - throw new ProcessImageException(query, ex); - } - finally - { - if (query.DisposeSource && query.Source is IDisposable source) - { - source.Dispose(); - } - - watch.Stop(); - _totalProcessingTime += watch.ElapsedMilliseconds; - } - } - - /// - /// Processes the loaded image. Inheritors should NOT save the image, this is done by the main method. - /// - /// Query - /// Processor instance - /// - /// Should be true if any effect has been applied that potentially changes the image visually (like background color, contrast, sharpness etc.). - /// Resize and compression quality does NOT count as FX. - /// - protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory processor, out bool fxApplied) - { - fxApplied = false; - - // Resize - var size = query.MaxWidth != null || query.MaxHeight != null - ? new Size(query.MaxWidth ?? 0, query.MaxHeight ?? 0) - : Size.Empty; - - if (!size.IsEmpty) - { - processor.Resize(new ResizeLayer( - size, - resizeMode: ConvertScaleMode(query.ScaleMode), - anchorPosition: ConvertAnchorPosition(query.AnchorPosition), - upscale: false)); - } - - if (query.BackgroundColor.HasValue()) - { - processor.BackgroundColor(ColorTranslator.FromHtml(query.BackgroundColor)); - fxApplied = true; - } - - // Format - if (query.Format != null) - { - var format = query.Format as ISupportedImageFormat; - - if (format == null && query.Format is string) - { - var requestedFormat = ((string)query.Format).ToLowerInvariant(); - switch (requestedFormat) - { - case "jpg": - case "jpeg": - format = new JpegFormat(); - break; - case "png": - format = new PngFormat(); - break; - case "gif": - format = new GifFormat(); - break; - } - } - - if (format != null) - { - processor.Format(format); - } - } - - // Set Quality - if (query.Quality.HasValue) - { - processor.Quality(query.Quality.Value); - } - } - - private string NormalizePath(string path) - { - if (path.IsWebUrl()) - { - throw new NotSupportedException($"Remote images cannot be processed: Path: {path}"); - } - - if (!PathHelper.IsAbsolutePhysicalPath(path)) - { - path = CommonHelper.MapPath(path); - } - - return path; - } - - private void ValidateQuery(ProcessImageQuery query) - { - if (query.Source == null) - { - throw new ArgumentException("During image processing 'ProcessImageQuery.Source' must not be null.", nameof(query)); - } - } - - public long TotalProcessingTimeMs - { - get { return _totalProcessingTime; } - } - - private ResizeMode ConvertScaleMode(string mode) - { - switch (mode.EmptyNull().ToLower()) - { - case "boxpad": - return ResizeMode.BoxPad; - case "crop": - return ResizeMode.Crop; - case "min": - return ResizeMode.Min; - case "pad": - return ResizeMode.Pad; - case "stretch": - return ResizeMode.Stretch; - default: - return ResizeMode.Max; - } - } - - private AnchorPosition ConvertAnchorPosition(string anchor) - { - switch (anchor.EmptyNull().ToLower()) - { - case "top": - return AnchorPosition.Top; - case "bottom": - return AnchorPosition.Bottom; - case "left": - return AnchorPosition.Left; - case "right": - return AnchorPosition.Right; - case "top-left": - return AnchorPosition.TopLeft; - case "top-right": - return AnchorPosition.TopRight; - case "bottom-left": - return AnchorPosition.BottomLeft; - case "bottom-right": - return AnchorPosition.BottomRight; - default: - return AnchorPosition.Center; - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/DownloadService.cs b/src/Libraries/SmartStore.Services/Media/DownloadService.cs index d7890e30d9..89297b15b0 100644 --- a/src/Libraries/SmartStore.Services/Media/DownloadService.cs +++ b/src/Libraries/SmartStore.Services/Media/DownloadService.cs @@ -10,9 +10,6 @@ using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Plugins; -using SmartStore.Services.Configuration; -using SmartStore.Services.Media.Storage; namespace SmartStore.Services.Media { @@ -20,76 +17,80 @@ public partial class DownloadService : IDownloadService { private readonly IRepository _downloadRepository; private readonly IMediaService _mediaService; - private readonly Provider _storageProvider; - public DownloadService( - IRepository downloadRepository, - IMediaService mediaService, - ISettingService settingService, - IProviderManager providerManager) + public DownloadService(IRepository downloadRepository, IMediaService mediaService) { _downloadRepository = downloadRepository; _mediaService = mediaService; + } - var systemName = settingService.GetSettingByKey("Media.Storage.Provider", DatabaseMediaStorageProvider.SystemName); - - _storageProvider = providerManager.GetProvider(systemName); - } - - public virtual Download GetDownloadById(int downloadId) + public virtual Download GetDownloadById(int downloadId) { if (downloadId == 0) return null; - + var download = _downloadRepository.Table.Expand(x => x.MediaFile).FirstOrDefault(x => x.Id == downloadId); return download; } - public virtual IList GetDownloadsByIds(int[] downloadIds) - { - if (downloadIds == null || downloadIds.Length == 0) - return new List(); + public virtual IList GetDownloadsByIds(int[] downloadIds) + { + if (downloadIds == null || downloadIds.Length == 0) + return new List(); - var query = from dl in _downloadRepository.Table.Expand(x => x.MediaFile) + var query = from dl in _downloadRepository.Table.Expand(x => x.MediaFile) where downloadIds.Contains(dl.Id) - select dl; - - var downloads = query.ToList(); - - // sort by passed identifier sequence - return downloads.OrderBySequence(downloadIds).ToList(); - } - - public virtual IList GetDownloadsFor(TEntity entity) where TEntity : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - - return GetDownloadsFor(entity.Id, entity.GetUnproxiedType().Name); - } - - public virtual IList GetDownloadsFor(int entityId, string entityName) - { - if (entityId > 0) - { - var downloads = (from x in _downloadRepository.Table.Expand(x => x.MediaFile) + select dl; + + var downloads = query.ToList(); + + // sort by passed identifier sequence + return downloads.OrderBySequence(downloadIds).ToList(); + } + + public virtual IList GetDownloadsFor(TEntity entity, bool versionedFilesOnly = false) where TEntity : BaseEntity + { + Guard.NotNull(entity, nameof(entity)); + + return GetDownloadsFor(entity.Id, entity.GetUnproxiedType().Name, versionedFilesOnly); + } + + public virtual IList GetDownloadsFor(int entityId, string entityName, bool versionedFilesOnly = false) + { + if (entityId > 0) + { + var downloads = (from x in _downloadRepository.Table.Expand(x => x.MediaFile) where x.EntityId == entityId && x.EntityName == entityName - select x).ToList(); + select x).ToList(); - if (downloads.Any()) - { - var idsOrderedByVersion = downloads - .Select(x => new { x.Id, Version = new SemanticVersion(x.FileVersion.HasValue() ? x.FileVersion : "1.0.0.0") }) - .OrderBy(x => x.Version) - .Select(x => x.Id); + if (versionedFilesOnly) + { + downloads = downloads.Where(x => !string.IsNullOrEmpty(x.FileVersion)).ToList(); + } - downloads = new List(downloads.OrderBySequence(idsOrderedByVersion)); + if (downloads.Any()) + { + var idsOrderedByVersion = downloads + .Select(x => + { + if (SemanticVersion.TryParse(x.FileVersion.HasValue() ? x.FileVersion : "1.0.0.0", out var version)) + { + return new { x.Id, Version = version }; + } + + return new { x.Id, Version = new SemanticVersion(0, 0, 0, 0) }; + }) + .OrderByDescending(x => x.Version) + .Select(x => x.Id); - return downloads; - } - } + downloads = new List(downloads.OrderBySequence(idsOrderedByVersion)); - return new List(); - } + return downloads; + } + } + + return new List(); + } public virtual Download GetDownloadByVersion(int entityId, string entityName, string fileVersion) { @@ -98,7 +99,7 @@ public virtual Download GetDownloadByVersion(int entityId, string entityName, st var download = (from x in _downloadRepository.Table.Expand(x => x.MediaFile) where x.EntityId == entityId && x.EntityName.Equals(entityName) && x.FileVersion.Equals(fileVersion) select x).FirstOrDefault(); - + return download; } @@ -112,7 +113,7 @@ public virtual Multimap GetDownloadsByEntityIds(int[] entityIds, var query = _downloadRepository.TableUntracked.Expand(x => x.MediaFile) .Where(x => entityIds.Contains(x.EntityId) && x.EntityName == entityName) - .OrderBy(x => x.FileVersion); + .OrderBy(x => x.FileVersion); var map = query .ToList() @@ -138,37 +139,35 @@ public virtual Download GetDownloadByGuid(Guid downloadGuid) public virtual void DeleteDownload(Download download) { - Guard.NotNull(download, nameof(download)); + Guard.NotNull(download, nameof(download)); - _downloadRepository.Delete(download); + _downloadRepository.Delete(download); } public virtual void InsertDownload(Download download) { - Guard.NotNull(download, nameof(download)); + Guard.NotNull(download, nameof(download)); _downloadRepository.Insert(download); } - public virtual void InsertDownload(Download download, Stream stream, string fileName) + public virtual MediaFileInfo InsertDownload(Download download, Stream stream, string fileName) { Guard.NotNull(download, nameof(download)); - var path = _mediaService.CombinePaths(SystemAlbumProvider.Downloads, fileName.ToValidFileName()); + var path = _mediaService.CombinePaths(SystemAlbumProvider.Downloads, fileName); var file = _mediaService.SaveFile(path, stream, dupeFileHandling: DuplicateFileHandling.Rename); file.File.Hidden = true; download.MediaFile = file.File; _downloadRepository.Insert(download); - // Why saving a second time?? Don't do it! - // Save to storage. - //_storageProvider.Value.Save(file.File, stream); + return file; } - public virtual void UpdateDownload(Download download) + public virtual void UpdateDownload(Download download) { - Guard.NotNull(download, nameof(download)); + Guard.NotNull(download, nameof(download)); download.UpdatedOnUtc = DateTime.UtcNow; @@ -189,7 +188,7 @@ public virtual bool IsDownloadAllowed(OrderItem orderItem) return false; var product = orderItem.Product; - if (product == null || !product.IsDownload) + if (product == null || product.Deleted || !product.IsDownload) return false; //payment status @@ -250,11 +249,10 @@ public virtual bool IsLicenseDownloadAllowed(OrderItem orderItem) orderItem.LicenseDownloadId > 0; } - public virtual byte[] LoadDownloadBinary(Download download) - { - Guard.NotNull(download, nameof(download)); - - return _storageProvider.Value.Load(download.MediaFile); - } + public virtual byte[] LoadDownloadBinary(Download download) + { + Guard.NotNull(download, nameof(download)); + return _mediaService.StorageProvider.Load(download.MediaFile); + } } } diff --git a/src/Libraries/SmartStore.Services/Media/Events.cs b/src/Libraries/SmartStore.Services/Media/Events.cs index b292015375..583f34a90a 100644 --- a/src/Libraries/SmartStore.Services/Media/Events.cs +++ b/src/Libraries/SmartStore.Services/Media/Events.cs @@ -2,99 +2,92 @@ using System.Drawing; using System.IO; using System.Web; -using ImageProcessor; +using SmartStore.Services.Media.Imaging; namespace SmartStore.Services.Media { - /// - /// Published for every uploaded image which does NOT exceed maximum - /// allowed size. This gives subscribers the chance to still process the image, - /// e.g. to achive better compression before saving image data to storage. - /// This event does NOT get published when the uploaded image is about to be processed anyway. - /// - /// - /// A subscriber should NOT resize the image. But if you do - and you shouldn't :-) - , don't forget to set . - /// - public class ImageUploadValidatedEvent - { - public ImageUploadValidatedEvent(ProcessImageQuery query, Size size) - { - Query = query; - Size = size; - } + /// + /// Published for every uploaded image which does NOT exceed maximum + /// allowed size. This gives subscribers the chance to still process the image, + /// e.g. to achive better compression before saving image data to storage. + /// This event does NOT get published when the uploaded image is about to be processed anyway. + /// + /// + /// A subscriber should NOT resize the image. But if you do - and you shouldn't :-) - , don't forget to set . + /// + public class ImageUploadedEvent + { + public ImageUploadedEvent(ProcessImageQuery query, Size size) + { + Query = query; + Size = size; + } - /// - /// Contains the source (as byte[]), max size, format and default image quality instructions. - /// - public ProcessImageQuery Query { get; private set; } + /// + /// Contains the source (as byte[], STream or path string), max size, format and default image quality instructions. + /// + public ProcessImageQuery Query { get; private set; } - /// - /// The original size of the uploaded image. May be empty. - /// - public Size Size { get; private set; } + /// + /// The original size of the uploaded image. May be empty. + /// + public Size Size { get; private set; } - /// - /// The processing result. If null, the original data - /// from Query.Source will be put to storage. - /// - public Stream ResultStream { get; set; } + /// + /// The processing result. If null, the original data + /// from Query.Source will be put to storage. + /// + public IImage ResultImage { get; set; } + } - /// - /// Size of the result image. - /// - public Size ResultSize { get; set; } - } + /// + /// Published after image query has been created and initialized + /// by the media middleware controller with data from HttpContent.Request.QueryString. + /// This event implies that a thumbnail is about to be created. + /// + public class ImageQueryCreatedEvent + { + public ImageQueryCreatedEvent(ProcessImageQuery query, HttpContextBase httpContext, string mimeType, string extension) + { + Query = query; + HttpContext = httpContext; + MimeType = mimeType; + Extension = extension; + } - /// - /// Published after image query has been created and initialized - /// by the media middleware controller with data from HttpContent.Request.QueryString. - /// This event implies that a thumbnail is about to be created. - /// - public class ImageQueryCreatedEvent - { - public ImageQueryCreatedEvent(ProcessImageQuery query, HttpContextBase httpContext, string mimeType, string extension) - { - Query = query; - HttpContext = httpContext; - MimeType = mimeType; - Extension = extension; - } + public ProcessImageQuery Query { get; private set; } + public HttpContextBase HttpContext { get; private set; } + public string MimeType { get; private set; } + public string Extension { get; private set; } + } - public ProcessImageQuery Query { get; private set; } - public HttpContextBase HttpContext { get; private set; } - public string MimeType { get; private set; } - public string Extension { get; private set; } - } + /// + /// Published before processing begins, but after the source has been loaded. + /// + public class ImageProcessingEvent + { + public ImageProcessingEvent(ProcessImageQuery query, IImage image) + { + Query = query; + Image = image; + } - /// - /// Published before processing begins, but after the source has been loaded. - /// - public class ImageProcessingEvent - { - public ImageProcessingEvent(ProcessImageQuery query, ImageFactory processor) - { - Query = query; - Processor = processor; - } + public ProcessImageQuery Query { get; private set; } + public IImage Image { get; private set; } + } - public ProcessImageQuery Query { get; private set; } - public ImageFactory Processor { get; private set; } - } + /// + /// Published after processing finishes. + /// + public class ImageProcessedEvent + { + public ImageProcessedEvent(ProcessImageQuery query, ProcessImageResult result) + { + Query = query; + Result = result; + } - /// - /// Published after processing finishes and the result is saved to a stream (ProcessImageResult.Result) - /// - public class ImageProcessedEvent - { - public ImageProcessedEvent(ProcessImageQuery query, ImageFactory processor, ProcessImageResult result) - { - Query = query; - Processor = processor; - Result = result; - } - - public ProcessImageQuery Query { get; private set; } - public ImageFactory Processor { get; private set; } - public ProcessImageResult Result { get; private set; } - } + public ProcessImageQuery Query { get; private set; } + public ProcessImageResult Result { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Extensions/IFolderServiceExtensions.cs b/src/Libraries/SmartStore.Services/Media/Extensions/IFolderServiceExtensions.cs similarity index 69% rename from src/Libraries/SmartStore.Services/Media/v4/Extensions/IFolderServiceExtensions.cs rename to src/Libraries/SmartStore.Services/Media/Extensions/IFolderServiceExtensions.cs index 3826896be2..e48e0b7949 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Extensions/IFolderServiceExtensions.cs +++ b/src/Libraries/SmartStore.Services/Media/Extensions/IFolderServiceExtensions.cs @@ -10,31 +10,53 @@ namespace SmartStore.Services.Media { public static class IFolderServiceExtensions { + /// + /// Gets a folder entity by path from the database, e.g. "catalog/subfolder1/subfolder2". + /// The first token always refers to an album. This method bypasses the cache. + /// public static MediaFolder GetFolderByPath(this IFolderService service, string path, bool withFiles = false) { return service.GetFolderById(service.GetNodeByPath(path)?.Value?.Id ?? 0, withFiles); } + /// + /// Finds the folder node for a given object. + /// + /// The found folder node or null. public static TreeNode FindNode(this IFolderService service, MediaFile mediaFile) { return service.GetNodeById(mediaFile?.FolderId ?? 0); } + /// + /// Finds the root album node for a given object. + /// + /// The found album node or null. public static TreeNode FindAlbum(this IFolderService service, MediaFile mediaFile) { return FindNode(service, mediaFile)?.Closest(x => x.Value.IsAlbum); } + /// + /// Finds the root album node for a given folder id. + /// + /// The found album node or null. public static TreeNode FindAlbum(this IFolderService service, int folderId) { return service.GetNodeById(folderId)?.Closest(x => x.Value.IsAlbum); } + /// + /// Checks whether all passed files are contained in the same album. + /// public static bool AreInSameAlbum(this IFolderService service, params MediaFile[] files) { return files.Select(x => FindAlbum(service, x)).Distinct().Count() <= 1; } + /// + /// Checks whether all passed folder ids are children of the same album. + /// public static bool AreInSameAlbum(this IFolderService service, params int[] folderIds) { return folderIds.Select(x => FindAlbum(service, x)).Distinct().Count() <= 1; diff --git a/src/Libraries/SmartStore.Services/Media/Extensions/IMediaServiceExtensions.cs b/src/Libraries/SmartStore.Services/Media/Extensions/IMediaServiceExtensions.cs new file mode 100644 index 0000000000..47cc315cfa --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Extensions/IMediaServiceExtensions.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; +using SmartStore.Services.Media.Imaging; + +namespace SmartStore.Services.Media +{ + public static class IMediaServiceExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MediaSearchResult SearchFiles(this IMediaService service, MediaSearchQuery query, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking) + { + return service.SearchFiles(query, null, flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task SearchFilesAsync(this IMediaService service, MediaSearchQuery query, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking) + { + return await service.SearchFilesAsync(query, null, flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetUrl(this IMediaService service, int? fileId, ProcessImageQuery imageQuery, string host = null, bool doFallback = true) + { + return service.GetUrl(service.GetFileById(fileId ?? 0, MediaLoadFlags.AsNoTracking), imageQuery, host, doFallback); + } + + public static string GetUrl(this IMediaService service, int? fileId, int thumbnailSize, string host = null, bool doFallback = true) + { + ProcessImageQuery query = thumbnailSize > 0 + ? new ProcessImageQuery { MaxSize = thumbnailSize } + : null; + + return service.GetUrl(service.GetFileById(fileId ?? 0, MediaLoadFlags.AsNoTracking), query, host, doFallback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetUrl(this IMediaService service, MediaFile file, int thumbnailSize, string host = null, bool doFallback = true) + { + ProcessImageQuery query = thumbnailSize > 0 + ? new ProcessImageQuery { MaxSize = thumbnailSize } + : null; + + return service.GetUrl(file != null ? service.ConvertMediaFile(file) : null, query, host, doFallback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetUrl(this IMediaService service, MediaFileInfo file, int thumbnailSize, string host = null, bool doFallback = true) + { + ProcessImageQuery query = thumbnailSize > 0 + ? new ProcessImageQuery { MaxSize = thumbnailSize } + : null; + + return service.GetUrl(file, query, host, doFallback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetFallbackUrl(this IMediaService service, int thumbnailSize = 0) + { + ProcessImageQuery query = thumbnailSize > 0 + ? new ProcessImageQuery { MaxSize = thumbnailSize } + : null; + + return service.GetUrl((MediaFileInfo)null, query, null, true); + } + + /// + /// Tries to find an equal file by comparing the source buffer to a list of files. + /// + /// Binary source file data to find a match for. + /// The sequence of files to seek within for duplicates. + /// A file from the collection whose content is equal to . + /// The passed file binary when no file equals in the sequence, null otherwise. + public static byte[] FindEqualFile(this IMediaService service, byte[] sourceBuffer, IEnumerable files, out MediaFile equalFile) + { + Guard.NotNull(sourceBuffer, nameof(sourceBuffer)); + + if (!service.FindEqualFile(new MemoryStream(sourceBuffer), files, false, out equalFile)) + { + return sourceBuffer; + } + + return null; + } + + /// + /// Tries to find an equal file by file name, then by comparing the binary contents of the matched files to binary content. + /// + /// The full physical path to the source file to find a duplicate for (e.g. a local or downloaded file during an import process). + /// The id of the folder in which to look for duplicates. + /// Whether to search in subfolders too. + /// The first file whose content is equal to the content of . + /// true when a duplicate file was found, false otherwise. + public static bool FindEqualFile(this IMediaService service, string sourcePath, int targetFolderId, bool deepSearch, out MediaFile equalFile) + { + Guard.NotEmpty(sourcePath, nameof(sourcePath)); + + var fi = new FileInfo(sourcePath); + if (!fi.Exists) + { + equalFile = null; + return false; + } + + return FindEqualFile(service, fi.OpenRead(), fi.Name, targetFolderId, deepSearch, out equalFile); + } + + /// + /// Tries to find an equal file by file name, then by comparing the binary contents of the matched files to content. + /// + /// The source file stream to find a duplicate for (e.g. a local or downloaded file during an import process). + /// The file name used to determine potential duplicates to check against. + /// The id of the folder in which to look for duplicates. + /// Whether to search in subfolders too. + /// The first file whose content is equal to the content of . + /// true when a duplicate file was found, false otherwise. + public static bool FindEqualFile(this IMediaService service, Stream source, string fileName, int targetFolderId, bool deepSearch, out MediaFile equalFile) + { + Guard.NotNull(source, nameof(source)); + Guard.NotEmpty(fileName, nameof(fileName)); + Guard.IsPositive(targetFolderId, nameof(targetFolderId)); + + equalFile = null; + + var query = new MediaSearchQuery + { + FolderId = targetFolderId, + DeepSearch = deepSearch, + ExactMatch = true, + Term = fileName, + IncludeAltForTerm = false + }; + + var matches = service.SearchFiles(query); + + if (matches.TotalCount == 0) + { + return false; + } + + return service.FindEqualFile(source, matches.Select(x => x.File), true, out equalFile); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Extensions/IMediaTrackerExtensions.cs b/src/Libraries/SmartStore.Services/Media/Extensions/IMediaTrackerExtensions.cs new file mode 100644 index 0000000000..6f14b61f58 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Extensions/IMediaTrackerExtensions.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using SmartStore.Core; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.Media +{ + public static class IMediaTrackerExtensions + { + public static void Track(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() + { + Guard.NotNull(path, nameof(path)); + + var getter = path.CompileFast(); + var mediaFileId = getter.Invoke(entity); + if (mediaFileId > 0) + tracker.Track(entity, mediaFileId, path.ExtractPropertyInfo().Name); + } + + public static void Track(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() + { + Guard.NotNull(path, nameof(path)); + + var getter = path.CompileFast(); + var mediaFileId = getter.Invoke(entity); + if (mediaFileId > 0) + tracker.Track(entity, mediaFileId.Value, path.ExtractPropertyInfo().Name); + } + + public static void Untrack(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() + { + Guard.NotNull(path, nameof(path)); + + var getter = path.CompileFast(); + var mediaFileId = getter.Invoke(entity); + if (mediaFileId > 0) + tracker.Untrack(entity, mediaFileId, path.ExtractPropertyInfo().Name); + } + + public static void Untrack(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() + { + Guard.NotNull(path, nameof(path)); + + var getter = path.CompileFast(); + var mediaFileId = getter.Invoke(entity); + if (mediaFileId > 0) + tracker.Untrack(entity, mediaFileId.Value, path.ExtractPropertyInfo().Name); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Track(this IMediaTracker tracker, MediaTrack track) + { + if (track == null) + return; + + tracker.TrackMany(new[] { track }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UpdateTracks(this IMediaTracker tracker, BaseEntity entity, int? prevMediaFileId, int? currentMediaFileId, string propertyName) + { + UpdateTracks(tracker, entity.Id, entity.GetEntityName(), prevMediaFileId, currentMediaFileId, propertyName); + } + + public static void UpdateTracks(this IMediaTracker tracker, int entityId, string entityName, int? prevMediaFileId, int? currentMediaFileId, string propertyName) + { + var tracks = new List(2); + bool isModified = prevMediaFileId != currentMediaFileId; + + if (prevMediaFileId > 0 && isModified) + { + tracks.Add(new MediaTrack + { + EntityId = entityId, + EntityName = entityName, + MediaFileId = prevMediaFileId.Value, + Property = propertyName, + Operation = MediaTrackOperation.Untrack + }); + } + + if (currentMediaFileId > 0 && isModified) + { + tracks.Add(new MediaTrack + { + EntityId = entityId, + EntityName = entityName, + MediaFileId = currentMediaFileId.Value, + Property = propertyName, + Operation = MediaTrackOperation.Track + }); + } + + if (tracks.Count > 0) + { + tracker.TrackMany(tracks); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Extensions/MediaFileExtensions.cs b/src/Libraries/SmartStore.Services/Media/Extensions/MediaFileExtensions.cs new file mode 100644 index 0000000000..4b54a3295b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Extensions/MediaFileExtensions.cs @@ -0,0 +1,70 @@ +using System.IO; +using SmartStore.Core; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.IO; +using SmartStore.Services.Media.Storage; + +namespace SmartStore.Services.Media +{ + public static class MediaFileExtensions + { + /// + /// Applies Blob to file + /// + /// The file binary (can be null) + public static void ApplyBlob(this MediaFile file, byte[] blob) + { + Guard.NotNull(file, nameof(file)); + + if (blob == null || blob.LongLength == 0) + { + file.MediaStorageId = null; + file.MediaStorage = null; + } + else + { + if (file.MediaStorage != null) + { + file.MediaStorage.Data = blob; + } + else + { + file.MediaStorage = new MediaStorage { Data = blob }; + } + } + } + + /// + /// Refreshes file metadata like size, dimensions etc. + /// + /// The file stream (can be null) + public static void RefreshMetadata(this MediaFile file, Stream stream) + { + Guard.NotNull(file, nameof(file)); + + file.Size = stream != null ? (int)stream.Length : 0; + file.Width = null; + file.Height = null; + file.PixelSize = null; + + if (stream != null && file.MediaType == MediaType.Image) + { + try + { + var size = ImageHeader.GetDimensions(stream, file.MimeType); + file.Width = size.Width; + file.Height = size.Height; + file.PixelSize = size.Width * size.Height; + } + catch + { + // Don't attempt again + file.Width = 0; + file.Height = 0; + file.PixelSize = 0; + } + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Handlers/IMediaHandler.cs b/src/Libraries/SmartStore.Services/Media/Handlers/IMediaHandler.cs similarity index 100% rename from src/Libraries/SmartStore.Services/Media/v4/Handlers/IMediaHandler.cs rename to src/Libraries/SmartStore.Services/Media/Handlers/IMediaHandler.cs diff --git a/src/Libraries/SmartStore.Services/Media/v4/Handlers/ImageHandler.cs b/src/Libraries/SmartStore.Services/Media/Handlers/ImageHandler.cs similarity index 76% rename from src/Libraries/SmartStore.Services/Media/v4/Handlers/ImageHandler.cs rename to src/Libraries/SmartStore.Services/Media/Handlers/ImageHandler.cs index 264b57735f..471adb27d1 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Handlers/ImageHandler.cs +++ b/src/Libraries/SmartStore.Services/Media/Handlers/ImageHandler.cs @@ -5,22 +5,23 @@ using System.Text; using System.Threading.Tasks; using SmartStore.Core.Logging; +using SmartStore.Services.Media.Imaging; namespace SmartStore.Services.Media { public class ImageHandler : ImageHandlerBase { private readonly IImageProcessor _imageProcessor; - - public ImageHandler(IImageProcessor imageProcessor, IImageCache imageCache) - : base(imageCache) + + public ImageHandler(IImageProcessor imageProcessor, IImageCache imageCache, MediaExceptionFactory exceptionFactory) + : base(imageCache, exceptionFactory) { _imageProcessor = imageProcessor; } protected override bool IsProcessable(MediaHandlerContext context) { - return context.ImageQuery.NeedsProcessing(true) && _imageProcessor.IsSupportedImage(context.PathData.Extension); + return context.ImageQuery.NeedsProcessing(true) && _imageProcessor.Factory.IsSupportedImage(context.PathData.Extension); } protected override Task ProcessImageAsync(MediaHandlerContext context, CachedImage cachedImage, Stream inputStream) @@ -37,17 +38,19 @@ protected override Task ProcessImageAsync(MediaHandlerContext context, CachedIma { Logger.DebugFormat($"Processed image '{cachedImage.FileName}' in {result.ProcessTimeMs} ms.", null); - if (!cachedImage.Extension.IsCaseInsensitiveEqual(result.FileExtension)) + var ext = result.Image.Format.DefaultExtension; + + if (!cachedImage.Extension.IsCaseInsensitiveEqual(ext)) { // jpg <> jpeg - cachedImage.Path = Path.ChangeExtension(cachedImage.Path, result.FileExtension); - cachedImage.Extension = result.FileExtension; + cachedImage.Path = Path.ChangeExtension(cachedImage.Path, ext); + cachedImage.Extension = ext; } - context.ResultStream = result.OutputStream; + context.ResultImage = result.Image; } - return Task.FromResult(0); + return Task.CompletedTask; } } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Handlers/ImageHandlerBase.cs b/src/Libraries/SmartStore.Services/Media/Handlers/ImageHandlerBase.cs similarity index 82% rename from src/Libraries/SmartStore.Services/Media/v4/Handlers/ImageHandlerBase.cs rename to src/Libraries/SmartStore.Services/Media/Handlers/ImageHandlerBase.cs index 90cf6db6da..cf00df9baa 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Handlers/ImageHandlerBase.cs +++ b/src/Libraries/SmartStore.Services/Media/Handlers/ImageHandlerBase.cs @@ -1,21 +1,25 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; +using SmartStore.Services.Media.Imaging; using SmartStore.Utilities.Threading; namespace SmartStore.Services.Media { public abstract class ImageHandlerBase : IMediaHandler { - protected ImageHandlerBase(IImageCache imageCache) + protected ImageHandlerBase(IImageCache imageCache, MediaExceptionFactory exceptionFactory) { ImageCache = imageCache; + ExceptionFactory = exceptionFactory; } public ILogger Logger { get; set; } = NullLogger.Instance; public IImageCache ImageCache { get; set; } + public MediaExceptionFactory ExceptionFactory { get; set; } + public Localizer T { get; set; } = NullLocalizer.Instance; public virtual int Order => -100; @@ -28,9 +32,8 @@ public async Task ExecuteAsync(MediaHandlerContext context) var query = context.ImageQuery; var pathData = context.PathData; - pathData.Extension = "jpg"; - var cachedImage = ImageCache.Get4(context.MediaFileId, pathData, query); + var cachedImage = ImageCache.Get(context.MediaFileId, pathData, query); if (!pathData.Extension.IsCaseInsensitiveEqual(cachedImage.Extension)) { @@ -46,7 +49,7 @@ public async Task ExecuteAsync(MediaHandlerContext context) { // Empty file means: thumb extraction failed before and will most likely fail again. // Don't bother proceeding. - context.Exception = new ExtractThumbnailException(cachedImage.FileName, null); + context.Exception = ExceptionFactory.ExtractThumbnail(cachedImage.FileName); context.Executed = true; return; } @@ -70,10 +73,9 @@ public async Task ExecuteAsync(MediaHandlerContext context) } var inputStream = sourceFile.OpenRead(); - if (inputStream == null) { - context.Exception = new ExtractThumbnailException("No input stream", null); // TODO: (mm) better exception + context.Exception = ExceptionFactory.ExtractThumbnail(sourceFile.Path, T("Admin.Media.Exception.NullInputStream")); context.Executed = true; return; } @@ -92,7 +94,7 @@ public async Task ExecuteAsync(MediaHandlerContext context) // Therefore we create an empty file to prevent repetitive processing. using (var memStream = new MemoryStream()) { - await ImageCache.Put4Async(cachedImage, memStream); + await ImageCache.PutAsync(cachedImage, memStream); } } @@ -108,10 +110,9 @@ public async Task ExecuteAsync(MediaHandlerContext context) } } - if (context.ResultStream != null && context.ResultStream.Length > 0) + if (context.ResultImage != null) { - await ImageCache.Put4Async(cachedImage, context.ResultStream); - context.ResultStream.Position = 0; + ImageCache.Put(cachedImage, context.ResultImage); context.ResultFile = cachedImage.File; } diff --git a/src/Libraries/SmartStore.Services/Media/Handlers/MediaHandlerContext.cs b/src/Libraries/SmartStore.Services/Media/Handlers/MediaHandlerContext.cs new file mode 100644 index 0000000000..0ac54584ec --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Handlers/MediaHandlerContext.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; +using SmartStore.Core.Security; +using SmartStore.Services.Media.Imaging; +using SmartStore.Utilities; + +namespace SmartStore.Services.Media +{ + public class MediaHandlerContext + { + private Lazy _lazySourceFile; + private IFile _sourceFile; + + public MediaHandlerContext() + { + _lazySourceFile = new Lazy(() => GetSourceFile(), false); + } + + public IMediaService MediaService { get; set; } + public IPermissionService PermissionService { get; set; } + public Customer CurrentCustomer { get; set; } + public HttpContextBase HttpContext { get; set; } + + public int MediaFileId { get; set; } + public string RawPath { get; set; } + + public MediaPathData PathData { get; set; } + public ProcessImageQuery ImageQuery { get; set; } + + public IFile SourceFile + { + get => _sourceFile ?? _lazySourceFile.Value; + set => _sourceFile = value; + } + + public bool Executed { get; set; } + public Exception Exception { get; set; } + public IFile ResultFile { get; set; } + public IImage ResultImage { get; set; } + + private IFile GetSourceFile() + { + if (MediaFileId == 0) + { + // This is most likely a request for a default placeholder image + var fallbackImagePath = Path.Combine(MediaUrlGenerator.FallbackImagesRootPath, RawPath).Replace('\\', '/'); + var fi = new FileInfo(CommonHelper.MapPath("~/" + fallbackImagePath, false)); + if (!fi.Exists) + return null; + + return new LocalFileSystem.LocalFile(fallbackImagePath, fi); + } + else + { + // Get file from DB + var mediaFile = MediaService.GetFileById(MediaFileId, MediaLoadFlags.AsNoTracking); + + // File must exist + if (mediaFile == null) + return null; + + // Serve deleted or hidden files only with sufficient permission + if ((mediaFile.Deleted || mediaFile.Hidden) && !PermissionService.Authorize(Permissions.Media.Update, CurrentCustomer)) + return null; + + //// File's mime must match requested mime + //if (!mediaFile.MimeType.IsCaseInsensitiveEqual(prevMime ?? pathData.MimeType)) + // return null; + + // File path must match requested path and file title + // TODO: (mm) (mc) what about MIME and EXT? + if (mediaFile.FolderId != PathData.Folder?.Id || !mediaFile.Title.IsCaseInsensitiveEqual(PathData.FileTitle)) + return null; + + return mediaFile; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/IDownloadService.cs b/src/Libraries/SmartStore.Services/Media/IDownloadService.cs index 9fd8be4abd..39e097e40e 100644 --- a/src/Libraries/SmartStore.Services/Media/IDownloadService.cs +++ b/src/Libraries/SmartStore.Services/Media/IDownloadService.cs @@ -17,27 +17,27 @@ public partial interface IDownloadService /// Download Download GetDownloadById(int downloadId); - /// - /// Gets downloads by identifiers - /// - /// Download identifiers - /// List of download entities - IList GetDownloadsByIds(int[] downloadIds); + /// + /// Gets downloads by identifiers + /// + /// Download identifiers + /// List of download entities + IList GetDownloadsByIds(int[] downloadIds); - /// - /// Gets downloads assigned to an entity - /// - /// Entity to get download for - /// List of download entities sorted by FileVersion - IList GetDownloadsFor(TEntity entity) where TEntity : BaseEntity; + /// + /// Gets downloads assigned to an entity + /// + /// Entity to get download for + /// List of download entities sorted by FileVersion + IList GetDownloadsFor(TEntity entity, bool versionedFilesOnly = false) where TEntity : BaseEntity; - /// - /// Gets downloads by entity identifier - /// - /// Entity identifier - /// Entity name - /// List of download entities - IList GetDownloadsFor(int entityId, string entityName); + /// + /// Gets downloads by entity identifier + /// + /// Entity identifier + /// Entity name + /// List of download entities + IList GetDownloadsFor(int entityId, string entityName, bool versionedFilesOnly = false); /// /// Gets downloads by entity identifier & fileversion @@ -81,13 +81,14 @@ public partial interface IDownloadService /// Download entity. /// Data stream. /// File name. - void InsertDownload(Download download, Stream stream, string fileName); + /// The inserted object instance. + MediaFileInfo InsertDownload(Download download, Stream stream, string fileName); - /// - /// Updates the download. - /// - /// Download. - void UpdateDownload(Download download); + /// + /// Updates the download. + /// + /// Download. + void UpdateDownload(Download download); /// /// Gets a value indicating whether download is allowed @@ -103,11 +104,11 @@ public partial interface IDownloadService /// True if license download is allowed; otherwise, false. bool IsLicenseDownloadAllowed(OrderItem orderItem); - /// - /// Load binary data of a download - /// - /// Download - /// Binary data - byte[] LoadDownloadBinary(Download download); - } + /// + /// Load binary data of a download + /// + /// Download + /// Binary data + byte[] LoadDownloadBinary(Download download); + } } diff --git a/src/Libraries/SmartStore.Services/Media/IImageCache.cs b/src/Libraries/SmartStore.Services/Media/IImageCache.cs deleted file mode 100644 index c85fca0d5c..0000000000 --- a/src/Libraries/SmartStore.Services/Media/IImageCache.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.IO; - -namespace SmartStore.Services.Media -{ - /// - /// Image cache interface - /// - public interface IImageCache - { - /// - /// Adds an image to the cache. - /// - /// An instance of the object, which is returned by the Get() method. - /// The image binary buffer. - /// true when the operation succeded, false otherwise - void Put(CachedImage cachedImage, byte[] buffer); - void Put4(CachedImage cachedImage, Stream stream); - - /// - /// Asynchronously adds an image to the cache. - /// - /// An instance of the object, which is returned by the Get() method. - /// The image binary buffer. - /// true when the operation succeded, false otherwise - Task PutAsync(CachedImage cachedImage, byte[] buffer); - Task Put4Async(CachedImage cachedImage, Stream stream); - - /// - /// Gets an instance of the object, which contains information about a cached image. - /// - /// The picture id of the image to be resolved. - /// The seo friendly picture name of the image to be resolved. - /// The extension of the image to be resolved. - /// The image processing query. - /// An instance of the object - /// If the requested image does not exist in the cache, the value of the Exists property will be false. - CachedImage Get(int? pictureId, string seoFileName, string extension, ProcessImageQuery query = null); - CachedImage Get4(int? mediaFileId, MediaPathData data, ProcessImageQuery query = null); - - /// - /// Gets an instance of the object, which contains information about a cached image. - /// Use this overload to get thumbnail info about uploaded media manager asset files. - /// - /// The file to get info about. - /// The image processing query. - /// An instance of the object - /// If the requested image does not exist in the cache, the value of the Exists property will be false. - CachedImage Get(IFile file, ProcessImageQuery query); - - /// - /// Opens a readonly file stream to the cached image - /// - /// An instance of the object, which is returned by the GetCachedImage() method. - /// File stream - Stream Open(CachedImage cachedImage); - - /// - /// Deletes all cached images for the given - /// - /// The for which to delete cached images - void Delete(MediaFile picture); - - /// - /// Deletes all cached images for the given - /// - /// The for which to delete cached images - void Delete(IFile file); - - /// - /// Deletes all cached images (nukes all files in the cache folder) - /// - void Clear(); - - /// - /// Refreshes the file info. - /// - void RefreshInfo(CachedImage cachedImage); - - /// - /// Calculates statistics about the image cache data. - /// - /// The total count of files in the cache. - /// The total size of files in the cache (in bytes) - void CacheStatistics(out long fileCount, out long totalSize); - - /// - /// Resolves a publicly accessible http url to the image - /// - /// The path of the image relative to the cache root path - /// The image http url - string GetPublicUrl(string imagePath); - } -} diff --git a/src/Libraries/SmartStore.Services/Media/IMediaService.cs b/src/Libraries/SmartStore.Services/Media/IMediaService.cs new file mode 100644 index 0000000000..f259253086 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/IMediaService.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using SmartStore.Core.Domain.Media; +using SmartStore.Services.Media.Imaging; +using SmartStore.Services.Media.Storage; + +namespace SmartStore.Services.Media +{ + #region Enums + + [Flags] + public enum MediaLoadFlags + { + None = 0, + WithBlob = 1 << 0, + WithTags = 1 << 1, + WithTracks = 1 << 2, + WithFolder = 1 << 3, + AsNoTracking = 1 << 4, + Full = WithBlob | WithTags | WithTracks | WithFolder, + FullNoTracking = Full | AsNoTracking + } + + public enum SpecialMediaFolder + { + AllFiles = -500, + Trash = -400, + Orphans = -300, + TransientFiles = -200, + UnassignedFiles = -100 + } + + public enum FileHandling + { + SoftDelete, + MoveToRoot, + Delete + } + + public enum DuplicateFileHandling + { + ThrowError, + Overwrite, + Rename + } + + public enum DuplicateEntryHandling + { + ThrowError, + Overwrite, + // Folder: Overwrite, File: Rename + Rename, + Skip + } + + public enum MimeValidationType + { + NoValidation, + MimeTypeMustMatch, + MediaTypeMustMatch + } + + #endregion + + #region Result & Cargo objects + + public class DuplicateFileInfo + { + [JsonProperty("source")] + public MediaFileInfo SourceFile { get; set; } + + [JsonProperty("dest")] + public MediaFileInfo DestinationFile { get; set; } + + [JsonProperty("uniquePath")] + public string UniquePath { get; set; } + } + + public class FolderOperationResult + { + public string Operation { get; set; } + public MediaFolderInfo Folder { get; set; } + public DuplicateEntryHandling DuplicateEntryHandling { get; set; } + public IList DuplicateFiles { get; set; } + } + + public class FolderDeleteResult + { + public HashSet DeletedFolderIds { get; set; } = new HashSet(); + public IList DeletedFileNames { get; set; } = new List(); + public IList TrackedFileNames { get; set; } = new List(); + public IList LockedFileNames { get; set; } = new List(); + } + + public class FileOperationResult + { + public string Operation { get; set; } + public MediaFileInfo SourceFile { get; set; } + public MediaFileInfo DestinationFile { get; set; } + public DuplicateFileHandling DuplicateFileHandling { get; set; } + public bool IsDuplicate { get; set; } + public string UniquePath { get; set; } + } + + #endregion + + /// + /// Media service interface. + /// + public partial interface IMediaService + { + /// + /// Gets the underlying storage provider + /// + IMediaStorageProvider StorageProvider { get; } + + /// + /// Gets or sets a value indicating whether image post-processing is enabled. + /// It is recommended to turn this off during long-running processes - like product imports - + /// as post-processing can heavily decrease processing time. + /// + public bool ImagePostProcessingEnabled { get; set; } + + /// + /// Determines the number of files that match the filter criteria in . + /// + /// The query that defines the criteria. + /// The number of matching files. + int CountFiles(MediaSearchQuery query); + + /// + /// Determines the number of files that match the filter criteria in asynchronously. + /// + /// The query that defines the criteria. + /// The number of matching files. + Task CountFilesAsync(MediaSearchQuery query); + + /// + /// Determines the number of files that match the filter criteria in and groups them by folders. + /// + /// The filter that defines the criteria. + /// The grouped file counts (all, trash, unassigned, transient, all folders as dictionary) + FileCountResult CountFilesGrouped(MediaFilesFilter filter); + + /// + /// Searches files that match the filter criteria in . + /// + /// The query that defines the criteria. + /// An optional modifier function for the LINQ query that was internally derived from . Can be null. + /// Flags that affect the loading behaviour (eager-loading, tracking etc.) + /// The search result. + MediaSearchResult SearchFiles(MediaSearchQuery query, Func, IQueryable> queryModifier, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); + + /// + /// Searches files that match the filter criteria in . + /// + /// The query that defines the criteria. + /// An optional modifier function for the LINQ query that was internally derived from . Can be null. + /// Flags that affect the loading behaviour (eager-loading, tracking etc.) + /// The search result. + Task SearchFilesAsync(MediaSearchQuery query, Func, IQueryable> queryModifier, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); + + bool FileExists(string path); + MediaFileInfo GetFileByPath(string path, MediaLoadFlags flags = MediaLoadFlags.None); + MediaFileInfo GetFileById(int id, MediaLoadFlags flags = MediaLoadFlags.None); + MediaFileInfo GetFileByName(int folderId, string fileName, MediaLoadFlags flags = MediaLoadFlags.None); + IList GetFilesByIds(int[] ids, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); + bool CheckUniqueFileName(string path, out string newPath); + string CombinePaths(params string[] paths); + + /// + /// Tries to find an equal file by comparing the source stream to a list of files. + /// + /// The source stream to find a match for. + /// The sequence of files to seek within for duplicates. + /// Whether to leave the source stream open. + /// A file from the collection whose content is equal to . + /// true when a duplicate file was found, false otherwise. + bool FindEqualFile(Stream source, IEnumerable files, bool leaveOpen, out MediaFile equalFile); + + MediaFileInfo SaveFile(string path, Stream stream, bool isTransient = true, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); + Task SaveFileAsync(string path, Stream stream, bool isTransient = true, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); + void DeleteFile(MediaFile file, bool permanent, bool force = false); + FileOperationResult CopyFile(MediaFileInfo mediaFile, string destinationFileName, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); + MediaFileInfo MoveFile(MediaFile file, string destinationFileName, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); + MediaFileInfo ReplaceFile(MediaFile file, Stream inStream, string newFileName); + Task ReplaceFileAsync(MediaFile file, Stream inStream, string newFileName); + + bool FolderExists(string path); + MediaFolderInfo CreateFolder(string path); + MediaFolderInfo MoveFolder(string path, string destinationPath); + FolderOperationResult CopyFolder(string path, string destinationPath, DuplicateEntryHandling dupeEntryHandling = DuplicateEntryHandling.Skip); + FolderDeleteResult DeleteFolder(string path, FileHandling fileHandling = FileHandling.SoftDelete); + + MediaFileInfo ConvertMediaFile(MediaFile file); + string GetUrl(MediaFileInfo file, ProcessImageQuery query, string host = null, bool doFallback = true); + } +} diff --git a/src/Libraries/SmartStore.Services/Media/IMediaUrlGenerator.cs b/src/Libraries/SmartStore.Services/Media/IMediaUrlGenerator.cs new file mode 100644 index 0000000000..d90b168755 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/IMediaUrlGenerator.cs @@ -0,0 +1,35 @@ +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Stores; +using SmartStore.Services.Media.Imaging; + +namespace SmartStore.Services.Media +{ + /// + /// Generates URLs for media files + /// + public interface IMediaUrlGenerator + { + /// + /// Generates a public URL for a media file + /// + /// The file to create a URL for + /// Query for image processing / thumbnails. + /// + /// Store host for an absolute URL that also contains scheme and host parts. + /// null: tries to resolve host automatically based on or . + /// String.Empty: bypasses automatic host resolution and does NOT prepend host to path. + /// Any string: host name to use explicitly. + /// + /// + /// Specifies behaviour in case URL generation fails. + /// false: return null. + /// true: return URL to a fallback image which is ~/Content/images/default-image.png by default but can be modified with hidden setting Media.DefaultImageName + /// + /// The passed file's public URL. + string GenerateUrl( + MediaFileInfo file, + ProcessImageQuery imageQuery, + string host = null, + bool doFallback = true); + } +} diff --git a/src/Libraries/SmartStore.Services/Media/ImageCache.cs b/src/Libraries/SmartStore.Services/Media/ImageCache.cs deleted file mode 100644 index 30fcf30616..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ImageCache.cs +++ /dev/null @@ -1,435 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Text; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.IO; -using SmartStore.Core.Logging; -using System.Globalization; - -namespace SmartStore.Services.Media -{ - public class ImageCache : IImageCache - { - public const string IdFormatString = "0000000"; - internal const int MaxDirLength = 4; - - private readonly MediaSettings _mediaSettings; - private readonly IMediaFileSystem _fileSystem; - private readonly IImageProcessor _imageProcessor; - private readonly IFolderService _folderService; - private readonly MediaHelper _mediaHelper; - private readonly string _thumbsRootDir; - - public ImageCache( - MediaSettings mediaSettings, - IMediaFileSystem fileSystem, - IImageProcessor imageProcessor, - IFolderService folderService, - MediaHelper mediaHelper) - { - _mediaSettings = mediaSettings; - _fileSystem = fileSystem; - _imageProcessor = imageProcessor; - _folderService = folderService; - _mediaHelper = mediaHelper; - - _thumbsRootDir = "Thumbs/"; - } - - public ILogger Logger { get; set; } = NullLogger.Instance; - - public void Put4(CachedImage cachedImage, Stream stream) - { - if (PreparePut4(cachedImage, stream)) - { - var path = BuildPath(cachedImage.Path); - _fileSystem.SaveStream(path, stream); - cachedImage.Exists = true; - cachedImage.File = _fileSystem.GetFile(path); - } - } - - public async Task Put4Async(CachedImage cachedImage, Stream stream) - { - if (PreparePut4(cachedImage, stream)) - { - var path = BuildPath(cachedImage.Path); - await _fileSystem.SaveStreamAsync(path, stream); - cachedImage.Exists = true; - cachedImage.File = _fileSystem.GetFile(path); - } - } - - public void Put(CachedImage cachedImage, byte[] buffer) - { - if (PreparePut(cachedImage, buffer)) - { - var path = BuildPath(cachedImage.Path); - - if (cachedImage.Extension == "svg") - { - _fileSystem.WriteAllText(path, Encoding.UTF8.GetString(buffer)); - } - else - { - _fileSystem.WriteAllBytes(path, buffer); - } - - cachedImage.Exists = true; - cachedImage.File = _fileSystem.GetFile(path); - } - } - - public async Task PutAsync(CachedImage cachedImage, byte[] buffer) - { - if (PreparePut(cachedImage, buffer)) - { - var path = BuildPath(cachedImage.Path); - - // save file - if (cachedImage.Extension == "svg") - { - await _fileSystem.WriteAllTextAsync(path, Encoding.UTF8.GetString(buffer)); - } - else - { - await _fileSystem.WriteAllBytesAsync(path, buffer); - } - - // Refresh info - cachedImage.Exists = true; - cachedImage.File = _fileSystem.GetFile(path); - } - } - - private bool PreparePut4(CachedImage cachedImage, Stream stream) - { - Guard.NotNull(cachedImage, nameof(cachedImage)); - - if (stream == null) - { - return false; - } - - if (cachedImage.Exists) - { - _fileSystem.DeleteFile(BuildPath(cachedImage.Path)); - } - - // create folder if needed - string imageDir = System.IO.Path.GetDirectoryName(cachedImage.Path); - if (imageDir.HasValue()) - { - _fileSystem.CreateFolder(BuildPath(imageDir)); - } - - return true; - } - - private bool PreparePut(CachedImage cachedImage, byte[] buffer) - { - Guard.NotNull(cachedImage, nameof(cachedImage)); - - if (buffer == null) - { - return false; - } - - if (cachedImage.Exists) - { - _fileSystem.DeleteFile(BuildPath(cachedImage.Path)); - } - - // create folder if needed - string imageDir = System.IO.Path.GetDirectoryName(cachedImage.Path); - if (imageDir.HasValue()) - { - _fileSystem.CreateFolder(BuildPath(imageDir)); - } - - return true; - } - - public virtual CachedImage Get4(int? mediaFileId, MediaPathData data, ProcessImageQuery query = null) - { - Guard.NotNull(data, nameof(data)); - - var resultExtension = query?.GetResultExtension(); - if (resultExtension != null) - { - data.Extension = resultExtension; - } - - var imagePath = GetCachedImagePath4(mediaFileId, data, query); - var file = _fileSystem.GetFile(BuildPath(imagePath)); - - var result = new CachedImage(file) - { - Path = imagePath, - Extension = data.Extension, - IsRemote = _fileSystem.IsCloudStorage - }; - - return result; - } - - public virtual CachedImage Get(int? pictureId, string seoFileName, string extension, ProcessImageQuery query = null) - { - Guard.NotEmpty(extension, nameof(extension)); - - extension = query?.GetResultExtension() ?? extension.TrimStart('.').ToLower(); - var imagePath = GetCachedImagePath(pictureId, seoFileName, extension, query); - - var file = _fileSystem.GetFile(BuildPath(imagePath)); - - var result = new CachedImage(file) - { - Path = imagePath, - Extension = extension, - IsRemote = _fileSystem.IsCloudStorage - }; - - return result; - } - - public virtual CachedImage Get(IFile file, ProcessImageQuery query) - { - Guard.NotNull(file, nameof(file)); - Guard.NotNull(query, nameof(query)); - - var imagePath = GetCachedImagePath(file, query); - var thumbFile = _fileSystem.GetFile(BuildPath(imagePath)); - - var result = new CachedImage(thumbFile) - { - Path = imagePath, - Extension = file.Extension.TrimStart('.'), - IsRemote = _fileSystem.IsCloudStorage - }; - - return result; - } - - public virtual Stream Open(CachedImage cachedImage) - { - Guard.NotNull(cachedImage, nameof(cachedImage)); - - return _fileSystem.GetFile(BuildPath(cachedImage.Path)).OpenRead(); - } - - public virtual string GetPublicUrl(string imagePath) - { - if (imagePath.IsEmpty()) - return null; - - return _fileSystem.GetPublicUrl(BuildPath(imagePath), true).EmptyNull(); - } - - public virtual void RefreshInfo(CachedImage cachedImage) - { - Guard.NotNull(cachedImage, nameof(cachedImage)); - - var file = _fileSystem.GetFile(cachedImage.File.Path); - cachedImage.File = file; - cachedImage.Exists = file.Exists; - } - - //public virtual void Delete4(MediaFile mediaFile) - //{ - // var filter = string.Format("{0}{1}*.*", - // mediaFile.Id.ToString(IdFormatString), - // mediaFile.FolderId.HasValue ? "-" + mediaFile.FolderId.Value + "-" : ""); - - // var folder = _folderService.GetNodeById(mediaFile.FolderId ?? 0)?.Value; - // var data = new TokenizedMediaPath(folder, mediaFile.Name) { Extension = string.Empty }; - // var pathPattern = GetCachedImagePath4(mediaFile.Id, data); - - // var files = _fileSystem.SearchFiles(_thumbsRootDir, filter); - // //foreach (var file in files) - // //{ - // // _fileSystem.DeleteFile(file); - // //} - //} - - public virtual void Delete(MediaFile picture) - { - var filter = string.Format("{0}*.*", picture.Id.ToString(IdFormatString)); - - var files = _fileSystem.SearchFiles(_thumbsRootDir, filter); - foreach (var file in files) - { - _fileSystem.DeleteFile(file); - } - } - - public virtual void Delete(IFile file) - { - // TODO: (mc) this could lead to more thumbs getting deleted as desired. But who cares? :-) - var filter = string.Format("{0}*.*", file.Title); - - var files = _fileSystem.SearchFiles(BuildPath(file.Directory), filter); - foreach (var f in files) - { - _fileSystem.DeleteFile(f); - } - } - - public virtual void Clear() - { - for (int i = 0; i < 10; i++) - { - - try - { - _fileSystem.DeleteFolder(_thumbsRootDir); - _fileSystem.CreateFolder(_thumbsRootDir); - } - catch (Exception ex) - { - Logger.Error(ex); - } - } - } - - public void CacheStatistics(out long fileCount, out long totalSize) - { - fileCount = 0; - totalSize = 0; - - if (!_fileSystem.FolderExists(_thumbsRootDir)) - { - return; - } - - fileCount = _fileSystem.SearchFiles(_thumbsRootDir, "*.*").Count(); - totalSize = _fileSystem.ListFolders(_thumbsRootDir).Sum(x => x.Size); - } - - #region Utils - - protected string GetCachedImagePath4(int? mediaFileId, MediaPathData data, ProcessImageQuery query = null) - { - string result = ""; - - // xxxxxxx - if (mediaFileId.GetValueOrDefault() > 0) - { - result = mediaFileId.Value.ToString(IdFormatString); - } - - //// INFO: (mm) don't include folder id in pathes for now. It results in more complex image cache invalidation code. - //// xxxxxxx-f - //if (data.Folder != null) - //{ - // result = result.Grow(data.Folder.Id.ToString(CultureInfo.InvariantCulture), "-"); - //} - - // xxxxxxx-f-abc - result = result.Grow(data.FileTitle, "-"); - - if (result.IsEmpty()) - { - // files without name? No way! - return null; - } - - if (query != null && query.NeedsProcessing()) - { - // xxxxxxx-f-abc-w100-h100 - result += query.CreateHash(); - } - - if (_mediaSettings.MultipleThumbDirectories && result.Length > MaxDirLength) - { - // Get the first four letters of the file name - // 0001/xxxxxxx-f-abc-w100-h100 - var subDirectoryName = result.Substring(0, MaxDirLength); - result = subDirectoryName + "/" + result; - } - - // 0001/xxxxxxx-f-abc-w100-h100.png - return result.Grow(data.Extension, "."); - } - - /// - /// Returns the file name with the subfolder (when multidirs are enabled) - /// - /// - /// File name without extension - /// Dot-less file extension - /// - /// - private string GetCachedImagePath(int? pictureId, string seoFileName, string extension, ProcessImageQuery query = null) - { - string firstPart = ""; - - if (pictureId.GetValueOrDefault() > 0) - { - firstPart = pictureId.Value.ToString(IdFormatString) + (seoFileName.IsEmpty() ? "" : "-"); - } - - if (firstPart.IsEmpty() && seoFileName.IsEmpty()) - { - // files without name? No way! - return null; - } - - seoFileName = seoFileName.EmptyNull(); - - string imageFileName; - if (query == null || !query.NeedsProcessing()) - { - imageFileName = String.Concat(firstPart, seoFileName); - } - else - { - imageFileName = String.Concat(firstPart, seoFileName, query.CreateHash()); - } - - if (_mediaSettings.MultipleThumbDirectories && imageFileName != null && imageFileName.Length > MaxDirLength) - { - // Get the first four letters of the file name - var subDirectoryName = imageFileName.Substring(0, MaxDirLength); - imageFileName = subDirectoryName + "/" + imageFileName; - } - - return imageFileName + "." + extension; - } - - /// - /// Returns the images thumb path as is plus query (required for uploaded images) - /// - /// Image file to get thumbnail for - /// - /// - private string GetCachedImagePath(IFile file, ProcessImageQuery query) - { - if (!_imageProcessor.IsSupportedImage(file.Extension)) - { - throw new InvalidOperationException("Thumbnails for '{0}' files are not supported".FormatInvariant(file.Extension)); - } - - // TODO: (mc) prevent creating thumbs for thumbs AND check equality of source and target - - var imageFileName = file.Title + query.CreateHash(); - var extension = (query.GetResultExtension() ?? file.Extension).EnsureStartsWith(".").ToLower(); - var path = _fileSystem.Combine(file.Directory, imageFileName + extension); - - return path.TrimStart('/', '\\'); - } - - private string BuildPath(string imagePath) - { - if (imagePath.IsEmpty()) - return null; - - return _thumbsRootDir + imagePath; - } - - #endregion - - } - -} diff --git a/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs b/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs deleted file mode 100644 index fe1cefe750..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.IO; - -namespace SmartStore.Services.Media -{ - public static class ImageCacheExtensions - { - /// - /// Gets an instance of the object, which contains information about a cached image. - /// - /// The picture object for which to resolve a cached image. - /// The image processing query. - /// An instance of the object - /// If the requested image does not exist in the cache, the value of the Exists property will be false. - public static CachedImage Get(this IImageCache imageCache, MediaFile picture, ProcessImageQuery query = null) - { - Guard.NotNull(picture, nameof(picture)); - - return imageCache.Get(picture.Id, picture.Name, MimeTypes.MapMimeTypeToExtension(picture.MimeType), query); - } - - /// - /// Adds an image to the cache. - /// - /// The picture id, which will be part of the resulting file name. - /// The seo friendly picture name, which will be part of the resulting file name. - /// The extension of the resulting file - /// The image binary data. - /// The image processing query. This object, if not null, is hashed and appended to the resulting file name. - public static void Put(this IImageCache imageCache, int? pictureId, string seoFileName, string extension, byte[] buffer, ProcessImageQuery query = null) - { - var cachedImage = imageCache.Get(pictureId, seoFileName, extension, query); - imageCache.Put(cachedImage, buffer); - } - - /// - /// Adds an image to the cache. - /// - /// The picture object needed for building the resulting file name. - /// The image binary data. - /// The image processing query. This object, if not null, is hashed and appended to the resulting file name. - public static void Put(this IImageCache imageCache, MediaFile picture, byte[] buffer, ProcessImageQuery query = null) - { - Guard.NotNull(picture, nameof(picture)); - imageCache.Put(picture.Id, picture.Name, MimeTypes.MapMimeTypeToExtension(picture.MimeType), buffer, query); - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/BitDepth.cs b/src/Libraries/SmartStore.Services/Media/Imaging/BitDepth.cs new file mode 100644 index 0000000000..8ee167be7b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/BitDepth.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Provides enumeration for the available bit depths. + /// + public enum BitDepth + { + /// + /// 1 bit per pixel + /// + Bit1 = 1, + + /// + /// 4 bits per pixel + /// + Bit4 = 4, + + /// + /// 8 bits per pixel + /// + Bit8 = 8, + + /// + /// 16 bits per pixel + /// + Bit16 = 16, + + /// + /// 24 bits per pixel + /// + Bit24 = 24, + + /// + /// 32 bits per pixel + /// + Bit32 = 32 + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/CachedImage.cs b/src/Libraries/SmartStore.Services/Media/Imaging/CachedImage.cs new file mode 100644 index 0000000000..421c730b4c --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/CachedImage.cs @@ -0,0 +1,83 @@ +using System; +using System.Drawing; +using SmartStore.Core.IO; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Contains information about a cached image + /// + /// + /// An instance of this object is always returned, even when + /// the requested image does not physically exists in the storage. + /// + public class CachedImage + { + private bool? _exists; + private string _mimeType; + private Size? _size; + + public CachedImage(IFile file) + { + Guard.NotNull(file, nameof(file)); + + File = file; + } + + /// + /// The abstracted file object + /// + public IFile File { get; internal set; } + + /// + /// true when the image exists in the cache, false otherwise. + /// + public bool Exists + { + get => _exists ?? (_exists = File.Exists).Value; + // For internal use + set => _exists = value; + } + + /// + /// The name of the file (without path) + /// + public string FileName => System.IO.Path.GetFileName(this.Path); + + public long FileSize => !Exists ? 0 : File.Size; + + /// + /// The file extension (without 'dot') + /// + public string Extension { get; set; } + + /// + /// The filemime type + /// + public string MimeType => _mimeType ?? (_mimeType = MimeTypes.MapNameToMimeType(FileName)); + + /// + /// The image width and height. + /// + public Size Size + { + get => _size ?? (_size = File.Dimensions).Value; + internal set => _size = value; + } + + /// + /// The path relative to the cache root folder + /// + public string Path { get; set; } + + /// + /// The last modified date or null if the file does not exist + /// + public DateTime? LastModifiedUtc => Exists ? File.LastUpdated : (DateTime?)null; + + /// + /// Checks whether the file is remote (outside the application's physical root) + /// + public bool IsRemote { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/DefaultImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/Imaging/DefaultImageProcessor.cs new file mode 100644 index 0000000000..503fccb3bf --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/DefaultImageProcessor.cs @@ -0,0 +1,203 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Drawing; +using SmartStore.Core.Logging; +using SmartStore.Core.Events; +using SmartStore.Utilities; +using SmartStore.Core.IO; + +namespace SmartStore.Services.Media.Imaging +{ + public class DefaultImageProcessor : IImageProcessor + { + private static long _totalProcessingTime; + + private readonly IEventPublisher _eventPublisher; + + public DefaultImageProcessor(IImageFactory imageFactory, IEventPublisher eventPublisher) + { + Factory = imageFactory; + _eventPublisher = eventPublisher; + } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public IImageFactory Factory { get; } + + public ProcessImageResult ProcessImage(ProcessImageQuery query, bool disposeOutput = true) + { + Guard.NotNull(query, nameof(query)); + + ValidateQuery(query); + + var watch = new Stopwatch(); + long len; + IProcessableImage image = null; + + try + { + watch.Start(); + + var source = query.Source; + + // Load source + if (source is byte[] b) + { + using var memStream = new MemoryStream(b); + image = Factory.LoadImage(memStream); + len = b.LongLength; + } + else if (source is Stream s) + { + image = Factory.LoadImage(s); + len = s.Length; + } + else if (source is string str) + { + str = NormalizePath(str); + image = Factory.LoadImage(str); + len = (new FileInfo(str)).Length; + } + else if (source is IFile file) + { + using (var fs = file.OpenRead()) + { + image = Factory.LoadImage(fs); + len = file.Size; + } + } + else + { + throw new ProcessImageException("Invalid source type '{0}' in query.".FormatInvariant(query.Source.GetType().FullName), query); + } + + var sourceFormat = image.Format; + + // Pre-process event + _eventPublisher.Publish(new ImageProcessingEvent(query, image)); + + var result = new ProcessImageResult + { + Query = query, + SourceFormat = image.Format, + Image = image, + DisposeImage = disposeOutput + }; + + // Core processing + ProcessImageCore(query, image, out var fxApplied); + + result.HasAppliedVisualEffects = fxApplied; + + // Post-process event + _eventPublisher.Publish(new ImageProcessedEvent(query, result)); + + result.ProcessTimeMs = watch.ElapsedMilliseconds; + + return result; + } + catch (Exception ex) + { + throw new ProcessImageException(query, ex); + } + finally + { + if (query.DisposeSource && query.Source is IDisposable source) + { + source.Dispose(); + } + + watch.Stop(); + _totalProcessingTime += watch.ElapsedMilliseconds; + } + } + + /// + /// Processes the loaded image. Inheritors should NOT save the image, this is done by the caller. + /// + /// Query + /// Image instance + /// + /// Should be true if any effect has been applied that potentially changes the image visually (like background color, contrast, sharpness etc.). + /// Resize and compression quality does NOT count as FX. + /// + protected virtual void ProcessImageCore(ProcessImageQuery query, IProcessableImage image, out bool fxApplied) + { + bool fxAppliedInternal = false; + + // Resize + var size = query.MaxWidth != null || query.MaxHeight != null + ? new Size(query.MaxWidth ?? 0, query.MaxHeight ?? 0) + : Size.Empty; + + image.Transform(transformer => + { + if (!size.IsEmpty) + { + transformer.Resize(new ResizeOptions + { + Size = size, + ResizeMode = ProcessImageQuery.ConvertScaleMode(query.ScaleMode), + AnchorPosition = ProcessImageQuery.ConvertAnchorPosition(query.AnchorPosition) + }); ; + } + + if (query.BackgroundColor.HasValue()) + { + transformer.BackgroundColor(ColorTranslator.FromHtml(query.BackgroundColor)); + fxAppliedInternal = true; + } + + // Format + if (query.Format != null) + { + var requestedFormat = query.Format as IImageFormat; + + if (requestedFormat == null && query.Format is string) + { + requestedFormat = Factory.GetImageFormat(((string)query.Format).ToLowerInvariant()); + } + + if (requestedFormat != null && requestedFormat.DefaultMimeType != image.Format.DefaultMimeType) + { + transformer.Format(requestedFormat); + } + } + + // Quality + if (query.Quality.HasValue) + { + transformer.Quality(query.Quality.Value); + } + }); + + fxApplied = fxAppliedInternal; + } + + protected virtual string NormalizePath(string path) + { + if (path.IsWebUrl()) + { + throw new NotSupportedException($"Remote images cannot be processed: Path: {path}"); + } + + if (!PathHelper.IsAbsolutePhysicalPath(path)) + { + path = CommonHelper.MapPath(path); + } + + return path; + } + + private void ValidateQuery(ProcessImageQuery query) + { + if (query.Source == null) + { + throw new ArgumentException("During image processing 'ProcessImageQuery.Source' must not be null.", nameof(query)); + } + } + + public long TotalProcessingTimeMs => _totalProcessingTime; + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/IImage.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImage.cs new file mode 100644 index 0000000000..f5d573a31c --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImage.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Represents a processable image. + /// + public interface IProcessableImage : IImage + { + /// + /// Transforms the image. + /// + /// + IImage Transform(Action transformer); + } + + /// + /// Represents an image. + /// + public interface IImage : IImageInfo, IDisposable + { + /// + /// Gets the original width and height of the source image before any transform has been applied. + /// + Size SourceSize { get; } + + /// + /// Saves the current image to the specified output stream. + /// + /// The stream to save the image to. + /// The current . + IImage Save(Stream stream); + + /// + /// Saves the current image to the specified file path. + /// + /// The path to save the image to. + /// The current . + IImage Save(string path); + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/IImageCache.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImageCache.cs new file mode 100644 index 0000000000..0e37de0865 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImageCache.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Image cache interface + /// + public interface IImageCache + { + /// + /// Adds an image to the cache. + /// + /// An instance of the object which is returned by the Get() method. + /// The image object. + void Put(CachedImage cachedImage, IImage image); + + /// + /// Adds an image to the cache. + /// + /// An instance of the object which is returned by the Get() method. + /// The file input stream. + void Put(CachedImage cachedImage, Stream stream); + + /// + /// Asynchronously adds an image to the cache. + /// + /// An instance of the object which is returned by the Get() method. + /// The file input stream. + Task PutAsync(CachedImage cachedImage, Stream stream); + + /// + /// Gets an instance of the object which contains information about a cached image. + /// + /// The id of the file to resolve a thumbnail for. + /// The path data. + /// The image processing query. + /// An instance of the object + /// If the requested thumbnail does not exist in the cache, the value of the Exists property will be false. + CachedImage Get(int? mediaFileId, MediaPathData pathData, ProcessImageQuery query = null); + + /// + /// Deletes all cached images for the given + /// + /// The for which to delete cached images + void Delete(MediaFile picture); + + /// + /// Deletes all cached images (nukes all files in the cache folder) + /// + void Clear(); + + /// + /// Refreshes the file info. + /// + void RefreshInfo(CachedImage cachedImage); + + /// + /// Calculates statistics about the image cache data. + /// + /// The total count of files in the cache. + /// The total size of files in the cache (in bytes) + void CacheStatistics(out long fileCount, out long totalSize); + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/IImageFactory.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImageFactory.cs new file mode 100644 index 0000000000..7ebc1f1df0 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImageFactory.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// A provider interface for imaging libraries. + /// + public interface IImageFactory + { + /// + /// Determines whether the given file name is processable by the image provider + /// + /// The file extension to check. + /// A value indicating whether processing is possible + bool IsSupportedImage(string extension); + + /// + /// Resolves the image format instance for a given file extension. + /// + /// The (dot-less) file extension. + /// An object that adapts the library specific format implementation. + IImageFormat GetImageFormat(string extension); + + /// + /// Loads an image by path. + /// + /// The full physical path to the image file. + /// Whether to preserve exif metadata. Defaults to false. + /// An object that adapts the library specific imaging implementation. + IProcessableImage LoadImage(string path, bool preserveExif = false); + + /// + /// Loads an image by stream. + /// + /// The stream that contains image data. + /// Whether to preserve exif metadata. Defaults to false. + /// An object that adapts the library specific imaging implementation. + IProcessableImage LoadImage(Stream stream, bool preserveExif = false); + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/IImageFormat.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImageFormat.cs new file mode 100644 index 0000000000..25e8516973 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImageFormat.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Defines the contract for an image format. + /// + public interface IImageFormat // TODO: >> IEquatable & Equals & GetHashCode() + { + /// + /// Gets the name that describes this image format. + /// + string Name { get; } + + /// + /// Gets the default file extension. + /// + string DefaultExtension { get; } + + /// + /// Gets the default mimetype that the image format uses + /// + string DefaultMimeType { get; } + + /// + /// Gets the file extensions this image format commonly uses. + /// + IEnumerable FileExtensions { get; } + + /// + /// Gets all the mimetypes that have been used by this image format. + /// + IEnumerable MimeTypes { get; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/IImageInfo.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImageInfo.cs new file mode 100644 index 0000000000..9ab9cedad4 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImageInfo.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Encapsulates properties that describe basic image information including dimensions, pixel type information + /// and additional metadata. + /// + public interface IImageInfo + { + /// + /// Gets width and height. + /// + Size Size { get; } + + /// + /// Gets the color depth in number of bits per pixel (1, 4, 8, 16, 24, 32) + /// + BitDepth BitDepth { get; } + + /// + /// Gets the format of the image. + /// + IImageFormat Format { get; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/IImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImageProcessor.cs similarity index 53% rename from src/Libraries/SmartStore.Services/Media/IImageProcessor.cs rename to src/Libraries/SmartStore.Services/Media/Imaging/IImageProcessor.cs index a60f67c75b..91f0e94c6e 100644 --- a/src/Libraries/SmartStore.Services/Media/IImageProcessor.cs +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImageProcessor.cs @@ -1,18 +1,17 @@ using System; +using System.IO; -namespace SmartStore.Services.Media +namespace SmartStore.Services.Media.Imaging { /// - /// A service interface responsible for resizing/processing images. + /// A service interface responsible for processing images. /// public interface IImageProcessor { /// - /// Determines whether the given file name is processable by the image resizer + /// The image factory adapter used to load and process images. /// - /// The file extension to check. - /// A value indicating whether processing is possible - bool IsSupportedImage(string extension); + IImageFactory Factory { get; } /// /// Processes an image @@ -22,9 +21,9 @@ public interface IImageProcessor /// The resizing result encapsulated in type ProcessImageResult ProcessImage(ProcessImageQuery query, bool disposeOutput = true); - /// - /// Gets the cumulative total processing time since app start in miliseconds - /// - long TotalProcessingTimeMs { get; } - } + /// + /// Gets the cumulative total processing time since app start in miliseconds + /// + long TotalProcessingTimeMs { get; } + } } diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/IImageTransformer.cs b/src/Libraries/SmartStore.Services/Media/Imaging/IImageTransformer.cs new file mode 100644 index 0000000000..7fc73cc6d5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/IImageTransformer.cs @@ -0,0 +1,185 @@ +using System; +using System.Drawing; + +namespace SmartStore.Services.Media.Imaging +{ + public interface IImageTransformer + { + /// + /// Changes the opacity of the current image. + /// + /// The percentage by which to alter the images opacity. Any integer between 0 and 100. + IImageTransformer Alpha(int percentage); + + /// + /// Changes the background color of the current image. + /// + /// + /// The to paint the image background with. + /// + IImageTransformer BackgroundColor(Color color); + + /// + /// Alters the bit depth of the current image. + /// + /// The new bit depth. + IImageTransformer BitDepth(BitDepth bitDepth); + + /// + /// Changes the brightness of the current image. + /// + /// The percentage by which to alter the images brightness. Any integer between -100 and 100. + IImageTransformer Brightness(int percentage); + + /// + /// Changes the contrast of the current image. + /// + /// The percentage by which to alter the images contrast. Any integer between -100 and 100. + IImageTransformer Contrast(int percentage); + + /// + /// Crops the current image to the given location and size. + /// + /// The rectangle containing the coordinates to crop the image to. + IImageTransformer Crop(Rectangle rect); + + /// + /// Crops an image to the area of greatest entropy. + /// + /// The threshold in bytes to control the entropy. + IImageTransformer EntropyCrop(byte threshold = 128); + + /// + /// Flips the current image either horizontally or vertically. + /// + /// Whether to flip the image vertically. + /// Whether to flip the image both vertically and horizontally. + IImageTransformer Flip(bool flipVertically = false, bool flipBoth = false); + + /// + /// Sets the output format of the current image to the matching ImageFormat. + /// + /// The new format to set the image to. + IImageTransformer Format(IImageFormat format); + + /// + /// Adjust the gamma (intensity of the light) component of the given image. + /// + /// The value to adjust the gamma by (typically between .2 and 5). + IImageTransformer Gamma(float value); + + /// + /// Uses a Gaussian kernel to blur the current image. + /// + /// The size to set the Gaussian kernel to. + /// The sigma and threshold values applied to the kernel are 1.4 and 0 respectively. + IImageTransformer GaussianBlur(int size); + + /// + /// Uses a Gaussian kernel to sharpen the current image. + /// + /// The size to set the Gaussian kernel to. + /// The sigma and threshold values applied to the kernel are 1.4 and 0 respectively. + IImageTransformer GaussianSharpen(int size); + + /// + /// Alters the hue of the current image changing the overall color. + /// + /// The angle by which to alter the images hue. Any integer between 0 and 360. + /// Whether to rotate the hue of the current image altering each color. + IImageTransformer Hue(int degrees, bool rotate = false); + + /// + /// Applies an image mask to the current image. + /// + /// The mask image. + /// Size of the mask. + /// Opacity of the mask. + /// + /// Position of the mask. + /// Point is used to place the image mask if it has not the same dimensions as the original image. + /// Pass null to center the mask. + /// + IImageTransformer Mask(IImage image, Size size, int opacity, Point? position); + + /// + /// Adds an image overlay to the current image. + /// + /// The overlay image. + /// Size of the layer. + /// Opacity of the layer. + /// + /// Position of the layer. + /// Point is used to place the image overlay if it has not the same dimensions as the original image. + /// Pass null to center the overlay. + /// + IImageTransformer Overlay(IImage image, Size size, int opacity, Point? position); + + /// + /// Alters the output quality of the current image. + /// + /// A value between 1 and 100 to set the quality to. + /// This method will only effect the output quality of jpeg and webp images. + IImageTransformer Quality(int percentage); + + /// + /// Rotates the current image by the given angle. + /// + /// The angle at which to rotate the image in degrees. + /// If true, rotates the image without expanding the canvas to fit the image. + /// + /// Whether to keep the original image dimensions. . + /// Only applicable if is true. + /// If set to true, the image is zoomed to fit the bounding area. + /// If set to false, the area is cropped to fit the rotated image. + /// + IImageTransformer Rotate(float degrees, bool bounded = false, bool keepSize = false); + + /// + /// Resizes the current image according to the given options. + /// + /// The options. + /// + IImageTransformer Resize(ResizeOptions options); + + /// + /// Adds rounded corners to the current image. + /// + /// The radius at which the corner will be rounded. + IImageTransformer RoundedCorners(int radius); + + /// + /// Changes the saturation of the current image. + /// + /// The percentage by which to alter the images saturation. Any integer between -100 and 100. + IImageTransformer Saturation(int percentage); + + /// + /// Tints the current image with the given color. + /// + /// The color to tint the image with. + IImageTransformer Tint(Color color); + + /// + /// Adds a vignette image effect to the current image. + /// + /// The color of the vignette. Defaults to black. + /// + IImageTransformer Vignette(Color? color = null); + } + + public static class IImageTransformerExtensions + { + /// + /// Resizes the current image to the given dimensions. + /// + /// + /// The containing the width and height to set the image to. + /// + /// + public static IImageTransformer Resize(this IImageTransformer transformer, Size size) + { + return transformer.Resize(new ResizeOptions { Size = size }); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ImageCache.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ImageCache.cs new file mode 100644 index 0000000000..a841a21bcb --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ImageCache.cs @@ -0,0 +1,223 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.Media.Imaging +{ + public class ImageCache : IImageCache + { + public const string IdFormatString = "0000000"; + internal const int MaxDirLength = 4; + + private readonly MediaSettings _mediaSettings; + private readonly IMediaFileSystem _fileSystem; + private readonly string _thumbsRootDir; + + public ImageCache( + MediaSettings mediaSettings, + IMediaFileSystem fileSystem, + MediaHelper mediaHelper) + { + _mediaSettings = mediaSettings; + _fileSystem = fileSystem; + + _thumbsRootDir = "Thumbs/"; + } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public void Put(CachedImage cachedImage, IImage image) + { + var path = BuildPath(cachedImage.Path); + using var stream = _fileSystem.GetFile(path).OpenWrite(); + + if (PreparePut(cachedImage, stream)) + { + image.Save(stream); + image.Dispose(); + PostPut(cachedImage, path); + } + } + + public void Put(CachedImage cachedImage, Stream stream) + { + if (PreparePut(cachedImage, stream)) + { + var path = BuildPath(cachedImage.Path); + _fileSystem.SaveStream(path, stream); + PostPut(cachedImage, path); + } + } + + public async Task PutAsync(CachedImage cachedImage, Stream stream) + { + if (PreparePut(cachedImage, stream)) + { + var path = BuildPath(cachedImage.Path); + await _fileSystem.SaveStreamAsync(path, stream); + PostPut(cachedImage, path); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PostPut(CachedImage cachedImage, string path) + { + cachedImage.Exists = true; + cachedImage.File = _fileSystem.GetFile(path); + } + + private bool PreparePut(CachedImage cachedImage, Stream stream) + { + Guard.NotNull(cachedImage, nameof(cachedImage)); + + if (stream == null) + { + return false; + } + + // create folder if needed + string imageDir = System.IO.Path.GetDirectoryName(cachedImage.Path); + if (imageDir.HasValue()) + { + _fileSystem.CreateFolder(BuildPath(imageDir)); + } + + return true; + } + + public virtual CachedImage Get(int? mediaFileId, MediaPathData data, ProcessImageQuery query = null) + { + Guard.NotNull(data, nameof(data)); + + var resultExtension = query?.GetResultExtension(); + if (resultExtension != null) + { + data.Extension = resultExtension; + } + + var imagePath = GetCachedImagePath(mediaFileId, data, query); + var file = _fileSystem.GetFile(BuildPath(imagePath)); + + var result = new CachedImage(file) + { + Path = imagePath, + Extension = data.Extension, + IsRemote = _fileSystem.IsCloudStorage + }; + + return result; + } + + public virtual void RefreshInfo(CachedImage cachedImage) + { + Guard.NotNull(cachedImage, nameof(cachedImage)); + + var file = _fileSystem.GetFile(cachedImage.File.Path); + cachedImage.File = file; + cachedImage.Exists = file.Exists; + } + + public virtual void Delete(MediaFile picture) + { + var filter = string.Format("{0}*.*", picture.Id.ToString(IdFormatString)); + + var files = _fileSystem.SearchFiles(_thumbsRootDir, filter); + foreach (var file in files) + { + _fileSystem.DeleteFile(file); + } + } + + public virtual void Clear() + { + for (int i = 0; i < 10; i++) + { + + try + { + _fileSystem.DeleteFolder(_thumbsRootDir); + _fileSystem.CreateFolder(_thumbsRootDir); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + } + + public void CacheStatistics(out long fileCount, out long totalSize) + { + fileCount = 0; + totalSize = 0; + + if (!_fileSystem.FolderExists(_thumbsRootDir)) + { + return; + } + + fileCount = _fileSystem.SearchFiles(_thumbsRootDir, "*.*").Count(); + totalSize = _fileSystem.ListFolders(_thumbsRootDir).Sum(x => x.Size); + } + + #region Utils + + protected string GetCachedImagePath(int? mediaFileId, MediaPathData data, ProcessImageQuery query = null) + { + string result = ""; + + // xxxxxxx + if (mediaFileId.GetValueOrDefault() > 0) + { + result = mediaFileId.Value.ToString(IdFormatString); + } + + //// INFO: (mm) don't include folder id in pathes for now. It results in more complex image cache invalidation code. + //// xxxxxxx-f + //if (data.Folder != null) + //{ + // result = result.Grow(data.Folder.Id.ToString(CultureInfo.InvariantCulture), "-"); + //} + + // xxxxxxx-f-abc + result = result.Grow(data.FileTitle, "-"); + + if (result.IsEmpty()) + { + // files without name? No way! + return null; + } + + if (query != null && query.NeedsProcessing()) + { + // xxxxxxx-f-abc-w100-h100 + result += query.CreateHash(); + } + + if (_mediaSettings.MultipleThumbDirectories && result.Length > MaxDirLength) + { + // Get the first four letters of the file name + // 0001/xxxxxxx-f-abc-w100-h100 + var subDirectoryName = result.Substring(0, MaxDirLength); + result = subDirectoryName + "/" + result; + } + + // 0001/xxxxxxx-f-abc-w100-h100.png + return result.Grow(data.Extension, "."); + } + + private string BuildPath(string imagePath) + { + if (imagePath.IsEmpty()) + return null; + + return _thumbsRootDir + imagePath; + } + + #endregion + } + +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ImageSharpImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ImageSharpImageProcessor.cs new file mode 100644 index 0000000000..67363b4ea3 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ImageSharpImageProcessor.cs @@ -0,0 +1,406 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +//using System.Drawing; +//using ImageProcessor; +//using ImageProcessor.Imaging; +//using ImageProcessor.Imaging.Formats; +using SmartStore.Core.Logging; +//using ImageProcessor.Configuration; +using SixLabors.ImageSharp; +using SmartStore.Core.Events; +using SmartStore.Utilities; +using SmartStore.Core.IO; +using SixLabors.ImageSharp.Formats; +using ImageSharpConfig = SixLabors.ImageSharp.Configuration; +using System.Collections.Concurrent; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Jpeg; + +namespace SmartStore.Services.Media +{ + public partial class ImageSharpImageProcessor : IImageProcessor + { + class SourceImage + { + public Image Image { get; set; } + public IImageFormat Format { get; set; } + public Stream Stream { get; set; } + public long Length { get; set; } + public int Width { get; set; } + public int Height { get; set; } + } + + private static readonly ConcurrentDictionary _supportedFormats = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static long _totalProcessingTime; + + private readonly IEventPublisher _eventPublisher; + + static ImageSharpImageProcessor() + { + ImageSharpConfig.Default.MemoryAllocator = new SixLabors.ImageSharp.Memory.SimpleGcMemoryAllocator(); + + } + + public ImageSharpImageProcessor(IEventPublisher eventPublisher) + { + _eventPublisher = eventPublisher; + } + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public bool IsSupportedImage(string extension) + { + if (extension.IsEmpty()) + { + return false; + } + + if (extension[0] == '.' && extension.Length > 1) + { + extension = extension.Substring(1); + } + + return _supportedFormats.GetOrAdd(extension, k => + { + return ImageSharpConfig.Default.ImageFormats + .SelectMany(x => x.FileExtensions) + .Any(x => x.Equals(extension, StringComparison.OrdinalIgnoreCase)); + }); + + //return ImageProcessorBootstrapper.Instance.SupportedImageFormats + // .SelectMany(x => x.FileExtensions) + // .Any(x => x.Equals(extension, StringComparison.OrdinalIgnoreCase)); + } + + public ProcessImageResult ProcessImage(ProcessImageQuery query, bool disposeOutput = true) + { + Guard.NotNull(query, nameof(query)); + + ValidateQuery(query); + + var watch = new Stopwatch(); + SourceImage source = null; + + try + { + watch.Start(); + + // Load source + source = LoadImage(query); + + // Pre-process event + _eventPublisher.Publish(new ImageProcessingEvent(query, source.Image)); + + var result = new ProcessImageResult + { + Query = query, + SourceWidth = source.Image.Width, + SourceHeight = source.Image.Height, + SourceMimeType = source.Format.DefaultMimeType, + DisposeOutputStream = disposeOutput + }; + + // Core processing + ProcessImageCore(query, source.Image, out var fxApplied); + + // Create & prepare result + //var outStream = new MemoryStream(); + //source.Image.Save(outStream, source.Format); // TODO: outFormat + var dest = SaveImage(source, query); + + var fmt = dest.Format; + result.FileExtension = fmt.FileExtensions.First(); + result.MimeType = fmt.DefaultMimeType; + + result.HasAppliedVisualEffects = fxApplied; + result.Width = source.Image.Width; + result.Height = source.Image.Height; + + if (source.Length > 0) + { + // Check whether it is more beneficial to return the source instead of the result. + // Prefer result only if its size is smaller than the source size. + // Result size may be larger if a high-compressed image has been uploaded. + // During image processing the source compression algorithm gets lost and the image may be larger in size + // after encoding with default encoders. + var compare = + // only when image was not altered visually... + !fxApplied + // ...size has not changed + && result.Width == result.SourceWidth + && result.Height == result.SourceHeight + // ...and format has not changed + && result.MimeType == result.SourceMimeType; + + if (compare && source.Length <= dest.OutStream.Length) + { + // Source is smaller. Throw away result and get back to source. + dest.OutStream.Dispose(); + dest.OutStream = null; + source.Stream.Position = 0; + result.OutputStream = source.Stream; + } + } + + // Set output stream + if (result.OutputStream == null) + { + result.OutputStream = dest.OutStream; + } + + // Post-process event + _eventPublisher.Publish(new ImageProcessedEvent(query, source.Image, result)); + + result.OutputStream.Position = 0; + + result.ProcessTimeMs = watch.ElapsedMilliseconds; + + return result; + } + catch (Exception ex) + { + throw new ProcessImageException(query, ex); + } + finally + { + if (source?.Image != null) + { + source?.Image.Dispose(); + } + + if (source?.Stream != null && query.DisposeSource) + { + source?.Stream.Dispose(); + } + + watch.Stop(); + _totalProcessingTime += watch.ElapsedMilliseconds; + } + } + + private SourceImage LoadImage(ProcessImageQuery query) + { + var source = query.Source; + + Image image; + IImageFormat format; + Stream stream; + long len; + + // Load source + if (source is byte[] b) + { + stream = new MemoryStream(b); + len = b.LongLength; + } + else if (source is Stream s) + { + stream = s; + len = s.Length; + } + else if (source is string str) + { + var fi = new FileInfo(NormalizePath(str)); + stream = fi.OpenRead(); + len = fi.Length; + } + else if (source is IFile file) + { + len = file.Size; + stream = file.OpenRead(); + } + else + { + throw new ProcessImageException("Invalid source type '{0}' in query.".FormatInvariant(query.Source.GetType().FullName), query); + } + + image = Image.Load(stream, out format); + + return new SourceImage + { + Image = image, + Format = format, + Stream = stream, + Length = len, + Width = image.Width, + Height = image.Height + }; + } + + private (MemoryStream OutStream, IImageFormat Format, IImageEncoder Encoder) SaveImage(SourceImage source, ProcessImageQuery query) + { + // Format + IImageFormat format = null; + if (query.Format != null) + { + format = query.Format as IImageFormat; + + if (format == null && query.Format is string) + { + var requestedFormat = ((string)query.Format).ToLowerInvariant(); + format = ImageSharpConfig.Default.ImageFormatsManager.FindFormatByFileExtension(requestedFormat); + } + } + + format ??= source.Format; + + // Encoder + var encoder = ImageSharpConfig.Default.ImageFormatsManager.FindEncoder(format); + + // Set Quality + if (query.Quality.HasValue && encoder is JpegEncoder) + { + // Create new encoder instance to avoid altering a shared static encoder instance. + encoder = new JpegEncoder { Quality = query.Quality }; + } + + var outStream = new MemoryStream(); + source.Image.Save(outStream, encoder); + + return (outStream, format, encoder); + } + + /// + /// Processes the loaded image. Inheritors should NOT save the image, this is done by the main method. + /// + /// Query + /// Processor instance + /// + /// Should be true if any effect has been applied that potentially changes the image visually (like background color, contrast, sharpness etc.). + /// Resize and compression quality does NOT count as FX. + /// + protected virtual void ProcessImageCore(ProcessImageQuery query, Image image, out bool fxApplied) + { + bool fxAppliedPrivate = false; + + // Resize + var size = query.MaxWidth != null || query.MaxHeight != null + ? new Size(query.MaxWidth ?? 0, query.MaxHeight ?? 0) + : Size.Empty; + + image.Mutate(x => + { + if (!size.IsEmpty && (image.Width > size.Width || image.Height > size.Height)) + { + //image.Resize(new ResizeLayer( + // size, + // resizeMode: ConvertScaleMode(query.ScaleMode), + // anchorPosition: ConvertAnchorPosition(query.AnchorPosition), + // upscale: false)); + + x.Resize(new ResizeOptions + { + Size = size, + Mode = ConvertScaleMode(query.ScaleMode), + Position = ConvertAnchorPosition(query.AnchorPosition) + }); + } + + if (query.BackgroundColor.HasValue()) + { + x.BackgroundColor(Color.ParseHex(query.BackgroundColor)); + fxAppliedPrivate = true; + } + }); + + //// Format + //if (query.Format != null) + //{ + // var format = query.Format as IImageFormat; + + // if (format == null && query.Format is string) + // { + // var requestedFormat = ((string)query.Format).ToLowerInvariant(); + // format = ImageSharpConfig.Default.ImageFormatsManager.FindFormatByFileExtension(requestedFormat); + // } + + // if (format != null) + // { + // image.Format(format); + // } + //} + + //// Set Quality + //if (query.Quality.HasValue) + //{ + // image.Quality(query.Quality.Value); + //} + + fxApplied = fxAppliedPrivate; + } + + private string NormalizePath(string path) + { + if (path.IsWebUrl()) + { + throw new NotSupportedException($"Remote images cannot be processed: Path: {path}"); + } + + if (!PathHelper.IsAbsolutePhysicalPath(path)) + { + path = CommonHelper.MapPath(path); + } + + return path; + } + + private void ValidateQuery(ProcessImageQuery query) + { + if (query.Source == null) + { + throw new ArgumentException("During image processing 'ProcessImageQuery.Source' must not be null.", nameof(query)); + } + } + + public long TotalProcessingTimeMs + { + get { return _totalProcessingTime; } + } + + private ResizeMode ConvertScaleMode(string mode) + { + switch (mode.EmptyNull().ToLower()) + { + case "boxpad": + return ResizeMode.BoxPad; + case "crop": + return ResizeMode.Crop; + case "min": + return ResizeMode.Min; + case "pad": + return ResizeMode.Pad; + case "stretch": + return ResizeMode.Stretch; + default: + return ResizeMode.Max; + } + } + + private AnchorPositionMode ConvertAnchorPosition(string anchor) + { + switch (anchor.EmptyNull().ToLower()) + { + case "top": + return AnchorPositionMode.Top; + case "bottom": + return AnchorPositionMode.Bottom; + case "left": + return AnchorPositionMode.Left; + case "right": + return AnchorPositionMode.Right; + case "top-left": + return AnchorPositionMode.TopLeft; + case "top-right": + return AnchorPositionMode.TopRight; + case "bottom-left": + return AnchorPositionMode.BottomLeft; + case "bottom-right": + return AnchorPositionMode.BottomRight; + default: + return AnchorPositionMode.Center; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ImageWrapper.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ImageWrapper.cs new file mode 100644 index 0000000000..622b2db499 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ImageWrapper.cs @@ -0,0 +1,88 @@ +using System; +using System.Drawing; +using System.IO; + +namespace SmartStore.Services.Media.Imaging +{ + public class ImageWrapper : DisposableObject, IImage + { + private readonly bool _disposeStream; + + public ImageWrapper(Stream stream, Size size, IImageFormat format, BitDepth bitDepth = BitDepth.Bit24, bool disposeStream = true) + { + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(format, nameof(format)); + + InStream = stream; + Format = format; + Size = size; + SourceSize = size; + BitDepth = bitDepth; + _disposeStream = disposeStream; + } + + #region IImageInfo + + /// + public Size Size { get; } + + /// + public BitDepth BitDepth { get; set; } = BitDepth.Bit24; + + /// + public IImageFormat Format { get; set; } + + #endregion + + public Stream InStream { get; } + + /// + public Size SourceSize { get; set; } + + /// + public IImage Save(string path) + { + Guard.NotEmpty(path, nameof(path)); + + var di = new DirectoryInfo(Path.GetDirectoryName(path)); + if (!di.Exists) + { + di.Create(); + } + + using var stream = File.OpenWrite(path); + return Save(stream); + } + + /// + public IImage Save(Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + if (stream.CanSeek) + { + stream.SetLength(0); + } + + InStream.CopyTo(stream); + + if (stream.CanSeek) + { + stream.Position = 0; + } + + if (InStream.CanSeek) + { + InStream.Position = 0; + } + + return this; + } + + protected override void OnDispose(bool disposing) + { + if (disposing && _disposeStream) + InStream.Dispose(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImage.cs b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImage.cs new file mode 100644 index 0000000000..ed3257e879 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImage.cs @@ -0,0 +1,75 @@ +using System; +using System.Drawing; +using System.IO; +using ImageProcessor; + +namespace SmartStore.Services.Media.Imaging.Impl +{ + public class IPImage : DisposableObject, IProcessableImage + { + private readonly ImageFactory _image; + + public IPImage(ImageFactory image) + { + _image = image; + SourceSize = new Size(image.Image.Width, image.Image.Height); + } + + #region IImageInfo + + /// + public Size Size => _image.Image.Size; + + /// + public BitDepth BitDepth + { + get => (BitDepth)Convert.ToInt32(_image.CurrentBitDepth); + set => _image.BitDepth(Convert.ToInt64(value)); + } + + /// + public IImageFormat Format + { + get => new IPImageFormat(_image.CurrentImageFormat); + set + { + Guard.NotNull(value, nameof(value)); + _image.Format(((IPImageFormat)value).WrappedFormat); + } + } + + #endregion + + public ImageFactory ImageFactory => _image; + + /// + public Size SourceSize { get; } + + /// + public IImage Transform(Action transformer) + { + transformer(new IPImageTransformer(_image)); + return this; + } + + /// + public IImage Save(Stream stream) + { + _image.Save(stream); + return this; + } + + /// + public IImage Save(string path) + { + _image.Save(path); + return this; + } + + protected override void OnDispose(bool disposing) + { + if (disposing) + _image.Dispose(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageFactory.cs b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageFactory.cs new file mode 100644 index 0000000000..f1fdc2633d --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageFactory.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.Linq; +using ImageProcessor; +using ImageProcessor.Configuration; +using ImageProcessor.Imaging.Formats; +using ImageProcessor.Plugins.WebP.Imaging.Formats; + +namespace SmartStore.Services.Media.Imaging.Impl +{ + public class IPImageFactory : IImageFactory + { + static IPImageFactory() + { + ImageProcessorBootstrapper.Instance.AddImageFormats(new WebPFormat { Quality = 100 }); + } + + public bool IsSupportedImage(string extension) + { + return GetInternalImageFormat(extension) != null; + } + + public IImageFormat GetImageFormat(string extension) + { + var internalFormat = GetInternalImageFormat(extension); + if (internalFormat != null) + { + return new IPImageFormat(internalFormat); + } + + return null; + } + + private ISupportedImageFormat GetInternalImageFormat(string extension) + { + if (extension.IsEmpty()) + { + return null; + } + + if (extension[0] == '.' && extension.Length > 1) + { + extension = extension.Substring(1); + } + + return ImageProcessorBootstrapper.Instance.SupportedImageFormats + .FirstOrDefault(x => x.FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)); + } + + public IProcessableImage LoadImage(string path, bool preserveExif = false) + { + return new IPImage(new ImageFactory(preserveExif, fixGamma: false).Load(path)); + } + + public IProcessableImage LoadImage(Stream stream, bool preserveExif = false) + { + return new IPImage(new ImageFactory(preserveExif, fixGamma: false).Load(stream)); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageFormat.cs b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageFormat.cs new file mode 100644 index 0000000000..8f84be635b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageFormat.cs @@ -0,0 +1,28 @@ +using ImageProcessor.Imaging.Formats; +using System.Collections.Generic; + +namespace SmartStore.Services.Media.Imaging.Impl +{ + internal class IPImageFormat : IImageFormat + { + private readonly ISupportedImageFormat _format; + + public IPImageFormat(ISupportedImageFormat format) + { + Guard.NotNull(format, nameof(format)); + _format = format; + } + + public ISupportedImageFormat WrappedFormat => _format; + + public string Name => _format.DefaultExtension; + + public string DefaultExtension => _format.DefaultExtension == "jpeg" ? "jpg" : _format.DefaultExtension; + + public string DefaultMimeType => _format.MimeType; + + public IEnumerable FileExtensions => _format.FileExtensions; + + public IEnumerable MimeTypes => new[] { _format.MimeType }; + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageTransformer.cs b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageTransformer.cs new file mode 100644 index 0000000000..fbd2708d1f --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/Impl/IPImageTransformer.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ImageProcessor; +using ImageProcessor.Imaging; + +namespace SmartStore.Services.Media.Imaging.Impl +{ + internal class IPImageTransformer : IImageTransformer + { + private readonly ImageFactory _image; + + public IPImageTransformer(ImageFactory image) + { + _image = image; + } + + public IImageTransformer Resize(Size size) + { + return Resize(new ResizeOptions { Size = size }); + } + + public IImageTransformer Resize(ResizeOptions options) + { + Guard.NotNull(options, nameof(options)); + + var layer = new ResizeLayer( + size: options.Size, + resizeMode: (ImageProcessor.Imaging.ResizeMode)options.ResizeMode, + anchorPosition: (ImageProcessor.Imaging.AnchorPosition)options.AnchorPosition, + upscale: options.Upscale, + centerCoordinates: options.CenterCoordinates, + maxSize: null, + restrictedSizes: null, + anchorPoint: options.AnchorPoint); + + _image.Resize(layer); + return this; + } + + public IImageTransformer BackgroundColor(Color color) + { + _image.BackgroundColor(color); + return this; + } + + public IImageTransformer Alpha(int percentage) + { + _image.Alpha(percentage); + return this; + } + + public IImageTransformer Brightness(int percentage) + { + _image.Brightness(percentage); + return this; + } + + public IImageTransformer Contrast(int percentage) + { + _image.Contrast(percentage); + return this; + } + + public IImageTransformer Crop(Rectangle rect) + { + _image.Crop(rect); + return this; + } + + public IImageTransformer EntropyCrop(byte threshold = 128) + { + _image.EntropyCrop(threshold); + return this; + } + + public IImageTransformer Flip(bool flipVertically = false, bool flipBoth = false) + { + _image.Flip(flipVertically, flipBoth); + return this; + } + + public IImageTransformer Format(IImageFormat format) + { + if (format is IPImageFormat ipFormat) + { + _image.Format(ipFormat.WrappedFormat); + return this; + } + + throw new ArgumentException("Passed image type must be '{0}'.".FormatCurrent(typeof(IPImageFormat)), nameof(format)); + } + + public IImageTransformer Gamma(float value) + { + _image.Gamma(value); + return this; + } + + public IImageTransformer GaussianBlur(int size) + { + _image.GaussianBlur(size); + return this; + } + + public IImageTransformer GaussianSharpen(int size) + { + _image.GaussianSharpen(size); + return this; + } + + public IImageTransformer Hue(int degrees, bool rotate = false) + { + _image.Hue(degrees, rotate); + return this; + } + + public IImageTransformer Quality(int percentage) + { + _image.Quality(percentage); + return this; + } + + public IImageTransformer BitDepth(BitDepth bitDepth) + { + _image.BitDepth((long)bitDepth); + return this; + } + + public IImageTransformer Overlay(IImage image, Size size, int opacity, Point? position) + { + if (image is IPImage ipImage) + { + _image.Overlay(new ImageLayer + { + Image = ipImage.ImageFactory.Image, + Size = size, + Opacity = opacity, + Position = position + }); + + return this; + } + + throw new ArgumentException("Passed image type must be '{0}'.".FormatCurrent(typeof(IPImage)), nameof(image)); + } + + public IImageTransformer Mask(IImage image, Size size, int opacity, Point? position) + { + if (image is IPImage ipImage) + { + _image.Mask(new ImageLayer + { + Image = ipImage.ImageFactory.Image, + Size = size, + Opacity = opacity, + Position = position + }); + + return this; + } + + throw new ArgumentException("Passed image type must be '{0}'.".FormatCurrent(typeof(IPImage)), nameof(image)); + } + + public IImageTransformer Rotate(float degrees, bool bounded = false, bool keepSize = false) + { + if (bounded) + _image.Rotate(degrees); + else + _image.RotateBounded(degrees, keepSize); + + return this; + } + + public IImageTransformer RoundedCorners(int radius) + { + _image.RoundedCorners(radius); + return this; + } + + public IImageTransformer Saturation(int percentage) + { + _image.Saturation(percentage); + return this; + } + + public IImageTransformer Tint(Color color) + { + _image.Tint(color); + return this; + } + + public IImageTransformer Vignette(Color? color = null) + { + _image.Vignette(color); + return this; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageException.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageException.cs new file mode 100644 index 0000000000..a51f9fefbd --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageException.cs @@ -0,0 +1,47 @@ +using System; + +namespace SmartStore.Services.Media.Imaging +{ + public sealed class ProcessImageException : Exception + { + public ProcessImageException() + : this((string)null, null) + { + } + + public ProcessImageException(ProcessImageQuery query) + : this(query, null) + { + } + + public ProcessImageException(string message, ProcessImageQuery query) + : base(message) + { + Query = query; + } + + public ProcessImageException(ProcessImageQuery query, Exception innerException) + : base(CreateMessage(query, innerException), innerException) + { + Query = query; + } + + private static string CreateMessage(ProcessImageQuery query, Exception innerException) + { + var fileName = query?.FileName; + + var msg = fileName.HasValue() + ? "Error while processing image '{0}'".FormatCurrent(fileName.NaIfEmpty()) + : "Error while processing image"; + + if (innerException != null) + { + msg += " (" + innerException.Message + ")"; + } + + return msg; + } + + public ProcessImageQuery Query { get; private set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageQuery.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageQuery.cs new file mode 100644 index 0000000000..1c754697e7 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageQuery.cs @@ -0,0 +1,295 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Collections.Specialized; +using SmartStore.Collections; +using System.Drawing; + +namespace SmartStore.Services.Media.Imaging +{ + public class ProcessImageQuery : QueryString + { + // Key = Supported token name, Value = Validator + private readonly static Dictionary> _supportedTokens = new Dictionary> + { + ["w"] = (k, v) => ValidateSizeToken(k, v), + ["h"] = (k, v) => ValidateSizeToken(k, v), + ["size"] = (k, v) => ValidateSizeToken(k, v), + ["q"] = (k, v) => ValidateQualityToken(k, v), + ["m"] = (k, v) => ValidateScaleModeToken(k, v) + }; + + public ProcessImageQuery() + : this(null, new NameValueCollection()) + { + } + + public ProcessImageQuery(byte[] source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(Stream source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(Image source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(string source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(object source, NameValueCollection query) + { + Guard.NotNull(query, nameof(query)); + + Source = source; + DisposeSource = true; + Notify = true; + + // Add tokens sanitized + query.AllKeys.Each(key => Add(key, query[key], false)); + } + + public ProcessImageQuery(ProcessImageQuery query) + : base(query) + { + Guard.NotNull(query, nameof(query)); + + Source = query.Source; + Format = query.Format; + DisposeSource = query.DisposeSource; + } + + /// + /// The source image's physical path, app-relative virtual path, or a Stream, byte array, Image or IFile instance. + /// + public object Source { get; set; } + + public string FileName { get; set; } + + /// + /// Whether to dispose the source stream after processing completes + /// + public bool DisposeSource { get; set; } + + /// + /// Whether to execute an applicable post processor which + /// can reduce the resulting file size drastically, but also + /// can slow down processing time. + /// + public bool ExecutePostProcessor { get; set; } = true; + + public int? MaxWidth + { + get => Get("w") ?? Get("size"); + set => Set("w", value); + } + + public int? MaxHeight + { + get => Get("h") ?? Get("size"); + set => Set("h", value); + } + + public int? MaxSize + { + get => Get("size"); + set => Set("size", value); + } + + public int? Quality + { + get => Get("q"); + set => Set("q", value); + } + + /// + /// max (default) | boxpad | crop | min | pad | stretch + /// + public string ScaleMode + { + get => Get("m"); + set => Set("m", value); + } + + /// + /// center (default) | top | bottom | left | right | top-left | top-right | bottom-left | bottom-right + /// + public string AnchorPosition + { + get => Get("pos"); + set => Set("pos", value); + } + + public string BackgroundColor + { + get => Get("bg"); + set => Set("bg", value); + } + + /// + /// Gets or sets the output file format either as a string ("png", "jpg", "gif" and "svg"), + /// or as a format object instance. + /// When format is not specified, the original format of the source image is used (unless it is not a web safe format - jpeg is the fallback in that scenario). + /// + public object Format { get; set; } + + public bool IsValidationMode { get; set; } + + public bool Notify { get; set; } + + public override QueryString Add(string name, string value, bool isUnique) + { + // Keep away invalid tokens from underlying query + if (name != null && _supportedTokens.TryGetValue(name, out var validator) && validator(name, value)) + { + return base.Add(name, value, isUnique); + } + + return this; + } + + private T Get(string name) + { + return this[name].Convert(); + } + + private void Set(string name, T val) + { + if (val == null) + Remove(name); + else + Add(name, val.Convert(), true); + } + + + public bool NeedsProcessing(bool ignoreQualityFlag = false) + { + if (this.Count == 0) + return false; + + if (object.Equals(Format, "svg")) + { + // SVG cannot be processed. + return false; + } + + if (ignoreQualityFlag && this.Count == 1 && this["q"] != null) + { + // Return false if ignoreQualityFlag is true and "q" is the only flag. + return false; + } + + if (this.Count == 1 && Quality >= 90) + { + // If "q" is the only flag and its value is >= 90, we don't need to process + return false; + } + + return true; + } + + public string CreateHash() + { + var hash = string.Empty; + + foreach (var key in this.AllKeys) + { + if (key == "m" && this["m"] == "max") + continue; // Mode 'max' is default and can be omitted + + hash += "-" + key + this[key]; + } + + return hash; + } + + public string GetResultExtension() + { + if (Format == null) + { + return null; + } + else if (Format is IImageFormat imageFormat) + { + return imageFormat.DefaultExtension; + } + else if (Format is string str) + { + return str; + } + + return null; + } + + #region Static Helpers + + public static ResizeMode ConvertScaleMode(string mode) + { + switch (mode.EmptyNull().ToLower()) + { + case "boxpad": + return ResizeMode.BoxPad; + case "crop": + return ResizeMode.Crop; + case "min": + return ResizeMode.Min; + case "pad": + return ResizeMode.Pad; + case "stretch": + return ResizeMode.Stretch; + default: + return ResizeMode.Max; + } + } + + public static AnchorPosition ConvertAnchorPosition(string anchor) + { + switch (anchor.EmptyNull().ToLower()) + { + case "top": + return SmartStore.Services.Media.Imaging.AnchorPosition.Top; + case "bottom": + return SmartStore.Services.Media.Imaging.AnchorPosition.Bottom; + case "left": + return SmartStore.Services.Media.Imaging.AnchorPosition.Left; + case "right": + return SmartStore.Services.Media.Imaging.AnchorPosition.Right; + case "top-left": + return SmartStore.Services.Media.Imaging.AnchorPosition.TopLeft; + case "top-right": + return SmartStore.Services.Media.Imaging.AnchorPosition.TopRight; + case "bottom-left": + return SmartStore.Services.Media.Imaging.AnchorPosition.BottomLeft; + case "bottom-right": + return SmartStore.Services.Media.Imaging.AnchorPosition.BottomRight; + default: + return SmartStore.Services.Media.Imaging.AnchorPosition.Center; + } + } + + private static bool ValidateSizeToken(string key, string value) + { + return uint.TryParse(value, out var size) && size < 10000; + } + + private static bool ValidateQualityToken(string key, string value) + { + return uint.TryParse(value, out var q) && q <= 100; + } + + private static bool ValidateScaleModeToken(string key, string value) + { + return (new[] { "max", "boxpad", "crop", "min", "pad", "stretch" }).Contains(value); + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageResult.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageResult.cs new file mode 100644 index 0000000000..6313e9d198 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ProcessImageResult.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; + +namespace SmartStore.Services.Media.Imaging +{ + public class ProcessImageResult : DisposableObject + { + public ProcessImageQuery Query { get; set; } + + public IImageFormat SourceFormat { get; set; } + public IImage Image { get; set; } + internal bool DisposeImage { get; set; } + + /// + /// Is true if any effect has been applied that changed the image visually (like background color, contrast, sharpness etc.). + /// Resize and compression quality does NOT count as FX. + /// + public bool HasAppliedVisualEffects { get; set; } + + public long ProcessTimeMs { get; set; } + + protected override void OnDispose(bool disposing) + { + if (disposing && DisposeImage && Image != null) + { + Image.Dispose(); + Image = null; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/ResizeOptions.cs b/src/Libraries/SmartStore.Services/Media/Imaging/ResizeOptions.cs new file mode 100644 index 0000000000..421dc6239b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/ResizeOptions.cs @@ -0,0 +1,181 @@ +using System; +using System.Drawing; +using System.Linq; + +namespace SmartStore.Services.Media.Imaging +{ + /// + /// Enumerated resize modes to apply to resized images. + /// + public enum ResizeMode + { + /// + /// Pads the resized image to fit the bounds of its container. + /// If only one dimension is passed, will maintain the original aspect ratio. + /// + Pad, + + /// + /// Stretches the resized image to fit the bounds of its container. + /// + Stretch, + + /// + /// Crops the resized image to fit the bounds of its container. + /// + Crop, + + /// + /// Constrains the resized image to fit the bounds of its container maintaining + /// the original aspect ratio. + /// + Max, + + /// + /// Resizes the image until the shortest side reaches the set given dimension. + /// Sets to false only allowing downscaling. + /// + Min, + + /// + /// Pads the image to fit the bound of the container without resizing the + /// original source. Sets to true. + /// When downscaling, performs the same functionality as + /// + BoxPad + } + + /// + /// Enumerated anchor positions to apply to resized images. + /// + public enum AnchorPosition + { + /// + /// Anchors the position of the image to the center of it's bounding container. + /// + Center, + + /// + /// Anchors the position of the image to the top of it's bounding container. + /// + Top, + + /// + /// Anchors the position of the image to the bottom of it's bounding container. + /// + Bottom, + + /// + /// Anchors the position of the image to the left of it's bounding container. + /// + Left, + + /// + /// Anchors the position of the image to the right of it's bounding container. + /// + Right, + + /// + /// Anchors the position of the image to the top left side of it's bounding container. + /// + TopLeft, + + /// + /// Anchors the position of the image to the top right side of it's bounding container. + /// + TopRight, + + /// + /// Anchors the position of the image to the bottom right side of it's bounding container. + /// + BottomRight, + + /// + /// Anchors the position of the image to the bottom left side of it's bounding container. + /// + BottomLeft + } + + public class ResizeOptions + { + /// + /// Gets or sets the size. + /// + public Size Size { get; set; } + + /// + /// Gets or sets the resize mode. + /// + public ResizeMode ResizeMode { get; set; } = ResizeMode.Pad; + + /// + /// Gets or sets the anchor position. + /// + public AnchorPosition AnchorPosition { get; set; } = AnchorPosition.Center; + + /// + /// Gets or sets a value indicating whether to allow up-scaling of images. + /// For this is always true. + /// + public bool Upscale { get; set; } + + /// + /// Gets or sets the center coordinates. + /// + public float[] CenterCoordinates { get; set; } + + /// + /// Gets or sets the anchor point. + /// + public Point? AnchorPoint { get; set; } + + /// + /// Returns a value that indicates whether the specified object is an + /// object that is equivalent to + /// this object. + /// + /// + /// The object to test. + /// + /// + /// True if the given object is an object that is equivalent to + /// this object; otherwise, false. + /// + public override bool Equals(object obj) + { + if (!(obj is ResizeOptions options)) + { + return false; + } + + return this.Size == options.Size + && this.ResizeMode == options.ResizeMode + && this.AnchorPosition == options.AnchorPosition + && this.Upscale == options.Upscale + && ((this.CenterCoordinates != null + && options.CenterCoordinates != null + && this.CenterCoordinates.SequenceEqual(options.CenterCoordinates)) + || (this.CenterCoordinates == options.CenterCoordinates)) + && this.AnchorPoint == options.AnchorPoint; + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = this.Size.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)this.ResizeMode; + hashCode = (hashCode * 397) ^ (int)this.AnchorPosition; + hashCode = (hashCode * 397) ^ this.Upscale.GetHashCode(); + hashCode = (hashCode * 397) ^ (this.CenterCoordinates?.GetHashCode() ?? 0); + return (hashCode * 397) ^ this.AnchorPoint.GetHashCode(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Imaging/UnsupportedImageFormat.cs b/src/Libraries/SmartStore.Services/Media/Imaging/UnsupportedImageFormat.cs new file mode 100644 index 0000000000..13b5cdd1ed --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Imaging/UnsupportedImageFormat.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Services.Media.Imaging +{ + public class UnsupportedImageFormat : IImageFormat + { + public UnsupportedImageFormat(string mimeType, string extension) + { + Guard.NotEmpty(mimeType, nameof(mimeType)); + Guard.NotEmpty(extension, nameof(extension)); + + if (extension[0] == '.' && extension.Length > 1) + { + extension = extension.Substring(1); + } + + Name = extension; + DefaultMimeType = mimeType; + DefaultExtension = extension; + } + + public string Name { get; } + public string DefaultExtension { get; } + public string DefaultMimeType { get; } + + public IEnumerable FileExtensions => new string[] { DefaultExtension }; + + public IEnumerable MimeTypes => new string[] { DefaultMimeType }; + } +} diff --git a/src/Libraries/SmartStore.Services/Media/IPictureService.cs b/src/Libraries/SmartStore.Services/Media/Legacy/IPictureService.cs similarity index 100% rename from src/Libraries/SmartStore.Services/Media/IPictureService.cs rename to src/Libraries/SmartStore.Services/Media/Legacy/IPictureService.cs diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/Legacy/PictureService.cs similarity index 100% rename from src/Libraries/SmartStore.Services/Media/PictureService.cs rename to src/Libraries/SmartStore.Services/Media/Legacy/PictureService.cs diff --git a/src/Libraries/SmartStore.Services/Media/MediaExceptionFactory.cs b/src/Libraries/SmartStore.Services/Media/MediaExceptionFactory.cs new file mode 100644 index 0000000000..5ae2854de5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/MediaExceptionFactory.cs @@ -0,0 +1,148 @@ +using System; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Localization; +using SmartStore.Utilities; + +namespace SmartStore.Services.Media +{ + #region Exception classes + + public sealed class MediaFileNotFoundException : SmartException + { + public MediaFileNotFoundException(string message) : base(message) { } + } + + public sealed class MediaFolderNotFoundException : SmartException + { + public MediaFolderNotFoundException(string message) : base(message) { } + } + + public sealed class DuplicateMediaFileException : SmartException + { + public DuplicateMediaFileException(string message, MediaFileInfo dupeFile, string uniquePath) : base(message) + { + File = dupeFile; + UniquePath = uniquePath; + } + + public MediaFileInfo File { get; } + public string UniquePath { get; } + } + + public sealed class DuplicateMediaFolderException : SmartException + { + public DuplicateMediaFolderException(string message, MediaFolderNode dupeFolder) : base(message) + { + Folder = dupeFolder; + } + + public MediaFolderNode Folder { get; } + } + + public sealed class NotSameAlbumException : SmartException + { + public NotSameAlbumException(string message) : base(message) { } + } + + public sealed class DeniedMediaTypeException : SmartException + { + public DeniedMediaTypeException(string message) : base(message) { } + } + + public sealed class ExtractThumbnailException : SmartException + { + public ExtractThumbnailException(string message) : base(message) { } + public ExtractThumbnailException(string message, Exception innerException) : base(message, innerException) { } + } + + public sealed class MaxMediaFileSizeExceededException : SmartException + { + public MaxMediaFileSizeExceededException(string message) : base(message) { } + } + + public sealed class DeleteTrackedFileException : SmartException + { + public DeleteTrackedFileException(string message, MediaFile file, Exception innerException) : base(message, innerException) + { + File = file; + } + + public MediaFile File { get; } + } + + #endregion + + public class MediaExceptionFactory + { + public Localizer T { get; set; } = NullLocalizer.Instance; + + public MediaFileNotFoundException FileNotFound(string path) + { + return new MediaFileNotFoundException(T("Admin.Media.Exception.FileNotFound", path)); + } + + public MediaFolderNotFoundException FolderNotFound(string path) + { + return new MediaFolderNotFoundException(T("Admin.Media.Exception.FolderNotFound", path)); + } + + public DuplicateMediaFileException DuplicateFile(string fullPath, MediaFileInfo dupeFile, string uniquePath) + { + return new DuplicateMediaFileException(T("Admin.Media.Exception.DuplicateFile", fullPath), dupeFile, uniquePath); + } + + public DuplicateMediaFolderException DuplicateFolder(string fullPath, MediaFolderNode dupeFolder) + { + return new DuplicateMediaFolderException(T("Admin.Media.Exception.DuplicateFolder", fullPath), dupeFolder); + } + + public NotSameAlbumException NotSameAlbum(string sourcePath, string destPath) + { + return new NotSameAlbumException(T("Admin.Media.Exception.NotSameAlbum", sourcePath, destPath)); + } + + public DeniedMediaTypeException DeniedMediaType(string fileName, string currentType, string[] acceptedTypes = null) + { + var msg = T("Admin.Media.Exception.DeniedMediaType", fileName, currentType); + if (acceptedTypes != null && acceptedTypes.Length > 0) + { + var types = string.Join(", ", acceptedTypes); + msg += T("Admin.Media.Exception.DeniedMediaType.Hint", types, currentType); + } + + return new DeniedMediaTypeException(msg); + } + + public ExtractThumbnailException ExtractThumbnail(string path, string reason = null) + { + return new ExtractThumbnailException(T("Admin.Media.Exception.ExtractThumbnail", path, reason.NaIfEmpty())); + } + + public ExtractThumbnailException ExtractThumbnail(string path, Exception innerException) + { + Guard.NotNull(innerException, nameof(innerException)); + return new ExtractThumbnailException(T("Admin.Media.Exception.ExtractThumbnail", path, innerException.Message), innerException); + } + + public MaxMediaFileSizeExceededException MaxFileSizeExceeded(string fileName, long fileSize, long maxSize) + { + return new MaxMediaFileSizeExceededException(T( + "Admin.Media.Exception.MaxFileSizeExceeded", + fileName.NaIfEmpty(), + Prettifier.BytesToString(fileSize), + Prettifier.BytesToString(maxSize))); + } + + public DeleteTrackedFileException DeleteTrackedFile(MediaFile file, Exception innerException) + { + Guard.NotNull(file, nameof(file)); + return new DeleteTrackedFileException(T("Admin.Media.Exception.DeleteReferenzedFile", file.Name), file, innerException); + } + + public InvalidOperationException IdenticalPaths(MediaFileInfo file) + { + Guard.NotNull(file, nameof(file)); + return new InvalidOperationException(T("Admin.Media.Exception.FileNamesIdentical", file.Path)); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaFileInfo.cs b/src/Libraries/SmartStore.Services/Media/MediaFileInfo.cs similarity index 55% rename from src/Libraries/SmartStore.Services/Media/v4/MediaFileInfo.cs rename to src/Libraries/SmartStore.Services/Media/MediaFileInfo.cs index 96e0f7f4f5..b69a3bb069 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaFileInfo.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaFileInfo.cs @@ -1,19 +1,23 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Newtonsoft.Json; using SmartStore.Core.Domain.Media; using SmartStore.Core.IO; +using SmartStore.Services.Media.Imaging; using SmartStore.Services.Media.Storage; namespace SmartStore.Services.Media { - public partial class MediaFileInfo : IFile + public partial class MediaFileInfo : IFile, ICloneable { - private string _url; - + private string _alt; + private string _title; + private readonly IMediaStorageProvider _storageProvider; private readonly IMediaUrlGenerator _urlGenerator; @@ -31,6 +35,29 @@ public MediaFileInfo(MediaFile file, IMediaStorageProvider storageProvider, IMed _urlGenerator = urlGenerator; } + #region Clone + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + object ICloneable.Clone() + { + return Clone(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MediaFileInfo Clone() + { + var clone = new MediaFileInfo(File, _storageProvider, _urlGenerator, Directory) + { + ThumbSize = this.ThumbSize, + _alt = this._alt, + _title = this._title + }; + + return clone; + } + + #endregion + [JsonIgnore] public MediaFile File { get; } @@ -58,7 +85,63 @@ public MediaFileInfo(MediaFile file, IMediaStorageProvider storageProvider, IMed [JsonProperty("createdOn")] public DateTime CreatedOn => File.CreatedOnUtc; - public static explicit operator MediaFile(MediaFileInfo fileInfo) => fileInfo.File; + [JsonProperty("alt")] + public string Alt + { + get => _alt ?? File.Alt; + set => _alt = value; + } + + [JsonProperty("titleAttr")] + public string TitleAttribute + { + get => _title ?? File.Title; + set => _title = value; + } + + public static explicit operator MediaFile(MediaFileInfo fileInfo) + { + return fileInfo.File; + } + + #region Url + + private readonly IDictionary<(int size, string host), string> _cachedUrls = new Dictionary<(int, string), string>(); + + [JsonProperty("url")] + internal string Url => GetUrl(0, string.Empty); + + [JsonProperty("thumbUrl")] + internal string ThumbUrl => GetUrl(ThumbSize, string.Empty); + + [JsonIgnore] + internal int ThumbSize + { + // For serialization of "ThumbUrl" in MediaManager + get; set; + } = 256; + + public string GetUrl(int maxSize = 0, string host = null) + { + var cacheKey = (maxSize, host); + if (!_cachedUrls.TryGetValue(cacheKey, out var url)) + { + url = _urlGenerator.GenerateUrl(this, null, host, false); + + if (maxSize > 0) + { + // (perf) Instead of calling GenerateUrl() with a processing query we simply + // append the query part to the string. + url += "?size=" + maxSize.ToString(CultureInfo.InvariantCulture); + } + + _cachedUrls[cacheKey] = url; + } + + return url; + } + + #endregion #region IFile @@ -86,14 +169,8 @@ public MediaFileInfo(MediaFile file, IMediaStorageProvider storageProvider, IMed [JsonProperty("dimensions")] public Size Dimensions { get; } - [JsonProperty("url")] - public string Url - { - get { return _url ?? (_url = _urlGenerator.GenerateUrl(this, null, string.Empty)); } - } - [JsonIgnore] - public bool Exists => File.Id > 0; + public bool Exists => File?.Id > 0; public Stream OpenRead() { diff --git a/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs b/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs index 915eb4fc90..26599c17d8 100644 --- a/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs @@ -5,52 +5,52 @@ namespace SmartStore.Services.Media { - public interface IMediaFileSystem : IFileSystem - { - } - - public class MediaFileSystem : LocalFileSystem, IMediaFileSystem - { - private static string _mediaPublicPath; - - public MediaFileSystem() - : base(GetMediaBasePath(), "~/" + GetMediaPublicPath()) - { - if (DataSettings.DatabaseIsInstalled()) - { - this.CreateFolder("Storage"); - this.CreateFolder("Thumbs"); - this.CreateFolder("QueuedEmailAttachment"); - } - } - - public static string GetMediaBasePath() - { - var path = CommonHelper.GetAppSetting("sm:MediaStoragePath")?.Trim().NullEmpty(); - if (path == null) - { - path = "/App_Data/Tenants/" + DataSettings.Current.TenantName + "/Media"; - } - - return path; - } - - public static string GetMediaPublicPath() - { - if (_mediaPublicPath == null) - { - var path = CommonHelper.GetAppSetting("sm:MediaPublicPath")?.Trim().NullEmpty() ?? "media"; - - if (path.IsWebUrl()) - { - throw new NotSupportedException("Fully qualified URLs are not supported for the 'sm:MediaPublicPath' setting."); - } - - _mediaPublicPath = path.TrimStart('~', '/').Replace('\\', '/').ToLower().EnsureEndsWith("/"); - } - - - return _mediaPublicPath; - } - } + public interface IMediaFileSystem : IFileSystem + { + } + + public class MediaFileSystem : LocalFileSystem, IMediaFileSystem + { + private static string _mediaPublicPath; + + public MediaFileSystem() + : base(GetMediaBasePath(), "~/" + GetMediaPublicPath()) + { + if (DataSettings.DatabaseIsInstalled()) + { + this.CreateFolder("Storage"); + this.CreateFolder("Thumbs"); + this.CreateFolder("QueuedEmailAttachment"); + } + } + + public static string GetMediaBasePath() + { + var path = CommonHelper.GetAppSetting("sm:MediaStoragePath")?.Trim().NullEmpty(); + if (path == null) + { + path = "/App_Data/Tenants/" + DataSettings.Current.TenantName + "/Media"; + } + + return path; + } + + public static string GetMediaPublicPath() + { + if (_mediaPublicPath == null) + { + var path = CommonHelper.GetAppSetting("sm:MediaPublicPath")?.Trim().NullEmpty() ?? "media"; + + if (path.IsWebUrl()) + { + throw new NotSupportedException("Fully qualified URLs are not supported for the 'sm:MediaPublicPath' setting."); + } + + _mediaPublicPath = path.TrimStart('~', '/').Replace('\\', '/').ToLower().EnsureEndsWith("/"); + } + + + return _mediaPublicPath; + } + } } diff --git a/src/Libraries/SmartStore.Services/Media/MediaHelper.cs b/src/Libraries/SmartStore.Services/Media/MediaHelper.cs index e17ffd108b..aee99c3af7 100644 --- a/src/Libraries/SmartStore.Services/Media/MediaHelper.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaHelper.cs @@ -1,166 +1,86 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Linq.Expressions; -using SmartStore.ComponentModel; -using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Infrastructure; +using System.Linq; namespace SmartStore.Services.Media { public partial class MediaHelper - { - private readonly IFolderService _folderService; - - public MediaHelper(IFolderService folderService) - { - _folderService = folderService; - } - - public bool TokenizePath(string path, out MediaPathData data) - { - data = null; - - if (path.IsEmpty()) - { - return false; - } - - var dir = Path.GetDirectoryName(path); - if (dir.HasValue()) - { - var node = _folderService.GetNodeByPath(dir); - if (node != null) - { - data = new MediaPathData(node, path.Substring(dir.Length + 1)); - return true; - } - } - - return false; - } - - #region Legacy (remove later) - - // TODO: (mm) remove this stuff - - public static void UpdateDownloadTransientStateFor(TEntity entity, Expression> downloadIdProp, bool save = false) where TEntity : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(downloadIdProp, nameof(downloadIdProp)); - - var propName = downloadIdProp.ExtractMemberInfo().Name; - int currentDownloadId = downloadIdProp.CompileFast(PropertyCachingStrategy.EagerCached).Invoke(entity); - var rs = EngineContext.Current.Resolve>(); - - UpdateTransientStateForEntityInternal(entity, propName, currentDownloadId, rs, null, save); - } - - public static void UpdateDownloadTransientStateFor(TEntity entity, Expression> downloadIdProp, bool save = false) where TEntity : BaseEntity - { - Guard.NotNull(entity, nameof(entity)); - Guard.NotNull(downloadIdProp, nameof(downloadIdProp)); - - var propName = downloadIdProp.ExtractMemberInfo().Name; - int currentDownloadId = downloadIdProp.CompileFast(PropertyCachingStrategy.EagerCached).Invoke(entity).GetValueOrDefault(); - var rs = EngineContext.Current.Resolve>(); - - UpdateTransientStateForEntityInternal(entity, propName, currentDownloadId, rs, null, save); - } - - public static void UpdateDownloadTransientState(int? prevDownloadId, int? currentDownloadId, bool save = false) - { - var rs = EngineContext.Current.Resolve>(); - UpdateTransientStateCore(prevDownloadId.GetValueOrDefault(), currentDownloadId.GetValueOrDefault(), rs, null, save); - } - - public static void UpdateDownloadTransientState(int prevDownloadId, int currentDownloadId, bool save = false) - { - var rs = EngineContext.Current.Resolve>(); - UpdateTransientStateCore(prevDownloadId, currentDownloadId, rs, null, save); - } - - internal static void UpdateTransientStateForEntityInternal( - TEntity entity, - string propName, - int currentMediaId, - IRepository rs, - Action deleteAction, - bool save) where TEntity : BaseEntity where TMedia : BaseEntity - { - //object obj = null; - //int prevMediaId = 0; - //if (rs.Context.TryGetModifiedProperty(entity, propName, out obj)) - //{ - // prevMediaId = ((int?)obj).GetValueOrDefault(); - //} - - //UpdateTransientStateCore(prevMediaId, currentMediaId, rs, deleteAction, save); - } - - internal static void UpdateTransientStateCore( - int prevMediaId, - int currentMediaId, - IRepository rs, - Action deleteAction, - bool save) - where TMedia : BaseEntity - { - var autoCommit = rs.AutoCommitEnabled; - rs.AutoCommitEnabled = false; - - try - { - TMedia media = null; - bool shouldSave = false; - - bool isModified = prevMediaId != currentMediaId; - - if (currentMediaId > 0 && isModified) - { - // new entity with a media or media replaced - media = rs.GetById(currentMediaId); - if (media != null) - { - var transient = media as ITransient; - if (transient != null) - { - transient.IsTransient = false; - shouldSave = true; - } - } - } - - if (prevMediaId > 0 && isModified) - { - // ID changed, so delete old record - media = rs.GetById(prevMediaId); - if (media != null) - { - if (deleteAction == null) - { - rs.Delete(media); - } - else - { - deleteAction(media); - } - shouldSave = true; - } - } - - if (save && shouldSave) - { - rs.Context.SaveChanges(); - } - } - finally - { - rs.AutoCommitEnabled = autoCommit; - } - } + { + #region Static + + private readonly static char[] _invalidFileNameChars = Path.GetInvalidFileNameChars().Concat(new[] { '&' }).ToArray(); + private readonly static char[] _invalidFolderNameChars = Path.GetInvalidPathChars().Concat(new[] { '&', '/', '\\' }).ToArray(); + + public static string NormalizeFileName(string fileName) + { + return string.Join("-", fileName.ToSafe().Split(_invalidFileNameChars)); + } + + public static string NormalizeFolderName(string folderName) + { + return string.Join("-", folderName.ToSafe().Split(_invalidFolderNameChars)); + } #endregion + + private readonly IFolderService _folderService; + + public MediaHelper(IFolderService folderService) + { + _folderService = folderService; + } + + public bool TokenizePath(string path, bool normalizeFileName, out MediaPathData data) + { + data = null; + + if (path.IsEmpty()) + { + return false; + } + + var dir = Path.GetDirectoryName(path); + if (dir.HasValue()) + { + var node = _folderService.GetNodeByPath(dir); + if (node != null) + { + data = new MediaPathData(node, path.Substring(dir.Length + 1), normalizeFileName); + return true; + } + } + + return false; + } + + public bool CheckUniqueFileName(string title, string ext, string destFileName, out string uniqueName) + { + return CheckUniqueFileName(title, ext, new HashSet(new[] { destFileName }, StringComparer.CurrentCultureIgnoreCase), out uniqueName); + } + + public bool CheckUniqueFileName(string title, string ext, HashSet destFileNames, out string uniqueName) + { + uniqueName = null; + + if (destFileNames.Count == 0) + { + return false; + } + + int i = 1; + while (true) + { + var test = string.Concat(title, "-", i, ".", ext.TrimStart('.')); + if (!destFileNames.Contains(test)) + { + // Found our gap + uniqueName = test; + return true; + } + + i++; + } + } } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaModule.cs b/src/Libraries/SmartStore.Services/Media/MediaModule.cs similarity index 86% rename from src/Libraries/SmartStore.Services/Media/v4/MediaModule.cs rename to src/Libraries/SmartStore.Services/Media/MediaModule.cs index 46a9da7fe0..80b3f5a9e1 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaModule.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaModule.cs @@ -4,6 +4,8 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; using SmartStore.Services.Configuration; +using SmartStore.Services.Media.Imaging; +using SmartStore.Services.Media.Imaging.Impl; using SmartStore.Services.Media.Migration; using SmartStore.Services.Media.Storage; @@ -20,9 +22,11 @@ public MediaModule(ITypeFinder typeFinder) protected override void Load(ContainerBuilder builder) { - // Migration utils + // Utils builder.RegisterType().InstancePerRequest(); + builder.RegisterType().InstancePerRequest(); builder.RegisterType().InstancePerRequest(); + builder.RegisterType().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -31,11 +35,13 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); + + // ImageProcessor adapter factory + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); // Register factory for currently active media storage provider @@ -61,7 +67,7 @@ protected override void Load(ContainerBuilder builder) private static Func MediaStorageProviderFactory(IComponentContext c) { - var systemName = c.Resolve().GetSettingByKey("Media.Storage.Provider", DatabaseMediaStorageProvider.SystemName); + var systemName = c.Resolve().GetSettingByKey("Media.Storage.Provider", FileSystemMediaStorageProvider.SystemName); var provider = c.Resolve().GetProvider(systemName); return () => provider.Value; } diff --git a/src/Libraries/SmartStore.Services/Media/MediaPathData.cs b/src/Libraries/SmartStore.Services/Media/MediaPathData.cs index 132e84fc8d..f6016760ee 100644 --- a/src/Libraries/SmartStore.Services/Media/MediaPathData.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaPathData.cs @@ -5,97 +5,74 @@ namespace SmartStore.Services.Media { - public class MediaPathData - { - private string _name; - private string _title; - private string _ext; - private string _mime; + public class MediaPathData + { + private string _name; + private string _title; + private string _ext; + private string _mime; - public MediaPathData(TreeNode node, string fileName) - { - Guard.NotNull(node, nameof(node)); - Guard.NotEmpty(fileName, nameof(fileName)); + public MediaPathData(TreeNode node, string fileName, bool normalizeFileName = false) + { + Guard.NotNull(node, nameof(node)); + Guard.NotEmpty(fileName, nameof(fileName)); - Node = node; - _name = fileName; - } + Node = node; + _name = normalizeFileName + ? MediaHelper.NormalizeFileName(fileName) + : fileName; + } - public MediaPathData(string path) - { - Guard.NotEmpty(path, nameof(path)); + public MediaPathData(string path, bool normalizeFileName = false) + { + Guard.NotEmpty(path, nameof(path)); - _name = Path.GetFileName(path); - } + _name = normalizeFileName + ? MediaHelper.NormalizeFileName(Path.GetFileName(path)) + : Path.GetFileName(path); + } - public MediaPathData(MediaPathData pathData) - { - Node = pathData.Node; - _name = pathData.FileName; - _title = pathData._title; - _ext = pathData._ext; - _mime = pathData._mime; - } + public MediaPathData(MediaPathData pathData) + { + Node = pathData.Node; + _name = pathData.FileName; + _title = pathData._title; + _ext = pathData._ext; + _mime = pathData._mime; + } - public TreeNode Node { get; } - public MediaFolderNode Folder - { - get => Node.Value; - } + public TreeNode Node { get; } + public MediaFolderNode Folder => Node.Value; - public string FileName - { - get - { - return _name; - } - set - { - Guard.NotEmpty(value, nameof(value)); + public string FileName + { + get => _name; + set + { + Guard.NotEmpty(value, nameof(value)); - _name = value; - _title = null; - _ext = null; - _mime = null; - } - } + _name = value; + _title = null; + _ext = null; + _mime = null; + } + } - public string FullPath - { - get => Folder.Path + "/" + _name; - } + public string FullPath => Folder.Path + "/" + _name; - public string FileTitle - { - get - { - return _title ?? (_title = Path.GetFileNameWithoutExtension(_name)); - } - } + public string FileTitle => _title ?? (_title = Path.GetFileNameWithoutExtension(_name)); - public string Extension - { - get - { - return _ext ?? (_ext = Path.GetExtension(_name).EmptyNull().TrimStart('.')); - } - set - { - _ext = value; - } - } + public string Extension + { + get => _ext ?? (_ext = Path.GetExtension(_name).EmptyNull().TrimStart('.')); + set => _ext = value?.TrimStart('.'); + } - public string MimeType - { - get - { - return _mime ?? (_mime = MimeTypes.MapNameToMimeType(_name)); - } - set - { - _mime = value; - } - } - } + public string MimeType + { + get => _mime ?? (_mime = MimeTypes.MapNameToMimeType(_name)); + set => _mime = value; + } + } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaService.Folder.cs b/src/Libraries/SmartStore.Services/Media/MediaService.Folder.cs similarity index 63% rename from src/Libraries/SmartStore.Services/Media/v4/MediaService.Folder.cs rename to src/Libraries/SmartStore.Services/Media/MediaService.Folder.cs index 9cde9fdde7..37cf4491b0 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaService.Folder.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaService.Folder.cs @@ -3,18 +3,11 @@ using System.Data.Entity; using System.IO; using System.Linq; -using System.Linq.Dynamic; -using System.Threading.Tasks; -using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Events; -using SmartStore.Core.Logging; -using SmartStore.Services.Media.Storage; -using SmartStore.Core.IO; -using System.Drawing; +using System.Linq.Dynamic.Core; using System.Runtime.CompilerServices; using SmartStore.Collections; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Media; namespace SmartStore.Services.Media { @@ -35,6 +28,12 @@ public MediaFolderInfo CreateFolder(string path) path = FolderService.NormalizePath(path, false); ValidateFolderPath(path, "CreateFolder", nameof(path)); + var dupe = _folderService.GetNodeByPath(path); + if (dupe != null) + { + throw _exceptionFactory.DuplicateFolder(path, dupe.Value); + } + var sep = "/"; var folderNames = path.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); bool flag = false; @@ -46,7 +45,7 @@ public MediaFolderInfo CreateFolder(string path) { for (int i = 0; i < folderNames.Length; i++) { - var folderName = folderNames[i].ToValidPath(); + var folderName = MediaHelper.NormalizeFolderName(folderNames[i]); path += (i > 0 ? sep : string.Empty) + folderName; if (!flag) @@ -59,7 +58,7 @@ public MediaFolderInfo CreateFolder(string path) } else { - if (i == 0) throw new NotSupportedException($"Creating top-level (album) folders is not supported. Folder: {path}."); + if (i == 0) throw new NotSupportedException(T("Admin.Media.Exception.TopLevelAlbum", path)); flag = true; } } @@ -76,7 +75,7 @@ public MediaFolderInfo CreateFolder(string path) } } } - + return new MediaFolderInfo(_folderService.GetNodeById(folderId)); } @@ -91,18 +90,18 @@ public MediaFolderInfo MoveFolder(string path, string destinationPath) var node = _folderService.GetNodeByPath(path); if (node == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } if (node.Value.IsAlbum) { - throw new NotSupportedException($"Moving or renaming root album folders is not supported. Folder: {node.Value.Name}."); + throw new NotSupportedException(T("Admin.Media.Exception.AlterRootAlbum", node.Value.Name)); } var folder = _folderService.GetFolderById(node.Value.Id); if (folder == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } ValidateFolderPath(destinationPath, "MoveFolder", nameof(destinationPath)); @@ -110,7 +109,7 @@ public MediaFolderInfo MoveFolder(string path, string destinationPath) // Destination must not exist if (FolderExists(destinationPath)) { - throw new ArgumentException("Folder '" + destinationPath + "' already exists."); + throw new ArgumentException(T("Admin.Media.Exception.DuplicateFolder", destinationPath)); } var destParent = FolderService.NormalizePath(Path.GetDirectoryName(destinationPath)); @@ -119,18 +118,18 @@ public MediaFolderInfo MoveFolder(string path, string destinationPath) var destParentNode = _folderService.GetNodeByPath(destParent); if (destParentNode == null) { - throw new MediaFolderNotFoundException(destinationPath); + throw _exceptionFactory.FolderNotFound(destinationPath); } // Cannot move outside source album if (!_folderService.AreInSameAlbum(folder.Id, destParentNode.Value.Id)) { - throw new NotSameAlbumException(node.Value.Path, destParent); + throw _exceptionFactory.NotSameAlbum(node.Value.Path, destParent); } if (destParentNode.IsDescendantOfOrSelf(node)) { - throw new ArgumentException("Destination folder '" + destinationPath + "' is not allowed to be a descendant of source folder '" + node.Value.Path + "'."); + throw new ArgumentException(T("Admin.Media.Exception.DescendantFolder", destinationPath, node.Value.Path)); } // Set new values @@ -145,7 +144,7 @@ public MediaFolderInfo MoveFolder(string path, string destinationPath) return new MediaFolderInfo(_folderService.GetNodeById(folder.Id)); } - public MediaFolderInfo CopyFolder(string path, string destinationPath, DuplicateEntryHandling dupeEntryHandling = DuplicateEntryHandling.Skip) + public FolderOperationResult CopyFolder(string path, string destinationPath, DuplicateEntryHandling dupeEntryHandling = DuplicateEntryHandling.Skip) { Guard.NotEmpty(path, nameof(path)); Guard.NotEmpty(destinationPath, nameof(destinationPath)); @@ -156,45 +155,49 @@ public MediaFolderInfo CopyFolder(string path, string destinationPath, Duplicate destinationPath = FolderService.NormalizePath(destinationPath); if (destinationPath.EnsureEndsWith("/").StartsWith(path.EnsureEndsWith("/"))) { - throw new ArgumentException("Destination folder '" + destinationPath + "' is not allowed to be a descendant of source folder '" + path + "'.", nameof(destinationPath)); + throw new ArgumentException(T("Admin.Media.Exception.DescendantFolder", destinationPath, path), nameof(destinationPath)); } var node = _folderService.GetNodeByPath(path); if (node == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } if (node.Value.IsAlbum) { - throw new NotSupportedException($"Copying root album folders is not supported. Folder: {node.Value.Name}."); + throw new NotSupportedException(T("Admin.Media.Exception.CopyRootAlbum", node.Value.Name)); } using (new DbContextScope(autoCommit: false, validateOnSave: false, autoDetectChanges: false)) { destinationPath += "/" + node.Value.Name; - return InternalCopyFolder(node, destinationPath, dupeEntryHandling); + var dupeFiles = new List(); + + // >>>> Do the heavy stuff + var folder = InternalCopyFolder(node, destinationPath, dupeEntryHandling, dupeFiles); + + var result = new FolderOperationResult + { + Operation = "copy", + DuplicateEntryHandling = dupeEntryHandling, + Folder = folder, + DuplicateFiles = dupeFiles + }; + + return result; } } - private MediaFolderInfo InternalCopyFolder(TreeNode source, string destPath, DuplicateEntryHandling dupeEntryHandling) + private MediaFolderInfo InternalCopyFolder(TreeNode sourceNode, string destPath, DuplicateEntryHandling dupeEntryHandling, IList dupeFiles) { // Get dest node var destNode = _folderService.GetNodeByPath(destPath); // Dupe handling - if (destNode != null) + if (destNode != null && dupeEntryHandling == DuplicateEntryHandling.ThrowError) { - switch (dupeEntryHandling) - { - case DuplicateEntryHandling.ThrowError: - throw new DuplicateMediaFolderException(source.Value.Path); - case DuplicateEntryHandling.Skip: - return new MediaFolderInfo(destNode); - case DuplicateEntryHandling.Rename: - case DuplicateEntryHandling.Overwrite: - break; - } + throw _exceptionFactory.DuplicateFolder(sourceNode.Value.Path, destNode.Value); } var doDupeCheck = destNode != null; @@ -212,7 +215,7 @@ private MediaFolderInfo InternalCopyFolder(TreeNode source, str // Get all source files in one go var files = _searcher.SearchFiles( - new MediaSearchQuery { FolderId = source.Value.Id }, + new MediaSearchQuery { FolderId = sourceNode.Value.Id }, MediaLoadFlags.AsNoTracking | MediaLoadFlags.WithTags).Load(); IDictionary destFiles = null; @@ -236,15 +239,34 @@ private MediaFolderInfo InternalCopyFolder(TreeNode source, str foreach (var file in batch) { destPathData.FileName = file.Name; + + // >>> Do copy var copy = InternalCopyFile( file, destPathData, false /* copyData */, dupeEntryHandling, () => destFiles?.Get(file.Name), - UniqueFileNameChecker); + UniqueFileNameChecker, + out var isDupe); - tuples.Add((file, copy)); + if (copy != null) + { + if (isDupe) + { + dupeFiles.Add(new DuplicateFileInfo + { + SourceFile = ConvertMediaFile(file, sourceNode.Value), + DestinationFile = ConvertMediaFile(copy, destNode.Value), + UniquePath = destPathData.FullPath + }); + } + if (!isDupe || dupeEntryHandling != DuplicateEntryHandling.Skip) + { + // When dupe: add to processing queue only if file was NOT skipped + tuples.Add((file, copy)); + } + } } // Save batch to DB (1st pass) @@ -265,98 +287,118 @@ private MediaFolderInfo InternalCopyFolder(TreeNode source, str } // Copy folders - foreach (var node in source.Children) + foreach (var node in sourceNode.Children) { destPath = destNode.Value.Path + "/" + node.Value.Name; - InternalCopyFolder(node, destPath, dupeEntryHandling); + InternalCopyFolder(node, destPath, dupeEntryHandling, dupeFiles); } return new MediaFolderInfo(destNode); void UniqueFileNameChecker(MediaPathData pathData) { - if (destNames != null && InternalCheckUniqueFileName(pathData.FileTitle, pathData.Extension, destNames, out var uniqueName)) + if (destNames != null && _helper.CheckUniqueFileName(pathData.FileTitle, pathData.Extension, destNames, out var uniqueName)) { pathData.FileName = uniqueName; } } } - public void DeleteFolder(string path, FileHandling fileHandling = FileHandling.SoftDelete) + public FolderDeleteResult DeleteFolder(string path, FileHandling fileHandling = FileHandling.SoftDelete) { Guard.NotEmpty(path, nameof(path)); path = FolderService.NormalizePath(path); ValidateFolderPath(path, "DeleteFolder", nameof(path)); - var node = _folderService.GetNodeByPath(path); - if (node == null) + var root = _folderService.GetNodeByPath(path); + if (root == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } - // Collect all affected subfolder ids also - var folderIds = _folderService.GetNodesFlattened(node.Value.Id, true).Select(x => x.Id).Reverse().ToArray(); + // Collect all affected subfolders also + var allNodes = root.FlattenNodes(true).Reverse().ToArray(); + var result = new FolderDeleteResult(); - using (new DbContextScope(autoDetectChanges: false, autoCommit: false)) + using (new DbContextScope(autoDetectChanges: false)) { using (_folderService.BeginScope(true)) { // Delete all from DB - foreach (var folderId in folderIds) + foreach (var node in allNodes) { - var folder = _folderService.GetFolderById(folderId); + var folder = _folderService.GetFolderById(node.Value.Id); if (folder != null) { - InternalDeleteFolder(folder, node.Value, fileHandling); + InternalDeleteFolder(folder, node, root, result, fileHandling); } } } } + + return result; } - private int InternalDeleteFolder(MediaFolder folder, MediaFolderNode node, FileHandling strategy) + private void InternalDeleteFolder( + MediaFolder folder, + TreeNode node, + TreeNode root, + FolderDeleteResult result, + FileHandling strategy) { - int numFiles = 0; - List lockedFiles = null; + // (perf) We gonna check file tracks, so we should preload all tracks. + _fileRepo.Context.LoadCollection(folder, (MediaFolder x) => x.Files, false, q => q.Include(f => f.Tracks)); + + var files = folder.Files.ToList(); + var lockedFiles = new List(files.Count); + var trackedFiles = new List(files.Count); // First delete files if (folder.Files.Any()) { - var albumId = strategy == FileHandling.MoveToRoot - ? _folderService.FindAlbum(folder.Id).Value.Id + var albumId = strategy == FileHandling.MoveToRoot + ? _folderService.FindAlbum(folder.Id).Value.Id : (int?)null; - var files = folder.Files.ToList(); - - lockedFiles = new List(files.Count); - foreach (var batch in files.Slice(500)) { foreach (var file in batch) { + if (strategy == FileHandling.Delete && file.Tracks.Any()) + { + // Don't delete tracked files + trackedFiles.Add(file); + continue; + } + if (strategy == FileHandling.Delete) { try { + result.DeletedFileNames.Add(file.Name); DeleteFile(file, true); } + catch (DeleteTrackedFileException) + { + trackedFiles.Add(file); + } catch (IOException) { lockedFiles.Add(file); - } + } } else if (strategy == FileHandling.SoftDelete) { DeleteFile(file, false); file.FolderId = null; + result.DeletedFileNames.Add(file.Name); } else if (strategy == FileHandling.MoveToRoot) { file.FolderId = albumId; + result.DeletedFileNames.Add(file.Name); } - - numFiles++; } _fileRepo.Context.SaveChanges(); @@ -380,34 +422,38 @@ private int InternalDeleteFolder(MediaFolder folder, MediaFolderNode node, FileH } } - if (lockedFiles == null || lockedFiles.Count == 0) + if (lockedFiles.Count > 0) { - // Don't delete folder if a containing file could not be deleted. - _folderService.DeleteFolder(folder); - _fileRepo.Context.SaveChanges(); + var fullPath = CombinePaths(root.Value.Path, lockedFiles[0].Name); + throw new IOException(T("Admin.Media.Exception.InUse", fullPath)); } - else + + if (lockedFiles.Count == 0 && trackedFiles.Count == 0 && node.Children.All(x => result.DeletedFolderIds.Contains(x.Value.Id))) { - var fullPath = CombinePaths(node.Path, lockedFiles[0].Name); - throw new IOException("Cannot delete file '{0}' because it is being used by another process.".FormatCurrent(fullPath)); + // Don't delete folder if a containing file could not be deleted, + // any tracked file was found or any of its child folders could not be deleted.. + _folderService.DeleteFolder(folder); + //_fileRepo.Context.SaveChanges(); + result.DeletedFolderIds.Add(folder.Id); } - return numFiles; + result.LockedFileNames = lockedFiles.Select(x => x.Name).ToList(); + result.TrackedFileNames = trackedFiles.Select(x => x.Name).ToList(); } #endregion #region Utils - private static void ValidateFolderPath(string path, string operation, string paramName) + private void ValidateFolderPath(string path, string operation, string paramName) { if (!IsPath(path)) { - // Destination cannot be an album - throw new ArgumentException("Invalid path specification '" + path + "' for '" + operation + "' operation.", paramName); + // Destination cannot be an album + throw new ArgumentException(T("Admin.Media.Exception.PathSpecification", path, operation), paramName); } } #endregion } -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaService.cs b/src/Libraries/SmartStore.Services/Media/MediaService.cs similarity index 59% rename from src/Libraries/SmartStore.Services/Media/v4/MediaService.cs rename to src/Libraries/SmartStore.Services/Media/MediaService.cs index c59dc51a11..dc6ef6ce92 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaService.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaService.cs @@ -1,21 +1,23 @@ using System; using System.Collections.Generic; using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using System.Drawing; using System.IO; using System.Linq; -using System.Linq.Dynamic; +using System.Linq.Dynamic.Core; +using System.Runtime.CompilerServices; using System.Threading.Tasks; -using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Media; using SmartStore.Core.Events; +using SmartStore.Core.IO; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; +using SmartStore.Data.Caching; +using SmartStore.Services.Localization; +using SmartStore.Services.Media.Imaging; using SmartStore.Services.Media.Storage; -using SmartStore.Core.IO; -using System.Drawing; -using System.Runtime.CompilerServices; -using System.Diagnostics; -using System.Runtime.InteropServices; namespace SmartStore.Services.Media { @@ -27,9 +29,12 @@ public partial class MediaService : IMediaService private readonly IMediaTypeResolver _typeResolver; private readonly IMediaUrlGenerator _urlGenerator; private readonly IEventPublisher _eventPublisher; + private readonly ILanguageService _languageService; + private readonly ILocalizedEntityService _localizedEntityService; private readonly MediaSettings _mediaSettings; private readonly IImageProcessor _imageProcessor; private readonly IImageCache _imageCache; + private readonly MediaExceptionFactory _exceptionFactory; private readonly IMediaStorageProvider _storageProvider; private readonly MediaHelper _helper; @@ -40,9 +45,12 @@ public MediaService( IMediaTypeResolver typeResolver, IMediaUrlGenerator urlGenerator, IEventPublisher eventPublisher, + ILanguageService languageService, + ILocalizedEntityService localizedEntityService, MediaSettings mediaSettings, IImageProcessor imageProcessor, IImageCache imageCache, + MediaExceptionFactory exceptionFactory, Func storageProvider, MediaHelper helper) { @@ -52,26 +60,29 @@ public MediaService( _typeResolver = typeResolver; _urlGenerator = urlGenerator; _eventPublisher = eventPublisher; + _languageService = languageService; + _localizedEntityService = localizedEntityService; _mediaSettings = mediaSettings; _imageProcessor = imageProcessor; _imageCache = imageCache; + _exceptionFactory = exceptionFactory; _storageProvider = storageProvider(); _helper = helper; } + public Localizer T { get; set; } = NullLocalizer.Instance; public ILogger Logger { get; set; } = NullLogger.Instance; - public IMediaStorageProvider StorageProvider - { - get => _storageProvider; - } + public IMediaStorageProvider StorageProvider => _storageProvider; + + public bool ImagePostProcessingEnabled { get; set; } = true; #region Query public int CountFiles(MediaSearchQuery query) { Guard.NotNull(query, nameof(query)); - + var q = _searcher.PrepareQuery(query, MediaLoadFlags.None); var count = q.Count(); return count; @@ -90,7 +101,7 @@ public FileCountResult CountFilesGrouped(MediaFilesFilter filter) Guard.NotNull(filter, nameof(filter)); // Base db query - var q = _searcher.PrepareFilterQuery(filter); + var q = _searcher.ApplyFilterQuery(filter); // Get ids of untrackable folders, 'cause no orphan check can be made for them. var untrackableFolderIds = _folderService.GetRootNode() @@ -100,15 +111,15 @@ public FileCountResult CountFilesGrouped(MediaFilesFilter filter) // Determine counts var result = (from f in q - group f by 1 into g - select new FileCountResult - { - Total = g.Count(), - Trash = g.Count(x => x.Deleted), - Unassigned = g.Count(x => !x.Deleted && x.FolderId == null), - Transient = g.Count(x => !x.Deleted && x.IsTransient == true), - Orphan = g.Count(x => !x.Deleted && x.FolderId > 0 && !untrackableFolderIds.Contains(x.FolderId.Value) && !x.Tracks.Any()) - }).FirstOrDefault() ?? new FileCountResult(); + group f by 1 into g + select new FileCountResult + { + Total = g.Count(), + Trash = g.Count(x => x.Deleted), + Unassigned = g.Count(x => !x.Deleted && x.FolderId == null), + Transient = g.Count(x => !x.Deleted && x.IsTransient == true), + Orphan = g.Count(x => !x.Deleted && x.FolderId > 0 && !untrackableFolderIds.Contains(x.FolderId.Value) && !x.Tracks.Any()) + }).FirstOrDefault() ?? new FileCountResult(); if (result.Total == 0) { @@ -118,9 +129,9 @@ group f by 1 into g // Determine file count for each folder var byFolders = from f in q - where f.FolderId > 0 && !f.Deleted - group f by f.FolderId.Value into grp - select grp; + where f.FolderId > 0 && !f.Deleted + group f by f.FolderId.Value into grp + select grp; result.Folders = byFolders .Select(grp => new { FolderId = grp.Key, Count = grp.Count() }) @@ -171,11 +182,11 @@ public bool FileExists(string path) { Guard.NotEmpty(path, nameof(path)); - if (_helper.TokenizePath(path, out var tokens)) + if (_helper.TokenizePath(path, false, out var tokens)) { return _fileRepo.Table.Any(x => x.FolderId == tokens.Folder.Id && x.Name == tokens.FileName); } - + return false; } @@ -183,11 +194,9 @@ public MediaFileInfo GetFileByPath(string path, MediaLoadFlags flags = MediaLoad { Guard.NotEmpty(path, nameof(path)); - if (_helper.TokenizePath(path, out var tokens)) + if (_helper.TokenizePath(path, false, out var tokens)) { - var table = _fileRepo.Table; - - // TODO: (mm) LoadFlags > Blob | Tags | Tracks + var table = _searcher.ApplyLoadFlags(_fileRepo.Table, flags); var entity = table.FirstOrDefault(x => x.FolderId == tokens.Folder.Id && x.Name == tokens.FileName); if (entity != null) @@ -249,11 +258,11 @@ public bool CheckUniqueFileName(string path, out string newPath) { Guard.NotEmpty(path, nameof(path)); - // TODO: (mm) throw when path is not a file path + // TODO: (mm) (mc) throw when path is not a file path newPath = null; - if (!_helper.TokenizePath(path, out var pathData)) + if (!_helper.TokenizePath(path, false, out var pathData)) { return false; } @@ -278,13 +287,14 @@ protected internal virtual bool CheckUniqueFileName(MediaPathData pathData) var q = new MediaSearchQuery { FolderId = pathData.Folder.Id, - Term = string.Concat(pathData.FileTitle, "*.", pathData.Extension) + Term = string.Concat(pathData.FileTitle, "*.", pathData.Extension), + Deleted = null }; var query = _searcher.PrepareQuery(q, MediaLoadFlags.AsNoTracking).Select(x => x.Name); var files = new HashSet(query.ToList(), StringComparer.CurrentCultureIgnoreCase); - if (InternalCheckUniqueFileName(pathData.FileTitle, pathData.Extension, files, out var uniqueName)) + if (_helper.CheckUniqueFileName(pathData.FileTitle, pathData.Extension, files, out var uniqueName)) { pathData.FileName = uniqueName; return true; @@ -293,41 +303,17 @@ protected internal virtual bool CheckUniqueFileName(MediaPathData pathData) return false; } - private bool InternalCheckUniqueFileName(string title, string ext, HashSet fileNames, out string uniqueName) - { - uniqueName = null; - - if (fileNames.Count == 0) - { - return false; - } - - int i = 1; - while (true) - { - var test = string.Concat(title, "-", i, ".", ext.TrimStart('.')); - if (!fileNames.Contains(test)) - { - // Found our gap - uniqueName = test; - return true; - } - - i++; - } - } - public string CombinePaths(params string[] paths) { return FolderService.NormalizePath(Path.Combine(paths), false); } - public bool FindEqualFile(Stream source, IEnumerable files, bool leaveOpen, out int equalFileId) + public bool FindEqualFile(Stream source, IEnumerable files, bool leaveOpen, out MediaFile equalFile) { Guard.NotNull(source, nameof(source)); Guard.NotNull(files, nameof(files)); - equalFileId = 0; + equalFile = null; try { @@ -339,7 +325,7 @@ public bool FindEqualFile(Stream source, IEnumerable files, bool leav { if (source.ContentsEqual(other, true)) { - equalFileId = file.Id; + equalFile = file; return true; } } @@ -362,29 +348,71 @@ public bool FindEqualFile(Stream source, IEnumerable files, bool leav #endregion - #region Create/Update/Delete + #region Create/Update/Delete/Replace + + public MediaFileInfo ReplaceFile(MediaFile file, Stream inStream, string newFileName) + { + Guard.NotNull(file, nameof(file)); + Guard.NotNull(inStream, nameof(inStream)); + Guard.NotEmpty(newFileName, nameof(newFileName)); + + var fileInfo = ConvertMediaFile(file); + var pathData = CreatePathData(fileInfo.Path); + pathData.FileName = newFileName; + + var storageItem = ProcessFile(ref file, pathData, inStream, false, DuplicateFileHandling.Overwrite, MimeValidationType.MediaTypeMustMatch); + + using (var scope = new DbContextScope(_fileRepo.Context, autoCommit: false)) + { + try + { + _storageProvider.Save(file, storageItem); + scope.Commit(); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + + return fileInfo; + } + + public async Task ReplaceFileAsync(MediaFile file, Stream inStream, string newFileName) + { + Guard.NotNull(file, nameof(file)); + Guard.NotNull(inStream, nameof(inStream)); + Guard.NotEmpty(newFileName, nameof(newFileName)); + + var fileInfo = ConvertMediaFile(file); + var pathData = CreatePathData(fileInfo.Path); + pathData.FileName = newFileName; - //public MediaFileInfo CreateFile(string path) - //{ - // if (_mediaHelper.TokenizePath(path, out var tokens)) - // { - // return SaveFile( - // null, // album - // new MediaFile { Name = tokens.FileName, FolderId = tokens.Folder.Id }, - // null, // stream - // false); - // } + var storageItem = ProcessFile(ref file, pathData, inStream, false, DuplicateFileHandling.Overwrite, MimeValidationType.MediaTypeMustMatch); + + using (var scope = new DbContextScope(_fileRepo.Context, autoCommit: false)) + { + try + { + await _storageProvider.SaveAsync(file, storageItem); + await scope.CommitAsync(); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } - // // TODO: (mm) throw - // throw new Exception(); - //} + return fileInfo; + } public MediaFileInfo SaveFile(string path, Stream stream, bool isTransient = true, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError) { var pathData = CreatePathData(path); var file = _fileRepo.Table.FirstOrDefault(x => x.Name == pathData.FileName && x.FolderId == pathData.Folder.Id); - stream = ProcessFile(ref file, pathData, stream, isTransient, dupeFileHandling); + var isNewFile = file == null; + var storageItem = ProcessFile(ref file, pathData, stream, isTransient, dupeFileHandling); using (var scope = new DbContextScope(_fileRepo.Context, autoCommit: false)) { @@ -394,8 +422,22 @@ public MediaFileInfo SaveFile(string path, Stream stream, bool isTransient = tru scope.Commit(); } - _storageProvider.Save(file, stream); - scope.Commit(); + try + { + _storageProvider.Save(file, storageItem); + scope.Commit(); + } + catch (Exception ex) + { + if (isNewFile) + { + // New file's metadata should be removed on storage save failure immediately + DeleteFile(file, true, true); + scope.Commit(); + } + + Logger.Error(ex); + } } return ConvertMediaFile(file, pathData.Folder); @@ -407,7 +449,7 @@ public async Task SaveFileAsync(string path, Stream stream, bool var file = await _fileRepo.Table.FirstOrDefaultAsync(x => x.Name == pathData.FileName && x.FolderId == pathData.Folder.Id); var isNewFile = file == null; - stream = ProcessFile(ref file, pathData, stream, isTransient, dupeFileHandling); + var storageItem = ProcessFile(ref file, pathData, stream, isTransient, dupeFileHandling); using (var scope = new DbContextScope(_fileRepo.Context, autoCommit: false)) { @@ -419,23 +461,18 @@ public async Task SaveFileAsync(string path, Stream stream, bool try { - await _storageProvider.SaveAsync(file, stream); + await _storageProvider.SaveAsync(file, storageItem); await scope.CommitAsync(); } catch (Exception ex) { - if (isNewFile) + if (isNewFile) { // New file's metadata should be removed on storage save failure immediately - if (file.MediaStorage != null) - { - file.MediaStorage.Data = null; - file.MediaStorage = null; - } - _fileRepo.Delete(file); + DeleteFile(file, true, true); await scope.CommitAsync(); } - + Logger.Error(ex); } } @@ -443,10 +480,11 @@ public async Task SaveFileAsync(string path, Stream stream, bool return ConvertMediaFile(file, pathData.Folder); } - public void DeleteFile(MediaFile file, bool permanent) + public void DeleteFile(MediaFile file, bool permanent, bool force = false) { Guard.NotNull(file, nameof(file)); + // Delete thumb _imageCache.Delete(file); if (!permanent) @@ -456,11 +494,34 @@ public void DeleteFile(MediaFile file, bool permanent) } else { - // Delete from storage - _storageProvider.Remove(file); + try + { + if (!force && file.Tracks.Any()) + { + throw _exceptionFactory.DeleteTrackedFile(file, null); + } + + // Delete entity + _fileRepo.Delete(file); - // Delete entity - _fileRepo.Delete(file); + // Delete from storage + _storageProvider.Remove(file); + } + catch (DbUpdateException ex) + { + if (ex.IsUniquenessViolationException()) + { + throw _exceptionFactory.DeleteTrackedFile(file, ex); + } + else + { + throw; + } + } + catch (Exception) + { + throw; + } } } @@ -468,31 +529,34 @@ protected MediaPathData CreatePathData(string path) { Guard.NotEmpty(path, nameof(path)); - if (!_helper.TokenizePath(path, out var pathData)) + if (!_helper.TokenizePath(path, true, out var pathData)) { - throw new ArgumentException("Invalid path '{0}'. Valid path expression is: {{albumName}}[/subfolders]/{{fileName}}.{{extension}}".FormatInvariant(path), nameof(path)); + throw new ArgumentException(T("Admin.Media.Exception.InvalidPathExample", path), nameof(path)); } if (pathData.Extension.IsEmpty()) { - throw new ArgumentException($"Cannot process files without file extension. Path: {path}", nameof(path)); + throw new ArgumentException(T("Admin.Media.Exception.FileExtension", path), nameof(path)); } return pathData; } - protected Stream ProcessFile( - ref MediaFile file, - MediaPathData pathData, - Stream inStream, - bool isTransient = true, - DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError) + protected MediaStorageItem ProcessFile( + ref MediaFile file, + MediaPathData pathData, + Stream inStream, + bool isTransient = true, + DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError, + MimeValidationType mediaValidationType = MimeValidationType.MimeTypeMustMatch) { if (file != null) { if (dupeFileHandling == DuplicateFileHandling.ThrowError) { - throw new DuplicateMediaFileException(pathData.FullPath, ConvertMediaFile(file)); + var fullPath = pathData.FullPath; + CheckUniqueFileName(pathData); + throw _exceptionFactory.DuplicateFile(fullPath, ConvertMediaFile(file, pathData.Folder), pathData.FullPath); } else if (dupeFileHandling == DuplicateFileHandling.Rename) { @@ -503,9 +567,21 @@ protected Stream ProcessFile( } } - if (file != null) + if (file != null && mediaValidationType != MimeValidationType.NoValidation) { - ValidateMimeTypes("Save", file.MimeType, pathData.MimeType); + if (mediaValidationType == MimeValidationType.MimeTypeMustMatch) + { + ValidateMimeTypes("Save", file.MimeType, pathData.MimeType); + } + else if (mediaValidationType == MimeValidationType.MediaTypeMustMatch) + { + ValidateMediaTypes("Save", _typeResolver.Resolve(pathData.Extension), file.MediaType); + } + + // Restore file if soft-deleted + file.Deleted = false; + + // Delete thumbnail _imageCache.Delete(file); } @@ -515,7 +591,17 @@ protected Stream ProcessFile( FolderId = pathData.Node.Value.Id }; - // TODO: (mm) Get pretty SeName + // Untrackable folders can never contain transient files. + if (!pathData.Folder.CanDetectTracks) + { + file.IsTransient = false; + } + + var name = pathData.FileName; + if (name != pathData.FileName) + { + pathData.FileName = name; + } file.Name = pathData.FileName; file.Extension = pathData.Extension; @@ -526,35 +612,40 @@ protected Stream ProcessFile( } // Process image - if (inStream != null && inStream.Length > 0 && file.MediaType == MediaType.Image && ProcessImage(file, inStream, out var outStream, out var size)) + if (inStream != null && inStream.Length > 0 && file.MediaType == MediaType.Image && ProcessImage(file, inStream, out var outImage)) { - inStream = outStream; - file.Size = (int)outStream.Length; - file.Width = size.Width; - file.Height = size.Height; - file.PixelSize = size.Width * size.Height; + file.Width = outImage.Size.Width; + file.Height = outImage.Size.Height; + file.PixelSize = outImage.Size.Width * outImage.Size.Height; + + return MediaStorageItem.FromImage(outImage); } else { file.RefreshMetadata(inStream); - } - return inStream; + return MediaStorageItem.FromStream(inStream); + } } - protected bool ProcessImage(MediaFile file, Stream inStream, out Stream outStream, out Size size) + protected bool ProcessImage(MediaFile file, Stream inStream, out IImage outImage) { - outStream = null; - size = Size.Empty; + outImage = null; - var originalSize = ImageHeader.GetDimensions(inStream, file.MimeType); + var originalSize = Size.Empty; + var format = _imageProcessor.Factory.GetImageFormat(file.Extension) ?? new UnsupportedImageFormat(file.MimeType, file.Extension); - if (!_imageProcessor.IsSupportedImage(file.Extension)) + try { - outStream = inStream; - size = originalSize; // e.g.: image/svg+xml + originalSize = ImageHeader.GetDimensions(inStream, file.MimeType); + } + catch { } + + if (format is UnsupportedImageFormat) + { + outImage = new ImageWrapper(inStream, originalSize, format); return true; - } + } var maxSize = _mediaSettings.MaximumImageSize; @@ -563,26 +654,16 @@ protected bool ProcessImage(MediaFile file, Stream inStream, out Stream outStrea Quality = _mediaSettings.DefaultImageQuality, Format = file.Extension, DisposeSource = true, + ExecutePostProcessor = ImagePostProcessingEnabled, IsValidationMode = true }; if (originalSize.IsEmpty || (originalSize.Height <= maxSize && originalSize.Width <= maxSize)) { // Give subscribers the chance to (pre)-process - var evt = new ImageUploadValidatedEvent(query, originalSize); + var evt = new ImageUploadedEvent(query, originalSize); _eventPublisher.Publish(evt); - - if (evt.ResultStream != null) - { - outStream = evt.ResultStream; - // Maybe subscriber forgot to set this, so check - size = evt.ResultSize.IsEmpty ? originalSize : evt.ResultSize; - } - else - { - outStream = inStream; - size = originalSize; - } + outImage = evt.ResultImage ?? new ImageWrapper(inStream, originalSize, format); return true; } @@ -591,8 +672,7 @@ protected bool ProcessImage(MediaFile file, Stream inStream, out Stream outStrea using (var result = _imageProcessor.ProcessImage(query, false)) { - size = new Size(result.Width, result.Height); - outStream = result.OutputStream; + outImage = result.Image; return true; } } @@ -601,30 +681,39 @@ protected bool ProcessImage(MediaFile file, Stream inStream, out Stream outStrea #region Copy & Move - public MediaFileInfo CopyFile(MediaFile file, string destinationFileName, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError) + public FileOperationResult CopyFile(MediaFileInfo mediaFile, string destinationFileName, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError) { - Guard.NotNull(file, nameof(file)); + Guard.NotNull(mediaFile, nameof(mediaFile)); Guard.NotEmpty(destinationFileName, nameof(destinationFileName)); - var destPathData = CreateDestinationPathData(file, destinationFileName); + var destPathData = CreateDestinationPathData(mediaFile.File, destinationFileName); var destFileName = destPathData.FileName; var destFolderId = destPathData.Folder.Id; - var dupe = file.FolderId == destPathData.Folder.Id + var dupe = mediaFile.FolderId == destPathData.Folder.Id // Source folder equals dest folder, so same file - ? file + ? mediaFile.File // Another dest folder, check for duplicate by file name : _fileRepo.Table.FirstOrDefault(x => x.Name == destFileName && x.FolderId == destFolderId); var copy = InternalCopyFile( - file, + mediaFile.File, destPathData, true /* copyData */, (DuplicateEntryHandling)((int)dupeFileHandling), () => dupe, - p => CheckUniqueFileName(p)); + p => CheckUniqueFileName(p), + out var isDupe); - return ConvertMediaFile(copy, destPathData.Folder); + return new FileOperationResult + { + Operation = "copy", + DuplicateFileHandling = dupeFileHandling, + SourceFile = mediaFile, + DestinationFile = ConvertMediaFile(copy, destPathData.Folder), + IsDuplicate = isDupe, + UniquePath = isDupe ? destPathData.FullPath : (string)null + }; } private MediaFile InternalCopyFile( @@ -633,35 +722,38 @@ private MediaFile InternalCopyFile( bool copyData, DuplicateEntryHandling dupeEntryHandling, Func dupeFileSelector, - Action uniqueFileNameChecker) + Action uniqueFileNameChecker, + out bool isDupe) { // Find dupe and handle + isDupe = false; var dupe = dupeFileSelector(); if (dupe != null) { switch (dupeEntryHandling) { case DuplicateEntryHandling.Skip: - return null; + isDupe = true; + uniqueFileNameChecker(destPathData); + return dupe; case DuplicateEntryHandling.ThrowError: - throw new DuplicateMediaFileException(destPathData.FullPath, ConvertMediaFile(dupe)); + var fullPath = destPathData.FullPath; + uniqueFileNameChecker(destPathData); + throw _exceptionFactory.DuplicateFile(fullPath, ConvertMediaFile(dupe), destPathData.FullPath); case DuplicateEntryHandling.Rename: uniqueFileNameChecker(destPathData); - if (dupe == file) - { - dupe = null; - } + dupe = null; break; case DuplicateEntryHandling.Overwrite: if (file.FolderId == destPathData.Folder.Id) { - throw new IOException("Overwrite operation is not possible if source and destination folders are identical."); + throw new IOException(T("Admin.Media.Exception.Overwrite")); } break; } } - var isOverwrite = dupe != null; + isDupe = dupe != null; var copy = dupe ?? new MediaFile(); // Simple clone @@ -670,6 +762,9 @@ private MediaFile InternalCopyFile( // Set folder id copy.FolderId = destPathData.Folder.Id; + // A copied file cannot stay in deleted state + copy.Deleted = false; + // Set name stuff if (!copy.Name.IsCaseInsensitiveEqual(destPathData.FileName)) { @@ -679,7 +774,7 @@ private MediaFile InternalCopyFile( } // Save to DB - if (isOverwrite) + if (isDupe) { _fileRepo.Update(copy); } @@ -699,20 +794,60 @@ private MediaFile InternalCopyFile( private void InternalCopyFileData(MediaFile file, MediaFile copy) { - // TODO: (mm) copy tags - // TODO: (mm) Copy localized values (Alt, Title) - _storageProvider.Save(copy, _storageProvider.OpenRead(file)); + _storageProvider.Save(copy, MediaStorageItem.FromStream(_storageProvider.OpenRead(file))); + _imageCache.Delete(copy); + + using (var scope = new DbContextScope(_fileRepo.Context, autoCommit: false)) + { + // Tags. + _fileRepo.Context.LoadCollection(file, (MediaFile x) => x.Tags); + + var existingTagsIds = copy.Tags.Select(x => x.Id).ToList(); + + foreach (var tag in file.Tags) + { + if (!existingTagsIds.Contains(tag.Id)) + { + copy.Tags.Add(tag); + existingTagsIds.Add(tag.Id); + } + } + + // Localized values. + var languages = _languageService.GetAllLanguages(true); + + foreach (var language in languages) + { + var title = file.GetLocalized(x => x.Title, language.Id, false, false).Value; + if (title.HasValue()) + { + _localizedEntityService.SaveLocalizedValue(copy, x => x.Title, title, language.Id); + } + + var alt = file.GetLocalized(x => x.Alt, language.Id, false, false).Value; + if (alt.HasValue()) + { + _localizedEntityService.SaveLocalizedValue(copy, x => x.Alt, alt, language.Id); + } + } + + scope.Commit(); + _fileRepo.Context.DetachEntities(); + } } - public MediaFileInfo MoveFile(MediaFile file, string destinationFileName) + public MediaFileInfo MoveFile(MediaFile file, string destinationFileName, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError) { - if (ValidateMoveOperation(file, destinationFileName, out var nameChanged, out var destPathData)) + if (ValidateMoveOperation(file, destinationFileName, dupeFileHandling, out var nameChanged, out var destPathData)) { file.FolderId = destPathData.Folder.Id; + // A moved file cannot stay in deleted state + file.Deleted = false; + if (nameChanged) { - var title = destPathData.FileTitle.ToValidFileName(); + var title = destPathData.FileTitle; var ext = destPathData.Extension.ToLower(); file.Name = title + "." + ext; @@ -733,9 +868,10 @@ public MediaFileInfo MoveFile(MediaFile file, string destinationFileName) } private bool ValidateMoveOperation( - MediaFile file, - string destinationFileName, - out bool nameChanged, + MediaFile file, + string destinationFileName, + DuplicateFileHandling dupeFileHandling, + out bool nameChanged, out MediaPathData destPathData) { Guard.NotNull(file, nameof(file)); @@ -746,28 +882,61 @@ private bool ValidateMoveOperation( var destFileName = destPathData.FileName; var destFolderId = destPathData.Folder.Id; var folderChanged = destFolderId != file.FolderId; + var shouldRestore = false; + nameChanged = !destFileName.IsCaseInsensitiveEqual(file.Name); if (file.FolderId.HasValue && folderChanged) { // When "Move" operation: ensure file stays in source album. - ValidateAlbums("Move", destFolderId, file.FolderId.Value); + ValidateAlbums("Move", file.FolderId.Value, destFolderId); } if (nameChanged) { - // Ensure bot MIME types are equal + // Ensure both MIME types are equal ValidateMimeTypes("Move", file.MimeType, destPathData.MimeType); } // Check whether destination file exists - var dupe = _fileRepo.Table.FirstOrDefault(x => x.Name == destFileName && x.FolderId == destFolderId); - if (dupe != null) + if (!folderChanged && file.Deleted) { - throw new DuplicateMediaFileException(destPathData.FullPath, ConvertMediaFile(dupe)); + // Special case where a file is moved from trash to its origin location. + // In this case the file should just be restored without any dupe check. + shouldRestore = true; + } + else + { + var dupe = _fileRepo.Table.FirstOrDefault(x => x.Name == destFileName && x.FolderId == destFolderId); + if (dupe != null) + { + if (!folderChanged) + { + throw _exceptionFactory.IdenticalPaths(ConvertMediaFile(file, destPathData.Folder)); + } + + switch (dupeFileHandling) + { + case DuplicateFileHandling.ThrowError: + var fullPath = destPathData.FullPath; + _helper.CheckUniqueFileName(destPathData.FileTitle, destPathData.Extension, dupe.Name, out _); + throw _exceptionFactory.DuplicateFile(fullPath, ConvertMediaFile(dupe, destPathData.Folder), destPathData.FullPath); + case DuplicateFileHandling.Rename: + if (_helper.CheckUniqueFileName(destPathData.FileTitle, destPathData.Extension, dupe.Name, out var uniqueName)) + { + nameChanged = true; + destPathData.FileName = uniqueName; + return true; + } + break; + case DuplicateFileHandling.Overwrite: + DeleteFile(dupe, true); + break; + } + } } - return folderChanged || nameChanged; + return folderChanged || nameChanged || shouldRestore; } #endregion @@ -784,15 +953,21 @@ public string GetUrl(MediaFileInfo file, ProcessImageQuery imageQuery, string ho #region Utils + [MethodImpl(MethodImplOptions.AggressiveInlining)] public MediaFileInfo ConvertMediaFile(MediaFile file) { - var folder = _folderService.FindNode(file)?.Value; - return new MediaFileInfo(file, _storageProvider, _urlGenerator, folder?.Path); + return ConvertMediaFile(file, _folderService.FindNode(file)?.Value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected MediaFileInfo ConvertMediaFile(MediaFile file, MediaFolderNode folder) { - return new MediaFileInfo(file, _storageProvider, _urlGenerator, folder?.Path); + var mediaFile = new MediaFileInfo(file, _storageProvider, _urlGenerator, folder?.Path) + { + ThumbSize = _mediaSettings.ProductThumbPictureSize + }; + + return mediaFile; } private void EnsureMetadataResolved(MediaFile file, bool saveOnResolve) @@ -872,19 +1047,21 @@ private static void MapMediaFile(MediaFile from, MediaFile to) private MediaPathData CreateDestinationPathData(MediaFile file, string destinationFileName) { - if (!_helper.TokenizePath(destinationFileName, out var pathData)) + if (!_helper.TokenizePath(destinationFileName, true, out var pathData)) { // Passed path is NOT a path, but a file name if (IsPath(destinationFileName)) { // ...but file name includes path chars, which is not allowed - throw new ArgumentException("Invalid path '{0}'.".FormatInvariant(Path.GetDirectoryName(destinationFileName)), nameof(destinationFileName)); + throw new ArgumentException( + T("Admin.Media.Exception.InvalidPath", Path.GetDirectoryName(destinationFileName)), + nameof(destinationFileName)); } if (file.FolderId == null) { - throw new NotSupportedException("Cannot operate on files without folder assignment."); + throw new NotSupportedException(T("Admin.Media.Exception.FolderAssignment")); } pathData = new MediaPathData(_folderService.GetNodeById(file.FolderId.Value), destinationFileName); @@ -893,11 +1070,21 @@ private MediaPathData CreateDestinationPathData(MediaFile file, string destinati return pathData; } - private static void ValidateMimeTypes(string operation, string mime1, string mime2) + private void ValidateMimeTypes(string operation, string mime1, string mime2) { if (!mime1.Equals(mime2, StringComparison.OrdinalIgnoreCase)) { - throw new NotSupportedException($"The file operation '{operation}' does not allow MIME type switching. Source mime: ${mime1}, target mime: ${mime2}."); + // TODO: (mm) Create this and all other generic exceptions by MediaExceptionFactory + throw new NotSupportedException(T("Admin.Media.Exception.MimeType", operation, mime1, mime2)); + } + } + + private void ValidateMediaTypes(string operation, string mime1, string mime2) + { + if (mime1 != mime2) + { + // TODO: (mm) Create this and all other generic exceptions by MediaExceptionFactory + throw new NotSupportedException(T("Admin.Media.Exception.MediaType", operation, mime1, mime2)); } } @@ -905,8 +1092,8 @@ private void ValidateAlbums(string operation, int folderId1, int folderId2) { if (!_folderService.AreInSameAlbum(folderId1, folderId2)) { - throw new NotSameAlbumException( - _folderService.GetNodeById(folderId1).Value.Path, + throw _exceptionFactory.NotSameAlbum( + _folderService.GetNodeById(folderId1).Value.Path, _folderService.GetNodeById(folderId2).Value.Path); } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaServiceFileSystemAdapter.cs b/src/Libraries/SmartStore.Services/Media/MediaServiceFileSystemAdapter.cs similarity index 87% rename from src/Libraries/SmartStore.Services/Media/v4/MediaServiceFileSystemAdapter.cs rename to src/Libraries/SmartStore.Services/Media/MediaServiceFileSystemAdapter.cs index 21efd49c01..c19f699cc1 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaServiceFileSystemAdapter.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaServiceFileSystemAdapter.cs @@ -18,18 +18,21 @@ public partial class MediaServiceFileSystemAdapter : IMediaFileSystem private readonly MediaHelper _mediaHelper; private readonly IFolderService _folderService; private readonly IMediaStorageProvider _storageProvider; + private readonly MediaExceptionFactory _exceptionFactory; private readonly string _mediaRootPath; public MediaServiceFileSystemAdapter( - IMediaService mediaService, + IMediaService mediaService, IFolderService folderService, - MediaHelper mediaHelper) + MediaHelper mediaHelper, + MediaExceptionFactory exceptionFactory) { _mediaService = mediaService; _folderService = folderService; _mediaHelper = mediaHelper; _storageProvider = mediaService.StorageProvider; - _mediaRootPath = "media4/"; // MediaFileSystem.GetMediaPublicPath(); // TODO: (mm) switch + _exceptionFactory = exceptionFactory; + _mediaRootPath = MediaFileSystem.GetMediaPublicPath(); } protected string Fix(string path) @@ -39,10 +42,7 @@ protected string Fix(string path) #region IFileSystem - public bool IsCloudStorage - { - get => _storageProvider.IsCloudStorage; - } + public bool IsCloudStorage => _storageProvider.IsCloudStorage; public string Root => string.Empty; @@ -70,13 +70,13 @@ public string GetStoragePath(string url) // Is a folder path, no need to strip off public URL stuff. return url; } - + // Strip off root, e.g. "media/" var path = url.Substring(_mediaRootPath.Length); // Strip off media id from path, e.g. "123/" var firstSlashIndex = path.IndexOf('/'); - + return path.Substring(firstSlashIndex); } @@ -90,7 +90,7 @@ public IEnumerable ListFiles(string path) var node = _folderService.GetNodeByPath(path); if (node == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } var query = new MediaSearchQuery @@ -106,7 +106,7 @@ public IEnumerable ListFolders(string path) var node = _folderService.GetNodeByPath(path); if (node == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } return node.Children.Select(x => new MediaFolderInfo(x)); @@ -119,7 +119,7 @@ public long CountFiles(string path, string pattern, Func predicate var node = _folderService.GetNodeByPath(path); if (node == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } var query = new MediaSearchQuery @@ -141,9 +141,9 @@ public IEnumerable SearchFiles(string path, string pattern, bool deep = var node = _folderService.GetNodeByPath(path); if (node == null) { - throw new MediaFolderNotFoundException(path); + throw _exceptionFactory.FolderNotFound(path); } - + var query = new MediaSearchQuery { FolderId = node.Value.Id, @@ -198,15 +198,14 @@ public IFile GetFile(string path) var file = _mediaService.GetFileByPath(path); if (file == null) { - var mediaFile = new MediaFile - { + var mediaFile = new MediaFile + { Name = Path.GetFileName(path), Extension = Path.GetExtension(path).TrimStart('.'), MimeType = MimeTypes.MapNameToMimeType(path), FolderId = _folderService.GetNodeByPath(Fix(Path.GetDirectoryName(path)))?.Value.Id }; - _mediaService.ConvertMediaFile(mediaFile); - + file = _mediaService.ConvertMediaFile(mediaFile); } @@ -226,9 +225,9 @@ public IFolder GetFolder(string path) public IFolder GetFolderForFile(string path) { - if (!_mediaHelper.TokenizePath(path, out var pathData)) + if (!_mediaHelper.TokenizePath(path, false, out var pathData)) { - throw new MediaFolderNotFoundException(Fix(Path.GetDirectoryName(path))); + throw _exceptionFactory.FolderNotFound(Fix(Path.GetDirectoryName(path))); } return new MediaFolderInfo(pathData.Node); @@ -244,14 +243,14 @@ public void CopyFile(string path, string newPath, bool overwrite = false) Guard.NotEmpty(path, nameof(path)); Guard.NotEmpty(newPath, nameof(newPath)); - var sourceFile = (MediaFile)_mediaService.GetFileByPath(path); + var sourceFile = _mediaService.GetFileByPath(path); if (sourceFile == null) { - throw new MediaFileNotFoundException(path); + throw _exceptionFactory.FileNotFound(path); } _mediaService.CopyFile( - sourceFile, + sourceFile, newPath, overwrite ? DuplicateFileHandling.Overwrite : DuplicateFileHandling.ThrowError); } @@ -264,7 +263,7 @@ public void RenameFile(string path, string newPath) var sourceFile = (MediaFile)_mediaService.GetFileByPath(path); if (sourceFile == null) { - throw new MediaFileNotFoundException(path); + throw _exceptionFactory.FileNotFound(path); } _mediaService.MoveFile(sourceFile, newPath); @@ -277,8 +276,8 @@ public void RenameFolder(string path, string newPath) public void CopyFolder(string path, string destinationPath, bool overwrite = true) { - var dupeEntryHandling = overwrite - ? DuplicateEntryHandling.Overwrite + var dupeEntryHandling = overwrite + ? DuplicateEntryHandling.Overwrite : DuplicateEntryHandling.ThrowError; _mediaService.CopyFolder(path, destinationPath, dupeEntryHandling); diff --git a/src/Libraries/SmartStore.Services/Media/MediaTypes/IMediaTypeResolver.cs b/src/Libraries/SmartStore.Services/Media/MediaTypes/IMediaTypeResolver.cs new file mode 100644 index 0000000000..eab1084e47 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/MediaTypes/IMediaTypeResolver.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; + +namespace SmartStore.Services.Media +{ + public interface IMediaTypeResolver + { + /// + /// Resolves the conceptual media type for a given file extension or mime type. + /// + /// The file extension (with dot). + /// Used to map to an extension if parameter is null or empty. + /// The media type or as fallback. + MediaType Resolve(string extension, string mimeType = null); + + /// + /// Parses and expands a given type filter, returning a distinct list of all suitable file extensions. + /// If is empty or '*', all supported file extensions are returned. + /// + /// A comma separated list of either file extensions and/or media type names, e.g.: "image,.mp4,audio,.pdf". Extensions must start with a dot. + /// All suitable file extensions. + IEnumerable ParseTypeFilter(string typeFilter); + + /// + /// Parses and expands a given type filter, returning a distinct list of all suitable file extensions. + /// + /// A list of either file extensions and/or media type names, e.g.: [ "image", ".mp4", "audio", ".pdf" ]. Extensions must start with a dot. + /// All suitable file extensions. + IEnumerable ParseTypeFilter(string[] typeFilter); + + /// + /// Get the cached "file extension to media type name" map. + /// "Key" is the dot-less file extension and "Value" is the corresponding media type name. + /// + /// The cached dictionary + IReadOnlyDictionary GetExtensionMediaTypeMap(); + } + + public static class IMediaTypeResolverExtensions + { + public static MediaType Resolve(this IMediaTypeResolver resolver, MediaFile file) + { + Guard.NotNull(file, nameof(file)); + return resolver.Resolve(file.Extension, file.MimeType); + } + + //public static MediaType Resolve(this IMediaTypeResolver resolver, IFile file) + //{ + // Guard.NotNull(file, nameof(file)); + // return resolver.Resolve(file.Extension?.TrimStart('.')); + //} + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/MediaType.cs b/src/Libraries/SmartStore.Services/Media/MediaTypes/MediaType.cs similarity index 81% rename from src/Libraries/SmartStore.Services/Media/v4/MediaTypes/MediaType.cs rename to src/Libraries/SmartStore.Services/Media/MediaTypes/MediaType.cs index d8111bd05d..ed885caf94 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/MediaType.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaTypes/MediaType.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore.Services.Media { @@ -10,13 +8,14 @@ public class MediaType : IEquatable { private readonly static IDictionary _defaultExtensionsMap = new Dictionary { - ["image"] = new[] { "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "tiff", "tif", "eps" }, + ["image"] = new[] { "png", "jpg", "jpeg", "jfif", "gif", "webp", "bmp", "svg", "ico" }, ["video"] = new[] { "mp4", "m4v", "mkv", "wmv", "avi", "asf", "mpg", "mpeg", "webm", "flv", "ogv", "mov", "3gp" }, ["audio"] = new[] { "mp3", "wav", "wma", "aac", "flac", "oga", "wav", "m4a", "ogg" }, ["document"] = new[] { "pdf", "doc", "docx", "ppt", "pptx", "pps", "ppsx", "docm", "odt", "ods", "dot", "dotx", "dotm", "psd", "xls", "xlsx", "rtf" }, - ["text"] = new[] { "txt", "xml", "csv", "htm", "html", "json", "css", "js" } + ["text"] = new[] { "txt", "xml", "csv", "htm", "html", "json", "css", "js" }, + ["bin"] = new string[0] }; - + private readonly static IDictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); public readonly static MediaType Image = new MediaType("image", _defaultExtensionsMap["image"]); @@ -24,12 +23,12 @@ public class MediaType : IEquatable public readonly static MediaType Audio = new MediaType("audio", _defaultExtensionsMap["audio"]); public readonly static MediaType Document = new MediaType("document", _defaultExtensionsMap["document"]); public readonly static MediaType Text = new MediaType("text", _defaultExtensionsMap["text"]); - public readonly static MediaType Binary = new MediaType("bin"); + public readonly static MediaType Binary = new MediaType("bin", _defaultExtensionsMap["bin"]); protected MediaType(string name, params string[] defaultExtensions) { Guard.NotEmpty(name, nameof(name)); - + Name = name; DefaultExtensions = defaultExtensions.OrderBy(x => x).ToArray(); @@ -40,11 +39,22 @@ protected MediaType(string name, params string[] defaultExtensions) public string[] DefaultExtensions { get; private set; } - public override string ToString() => Name; + public static IEnumerable AllExtensions => _defaultExtensionsMap.SelectMany(x => x.Value); - public static implicit operator string(MediaType obj) => obj?.Name; + public override string ToString() + { + return Name; + } - public static implicit operator MediaType(string obj) => GetMediaType(obj); + public static implicit operator string(MediaType obj) + { + return obj?.Name; + } + + public static implicit operator MediaType(string obj) + { + return GetMediaType(obj); + } internal static MediaType GetMediaType(string name) { diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/MediaTypeResolver.cs b/src/Libraries/SmartStore.Services/Media/MediaTypes/MediaTypeResolver.cs similarity index 69% rename from src/Libraries/SmartStore.Services/Media/v4/MediaTypes/MediaTypeResolver.cs rename to src/Libraries/SmartStore.Services/Media/MediaTypes/MediaTypeResolver.cs index f093d4a86b..04cfe4640a 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/MediaTypeResolver.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaTypes/MediaTypeResolver.cs @@ -1,12 +1,11 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Caching; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Configuration; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Events; using SmartStore.Core.IO; -using SmartStore.Core.Domain.Configuration; -using SmartStore.Core.Data.Hooks; using SmartStore.Utilities; namespace SmartStore.Services.Media @@ -14,7 +13,7 @@ namespace SmartStore.Services.Media public partial class MediaTypeResolver : DbSaveHook, IMediaTypeResolver { const string MapCacheKey = "media:exttypemap"; - + private readonly ICacheManager _cache; private readonly Lazy _mediaSettings; @@ -24,9 +23,10 @@ public partial class MediaTypeResolver : DbSaveHook, IMediaTypeResolver TypeHelper.NameOf(x => x.VideoTypes, true), TypeHelper.NameOf(x => x.AudioTypes, true), TypeHelper.NameOf(x => x.DocumentTypes, true), - TypeHelper.NameOf(x => x.TextTypes, true) + TypeHelper.NameOf(x => x.TextTypes, true), + TypeHelper.NameOf(x => x.BinTypes, true) }; - + public MediaTypeResolver(ICacheManager cache, Lazy mediaSettings) { _cache = cache; @@ -58,9 +58,43 @@ public MediaType Resolve(string extension, string mimeType = null) return (MediaType)mediaType ?? MediaType.Binary; } - private Dictionary GetExtensionMediaTypeMap() + public IEnumerable ParseTypeFilter(string typeFilter) + { + if (typeFilter.IsEmpty() || typeFilter == "*") + { + return GetExtensionMediaTypeMap().Keys; + } + else + { + return ParseTypeFilter(typeFilter.SplitSafe(",")); + } + } + + public IEnumerable ParseTypeFilter(string[] typeFilter) + { + if (typeFilter == null || typeFilter.Length == 0) + return Enumerable.Empty(); + + var extensions = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var filter in typeFilter.Select(x => x.Trim())) + { + if (filter[0] == '.') + { + extensions.Add(filter.Substring(1)); + } + else + { + extensions.AddRange(GetExtensionMediaTypeMap().Where(x => filter == "*" || x.Value == filter).Select(x => x.Key)); + } + } + + return extensions; + } + + public IReadOnlyDictionary GetExtensionMediaTypeMap() { - return _cache.Get(MapCacheKey, () => + return _cache.Get(MapCacheKey, () => { var mediaSettings = _mediaSettings.Value; var map = new Dictionary(); @@ -70,6 +104,7 @@ private Dictionary GetExtensionMediaTypeMap() AddExtensionsToMap(mediaSettings.AudioTypes, MediaType.Audio); AddExtensionsToMap(mediaSettings.DocumentTypes, MediaType.Document); AddExtensionsToMap(mediaSettings.TextTypes, MediaType.Text); + AddExtensionsToMap(mediaSettings.BinTypes, MediaType.Binary); return map; @@ -78,14 +113,14 @@ void AddExtensionsToMap(string extensions, MediaType forType) var arr = extensions.EmptyNull() .Replace(Environment.NewLine, " ") .ToLower() - .Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + .Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); if (arr.Length == 0) { arr = forType.DefaultExtensions; } - arr.Each(x => map[x] = forType.Name); + arr.Each(x => map[x.Trim()] = forType.Name); } }); } diff --git a/src/Libraries/SmartStore.Services/Media/MediaUrlGenerator.cs b/src/Libraries/SmartStore.Services/Media/MediaUrlGenerator.cs new file mode 100644 index 0000000000..21f31b28c4 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/MediaUrlGenerator.cs @@ -0,0 +1,121 @@ +using System.Globalization; +using System.Web; +using System.Web.Hosting; +using SmartStore.Core; +using SmartStore.Core.Domain.Media; +using SmartStore.Services.Configuration; +using SmartStore.Services.Media.Imaging; + +namespace SmartStore.Services.Media +{ + public partial class MediaUrlGenerator : IMediaUrlGenerator + { + private readonly HttpContextBase _httpContext; + + private readonly string _host; + private readonly string _appPath; + private readonly string _fallbackImageFileName; + + private static readonly string _processedImagesRootPath; + private static readonly string _fallbackImagesRootPath; + + static MediaUrlGenerator() + { + _processedImagesRootPath = MediaFileSystem.GetMediaPublicPath(); + _fallbackImagesRootPath = "content/images/"; + } + + public MediaUrlGenerator( + ISettingService settingService, + MediaSettings mediaSettings, + IStoreContext storeContext, + HttpContextBase httpContext) + { + _httpContext = httpContext; + + string appPath = "/"; + + if (HostingEnvironment.IsHosted) + { + appPath = HostingEnvironment.ApplicationVirtualPath.EmptyNull(); + + var cdn = storeContext.CurrentStore.ContentDeliveryNetwork; + if (cdn.HasValue() && !_httpContext.IsDebuggingEnabled && !_httpContext.Request.IsLocal) + { + _host = cdn; + } + else if (mediaSettings.AutoGenerateAbsoluteUrls) + { + var uri = httpContext.Request.Url; + _host = "//{0}{1}".FormatInvariant(uri.Authority, appPath); + } + else + { + _host = appPath; + } + } + + _host = _host.EmptyNull().EnsureEndsWith("/"); + _appPath = appPath.EnsureEndsWith("/"); + _fallbackImageFileName = settingService.GetSettingByKey("Media.DefaultImageName", "default-image.png"); + } + + public static string FallbackImagesRootPath => _fallbackImagesRootPath; + + public virtual string GenerateUrl( + MediaFileInfo file, + ProcessImageQuery imageQuery, + string host = null, + bool doFallback = true) + { + string path; + + // Build virtual path with pattern "media/{id}/{album}/{dir}/{NameWithExt}" + if (file?.Path != null) + { + path = _processedImagesRootPath + file.Id.ToString(CultureInfo.InvariantCulture) + "/" + file.Path; + } + else if (doFallback) + { + path = _processedImagesRootPath + "0/" + _fallbackImageFileName; + } + else + { + return null; + } + + if (host == null) + { + host = _host; + } + else if (host == string.Empty) + { + host = _appPath; + } + else + { + host = host.EnsureEndsWith("/"); + } + + var url = host; + + // Strip leading "/", the host/apppath has this already + if (path[0] == '/') + { + path = path.Substring(1); + } + + // Append media path + url += path; + + // Append query + var query = imageQuery?.ToString(false); + if (query != null && query.Length > 0) + { + url += query; + } + + return url; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Migration/MediaMigrator.cs b/src/Libraries/SmartStore.Services/Media/Migration/MediaMigrator.cs similarity index 76% rename from src/Libraries/SmartStore.Services/Media/v4/Migration/MediaMigrator.cs rename to src/Libraries/SmartStore.Services/Media/Migration/MediaMigrator.cs index 4f3a4c1071..5ad929d277 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Migration/MediaMigrator.cs +++ b/src/Libraries/SmartStore.Services/Media/Migration/MediaMigrator.cs @@ -2,9 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.IO; -using System.Text; -using System.Threading.Tasks; -using System.Data.Entity; using SmartStore.Core.Data; using SmartStore.Core.Domain.Media; using SmartStore.Core.Plugins; @@ -16,14 +13,26 @@ using System.Runtime.CompilerServices; using SmartStore.Core.Domain.Messages; using System.Diagnostics; +using System.Globalization; +using SmartStore.Core; +using SmartStore.Services.Media.Imaging; namespace SmartStore.Services.Media.Migration { public class MediaMigrator { + public class FileMin + { + public int Id { get; set; } + public string Name { get; set; } + public bool IsDupe { get; set; } + public string UniqueName { get; set; } + public int UniqueIndex { get; set; } + } + internal static bool Executed; internal const string MigrationName = "MediaManager"; - + private readonly ICommonServices _services; private readonly IProviderManager _providerManager; private readonly IMediaTypeResolver _mediaTypeResolver; @@ -36,7 +45,7 @@ public class MediaMigrator private readonly bool _isFsProvider; public MediaMigrator( - ICommonServices services, + ICommonServices services, IProviderManager providerManager, Func mediaStorageProvider, IMediaTypeResolver mediaTypeResolver, @@ -132,10 +141,10 @@ public void MigrateDownloads(SmartObjectContext ctx) var messagesFolderId = _albumRegistry.GetAlbumByName(SystemAlbumProvider.Messages)?.Id; var newFiles = new List(); - using (var scope = new DbContextScope(ctx, - validateOnSave: false, - hooksEnabled: false, - autoCommit: false, + using (var scope = new DbContextScope(ctx, + validateOnSave: false, + hooksEnabled: false, + autoCommit: false, autoDetectChanges: false)) { var messageTemplates = ctx.Set() @@ -164,12 +173,17 @@ public void MigrateDownloads(SmartObjectContext ctx) if (stub == null) continue; - if (stub.UseDownloadUrl || stub.Filename == "undefined" || string.IsNullOrEmpty(stub.Filename) || string.IsNullOrEmpty(stub.Extension)) + if (stub.UseDownloadUrl || string.IsNullOrEmpty(stub.Extension)) { // Something weird has happened in the past continue; - } - + } + + if (stub.Filename == "undefined" || string.IsNullOrEmpty(stub.Filename)) + { + stub.Filename = stub.Id.ToString(CultureInfo.InvariantCulture); + } + var isMailAttachment = false; if (messageTemplatesDict.TryGetValue(stub.Id, out var mt)) { @@ -232,11 +246,11 @@ public void MigrateDownloads(SmartObjectContext ctx) private void ReRefMessageTemplateAttachments( SmartObjectContext ctx, - Dictionary messageTemplatesDict, + Dictionary messageTemplatesDict, Dictionary downloads) { bool hasChanges = false; - + foreach (var kvp in messageTemplatesDict) { var downloadId = kvp.Key; @@ -269,31 +283,34 @@ private void ReRefMessageTemplateAttachments( private void MoveDownloadFiles( Dictionary newFilesDict, - Dictionary downloadsDict, + Dictionary downloadsDict, Dictionary downloadStubs) { - var downloadFiles = _mediaFileSystem.ListFiles("Downloads"); - foreach (var downloadFile in downloadFiles) + if (_mediaFileSystem.FolderExists("Downloads")) { - if (int.TryParse(downloadFile.Title, out var downloadId) && downloadId > 0 && downloadsDict.TryGetValue(downloadId, out var d)) + var downloadFiles = _mediaFileSystem.ListFiles("Downloads"); + foreach (var downloadFile in downloadFiles) { - var stub = downloadStubs.Get(d.Id); - if (stub == null || d.MediaFileId == null) - continue; - - var file = newFilesDict.Get(d.MediaFileId.Value); - if (file != null) + if (int.TryParse(downloadFile.Title, out var downloadId) && downloadId > 0 && downloadsDict.TryGetValue(downloadId, out var d)) { - try + var stub = downloadStubs.Get(d.Id); + if (stub == null || d.MediaFileId == null) + continue; + + var file = newFilesDict.Get(d.MediaFileId.Value); + if (file != null) { - // Copy now - var newPath = GetStoragePath(file); - if (!_mediaFileSystem.FileExists(newPath)) + try { - _mediaFileSystem.CopyFile(downloadFile.Path, newPath); - } + // Copy now + var newPath = GetStoragePath(file); + if (!_mediaFileSystem.FileExists(newPath)) + { + _mediaFileSystem.CopyFile(downloadFile.Path, newPath); + } + } + catch { } } - catch { } } } } @@ -301,14 +318,90 @@ private void MoveDownloadFiles( public void MigrateMediaFiles(SmartObjectContext ctx) { - var query = ctx.Set() - //.Where(x => x.Version == 0) - .Include(x => x.MediaStorage); + string prevName = null; + int fileIndex = 0; + + var query = ctx.Set().OrderBy(x => x.Name); + var totalCount = query.Count(); + var pageSize = Math.Max(1000, Math.Min(5000, totalCount / 200)); + var pageIndex = 0; + + using (var scope = new DbContextScope(ctx, + hooksEnabled: false, + autoCommit: false, + proxyCreation: false, + validateOnSave: false, + lazyLoading: false)) + { + while (true) + { + var files = new PagedList(query, pageIndex, pageSize); + if (files.Count == 0) + break; + + foreach (var file in files) + { + if (file.Version > 0) + continue; + + if (file.Extension.IsEmpty()) + { + file.Extension = MimeTypes.MapMimeTypeToExtension(file.MimeType); + } + + var name = file.Name; + var fixedName = name; + if (name.IsEmpty()) + { + name = fixedName = file.Id.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + else + { + name = fixedName = MediaHelper.NormalizeFileName(file.Name.Truncate(292)); + if (name == prevName) + { + // Make file name unique + fixedName = name + "-" + ++fileIndex; + } + else + { + fileIndex = 0; + } + } + + prevName = name; + + file.Name = fixedName + "." + file.Extension; + file.CreatedOnUtc = file.UpdatedOnUtc; + file.Version = 1; + + ProcessMediaFile(file); + } + + // Save to DB + int num = scope.Commit(); + + // Breathe + ctx.DetachEntities(deep: true); + + if (!files.HasNextPage) + break; + + pageIndex++; + } + } + } + + public void MigrateMediaFiles_Old(SmartObjectContext ctx) + { + var query = ctx.Set(); + //.Where(x => x.Version == 0) + //.Include(x => x.MediaStorage); var pager = new FastPager(query, 1000); - using (var scope = new DbContextScope(ctx, - hooksEnabled: false, + using (var scope = new DbContextScope(ctx, + hooksEnabled: false, autoCommit: false, proxyCreation: false, validateOnSave: false, @@ -320,13 +413,23 @@ public void MigrateMediaFiles(SmartObjectContext ctx) { if (file.Version > 0) continue; - + if (file.Extension.IsEmpty()) { file.Extension = MimeTypes.MapMimeTypeToExtension(file.MimeType); } - - file.Name = file.Name.Truncate(292) + "." + file.Extension; + + var name = file.Name; + if (name.IsEmpty()) + { + name = file.Id.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + else + { + name = MediaHelper.NormalizeFileName(file.Name.Truncate(292)); + } + + file.Name = name + "." + file.Extension; file.CreatedOnUtc = file.UpdatedOnUtc; file.Version = 1; @@ -382,18 +485,7 @@ void ProcessFolder(IFolder folder, int mediaFolderId) ProcessMediaFile(file); - if (!_isFsProvider) - { - using (var stream = uploadedFile.OpenRead()) - { - file.MediaStorage = new MediaStorage { Data = stream.ToByteArray() }; - } - } - else - { - newFiles.Add(new FilePair { MediaFile = file, UploadedFile = uploadedFile }); - } - + newFiles.Add(new FilePair { MediaFile = file, UploadedFile = uploadedFile }); fileSet.Add(file); } @@ -404,17 +496,27 @@ void ProcessFolder(IFolder folder, int mediaFolderId) int num = scope.Commit(); // Copy/Move files - if (_isFsProvider) + foreach (var newFile in newFiles) { - foreach (var newFile in newFiles) + if (_isFsProvider) { var newPath = GetStoragePath(newFile.MediaFile); if (!_mediaFileSystem.FileExists(newPath)) { - // TODO: (mm) should we actually MOVE the file? + // TODO: (mm) (mc) should we actually MOVE the file? _mediaFileSystem.CopyFile(newFile.UploadedFile.Path, newPath); } } + else + { + _mediaStorageProvider.Save(newFile.MediaFile, MediaStorageItem.FromFile(newFile.UploadedFile)); + } + } + + if (!_isFsProvider) + { + // MediaFile.MediaStorageId has been updated, we need to save again. + num = scope.Commit(); } } catch @@ -469,6 +571,12 @@ private void ProcessMediaFile(MediaFile file) file.Height = size.Height; file.PixelSize = size.Width * size.Height; } + catch + { + // Don't attempt again + file.Width = 0; + file.Height = 0; + } finally { stream.Dispose(); @@ -489,7 +597,7 @@ public void DetectTracks() { //if (albumName == SystemAlbumProvider.Downloads || albumName == SystemAlbumProvider.Messages) // continue; // Download and MessageTemplate tracks already added in MigrateDownload() - + _mediaTracker.DetectAllTracks(albumName, true); } } @@ -519,7 +627,7 @@ private string GetStoragePath(MediaFile file, bool createFolder = true) if (createFolder) { _mediaFileSystem.CreateFolder(subfolder); - } + } return _mediaFileSystem.Combine(subfolder, fileName); } diff --git a/src/Libraries/SmartStore.Services/Media/Migration/MediaMigrator3.cs b/src/Libraries/SmartStore.Services/Media/Migration/MediaMigrator3.cs new file mode 100644 index 0000000000..6163b4acf1 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Migration/MediaMigrator3.cs @@ -0,0 +1,83 @@ +using System.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.Media.Migration +{ + public class MediaMigrator3 + { + internal static bool Executed; + internal const string MigrationName = "MediaManager3"; + + private readonly IDbContext _db; + + public MediaMigrator3(IDbContext db) + { + _db = db; + } + + public void Migrate() + { + var setFolders = _db.Set(); + + // Insert new Albums: Catalog & Content + // Entity has a unique index of ParentId and Name. + var catalogAlbum = setFolders.FirstOrDefault(x => x.ParentId == null && x.Name == SystemAlbumProvider.Catalog) as MediaAlbum; + if (catalogAlbum == null) + { + catalogAlbum = new MediaAlbum { Name = SystemAlbumProvider.Catalog, ResKey = "Admin.Catalog", CanDetectTracks = true, Order = int.MinValue }; + setFolders.Add(catalogAlbum); + } + + var contentAlbum = setFolders.FirstOrDefault(x => x.ParentId == null && x.Name == SystemAlbumProvider.Content) as MediaAlbum; + if (contentAlbum == null) + { + contentAlbum = new MediaAlbum { Name = SystemAlbumProvider.Content, ResKey = "Admin.Media.Album.Content", CanDetectTracks = true, Order = int.MinValue + 10 }; + setFolders.Add(contentAlbum); + } + + _db.SaveChanges(); + + // Load all db albums into a dictionary (Key = AlbumName) + var albums = setFolders.OfType().ToDictionary(x => x.Name); + + // Reorganize files (product/category/brand >> catalog) + foreach (var oldName in new[] { "product", "category", "brand" }) + { + UpdateFolderId(oldName, catalogAlbum); + } + + // Reorganize files (news/blog/forum >> content) + foreach (var oldName in new[] { "blog", "news", "forum" }) + { + UpdateFolderId(oldName, contentAlbum); + } + + // Put all unassigned files to content album + _db.ExecuteSqlCommand($"UPDATE [MediaFile] SET [FolderId] = {contentAlbum.Id} WHERE ([FolderId] IS NULL)"); + + // Delete all obsolete albums + var namesToDelete = new[] { "product", "category", "brand", "blog", "news", "forum" }; + var toDelete = albums + .Select(x => x.Value) + .Where(x => namesToDelete.Contains(x.Name)) + .ToList(); + + if (toDelete.Any()) + { + setFolders.RemoveRange(toDelete); + } + + _db.SaveChanges(); + + void UpdateFolderId(string oldAlbumName, MediaAlbum newAlbum) + { + var oldAlbum = albums.Get(oldAlbumName); + if (oldAlbum != null) + { + _db.ExecuteSqlCommand($"UPDATE [MediaFile] SET [FolderId] = {newAlbum.Id} WHERE [FolderId] = {oldAlbum.Id}"); + } + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Migration/SeedingDbMigrationConsumer.cs b/src/Libraries/SmartStore.Services/Media/Migration/SeedingDbMigrationConsumer.cs similarity index 54% rename from src/Libraries/SmartStore.Services/Media/v4/Migration/SeedingDbMigrationConsumer.cs rename to src/Libraries/SmartStore.Services/Media/Migration/SeedingDbMigrationConsumer.cs index c66ba4d751..27c922ed2d 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Migration/SeedingDbMigrationConsumer.cs +++ b/src/Libraries/SmartStore.Services/Media/Migration/SeedingDbMigrationConsumer.cs @@ -12,11 +12,14 @@ public class SeedingDbMigrationConsumer : IConsumer { public void Handle(SeedingDbMigrationEvent message, IComponentContext componentContext) { - if (message.MigrationName != MediaMigrator.MigrationName) - return; - - var mediaMigrator = componentContext.Resolve(); - mediaMigrator.Migrate(); + if (message.MigrationName == MediaMigrator.MigrationName) + { + componentContext.Resolve().Migrate(); + } + else if (message.MigrationName == MediaMigrator3.MigrationName) + { + componentContext.Resolve().Migrate(); + } } } } diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs deleted file mode 100644 index 9a0cc3f09c..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace SmartStore.Services.Media -{ - public sealed class ProcessImageException : Exception - { - public ProcessImageException() - : this((string)null, null) - { - } - - public ProcessImageException(ProcessImageQuery query) - : this(query, null) - { - } - - public ProcessImageException(string message, ProcessImageQuery query) - : base(message) - { - Query = query; - } - - public ProcessImageException(ProcessImageQuery query, Exception innerException) - : base(CreateMessage(query, innerException), innerException) - { - Query = query; - } - - private static string CreateMessage(ProcessImageQuery query, Exception innerException) - { - var fileName = query?.FileName; - - var msg = fileName.HasValue() - ? "Error while processing image '{0}'".FormatCurrent(fileName.NaIfEmpty()) - : "Error while processing image"; - - if (innerException != null) - { - msg += " (" + innerException.Message + ")"; - } - - return msg; - } - - public ProcessImageQuery Query { get; private set; } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs deleted file mode 100644 index cba9253b04..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.IO; -using System.Collections.Specialized; -using System.Text; -using SmartStore.Collections; -using System.Drawing; -using ImageProcessor.Imaging.Formats; - -namespace SmartStore.Services.Media -{ - public class ProcessImageQuery : QueryString - { - private readonly static HashSet _supportedTokens = new HashSet { "w", "h", "q", "m", "size" }; - - public ProcessImageQuery() - : this(null, new NameValueCollection()) - { - } - - public ProcessImageQuery(byte[] source) - : this(source, new NameValueCollection()) - { - } - - public ProcessImageQuery(Stream source) - : this(source, new NameValueCollection()) - { - } - - public ProcessImageQuery(Image source) - : this(source, new NameValueCollection()) - { - } - - public ProcessImageQuery(string source) - : this(source, new NameValueCollection()) - { - } - - public ProcessImageQuery(object source, NameValueCollection query) - : base(SanitizeCollection(query)) - { - Guard.NotNull(query, nameof(query)); - - Source = source; - DisposeSource = true; - Notify = true; - } - - public ProcessImageQuery(ProcessImageQuery query) - : base(SanitizeCollection(query)) - { - Guard.NotNull(query, nameof(query)); - - Source = query.Source; - Format = query.Format; - DisposeSource = query.DisposeSource; - } - - private static NameValueCollection SanitizeCollection(NameValueCollection query) - { - // We just need the supported flags - var sanitizable = query.AllKeys.Any(x => !_supportedTokens.Contains(x)); - if (sanitizable) - { - var copy = new NameValueCollection(query); - foreach (var key in copy.AllKeys) - { - if (!_supportedTokens.Contains(key)) - { - copy.Remove(key); - } - } - - return copy; - } - - return query; - } - - /// - /// The source image's physical path, app-relative virtual path, or a Stream, byte array, Image or IFile instance. - /// - public object Source { get; set; } - - public string FileName { get; set; } - - /// - /// Whether to dispose the source stream after processing completes - /// - public bool DisposeSource { get; set; } - - /// - /// Whether to execute an applicable post processor which - /// can reduce the resulting file size drastically, but also - /// can slow down processing time. - /// - public bool ExecutePostProcessor { get; set; } - - public int? MaxWidth - { - get { return Get("w") ?? Get("size"); } - set { Set("w", value); } - } - - public int? MaxHeight - { - get { return Get("h") ?? Get("size"); } - set { Set("h", value); } - } - - public int? MaxSize - { - get { return Get("size"); } - set { Set("size", value); } - } - - public int? Quality - { - get { return Get("q"); } - set { Set("q", value); } - } - - /// - /// max (default) | boxpad | crop | min | pad | stretch - /// - public string ScaleMode - { - get { return Get("m"); } - set { Set("m", value); } - } - - /// - /// center (default) | top | bottom | left | right | top-left | top-right | bottom-left | bottom-right - /// - public string AnchorPosition - { - get { return Get("pos"); } - set { Set("pos", value); } - } - - public string BackgroundColor - { - get { return Get("bg"); } - set { Set("bg", value); } - } - - /// - /// Gets or sets the output file format either as a string ("png", "jpg", "gif" and "svg"), - /// or as a format object instance. - /// When format is not specified, the original format of the source image is used (unless it is not a web safe format - jpeg is the fallback in that scenario). - /// - public object Format { get; set; } - - public bool IsValidationMode { get; set; } - - public bool Notify { get; set; } - - private T Get(string name) - { - return base[name].Convert(); - } - - private void Set(string name, T val) - { - if (val == null) - base.Remove(name); - else - base.Add(name, val.Convert(), true); - } - - - public bool NeedsProcessing(bool ignoreQualityFlag = false) - { - if (base.Count == 0) - return false; - - if (object.Equals(Format, "svg")) - { - // SVG cannot be processed. - return false; - } - - if (ignoreQualityFlag && base.Count == 1 && base["q"] != null) - { - // Return false if ignoreQualityFlag is true and "q" is the only flag. - return false; - } - - if (base.Count == 1 && Quality == 100) - { - // If "q" is the only flag and its value is 100, we don't need to process - return false; - } - - return true; - } - - public string CreateHash() - { - var hash = string.Empty; - - foreach (var key in base.AllKeys) - { - if (key == "m" && base["m"] == "max") - continue; // Mode 'max' is default and can be omitted - - hash += "-" + key + base[key]; - - } - - return hash; - } - - public string GetResultExtension() - { - if (Format == null) - { - return null; - } - else if (Format is ISupportedImageFormat) - { - return ((ISupportedImageFormat)Format).DefaultExtension; - } - else if (Format is string) - { - return (string)Format; - } - - return null; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs deleted file mode 100644 index c46df1ff86..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.IO; - -namespace SmartStore.Services.Media -{ - public class ProcessImageResult : DisposableObject - { - public ProcessImageQuery Query { get; set; } - - public MemoryStream OutputStream { get; set; } - internal bool DisposeOutputStream { get; set; } - - public int? SourceWidth { get; set; } - public int? SourceHeight { get; set; } - public string SourceMimeType { get; set; } - - public string FileExtension { get; set; } - public string MimeType { get; set; } - public int Width { get; set; } - public int Height { get; set; } - - /// - /// Is true if any effect has been applied that changed the image visually (like background color, contrast, sharpness etc.). - /// Resize and compression quality does NOT count as FX. - /// - public bool HasAppliedVisualEffects { get; set; } - - public long ProcessTimeMs { get; set; } - - protected override void OnDispose(bool disposing) - { - if (disposing && DisposeOutputStream && OutputStream != null) - { - OutputStream.Dispose(); - OutputStream = null; - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Search/FileCountResult.cs b/src/Libraries/SmartStore.Services/Media/Search/FileCountResult.cs similarity index 100% rename from src/Libraries/SmartStore.Services/Media/v4/Search/FileCountResult.cs rename to src/Libraries/SmartStore.Services/Media/Search/FileCountResult.cs diff --git a/src/Libraries/SmartStore.Services/Media/v4/Search/IMediaSearcher.cs b/src/Libraries/SmartStore.Services/Media/Search/IMediaSearcher.cs similarity index 81% rename from src/Libraries/SmartStore.Services/Media/v4/Search/IMediaSearcher.cs rename to src/Libraries/SmartStore.Services/Media/Search/IMediaSearcher.cs index f3d3c9cd0f..0c8ee885b4 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Search/IMediaSearcher.cs +++ b/src/Libraries/SmartStore.Services/Media/Search/IMediaSearcher.cs @@ -10,7 +10,7 @@ public interface IMediaSearcher { IPagedList SearchFiles(MediaSearchQuery query, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); IQueryable PrepareQuery(MediaSearchQuery query, MediaLoadFlags flags); - IQueryable PrepareFilterQuery(MediaFilesFilter filter); + IQueryable ApplyFilterQuery(MediaFilesFilter filter, IQueryable sourceQuery = null); IQueryable ApplyLoadFlags(IQueryable query, MediaLoadFlags flags); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearchQuery.cs b/src/Libraries/SmartStore.Services/Media/Search/MediaSearchQuery.cs similarity index 75% rename from src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearchQuery.cs rename to src/Libraries/SmartStore.Services/Media/Search/MediaSearchQuery.cs index fb7de258ea..3fceaf4ddd 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Media/Search/MediaSearchQuery.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.Serialization; using Newtonsoft.Json; using SmartStore.Core.Domain.Media; @@ -16,59 +12,77 @@ public enum ImageDimension Large = 3, VeryLarge = 4 } - + + [DataContract] public partial class MediaFilesFilter { + [DataMember] [JsonProperty("mediaTypes")] public string[] MediaTypes { get; set; } + [DataMember] [JsonProperty("mimeTypes")] public string[] MimeTypes { get; set; } + [DataMember] [JsonProperty("extensions")] public string[] Extensions { get; set; } + [DataMember] [JsonProperty("dimensions")] public ImageDimension[] Dimensions { get; set; } + [DataMember] [JsonProperty("tags")] public int[] Tags { get; set; } + [DataMember] [JsonProperty("hidden")] public bool? Hidden { get; set; } + [DataMember] [JsonProperty("deleted")] public bool? Deleted { get; set; } + [DataMember] [JsonProperty("term")] public string Term { get; set; } + [DataMember] [JsonProperty("exact")] public bool ExactMatch { get; set; } + [DataMember] [JsonProperty("includeAlt")] public bool IncludeAltForTerm { get; set; } } + [DataContract] public partial class MediaSearchQuery : MediaFilesFilter { + [DataMember] [JsonProperty("folderId")] public int? FolderId { get; set; } + [DataMember] [JsonProperty("deep")] public bool DeepSearch { get; set; } + [DataMember] [JsonProperty("page")] public int PageIndex { get; set; } + [DataMember] [JsonProperty("pageSize")] public int PageSize { get; set; } = int.MaxValue; + [DataMember] [JsonProperty("sortBy")] - public string SortBy { get; set; } = nameof(MediaFile.Name); + public string SortBy { get; set; } = nameof(MediaFile.Id); + [DataMember] [JsonProperty("sortDesc")] - public bool SortDescending { get; set; } + public bool SortDesc { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearchResult.cs b/src/Libraries/SmartStore.Services/Media/Search/MediaSearchResult.cs similarity index 98% rename from src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearchResult.cs rename to src/Libraries/SmartStore.Services/Media/Search/MediaSearchResult.cs index 9b6f7f4634..09483acfe0 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearchResult.cs +++ b/src/Libraries/SmartStore.Services/Media/Search/MediaSearchResult.cs @@ -9,7 +9,7 @@ namespace SmartStore.Services.Media public partial class MediaSearchResult : PagedListBase, IEnumerable { private readonly IList _files; - + public MediaSearchResult(IPagedList pageable, Func converter) : base(pageable) { _files = pageable.Select(converter).ToList(); diff --git a/src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearcher.cs b/src/Libraries/SmartStore.Services/Media/Search/MediaSearcher.cs similarity index 79% rename from src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearcher.cs rename to src/Libraries/SmartStore.Services/Media/Search/MediaSearcher.cs index 69869e3d62..ba0c18c8e4 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Search/MediaSearcher.cs +++ b/src/Libraries/SmartStore.Services/Media/Search/MediaSearcher.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; -using System.Linq.Dynamic; +using System.Linq.Dynamic.Core; using SmartStore.Linq; using SmartStore.Core; using SmartStore.Core.Data; @@ -18,7 +18,7 @@ public class MediaSearcher : IMediaSearcher private readonly Work _folderService; public MediaSearcher( - IRepository fileRepo, + IRepository fileRepo, Work folderService) { _fileRepo = fileRepo; @@ -35,7 +35,7 @@ public virtual IQueryable PrepareQuery(MediaSearchQuery query, MediaL { Guard.NotNull(query, nameof(query)); - var q = PrepareFilterQuery(query); + var q = _fileRepo.Table; bool? shouldIncludeDeleted = false; // Folder @@ -81,6 +81,13 @@ public virtual IQueryable PrepareQuery(MediaSearchQuery query, MediaL q = q.Where(x => x.FolderId == null); } } + else + { + // (perf) Composite index + q = q.Where(x => x.FolderId == null || x.FolderId.HasValue); + } + + q = ApplyFilterQuery(query, q); if (query.Deleted == null && shouldIncludeDeleted.HasValue) { @@ -88,50 +95,62 @@ public virtual IQueryable PrepareQuery(MediaSearchQuery query, MediaL } // Sorting - var ordering = query.SortBy; - if (ordering.HasValue()) - { - if (query.SortDescending) ordering += " descending"; - q = q.OrderBy(ordering); - } + var ordering = query.SortBy.NullEmpty() ?? "Id"; + if (query.SortDesc) ordering += " descending"; + q = q.OrderBy(ordering); return ApplyLoadFlags(q, flags); } - public virtual IQueryable PrepareFilterQuery(MediaFilesFilter filter) + public virtual IQueryable ApplyFilterQuery(MediaFilesFilter filter, IQueryable sourceQuery = null) { Guard.NotNull(filter, nameof(filter)); - var q = _fileRepo.Table; + var q = sourceQuery ?? _fileRepo.Table; - // Deleted - if (filter.Deleted != null) - { - q = q.Where(x => x.Deleted == filter.Deleted.Value); - } - - // Hidden - if (filter.Hidden != null) + // Term + if (filter.Term.HasValue() && filter.Term != "*") { - q = q.Where(x => x.Hidden == filter.Hidden.Value); + // Convert file pattern to SQL 'LIKE' expression + q = ApplySearchTerm(q, filter.Term, filter.IncludeAltForTerm, filter.ExactMatch); } - // MimeType - if (filter.MimeTypes != null && filter.MimeTypes.Length > 0) + // MediaType + if (filter.MediaTypes != null && filter.MediaTypes.Length > 0) { - q = q.Where(x => filter.MimeTypes.Contains(x.MimeType)); + if (filter.MediaTypes.Length == 1) + { + var right = filter.MediaTypes[0]; + q = q.Where(x => x.MediaType == right); + } + else if (filter.MediaTypes.Length > 1) + { + q = q.Where(x => filter.MediaTypes.Contains(x.MediaType)); + } + else + { + // (perf) Composite index + q = q.Where(x => !string.IsNullOrEmpty(x.MediaType)); + } } // Extension if (filter.Extensions != null && filter.Extensions.Length > 0) { - q = q.Where(x => filter.Extensions.Contains(x.Extension)); - } - - // MediaType - if (filter.MediaTypes != null && filter.MediaTypes.Length > 0) - { - q = q.Where(x => filter.MediaTypes.Contains(x.MediaType)); + if (filter.Extensions.Length == 1) + { + var right = filter.Extensions[0]; + q = q.Where(x => x.Extension == right); + } + else if (filter.Extensions.Length > 1) + { + q = q.Where(x => filter.Extensions.Contains(x.Extension)); + } + else + { + // (perf) Composite index + q = q.Where(x => !string.IsNullOrEmpty(x.Extension)); + } } // Tags @@ -140,12 +159,12 @@ public virtual IQueryable PrepareFilterQuery(MediaFilesFilter filter) q = q.Where(x => x.Tags.Any(t => filter.Tags.Contains(t.Id))); } - // Extensions + // Image dimension if (filter.Dimensions != null && filter.Dimensions.Length > 0) { var predicates = new List>>(5); - foreach (var dim in filter.Dimensions.OrderBy(x => x).Distinct()) + foreach (var dim in filter.Dimensions.OrderBy(x => x).Distinct()) { if (dim == ImageDimension.VerySmall) predicates.Add(x => x.PixelSize > 0 && x.PixelSize <= 50000); @@ -170,13 +189,36 @@ public virtual IQueryable PrepareFilterQuery(MediaFilesFilter filter) } } - // Term - if (filter.Term.HasValue() && filter.Term != "*") + // Deleted + if (filter.Deleted != null) { - // Convert file pattern to SQL 'LIKE' expression - q = ApplySearchTerm(q, filter.Term, filter.IncludeAltForTerm, filter.ExactMatch); + q = q.Where(x => x.Deleted == filter.Deleted.Value); } + #region Currently unindexed + + // MimeType + if (filter.MimeTypes != null && filter.MimeTypes.Length > 0) + { + if (filter.MimeTypes.Length == 1) + { + var right = filter.MimeTypes[0]; + q = q.Where(x => x.MimeType == right); + } + else if (filter.MimeTypes.Length > 1) + { + q = q.Where(x => filter.MimeTypes.Contains(x.MimeType)); + } + } + + // Hidden + if (filter.Hidden != null) + { + q = q.Where(x => x.Hidden == filter.Hidden.Value); + } + + #endregion + return q; } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs b/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs index 2f9582d7c2..0fd3e2f053 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs @@ -13,253 +13,292 @@ namespace SmartStore.Services.Media.Storage { - [SystemName("MediaStorage.SmartStoreDatabase")] - [FriendlyName("Database")] - [DisplayOrder(0)] - public class DatabaseMediaStorageProvider : IMediaStorageProvider, ISupportsMediaMoving - { - class MediaStorageBlobStream : SqlBlobStream - { - public MediaStorageBlobStream( - IDbConnectionFactory connectionFactory, - string connectionString, - int mediaStorageId) - : base(connectionFactory, connectionString, "MediaStorage", "Data", "Id", mediaStorageId) - { - } - } - - private readonly IDbContext _dbContext; - private readonly IRepository _mediaStorageRepo; - private readonly IRepository _mediaFileRepo; - private readonly Lazy _dataProvider; - - private readonly bool _isSqlServer; - - public DatabaseMediaStorageProvider( - IDbContext dbContext, - IRepository mediaStorageRepo, - IRepository mediaFileRepo, - Lazy dataProvider) - { - _dbContext = dbContext; - _mediaStorageRepo = mediaStorageRepo; - _mediaFileRepo = mediaFileRepo; - _dataProvider = dataProvider; - _isSqlServer = DataSettings.Current.IsSqlServer; - } - - public static string SystemName - { - get { return "MediaStorage.SmartStoreDatabase"; } - } - - private Stream CreateBlobStream(int mediaStorageId) - { - return new MediaStorageBlobStream(_dataProvider.Value.GetConnectionFactory(), DataSettings.Current.DataConnectionString, mediaStorageId); - } - - public bool IsCloudStorage { get; } = false; - - public string GetPublicUrl(MediaFile mediaFile) - { - return null; - } - - public long GetSize(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - var id = mediaFile.MediaStorageId ?? 0; - if (id == 0) - { - return 0; - } - - if (_isSqlServer) - { - using (var stream = CreateBlobStream(id)) - { - return stream.Length; - } - } - else - { - return mediaFile.MediaStorage?.Data?.LongLength ?? 0; - } - } - - public Stream OpenRead(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (_isSqlServer) - { - if (mediaFile.MediaStorageId > 0) - { - return CreateBlobStream(mediaFile.MediaStorageId.Value); - } - - return null; - } - else - { - return mediaFile.MediaStorage?.Data?.ToStream(); - } - } - - public byte[] Load(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (mediaFile.MediaStorageId == null) - { - return new byte[0]; - } - - using (var stream = CreateBlobStream(mediaFile.MediaStorageId.Value)) - { - return stream.ToByteArray(); - } - } - - public async Task LoadAsync(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (mediaFile.MediaStorageId == null) - { - return new byte[0]; - } - - using (var stream = CreateBlobStream(mediaFile.MediaStorageId.Value)) - { - return await stream.ToByteArrayAsync(); - } - } - - public void Save(MediaFile mediaFile, Stream stream) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (_isSqlServer) - { - SaveFast(mediaFile, stream); - } - else - { - byte[] buffer; - using (stream ?? new MemoryStream()) - { - buffer = stream.ToByteArray(); - } - mediaFile.ApplyBlob(buffer); - } - - _mediaFileRepo.Update(mediaFile); - } - - public async Task SaveAsync(MediaFile mediaFile, Stream stream) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (_isSqlServer) - { - SaveFast(mediaFile, stream); - } - else - { - byte[] buffer; - using (stream ?? new MemoryStream()) - { - buffer = await stream.ToByteArrayAsync(); - } - mediaFile.ApplyBlob(buffer); - } - - _mediaFileRepo.Update(mediaFile); - } - - private int SaveFast(MediaFile mediaFile, Stream stream) - { - var sql = "INSERT INTO [MediaStorage] (Data) Values(@p0)"; - var storageId = ((DbContext)_dbContext).InsertInto(sql, stream); - mediaFile.MediaStorageId = storageId; - - return storageId; - } - - public void Remove(params MediaFile[] mediaFiles) - { - using (var scope = new DbContextScope(ctx: _mediaFileRepo.Context, autoCommit: false)) - { - foreach (var media in mediaFiles) - { - media.ApplyBlob(null); - } - - scope.Commit(); - } - } - - public void ChangeExtension(MediaFile mediaFile, string extension) - { - // Do nothing - } - - public void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, MediaFile mediaFile) - { - Guard.NotNull(target, nameof(target)); - Guard.NotNull(context, nameof(context)); - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (mediaFile.MediaStorageId != null) - { - // Let target store data (into a file for example) - target.Receive(context, mediaFile, OpenRead(mediaFile)); - - // Remove picture binary from DB - try - { - mediaFile.MediaStorageId = null; - mediaFile.MediaStorage = null; - _mediaFileRepo.Update(mediaFile); - } - catch { } - - context.ShrinkDatabase = true; - } - } - - public void Receive(MediaMoverContext context, MediaFile mediaFile, Stream stream) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(mediaFile, nameof(mediaFile)); - - // Store data for later bulk commit - if (stream != null && stream.Length > 0) - { - // Requires AutoDetectChanges set to true or remove explicit entity detaching - Save(mediaFile, stream); - } - } - - public async Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, Stream stream) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(mediaFile, nameof(mediaFile)); - - // Store data for later bulk commit - if (stream != null && stream.Length > 0) - { - // Requires AutoDetectChanges set to true or remove explicit entity detaching - await SaveAsync(mediaFile, stream); - } - } - - public void OnCompleted(MediaMoverContext context, bool succeeded) - { - // nothing to do - } - } + [SystemName("MediaStorage.SmartStoreDatabase")] + [FriendlyName("Database")] + [DisplayOrder(0)] + public class DatabaseMediaStorageProvider : IMediaStorageProvider, ISupportsMediaMoving + { + class MediaStorageBlobStream : SqlBlobStream + { + public MediaStorageBlobStream( + IDbConnectionFactory connectionFactory, + string connectionString, + int mediaStorageId) + : base(connectionFactory, connectionString, "MediaStorage", "Data", "Id", mediaStorageId) + { + } + } + + private readonly IDbContext _dbContext; + private readonly IRepository _mediaStorageRepo; + private readonly IRepository _mediaFileRepo; + private readonly Lazy _dataProvider; + + private readonly bool _isSqlServer; + + public DatabaseMediaStorageProvider( + IDbContext dbContext, + IRepository mediaStorageRepo, + IRepository mediaFileRepo, + Lazy dataProvider) + { + _dbContext = dbContext; + _mediaStorageRepo = mediaStorageRepo; + _mediaFileRepo = mediaFileRepo; + _dataProvider = dataProvider; + _isSqlServer = DataSettings.Current.IsSqlServer; + } + + public static string SystemName => "MediaStorage.SmartStoreDatabase"; + + private Stream CreateBlobStream(int mediaStorageId) + { + return new MediaStorageBlobStream(_dataProvider.Value.GetConnectionFactory(), DataSettings.Current.DataConnectionString, mediaStorageId); + } + + public bool IsCloudStorage { get; } = false; + + public string GetPublicUrl(MediaFile mediaFile) + { + return null; + } + + public long GetSize(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + var id = mediaFile.MediaStorageId ?? 0; + if (id == 0) + { + return 0; + } + + if (_isSqlServer) + { + using (var stream = CreateBlobStream(id)) + { + return stream.Length; + } + } + else + { + return mediaFile.MediaStorage?.Data?.LongLength ?? 0; + } + } + + public Stream OpenRead(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (_isSqlServer) + { + if (mediaFile.MediaStorageId > 0) + { + return CreateBlobStream(mediaFile.MediaStorageId.Value); + } + + return null; + } + else + { + return mediaFile.MediaStorage?.Data?.ToStream(); + } + } + + public byte[] Load(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (mediaFile.MediaStorageId == null) + { + return new byte[0]; + } + + using (var stream = CreateBlobStream(mediaFile.MediaStorageId.Value)) + { + return stream.ToByteArray(); + } + } + + public async Task LoadAsync(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (mediaFile.MediaStorageId == null) + { + return new byte[0]; + } + + using (var stream = CreateBlobStream(mediaFile.MediaStorageId.Value)) + { + return await stream.ToByteArrayAsync(); + } + } + + public void Save(MediaFile mediaFile, MediaStorageItem item) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (item == null) + { + mediaFile.ApplyBlob(null); + } + else + { + SaveInternal(mediaFile, item); + } + + _mediaFileRepo.Update(mediaFile); + } + + private void SaveInternal(MediaFile mediaFile, MediaStorageItem item) + { + using (item) + { + if (_isSqlServer) + { + SaveFast(mediaFile, item); + } + else + { + var buffer = item.SourceStream.ToByteArray(); + mediaFile.ApplyBlob(buffer); + mediaFile.Size = buffer.Length; + } + } + + _mediaFileRepo.Update(mediaFile); + } + + public async Task SaveAsync(MediaFile mediaFile, MediaStorageItem item) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (item == null) + { + mediaFile.ApplyBlob(null); + } + else + { + await SaveInternalAsync(mediaFile, item); + } + + _mediaFileRepo.Update(mediaFile); + } + + private async Task SaveInternalAsync(MediaFile mediaFile, MediaStorageItem item) + { + using (item) + { + if (_isSqlServer) + { + SaveFast(mediaFile, item); + } + else + { + var buffer = await item.SourceStream.ToByteArrayAsync(); + mediaFile.ApplyBlob(buffer); + mediaFile.Size = buffer.Length; + } + } + + _mediaFileRepo.Update(mediaFile); + } + + private int SaveFast(MediaFile mediaFile, MediaStorageItem item) + { + var sourceStream = item.SourceStream; + mediaFile.Size = (int)sourceStream.Length; + + if (mediaFile.MediaStorageId == null) + { + // Insert new blob + var sql = "INSERT INTO [MediaStorage] (Data) Values(@p0)"; + mediaFile.MediaStorageId = ((DbContext)_dbContext).InsertInto(sql, sourceStream); + return mediaFile.MediaStorageId.Value; + } + else + { + // Update existing blob + var sql = "UPDATE [MediaStorage] SET [Data] = @p0 WHERE Id = @p1"; + _dbContext.ExecuteSqlCommand(sql, false, null, sourceStream, mediaFile.MediaStorageId.Value); + return mediaFile.MediaStorageId.Value; + } + } + + public void Remove(params MediaFile[] mediaFiles) + { + using (var scope = new DbContextScope(ctx: _mediaFileRepo.Context, autoCommit: false)) + { + foreach (var media in mediaFiles) + { + media.ApplyBlob(null); + } + + scope.Commit(); + } + } + + public void ChangeExtension(MediaFile mediaFile, string extension) + { + // Do nothing + } + + public void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, MediaFile mediaFile) + { + Guard.NotNull(target, nameof(target)); + Guard.NotNull(context, nameof(context)); + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (mediaFile.MediaStorageId != null) + { + // Let target store data (into a file for example) + target.Receive(context, mediaFile, OpenRead(mediaFile)); + + // Remove picture binary from DB + try + { + _mediaStorageRepo.Delete(mediaFile.MediaStorageId.Value); + //mediaFile.MediaStorageId = null; + //mediaFile.MediaStorage = null; + + _mediaFileRepo.Update(mediaFile); + } + catch { } + + context.ShrinkDatabase = true; + } + } + + public void Receive(MediaMoverContext context, MediaFile mediaFile, Stream stream) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(mediaFile, nameof(mediaFile)); + + // Store data for later bulk commit + if (stream != null && stream.Length > 0) + { + // Requires AutoDetectChanges set to true or remove explicit entity detaching + Save(mediaFile, MediaStorageItem.FromStream(stream)); + } + } + + public async Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, Stream stream) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(mediaFile, nameof(mediaFile)); + + // Store data for later bulk commit + if (stream != null && stream.Length > 0) + { + // Requires AutoDetectChanges set to true or remove explicit entity detaching + await SaveAsync(mediaFile, MediaStorageItem.FromStream(stream)); + } + } + + public void OnCompleted(MediaMoverContext context, bool succeeded) + { + // nothing to do + } + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs b/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs index ff7be22c53..232c3c0aee 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs @@ -8,267 +8,291 @@ using SmartStore.Core.Domain.Media; using SmartStore.Core.IO; using SmartStore.Core.Plugins; +using SmartStore.Services.Media.Imaging; namespace SmartStore.Services.Media.Storage { - [SystemName("MediaStorage.SmartStoreFileSystem")] - [FriendlyName("File system")] - [DisplayOrder(1)] - public class FileSystemMediaStorageProvider : IMediaStorageProvider, ISupportsMediaMoving - { - const string MediaRootPath = "Storage"; - - private readonly IMediaFileSystem _fileSystem; - private readonly IDictionary _pathCache = new Dictionary(); - - public FileSystemMediaStorageProvider(IMediaFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - public static string SystemName - { - get { return "MediaStorage.SmartStoreFileSystem"; } - } - - public bool IsCloudStorage - { - get => _fileSystem.IsCloudStorage; - } - - public string GetPublicUrl(MediaFile mediaFile) - { - return _fileSystem.GetPublicUrl(GetPath(mediaFile), true); - } - - protected string GetPath(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (_pathCache.TryGetValue(mediaFile.Id, out var path)) - { - return path; - } - - var ext = mediaFile.Extension.NullEmpty() ?? MimeTypes.MapMimeTypeToExtension(mediaFile.MimeType); - - var fileName = mediaFile.Id.ToString(ImageCache.IdFormatString).Grow(ext, "."); - var subfolder = fileName.Substring(0, ImageCache.MaxDirLength); - - path = _fileSystem.Combine(subfolder, fileName); - path = _fileSystem.Combine(MediaRootPath, path); - - _pathCache[mediaFile.Id] = path; - - return path; - } - - public long GetSize(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - if (mediaFile.Size > 0) - { - return mediaFile.Size; - } - - var file = _fileSystem.GetFile(GetPath(mediaFile)); - if (file.Exists) - { - // Hopefully a future commit will save this - mediaFile.Size = (int)file.Size; - } - - return mediaFile.Size; - } - - public Stream OpenRead(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - var file = _fileSystem.GetFile(GetPath(mediaFile)); - return file.Exists ? file.OpenRead() : null; - } - - public byte[] Load(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - var filePath = GetPath(mediaFile); - return _fileSystem.ReadAllBytes(filePath) ?? new byte[0]; - } - - public async Task LoadAsync(MediaFile mediaFile) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - var filePath = GetPath(mediaFile); - return (await _fileSystem.ReadAllBytesAsync(filePath)) ?? new byte[0]; - } - - public void Save(MediaFile mediaFile, Stream stream) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - // TODO: (?) if the new file extension differs from the old one then the old file never gets deleted - - var filePath = GetPath(mediaFile); - - if (stream != null) - { - // create folder if it does not exist yet - _fileSystem.CreateFolder(Path.GetDirectoryName(filePath)); - - using (stream) - { - _fileSystem.SaveStream(filePath, stream); - } - } - else if (_fileSystem.FileExists(filePath)) - { - // Remove media storage if any - _fileSystem.DeleteFile(filePath); - } - } - - public async Task SaveAsync(MediaFile mediaFile, Stream stream) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - - // TODO: (?) if the new file extension differs from the old one then the old file never gets deleted - - var filePath = GetPath(mediaFile); - - if (stream != null) - { - using (stream) - { - await _fileSystem.SaveStreamAsync(filePath, stream); - } - } - else if (_fileSystem.FileExists(filePath)) - { - // Remove media storage if any - _fileSystem.DeleteFile(filePath); - } - } - - public void Remove(params MediaFile[] mediaFiles) - { - foreach (var media in mediaFiles) - { - _fileSystem.DeleteFile(GetPath(media)); - } - } - - public void ChangeExtension(MediaFile mediaFile, string extension) - { - Guard.NotNull(mediaFile, nameof(mediaFile)); - Guard.NotEmpty(extension, nameof(extension)); - - var sourcePath = GetPath(mediaFile); - var newPath = Path.ChangeExtension(sourcePath, extension); - - _fileSystem.RenameFile(sourcePath, newPath); - } - - public void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, MediaFile mediaFile) - { - Guard.NotNull(target, nameof(target)); - Guard.NotNull(context, nameof(context)); - Guard.NotNull(mediaFile, nameof(mediaFile)); - - var filePath = GetPath(mediaFile); - - try - { - // Let target store data (into database for example) - target.Receive(context, mediaFile, OpenRead(mediaFile)); - - // Remember file path: we must be able to rollback IO operations on transaction failure - context.AffectedFiles.Add(filePath); - } - catch (Exception exception) - { - Debug.WriteLine(exception.Message); - } - } - - public void Receive(MediaMoverContext context, MediaFile mediaFile, Stream stream) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(mediaFile, nameof(mediaFile)); - - // store data into file - if (stream != null && stream.Length > 0) - { - var filePath = GetPath(mediaFile); - - if (!_fileSystem.FileExists(filePath)) - { - // TBD: (mc) We only save the file if it doesn't exist yet. - // This should save time and bandwidth in the case where the target - // is a cloud based file system (like Azure BLOB). - // In such a scenario it'd be advisable to copy the files manually - // with other - maybe more performant - tools before performing the provider switch. - using (stream) - { - _fileSystem.SaveStream(filePath, stream); - } - - context.AffectedFiles.Add(filePath); - } - } - } - - public async Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, Stream stream) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(mediaFile, nameof(mediaFile)); - - // store data into file - if (stream != null && stream.Length > 0) - { - var filePath = GetPath(mediaFile); - - if (!_fileSystem.FileExists(filePath)) - { - // TBD: (mc) We only save the file if it doesn't exist yet. - // This should save time and bandwidth in the case where the target - // is a cloud based file system (like Azure BLOB). - // In such a scenario it'd be advisable to copy the files manually - // with other - maybe more performant - tools before performing the provider switch. - using (stream) - { - await _fileSystem.SaveStreamAsync(filePath, stream); - } - - context.AffectedFiles.Add(filePath); - } - } - } - - public void OnCompleted(MediaMoverContext context, bool succeeded) - { - if (context.AffectedFiles.Any()) - { - var toFileSystem = context.TargetSystemName.IsCaseInsensitiveEqual(SystemName); - - if ((!toFileSystem && succeeded) || (toFileSystem && !succeeded)) - { - // FS > DB sucessful OR DB > FS failed: delete all physical files - // run a background task for the deletion of files (fire & forget) - - Task.Factory.StartNew(state => - { - var files = state as string[]; - foreach (var file in files) - { - _fileSystem.DeleteFile(file); - } - }, context.AffectedFiles.ToArray()).ConfigureAwait(false); - } - } - } - } + [SystemName("MediaStorage.SmartStoreFileSystem")] + [FriendlyName("File system")] + [DisplayOrder(1)] + public class FileSystemMediaStorageProvider : IMediaStorageProvider, ISupportsMediaMoving + { + const string MediaRootPath = "Storage"; + + private readonly IMediaFileSystem _fileSystem; + private readonly IDictionary _pathCache = new Dictionary(); + + public FileSystemMediaStorageProvider(IMediaFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public static string SystemName => "MediaStorage.SmartStoreFileSystem"; + + public bool IsCloudStorage => _fileSystem.IsCloudStorage; + + public string GetPublicUrl(MediaFile mediaFile) + { + return _fileSystem.GetPublicUrl(GetPath(mediaFile), true); + } + + protected string GetPath(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (_pathCache.TryGetValue(mediaFile.Id, out var path)) + { + return path; + } + + var ext = mediaFile.Extension.NullEmpty() ?? MimeTypes.MapMimeTypeToExtension(mediaFile.MimeType); + + var fileName = mediaFile.Id.ToString(ImageCache.IdFormatString).Grow(ext, "."); + var subfolder = fileName.Substring(0, ImageCache.MaxDirLength); + + path = _fileSystem.Combine(subfolder, fileName); + path = _fileSystem.Combine(MediaRootPath, path); + + _pathCache[mediaFile.Id] = path; + + return path; + } + + public long GetSize(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + if (mediaFile.Size > 0) + { + return mediaFile.Size; + } + + var file = _fileSystem.GetFile(GetPath(mediaFile)); + if (file.Exists) + { + // Hopefully a future commit will save this + mediaFile.Size = (int)file.Size; + } + + return mediaFile.Size; + } + + public Stream OpenRead(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + var file = _fileSystem.GetFile(GetPath(mediaFile)); + return file.Exists ? file.OpenRead() : null; + } + + public byte[] Load(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + var filePath = GetPath(mediaFile); + return _fileSystem.ReadAllBytes(filePath) ?? new byte[0]; + } + + public async Task LoadAsync(MediaFile mediaFile) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + var filePath = GetPath(mediaFile); + return (await _fileSystem.ReadAllBytesAsync(filePath)) ?? new byte[0]; + } + + public void Save(MediaFile mediaFile, MediaStorageItem item) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + // TODO: (?) if the new file extension differs from the old one then the old file never gets deleted + + var filePath = GetPath(mediaFile); + + if (item != null) + { + // Create folder if it does not exist yet + var dir = Path.GetDirectoryName(filePath); + if (!_fileSystem.FolderExists(dir)) + { + _fileSystem.CreateFolder(dir); + } + + using (item) + { + using var outStream = _fileSystem.GetFile(filePath).OpenWrite(); + item.SaveTo(outStream, mediaFile); + } + } + else if (_fileSystem.FileExists(filePath)) + { + // Remove media storage if any + _fileSystem.DeleteFile(filePath); + } + } + + public async Task SaveAsync(MediaFile mediaFile, MediaStorageItem item) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + + // TODO: (?) if the new file extension differs from the old one then the old file never gets deleted + + var filePath = GetPath(mediaFile); + + if (item != null) + { + // Create folder if it does not exist yet + var dir = Path.GetDirectoryName(filePath); + if (!_fileSystem.FolderExists(dir)) + { + _fileSystem.CreateFolder(dir); + } + + using (item) + { + using var outStream = _fileSystem.GetFile(filePath).OpenWrite(); + await item.SaveToAsync(outStream, mediaFile); + } + } + else if (_fileSystem.FileExists(filePath)) + { + // Remove media storage if any + _fileSystem.DeleteFile(filePath); + } + } + + public void Remove(params MediaFile[] mediaFiles) + { + foreach (var media in mediaFiles) + { + _fileSystem.DeleteFile(GetPath(media)); + } + } + + public void ChangeExtension(MediaFile mediaFile, string extension) + { + Guard.NotNull(mediaFile, nameof(mediaFile)); + Guard.NotEmpty(extension, nameof(extension)); + + var sourcePath = GetPath(mediaFile); + var newPath = Path.ChangeExtension(sourcePath, extension); + + _fileSystem.RenameFile(sourcePath, newPath); + } + + public void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, MediaFile mediaFile) + { + Guard.NotNull(target, nameof(target)); + Guard.NotNull(context, nameof(context)); + Guard.NotNull(mediaFile, nameof(mediaFile)); + + var filePath = GetPath(mediaFile); + + try + { + // Let target store data (into database for example) + target.Receive(context, mediaFile, OpenRead(mediaFile)); + + // Remember file path: we must be able to rollback IO operations on transaction failure + context.AffectedFiles.Add(filePath); + } + catch (Exception exception) + { + Debug.WriteLine(exception.Message); + } + } + + public void Receive(MediaMoverContext context, MediaFile mediaFile, Stream stream) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(mediaFile, nameof(mediaFile)); + + // store data into file + if (stream != null && stream.Length > 0) + { + var filePath = GetPath(mediaFile); + + if (!_fileSystem.FileExists(filePath)) + { + // TBD: (mc) We only save the file if it doesn't exist yet. + // This should save time and bandwidth in the case where the target + // is a cloud based file system (like Azure BLOB). + // In such a scenario it'd be advisable to copy the files manually + // with other - maybe more performant - tools before performing the provider switch. + + // Create folder if it does not exist yet + var dir = Path.GetDirectoryName(filePath); + if (!_fileSystem.FolderExists(dir)) + { + _fileSystem.CreateFolder(dir); + } + + using (stream) + { + _fileSystem.SaveStream(filePath, stream); + } + + context.AffectedFiles.Add(filePath); + } + } + } + + public async Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, Stream stream) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(mediaFile, nameof(mediaFile)); + + // store data into file + if (stream != null && stream.Length > 0) + { + var filePath = GetPath(mediaFile); + + if (!_fileSystem.FileExists(filePath)) + { + // TBD: (mc) We only save the file if it doesn't exist yet. + // This should save time and bandwidth in the case where the target + // is a cloud based file system (like Azure BLOB). + // In such a scenario it'd be advisable to copy the files manually + // with other - maybe more performant - tools before performing the provider switch. + + // Create folder if it does not exist yet + var dir = Path.GetDirectoryName(filePath); + if (!_fileSystem.FolderExists(dir)) + { + _fileSystem.CreateFolder(dir); + } + + using (stream) + { + await _fileSystem.SaveStreamAsync(filePath, stream); + } + + context.AffectedFiles.Add(filePath); + } + } + } + + public void OnCompleted(MediaMoverContext context, bool succeeded) + { + if (context.AffectedFiles.Any()) + { + var toFileSystem = context.TargetSystemName.IsCaseInsensitiveEqual(SystemName); + + if ((!toFileSystem && succeeded) || (toFileSystem && !succeeded)) + { + // FS > DB sucessful OR DB > FS failed/aborted: delete all physical files. + // Run a background task for the deletion of files (fire & forget) + + Task.Factory.StartNew(state => + { + var files = state as string[]; + foreach (var file in files) + { + _fileSystem.DeleteFile(file); + } + }, context.AffectedFiles.ToArray()).ConfigureAwait(false); + } + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/IMediaMover.cs b/src/Libraries/SmartStore.Services/Media/Storage/IMediaMover.cs index 3283aefe32..4bea7cd4e2 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/IMediaMover.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/IMediaMover.cs @@ -2,14 +2,14 @@ namespace SmartStore.Services.Media.Storage { - public interface IMediaMover - { - /// - /// Moves media items from one storage provider to another - /// - /// The source media storage provider - /// The target media storage provider - /// true success, failure - bool Move(Provider sourceProvider, Provider targetProvider); - } + public interface IMediaMover + { + /// + /// Moves media items from one storage provider to another + /// + /// The source media storage provider + /// The target media storage provider + /// true success, failure + bool Move(Provider sourceProvider, Provider targetProvider); + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/IMediaStorageProvider.cs b/src/Libraries/SmartStore.Services/Media/Storage/IMediaStorageProvider.cs index a5d143a262..eeb715cf05 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/IMediaStorageProvider.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/IMediaStorageProvider.cs @@ -5,70 +5,70 @@ namespace SmartStore.Services.Media.Storage { - public interface IMediaStorageProvider : IProvider - { - /// - /// Gets a value indicating whether the provider saves data in a remote cloud storage (e.g. Azure) - /// - bool IsCloudStorage { get; } + public interface IMediaStorageProvider : IProvider + { + /// + /// Gets a value indicating whether the provider saves data in a remote cloud storage (e.g. Azure) + /// + bool IsCloudStorage { get; } - /// - /// Retrieves the public URL for a given file within the storage provider. - /// Returns null if the file is stored as a database BLOB. - /// - /// Media file item - /// The public URL. - string GetPublicUrl(MediaFile mediaFile); + /// + /// Retrieves the public URL for a given file within the storage provider. + /// Returns null if the file is stored as a database BLOB. + /// + /// Media file item + /// The public URL. + string GetPublicUrl(MediaFile mediaFile); - /// - /// Gets the size of the media item in bytes. - /// - /// Media file item - long GetSize(MediaFile mediaFile); + /// + /// Gets the size of the media item in bytes. + /// + /// Media file item + long GetSize(MediaFile mediaFile); - /// - /// Opens the media item for reading - /// - /// Media file item - Stream OpenRead(MediaFile mediaFile); + /// + /// Opens the media item for reading + /// + /// Media file item + Stream OpenRead(MediaFile mediaFile); - /// - /// Loads media item data - /// - /// Media storage item - byte[] Load(MediaFile mediaFile); + /// + /// Loads media item data + /// + /// Media storage item + byte[] Load(MediaFile mediaFile); - /// - /// Asynchronously loads media item data - /// - /// Media file item - Task LoadAsync(MediaFile mediaFile); + /// + /// Asynchronously loads media item data + /// + /// Media file item + Task LoadAsync(MediaFile mediaFile); - /// - /// Saves media item data - /// - /// Media file item - /// File stream - void Save(MediaFile mediaFile, Stream stream); + /// + /// Saves media item data + /// + /// Media file item + /// The source item + void Save(MediaFile mediaFile, MediaStorageItem item); - /// - /// Asynchronously saves media item data - /// - /// Media file item - /// File stream - Task SaveAsync(MediaFile mediaFile, Stream stream); + /// + /// Asynchronously saves media item data + /// + /// Media file item + /// The source item + Task SaveAsync(MediaFile mediaFile, MediaStorageItem item); - /// - /// Remove media storage item(s) - /// - /// Media file items - void Remove(params MediaFile[] mediaFiles); + /// + /// Remove media storage item(s) + /// + /// Media file items + void Remove(params MediaFile[] mediaFiles); - /// - /// Changes the extension of the stored file if the provider supports - /// - /// Media file item - /// The nex file extension - void ChangeExtension(MediaFile mediaFile, string extension); - } + /// + /// Changes the extension of the stored file if the provider supports + /// + /// Media file item + /// The nex file extension + void ChangeExtension(MediaFile mediaFile, string extension); + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/ISupportsMediaMoving.cs b/src/Libraries/SmartStore.Services/Media/Storage/ISupportsMediaMoving.cs index 484685c1d1..7a658c1f2a 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/ISupportsMediaMoving.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/ISupportsMediaMoving.cs @@ -4,37 +4,37 @@ namespace SmartStore.Services.Media.Storage { - public interface ISupportsMediaMoving - { - /// - /// Moves a media item to the target provider - /// - /// Target provider - /// Media storage mover context - /// Media file item - void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, MediaFile mediaFile); + public interface ISupportsMediaMoving + { + /// + /// Moves a media item to the target provider + /// + /// Target provider + /// Media storage mover context + /// Media file item + void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, MediaFile mediaFile); - /// - /// Data received by the source provider to be stored by the target provider - /// - /// Media storage mover context - /// Media file item - /// Source stream - void Receive(MediaMoverContext context, MediaFile mediaFile, Stream stream); + /// + /// Data received by the source provider to be stored by the target provider + /// + /// Media storage mover context + /// Media file item + /// Source stream + void Receive(MediaMoverContext context, MediaFile mediaFile, Stream stream); - /// - /// Data received by the source provider to be stored by the target provider (async) - /// - /// Media storage mover context - /// Media file item - /// Source stream - Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, Stream stream); + /// + /// Data received by the source provider to be stored by the target provider (async) + /// + /// Media storage mover context + /// Media file item + /// Source stream + Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, Stream stream); - /// - /// Called when batch media moving completes - /// - /// Media storage mover context - /// Whether media moving succeeded - void OnCompleted(MediaMoverContext context, bool succeeded); - } + /// + /// Called when batch media moving completes + /// + /// Media storage mover context + /// Whether media moving succeeded + void OnCompleted(MediaMoverContext context, bool succeeded); + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/MediaMover.cs b/src/Libraries/SmartStore.Services/Media/Storage/MediaMover.cs index f7b67d8808..bd5b52efcf 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/MediaMover.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/MediaMover.cs @@ -1,5 +1,7 @@ using System; +using System.Data.Entity; using System.Linq; +using System.Web.UI; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Media; @@ -7,138 +9,116 @@ using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; +using SmartStore.Data.Utilities; namespace SmartStore.Services.Media.Storage { - public class MediaMover : IMediaMover - { - private const int PAGE_SIZE = 100; - - private readonly IRepository _pictureRepository; - private readonly ICommonServices _services; - private readonly ILogger _logger; - - public MediaMover( - IRepository pictureRepository, - ICommonServices services, - ILogger logger) - { - _pictureRepository = pictureRepository; - _services = services; - _logger = logger; - } - - public Localizer T { get; set; } = NullLocalizer.Instance; - - protected virtual void PageEntities(IOrderedQueryable query, Action moveEntity) where TEntity : BaseEntity, IHasMedia - { - var pageIndex = 0; - IPagedList entities = null; - - do - { - if (entities != null) - { - // detach all entities from previous page to save memory - _services.DbContext.DetachEntities(entities); - entities.Clear(); - entities = null; - } - - // load max 100 entities at once - entities = new PagedList(query, pageIndex++, PAGE_SIZE); - - entities.Each(x => moveEntity(x)); - - // save the current batch to database - _services.DbContext.SaveChanges(); - } - while (entities.HasNextPage); - } - - public virtual bool Move(Provider sourceProvider, Provider targetProvider) - { - Guard.NotNull(sourceProvider, nameof(sourceProvider)); - Guard.NotNull(targetProvider, nameof(targetProvider)); - - var success = false; - var utcNow = DateTime.UtcNow; - var context = new MediaMoverContext(sourceProvider.Metadata.SystemName, targetProvider.Metadata.SystemName); - - var source = sourceProvider.Value as ISupportsMediaMoving; - var target = targetProvider.Value as ISupportsMediaMoving; - - // source and target must support media storage moving - if (source == null) - { - throw new ArgumentException(T("Admin.Media.StorageMovingNotSupported", sourceProvider.Metadata.SystemName)); - } - - if (target == null) - { - throw new ArgumentException(T("Admin.Media.StorageMovingNotSupported", targetProvider.Metadata.SystemName)); - } - - // source and target provider must not be equal - if (sourceProvider.Metadata.SystemName.IsCaseInsensitiveEqual(targetProvider.Metadata.SystemName)) - { - throw new ArgumentException(T("Admin.Media.CannotMoveToSameProvider")); - } - - // we are about to process data in chunks but want to commit ALL at once when ALL chunks have been processed successfully. - // autoDetectChanges true required for newly inserted binary data. - using (var scope = new DbContextScope(ctx: _services.DbContext, - autoDetectChanges: true, - proxyCreation: false, - validateOnSave: false, - autoCommit: false)) - { - using (var transaction = _services.DbContext.BeginTransaction()) - { - try - { - // Files - var queryFiles = _pictureRepository.Table.OrderBy(x => x.Id); - - PageEntities(queryFiles, file => - { - // move item from source to target - source.MoveTo(target, context, file); - - file.UpdatedOnUtc = utcNow; - ++context.MovedItems; - }); - - transaction.Commit(); - success = true; - } - catch (Exception exception) - { - success = false; - transaction.Rollback(); - - _services.Notifier.Error(exception.Message); - _logger.Error(exception); - } - } - } - - - if (success) - { - _services.Settings.SetSetting("Media.Storage.Provider", targetProvider.Metadata.SystemName); - } - - // inform both provider about ending - source.OnCompleted(context, success); - target.OnCompleted(context, success); - - if (success && context.ShrinkDatabase) - { - _services.DbContext.ShrinkDatabase(); - } - - return success; - } - } + public class MediaMover : IMediaMover + { + private const int PAGE_SIZE = 50; + + private readonly IRepository _mediaFileRepo; + private readonly ICommonServices _services; + private readonly ILogger _logger; + + public MediaMover(IRepository mediaFileRepo, ICommonServices services, ILogger logger) + { + _mediaFileRepo = mediaFileRepo; + _services = services; + _logger = logger; + } + + public Localizer T { get; set; } = NullLocalizer.Instance; + + public virtual bool Move(Provider sourceProvider, Provider targetProvider) + { + Guard.NotNull(sourceProvider, nameof(sourceProvider)); + Guard.NotNull(targetProvider, nameof(targetProvider)); + + var success = false; + var utcNow = DateTime.UtcNow; + var context = new MediaMoverContext(sourceProvider.Metadata.SystemName, targetProvider.Metadata.SystemName); + + var source = sourceProvider.Value as ISupportsMediaMoving; + var target = targetProvider.Value as ISupportsMediaMoving; + + // Source and target must support media storage moving + if (source == null) + { + throw new ArgumentException(T("Admin.Media.StorageMovingNotSupported", sourceProvider.Metadata.SystemName)); + } + + if (target == null) + { + throw new ArgumentException(T("Admin.Media.StorageMovingNotSupported", targetProvider.Metadata.SystemName)); + } + + // Source and target provider must not be equal + if (sourceProvider.Metadata.SystemName.IsCaseInsensitiveEqual(targetProvider.Metadata.SystemName)) + { + throw new ArgumentException(T("Admin.Media.CannotMoveToSameProvider")); + } + + // We are about to process data in chunks but want to commit ALL at once after ALL chunks have been processed successfully. + // AutoDetectChanges true required for newly inserted binary data. + using (var scope = new DbContextScope(ctx: _services.DbContext, + autoDetectChanges: true, + proxyCreation: false, + validateOnSave: false, + autoCommit: false)) + { + using (var transaction = scope.DbContext.BeginTransaction()) + { + try + { + var pager = new FastPager(_mediaFileRepo.Table, PAGE_SIZE); + while (pager.ReadNextPage(out var files)) + { + foreach (var file in files) + { + // Move item from source to target + source.MoveTo(target, context, file); + + file.UpdatedOnUtc = utcNow; + ++context.MovedItems; + } + + scope.DbContext.SaveChanges(); + + // Detach all entities from previous page to save memory + scope.DbContext.DetachEntities(files, deep: true); + } + + transaction.Commit(); + success = true; + } + catch (Exception exception) + { + success = false; + transaction.Rollback(); + + _services.Notifier.Error(exception); + _logger.Error(exception); + } + } + } + + + if (success) + { + _services.Settings.SetSetting("Media.Storage.Provider", targetProvider.Metadata.SystemName); + } + + // inform both provider about ending + source.OnCompleted(context, success); + target.OnCompleted(context, success); + + if (success && context.ShrinkDatabase) + { + _services.DbContext.ShrinkDatabase(); + } + + return success; + } + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/MediaMoverContext.cs b/src/Libraries/SmartStore.Services/Media/Storage/MediaMoverContext.cs index f0d8fc8068..46465ef509 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/MediaMoverContext.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/MediaMoverContext.cs @@ -2,44 +2,44 @@ namespace SmartStore.Services.Media.Storage { - public class MediaMoverContext - { - internal MediaMoverContext(string sourceSystemName, string targetSystemName) - { - SourceSystemName = sourceSystemName; - TargetSystemName = targetSystemName; - AffectedFiles = new List(); - CustomProperties = new Dictionary(); - } + public class MediaMoverContext + { + internal MediaMoverContext(string sourceSystemName, string targetSystemName) + { + SourceSystemName = sourceSystemName; + TargetSystemName = targetSystemName; + AffectedFiles = new List(); + CustomProperties = new Dictionary(); + } - /// - /// The system name of the source provider - /// - public string SourceSystemName { get; private set; } + /// + /// The system name of the source provider + /// + public string SourceSystemName { get; private set; } - /// - /// The system name of the target provider - /// - public string TargetSystemName { get; private set; } + /// + /// The system name of the target provider + /// + public string TargetSystemName { get; private set; } - /// - /// Current number of moved media items - /// - public int MovedItems { get; internal set; } + /// + /// Current number of moved media items + /// + public int MovedItems { get; internal set; } - /// - /// Paths of affected media files - /// - public List AffectedFiles { get; private set; } + /// + /// Paths of affected media files + /// + public List AffectedFiles { get; private set; } - /// - /// Whether to shrink database after succesful moving - /// - public bool ShrinkDatabase { get; set; } + /// + /// Whether to shrink database after succesful moving + /// + public bool ShrinkDatabase { get; set; } - /// - /// Use this dictionary for any custom data required along the move operation - /// - public Dictionary CustomProperties { get; set; } - } + /// + /// Use this dictionary for any custom data required along the move operation + /// + public Dictionary CustomProperties { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageItem.cs b/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageItem.cs new file mode 100644 index 0000000000..9fb06c72a4 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageItem.cs @@ -0,0 +1,161 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; +using SmartStore.Services.Media.Imaging; + +namespace SmartStore.Services.Media.Storage +{ + public abstract class MediaStorageItem : IDisposable + { + private Stream _sourceStream; + + public Stream SourceStream + { + get + { + if (_sourceStream == null) + { + _sourceStream = GetSourceStream(); + } + + if (_sourceStream.CanSeek) + { + _sourceStream.Position = 0; + } + + return _sourceStream; + } + } + + protected abstract Stream GetSourceStream(); + public abstract void SaveTo(Stream stream, MediaFile mediaFile); + public abstract Task SaveToAsync(Stream stream, MediaFile mediaFile); + + protected static int GetLength(Stream stream) + { + if (stream.CanSeek) + { + return (int)stream.Length; + } + else if (stream.Position > 0) + { + return (int)stream.Position; + } + + return 0; + } + + public virtual void Dispose() + { + if (_sourceStream != null) + { + _sourceStream.Dispose(); + _sourceStream = null; + } + } + + #region Factories + + public static MediaStorageItem FromImage(IImage image) + { + return new ImageStorageItem(image); + } + + public static MediaStorageItem FromStream(Stream stream) + { + return new StreamStorageItem(stream); + } + + public static MediaStorageItem FromFile(IFile file) + { + return new StreamStorageItem(file.OpenRead()); + } + + #endregion + + #region Impls + + public class ImageStorageItem : MediaStorageItem + { + private readonly IImage _image; + + public ImageStorageItem(IImage image) + { + _image = image; + } + + protected override Stream GetSourceStream() + { + var memStream = new MemoryStream(); + _image.Save(memStream); + return memStream; + } + + public override void SaveTo(Stream stream, MediaFile mediaFile) + { + _image.Save(stream); + mediaFile.Size = GetLength(stream); + } + + public override Task SaveToAsync(Stream stream, MediaFile mediaFile) + { + _image.Save(stream); + mediaFile.Size = GetLength(stream); + return Task.CompletedTask; + } + } + + public class StreamStorageItem : MediaStorageItem + { + private readonly Stream _stream; + + public StreamStorageItem(Stream stream) + { + _stream = stream; + } + + protected override Stream GetSourceStream() + { + return _stream; + } + + public override void SaveTo(Stream stream, MediaFile mediaFile) + { + if (stream.CanSeek) + { + stream.SetLength(0); + } + + SourceStream.CopyTo(stream); + + if (stream.CanSeek) + { + stream.Position = 0; + } + + mediaFile.Size = GetLength(stream); + } + + public override async Task SaveToAsync(Stream stream, MediaFile mediaFile) + { + if (stream.CanSeek) + { + stream.SetLength(0); + } + + await SourceStream.CopyToAsync(stream); + + if (stream.CanSeek) + { + stream.Position = 0; + } + + mediaFile.Size = GetLength(stream); + } + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Tracking/IMediaTrackDetector.cs b/src/Libraries/SmartStore.Services/Media/Tracking/IMediaTrackDetector.cs similarity index 100% rename from src/Libraries/SmartStore.Services/Media/v4/Tracking/IMediaTrackDetector.cs rename to src/Libraries/SmartStore.Services/Media/Tracking/IMediaTrackDetector.cs diff --git a/src/Libraries/SmartStore.Services/Media/v4/Tracking/IMediaTracker.cs b/src/Libraries/SmartStore.Services/Media/Tracking/IMediaTracker.cs similarity index 96% rename from src/Libraries/SmartStore.Services/Media/v4/Tracking/IMediaTracker.cs rename to src/Libraries/SmartStore.Services/Media/Tracking/IMediaTracker.cs index 77f845922f..4d3d3f4730 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Tracking/IMediaTracker.cs +++ b/src/Libraries/SmartStore.Services/Media/Tracking/IMediaTracker.cs @@ -18,7 +18,7 @@ public interface IMediaTracker void Track(TSetting settings, int? prevMediaFileId, Expression> path) where TSetting : ISettings, new(); void TrackMany(IEnumerable tracks); - void TrackMany(string albumName, IEnumerable tracks, bool isMigration); + void TrackMany(string albumName, IEnumerable tracks, bool isMigration = false); int DeleteAllTracks(string albumName); void DetectAllTracks(string albumName, bool isMigration = false); diff --git a/src/Libraries/SmartStore.Services/Media/v4/Tracking/MediaTracker.cs b/src/Libraries/SmartStore.Services/Media/Tracking/MediaTracker.cs similarity index 94% rename from src/Libraries/SmartStore.Services/Media/v4/Tracking/MediaTracker.cs rename to src/Libraries/SmartStore.Services/Media/Tracking/MediaTracker.cs index 7c1a797707..fae7b431e9 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Tracking/MediaTracker.cs +++ b/src/Libraries/SmartStore.Services/Media/Tracking/MediaTracker.cs @@ -17,11 +17,14 @@ using SmartStore.Core.Configuration; using System.Linq.Expressions; using SmartStore.ComponentModel; +using SmartStore.Core.Localization; namespace SmartStore.Services.Media { public class MediaTracker : IMediaTracker { + public Localizer T { get; set; } = NullLocalizer.Instance; + internal const string TrackedPropertiesKey = "media:trackedprops:all"; private readonly ICacheManager _cache; @@ -82,9 +85,9 @@ public IDisposable BeginScope(bool makeFilesTransientWhenOrphaned) } protected void TrackSetting( - TSetting settings, + TSetting settings, string propertyName, - int? prevMediaFileId, + int? prevMediaFileId, int? currentMediaFileId) where TSetting : ISettings, new() { Guard.NotNull(settings, nameof(settings)); @@ -129,7 +132,7 @@ protected virtual void TrackSingle(BaseEntity entity, int mediaFileId, string pr var album = _folderService.FindAlbum(file)?.Value; if (album == null) { - throw new InvalidOperationException("Cannot track a media file that is not assigned to any album."); + throw new InvalidOperationException(T("Admin.Media.Exception.TrackUnassignedFile")); } else if (!album.CanDetectTracks) { @@ -196,13 +199,13 @@ protected virtual void TrackManyCore(IEnumerable tracks, string albu var ctx = _dbContext; - using (var scope = new DbContextScope(ctx, - validateOnSave: false, - hooksEnabled: false, + using (var scope = new DbContextScope(ctx, + validateOnSave: false, + hooksEnabled: false, autoDetectChanges: false)) { // Get the album (necessary later to set FolderId)... - MediaFolderNode albumNode = albumName.HasValue() + MediaFolderNode albumNode = albumName.HasValue() ? _folderService.GetNodeByPath(albumName)?.Value : null; @@ -250,7 +253,7 @@ protected virtual void TrackManyCore(IEnumerable tracks, string albu if (track.Album.IsEmpty()) continue; // cannot track without album - if ((albumNode ?? _folderService.FindAlbum(file).Value).CanDetectTracks == false) + if ((albumNode ?? _folderService.FindAlbum(file)?.Value)?.CanDetectTracks == false) continue; // should not track in albums that do not support track detection // add or remove the track from file @@ -265,7 +268,7 @@ protected virtual void TrackManyCore(IEnumerable tracks, string albu { file.Tracks.Remove(track); _dbContext.ChangeState(dbTrack, System.Data.Entity.EntityState.Deleted); - } + } } if (file.Tracks.Count > 0) @@ -284,15 +287,18 @@ protected virtual void TrackManyCore(IEnumerable tracks, string albu // Save whole batch to database int num = ctx.SaveChanges(); - // Breathe - ctx.DetachEntities(deep: true); + if (num > 0) + { + // Breathe + ctx.DetachEntities(deep: true); + } } } public int DeleteAllTracks(string albumName) { Guard.NotEmpty(albumName, nameof(albumName)); - + string sql = "DELETE FROM [MediaTrack] WHERE [Album] = '{0}'".FormatCurrent(albumName); return _dbContext.ExecuteSqlCommand(sql); } @@ -305,14 +311,14 @@ public void DetectAllTracks(string albumName, bool isMigration = false) var albumInfo = _albumRegistry.GetAlbumByName(albumName); if (albumInfo == null) { - throw new InvalidOperationException($"The album '{albumName}' does not exist."); + throw new InvalidOperationException(T("Admin.Media.Exception.AlbumNonexistent", albumName)); } // load corresponding detector provider for current album... var provider = _albumProviderFactory[albumInfo.ProviderType] as IMediaTrackDetector; if (provider == null) { - throw new InvalidOperationException($"The album '{albumName}' does not support track detection."); + throw new InvalidOperationException(T("Admin.Media.Exception.AlbumNoTrack", albumName)); } if (!isMigration) diff --git a/src/Libraries/SmartStore.Services/Media/v4/Tracking/MediaTrackerHook.cs b/src/Libraries/SmartStore.Services/Media/Tracking/MediaTrackerHook.cs similarity index 83% rename from src/Libraries/SmartStore.Services/Media/v4/Tracking/MediaTrackerHook.cs rename to src/Libraries/SmartStore.Services/Media/Tracking/MediaTrackerHook.cs index 0802b1af31..82f77370f1 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Tracking/MediaTrackerHook.cs +++ b/src/Libraries/SmartStore.Services/Media/Tracking/MediaTrackerHook.cs @@ -1,10 +1,5 @@ using System; using System.Collections.Generic; -using System.Data.Entity.Infrastructure; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; using SmartStore.ComponentModel; using SmartStore.Core; using SmartStore.Core.Data; @@ -15,8 +10,8 @@ namespace SmartStore.Services.Media { [Important] - public sealed class MediaTrackerHook : DbSaveHook - { + public sealed class MediaTrackerHook : DbSaveHook + { // Track items for the current (SaveChanges) unit. private readonly HashSet _actionsUnit = new HashSet(); @@ -39,29 +34,29 @@ public MediaTrackerHook(Lazy mediaTracker, IDbContext dbContext) protected override void OnUpdating(BaseEntity entity, IHookedEntity entry) { - HookObject(entity, entry, true); + HookObject(entry, true); } protected override void OnDeleted(BaseEntity entity, IHookedEntity entry) { - HookObject(entity, entry, false); + HookObject(entry, false); } protected override void OnInserted(BaseEntity entity, IHookedEntity entry) { - HookObject(entity, entry, false); + HookObject(entry, false); } protected override void OnUpdated(BaseEntity entity, IHookedEntity entry) { - HookObject(entity, entry, false); + HookObject(entry, false); } - private void HookObject(BaseEntity entity, IHookedEntity entry, bool beforeSave) + private void HookObject(IHookedEntity entry, bool beforeSave) { if (Silent) return; - + var type = entry.EntityType; if (!_mediaTracker.Value.TryGetTrackedPropertiesFor(type, out var properties)) @@ -70,6 +65,7 @@ private void HookObject(BaseEntity entity, IHookedEntity entry, bool beforeSave) } var state = entry.InitialState; + var actions = new HashSet(); foreach (var prop in properties) { @@ -77,8 +73,6 @@ private void HookObject(BaseEntity entity, IHookedEntity entry, bool beforeSave) { if (entry.Entry.TryGetModifiedProperty(_dbContext, prop.Name, out object prevValue)) { - var actions = new HashSet(); - // Untrack the previous file relation (if not null) TryAddTrack(prop.Album, entry.Entity, prop.Name, prevValue, MediaTrackOperation.Untrack, actions); @@ -98,7 +92,7 @@ private void HookObject(BaseEntity entity, IHookedEntity entry, bool beforeSave) TryAddTrack(prop.Album, entry.Entity, prop.Name, value, state == EntityState.Added ? MediaTrackOperation.Track : MediaTrackOperation.Untrack); break; case EntityState.Modified: - if (_actionsTemp.TryGetValue(entry.Entity, out var actions)) + if (_actionsTemp.TryGetValue(entry.Entity, out actions)) { _actionsUnit.AddRange(actions); } @@ -116,13 +110,13 @@ private void TryAddTrack(string album, BaseEntity entity, string prop, object va if ((int)value > 0) { (actions ?? _actionsUnit).Add(new MediaTrack - { + { Album = album, - EntityId = entity.Id, - EntityName = entity.GetEntityName(), + EntityId = entity.Id, + EntityName = entity.GetEntityName(), Property = prop, - MediaFileId = (int)value, - Operation = operation + MediaFileId = (int)value, + Operation = operation }); } } @@ -143,7 +137,6 @@ public override void OnAfterSaveCompleted() var tracker = _mediaTracker.Value; using (tracker.BeginScope(false)) { - // TODO: (mm) make media setting: MakeFilesTransientWhenOrphaned tracker.TrackMany(_actionsUnit); } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Tracking/TrackedMediaPropertyTable.cs b/src/Libraries/SmartStore.Services/Media/Tracking/TrackedMediaPropertyTable.cs similarity index 99% rename from src/Libraries/SmartStore.Services/Media/v4/Tracking/TrackedMediaPropertyTable.cs rename to src/Libraries/SmartStore.Services/Media/Tracking/TrackedMediaPropertyTable.cs index 23a21336e4..c85cf70d0e 100644 --- a/src/Libraries/SmartStore.Services/Media/v4/Tracking/TrackedMediaPropertyTable.cs +++ b/src/Libraries/SmartStore.Services/Media/Tracking/TrackedMediaPropertyTable.cs @@ -13,7 +13,7 @@ namespace SmartStore.Services.Media public class TrackedMediaPropertyTable { private readonly IList _propertyList = new List(); - + protected internal TrackedMediaPropertyTable(string album) { Album = album; diff --git a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs index 8e2b0bc6c1..0c1c48f8ab 100644 --- a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs +++ b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs @@ -16,42 +16,42 @@ public partial class TransientMediaClearTask : AsyncTask { private readonly IMediaService _mediaService; private readonly IDownloadService _downloadService; - private readonly IRepository _fileRepository; - private readonly IRepository _downloadRepository; + private readonly IRepository _fileRepository; + private readonly IRepository _downloadRepository; - public TransientMediaClearTask( + public TransientMediaClearTask( IMediaService mediaService, IDownloadService downloadService, - IRepository fileRepository, - IRepository downloadRepository) + IRepository fileRepository, + IRepository downloadRepository) { _mediaService = mediaService; _downloadService = downloadService; - _fileRepository = fileRepository; - _downloadRepository = downloadRepository; + _fileRepository = fileRepository; + _downloadRepository = downloadRepository; } - public override async Task ExecuteAsync(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { - // Delete all media records which are in transient state since at least 3 hours. - var olderThan = DateTime.UtcNow.AddHours(-3); - var fileAutoCommit = _fileRepository.AutoCommitEnabled; + // Delete all media records which are in transient state since at least 3 hours. + var olderThan = DateTime.UtcNow.AddHours(-3); + var fileAutoCommit = _fileRepository.AutoCommitEnabled; var downloadAutoCommit = _downloadRepository.AutoCommitEnabled; - + _fileRepository.AutoCommitEnabled = false; _downloadRepository.AutoCommitEnabled = false; - try - { - using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) - { - var files = await _fileRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToListAsync(); - foreach (var file in files) - { + try + { + using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) + { + var files = await _fileRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToListAsync(); + foreach (var file in files) + { _mediaService.DeleteFile(file, true); - } + } - await _fileRepository.Context.SaveChangesAsync(); + await _fileRepository.Context.SaveChangesAsync(); var downloads = await _downloadRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToListAsync(); foreach (var download in downloads) @@ -72,10 +72,10 @@ public override async Task ExecuteAsync(TaskExecutionContext ctx) } } finally - { - _fileRepository.AutoCommitEnabled = fileAutoCommit; + { + _fileRepository.AutoCommitEnabled = fileAutoCommit; _downloadRepository.AutoCommitEnabled = downloadAutoCommit; - } + } } } } diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/IAlbumProvider.cs b/src/Libraries/SmartStore.Services/Media/v4/Album/IAlbumProvider.cs deleted file mode 100644 index ba2d00b9e5..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/IAlbumProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using SmartStore.Core.Domain.Media; - -namespace SmartStore.Services.Media -{ - public interface IAlbumProvider - { - IEnumerable GetAlbums(); - AlbumDisplayHint GetDisplayHint(MediaAlbum album); - } -} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Media/v4/Album/IFolderService.cs b/src/Libraries/SmartStore.Services/Media/v4/Album/IFolderService.cs deleted file mode 100644 index 00b84d4990..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Album/IFolderService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using SmartStore.Collections; -using SmartStore.Core.Domain.Media; - -namespace SmartStore.Services.Media -{ - public interface IFolderService : IScopedService - { - TreeNode GetRootNode(); - TreeNode GetNodeById(int id); - TreeNode GetNodeByPath(string path); - bool CheckUniqueFolderName(string path, out string newName); - - MediaFolder GetFolderById(int id, bool withFiles = false); - void InsertFolder(MediaFolder folder); - void DeleteFolder(MediaFolder folder); - void UpdateFolder(MediaFolder folder); - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Exceptions.cs b/src/Libraries/SmartStore.Services/Media/v4/Exceptions.cs deleted file mode 100644 index 4ca15675da..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Exceptions.cs +++ /dev/null @@ -1,64 +0,0 @@ -using SmartStore.Core.Domain.Media; -using System; - -namespace SmartStore.Services.Media -{ - public sealed class MediaFileNotFoundException : SmartException - { - public MediaFileNotFoundException(string path) - : base($"Media file {path} does not exist.") - { - } - } - - public sealed class MediaFolderNotFoundException : SmartException - { - public MediaFolderNotFoundException(string path) - : base($"Media folder {path} does not exist.") - { - } - } - - public sealed class DuplicateMediaFileException : SmartException - { - public DuplicateMediaFileException(string fullPath, MediaFileInfo dupeFile) - : base($"File {fullPath} already exists.") - { - File = dupeFile; - } - - public MediaFileInfo File { get; } - } - - public sealed class DuplicateMediaFolderException : SmartException - { - public DuplicateMediaFolderException(string fullPath) - : base($"Folder {fullPath} already exists.") - { - } - } - - public sealed class NotSameAlbumException : SmartException - { - public NotSameAlbumException(string path1, string path2) - : base($"The file operation requires that the destination path belongs to the source album. Source: {path1}, Destination: {path2}.") - { - } - } - - public sealed class DeniedMediaTypeException : SmartException - { - public DeniedMediaTypeException(string fileName, string currentType, string[] acceptedTypes) - : base($"The media type of '{fileName}' does not match the list of accesped media types. Accepted: {string.Join(", ", acceptedTypes)}, current: {currentType}") - { - } - } - - public sealed class ExtractThumbnailException : SmartException - { - public ExtractThumbnailException(string path, Exception innerException) - : base($"Thumbnail extraction for file '{path}' failed. See inner exception for details.", innerException) - { - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Extensions/IMediaServiceExtensions.cs b/src/Libraries/SmartStore.Services/Media/v4/Extensions/IMediaServiceExtensions.cs deleted file mode 100644 index ee8a0742f3..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Extensions/IMediaServiceExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.IO; - -namespace SmartStore.Services.Media -{ - public static class IMediaServiceExtensions - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MediaSearchResult SearchFiles(this IMediaService service, MediaSearchQuery query, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking) - { - return service.SearchFiles(query, null, flags); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async Task SearchFilesAsync(this IMediaService service, MediaSearchQuery query, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking) - { - return await service.SearchFilesAsync(query, null, flags); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetUrl(this IMediaService service, int? fileId, ProcessImageQuery imageQuery, string host = null, bool doFallback = true) - { - return service.GetUrl(service.GetFileById(fileId ?? 0), imageQuery, host, doFallback); - } - - public static string GetUrl(this IMediaService service, int? fileId, int thumbnailSize, string host = null, bool doFallback = true) - { - ProcessImageQuery query = thumbnailSize > 0 - ? new ProcessImageQuery { MaxSize = thumbnailSize } - : null; - - return service.GetUrl(service.GetFileById(fileId ?? 0), query, host, doFallback); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetUrl(this IMediaService service, MediaFile file, int thumbnailSize, string host = null, bool doFallback = true) - { - ProcessImageQuery query = thumbnailSize > 0 - ? new ProcessImageQuery { MaxSize = thumbnailSize } - : null; - - return service.GetUrl(file != null ? service.ConvertMediaFile(file) : null, query, host, doFallback); - } - - public static string GetUrl(this IMediaService service, MediaFileInfo file, int thumbnailSize, string host = null, bool doFallback = true) - { - ProcessImageQuery query = thumbnailSize > 0 - ? new ProcessImageQuery { MaxSize = thumbnailSize } - : null; - - return service.GetUrl(file, query, host, doFallback); - } - - public static string GetFallbackUrl(this IMediaService service, int thumbnailSize = 0) - { - ProcessImageQuery query = thumbnailSize > 0 - ? new ProcessImageQuery { MaxSize = thumbnailSize } - : null; - - return service.GetUrl((MediaFileInfo)null, query, null, true); - } - - /// - /// Tries to find an equal file by comparing the source buffer to a list of files. - /// - /// Binary source file data to find a match for. - /// The sequence of files to seek within for duplicates. - /// Id of equal file if any - /// The passed file binary when no file equals in the sequence, null otherwise. - public static byte[] FindEqualFile(this IMediaService service, byte[] sourceBuffer, IEnumerable files, out int equalFileId) - { - Guard.NotNull(sourceBuffer, nameof(sourceBuffer)); - - if (!service.FindEqualFile(new MemoryStream(sourceBuffer), files, false, out equalFileId)) - { - return sourceBuffer; - } - - return null; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Extensions/IMediaTrackerExtensions.cs b/src/Libraries/SmartStore.Services/Media/v4/Extensions/IMediaTrackerExtensions.cs deleted file mode 100644 index 6d56f2d18e..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Extensions/IMediaTrackerExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Runtime.CompilerServices; -using SmartStore.Core; -using SmartStore.Core.Domain.Media; - -namespace SmartStore.Services.Media -{ - public static class IMediaTrackerExtensions - { - public static void Track(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() - { - Guard.NotNull(path, nameof(path)); - - var getter = path.CompileFast(); - var mediaFileId = getter.Invoke(entity); - if (mediaFileId > 0) - tracker.Track(entity, mediaFileId, path.ExtractPropertyInfo().Name); - } - - public static void Track(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() - { - Guard.NotNull(path, nameof(path)); - - var getter = path.CompileFast(); - var mediaFileId = getter.Invoke(entity); - if (mediaFileId > 0) - tracker.Track(entity, mediaFileId.Value, path.ExtractPropertyInfo().Name); - } - - public static void Untrack(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() - { - Guard.NotNull(path, nameof(path)); - - var getter = path.CompileFast(); - var mediaFileId = getter.Invoke(entity); - if (mediaFileId > 0) - tracker.Untrack(entity, mediaFileId, path.ExtractPropertyInfo().Name); - } - - public static void Untrack(this IMediaTracker tracker, T entity, Expression> path) where T : BaseEntity, new() - { - Guard.NotNull(path, nameof(path)); - - var getter = path.CompileFast(); - var mediaFileId = getter.Invoke(entity); - if (mediaFileId > 0) - tracker.Untrack(entity, mediaFileId.Value, path.ExtractPropertyInfo().Name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Track(this IMediaTracker tracker, MediaTrack track) - { - if (track == null) - return; - - tracker.TrackMany(new[] { track }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UpdateTracks(this IMediaTracker tracker, BaseEntity entity, int? prevMediaFileId, int? currentMediaFileId, string propertyName) - { - UpdateTracks(tracker, entity.Id, entity.GetEntityName(), prevMediaFileId, currentMediaFileId, propertyName); - } - - public static void UpdateTracks(this IMediaTracker tracker, int entityId, string entityName, int? prevMediaFileId, int? currentMediaFileId, string propertyName) - { - var tracks = new List(2); - bool isModified = prevMediaFileId != currentMediaFileId; - - if (prevMediaFileId > 0 && isModified) - { - tracks.Add(new MediaTrack { EntityId = entityId, - EntityName = entityName, - MediaFileId = prevMediaFileId.Value, - Property = propertyName, - Operation = MediaTrackOperation.Untrack }); - } - - if (currentMediaFileId > 0 && isModified) - { - tracks.Add(new MediaTrack { EntityId = entityId, - EntityName = entityName, - MediaFileId = currentMediaFileId.Value, - Property = propertyName, - Operation = MediaTrackOperation.Track }); - } - - if (tracks.Count > 0) - { - tracker.TrackMany(tracks); - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Extensions/MediaFileExtensions.cs b/src/Libraries/SmartStore.Services/Media/v4/Extensions/MediaFileExtensions.cs deleted file mode 100644 index 1e567ee1e4..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Extensions/MediaFileExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.IO; -using SmartStore.Core; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.IO; -using SmartStore.Services.Media.Storage; - -namespace SmartStore.Services.Media -{ - public static class MediaFileExtensions - { - /// - /// Applies Blob to file - /// - /// The file binary (can be null) - public static void ApplyBlob(this MediaFile file, byte[] blob) - { - Guard.NotNull(file, nameof(file)); - - if (blob == null || blob.LongLength == 0) - { - file.MediaStorageId = null; - file.MediaStorage = null; - } - else - { - if (file.MediaStorage != null) - { - file.MediaStorage.Data = blob; - } - else - { - file.MediaStorage = new MediaStorage { Data = blob }; - } - } - } - - /// - /// Refreshes file metadata like size, dimensions etc. - /// - /// The file stream (can be null) - public static void RefreshMetadata(this MediaFile file, Stream stream) - { - Guard.NotNull(file, nameof(file)); - - file.Size = stream != null ? (int)stream.Length : 0; - file.Width = null; - file.Height = null; - file.PixelSize = null; - - if (stream != null && file.MediaType == MediaType.Image) - { - try - { - var size = ImageHeader.GetDimensions(stream, file.MimeType); - file.Width = size.Width; - file.Height = size.Height; - file.PixelSize = size.Width * size.Height; - } - catch { } - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/Handlers/MediaHandlerContext.cs b/src/Libraries/SmartStore.Services/Media/v4/Handlers/MediaHandlerContext.cs deleted file mode 100644 index 73162640df..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/Handlers/MediaHandlerContext.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; -using System.Web.Mvc; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.IO; -using SmartStore.Core.Security; -using SmartStore.Utilities; - -namespace SmartStore.Services.Media -{ - public class MediaHandlerContext - { - private Lazy _lazySourceFile; - private IFile _sourceFile; - - public MediaHandlerContext() - { - _lazySourceFile = new Lazy(() => GetSourceFile(), false); - } - - public IMediaService MediaService { get; set; } - public IPermissionService PermissionService { get; set; } - public Customer CurrentCustomer { get; set; } - public HttpContextBase HttpContext { get; set; } - - public int MediaFileId { get; set; } - public string RawPath { get; set; } - - public MediaPathData PathData { get; set; } - public ProcessImageQuery ImageQuery { get; set; } - - public IFile SourceFile - { - get => _sourceFile ?? _lazySourceFile.Value; - set => _sourceFile = value; - } - - public bool Executed { get; set; } - public Exception Exception { get; set; } - public IFile ResultFile { get; set; } - public Stream ResultStream { get; set; } - - private IFile GetSourceFile() - { - if (MediaFileId == 0) - { - // This is most likely a request for a default placeholder image - var fallbackImagePath = Path.Combine(MediaUrlGenerator.FallbackImagesRootPath, RawPath).Replace('\\', '/'); - var fi = new FileInfo(CommonHelper.MapPath("~/" + fallbackImagePath, false)); - if (!fi.Exists) - return null; - - return new LocalFileSystem.LocalFile(fallbackImagePath, fi); - } - else - { - // Get file from DB - var mediaFile = MediaService.GetFileById(MediaFileId, MediaLoadFlags.AsNoTracking); - - // File must exist - if (mediaFile == null) - return null; - - // Serve deleted or hidden files only with sufficient permission - if ((mediaFile.Deleted || mediaFile.Hidden) && !PermissionService.Authorize(Permissions.Media.Update, CurrentCustomer)) - return null; - - //// File's mime must match requested mime - //if (!mediaFile.MimeType.IsCaseInsensitiveEqual(prevMime ?? pathData.MimeType)) - // return null; - - // File path must match requested path and file title - // TODO: (mm) what about MIME and EXT? - if (mediaFile.FolderId != PathData.Folder?.Id || !mediaFile.Title.IsCaseInsensitiveEqual(PathData.FileTitle)) - return null; - - return mediaFile; - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/IMediaService.cs b/src/Libraries/SmartStore.Services/Media/v4/IMediaService.cs deleted file mode 100644 index 4bca883084..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/IMediaService.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using SmartStore.Core.Domain.Media; -using SmartStore.Services.Media.Storage; - -namespace SmartStore.Services.Media -{ - [Flags] - public enum MediaLoadFlags - { - None = 0, - WithBlob = 1 << 0, - WithTags = 1 << 1, - WithTracks = 1 << 2, - WithFolder = 1 << 3, - AsNoTracking = 1 << 4, - Full = WithBlob | WithTags | WithTracks | WithFolder, - FullNoTracking = Full | AsNoTracking - } - - public enum SpecialMediaFolder - { - AllFiles = -500, - Trash = -400, - Orphans = -300, - TransientFiles = -200, - UnassignedFiles = -100 - } - - public enum FileHandling - { - SoftDelete, - MoveToRoot, - Delete - } - - public enum DuplicateFileHandling - { - ThrowError, - Overwrite, - Rename - } - - public enum DuplicateEntryHandling - { - ThrowError, - Overwrite, - // Folder: Overwrite, File: Rename - Rename, - Skip - } - - public partial interface IMediaService - { - /// - /// Gets the underlying storage provider - /// - IMediaStorageProvider StorageProvider { get; } - - int CountFiles(MediaSearchQuery query); - Task CountFilesAsync(MediaSearchQuery query); - FileCountResult CountFilesGrouped(MediaFilesFilter filter); - MediaSearchResult SearchFiles(MediaSearchQuery query, Func, IQueryable> queryModifier, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); - Task SearchFilesAsync(MediaSearchQuery query, Func, IQueryable> queryModifier, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); - - bool FileExists(string path); - MediaFileInfo GetFileByPath(string path, MediaLoadFlags flags = MediaLoadFlags.None); - MediaFileInfo GetFileById(int id, MediaLoadFlags flags = MediaLoadFlags.None); - IList GetFilesByIds(int[] ids, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking); - bool CheckUniqueFileName(string path, out string newPath); - string CombinePaths(params string[] paths); - - /// - /// Tries to find an equal file by comparing the source stream to a list of files. - /// - /// The source stream to find a match for. - /// The sequence of files to seek within for duplicates. - /// Whether to leave the source stream open. - /// Id of equal file if any. - /// true when a duplicate file was found, false otherwise. - bool FindEqualFile(Stream source, IEnumerable files, bool leaveOpen, out int equalFileId); - - MediaFileInfo SaveFile(string path, Stream stream, bool isTransient = true, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); - Task SaveFileAsync(string path, Stream stream, bool isTransient = true, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); - void DeleteFile(MediaFile file, bool permanent); - MediaFileInfo CopyFile(MediaFile file, string destinationFileName, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError); - MediaFileInfo MoveFile(MediaFile file, string destinationFileName); - - bool FolderExists(string path); - MediaFolderInfo CreateFolder(string path); - MediaFolderInfo MoveFolder(string path, string destinationPath); - MediaFolderInfo CopyFolder(string path, string destinationPath, DuplicateEntryHandling dupeEntryHandling = DuplicateEntryHandling.Skip); - void DeleteFolder(string path, FileHandling fileHandling = FileHandling.SoftDelete); - - MediaFileInfo ConvertMediaFile(MediaFile file); - string GetUrl(MediaFileInfo file, ProcessImageQuery query, string host = null, bool doFallback = true); - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/IMediaUrlGenerator.cs b/src/Libraries/SmartStore.Services/Media/v4/IMediaUrlGenerator.cs deleted file mode 100644 index 14a00cc49d..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/IMediaUrlGenerator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using SmartStore.Core.Domain.Media; - -namespace SmartStore.Services.Media -{ - public interface IMediaUrlGenerator - { - string GenerateUrl( - MediaFileInfo file, - ProcessImageQuery imageQuery, - string host = null, - bool doFallback = true); - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/IMediaTypeResolver.cs b/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/IMediaTypeResolver.cs deleted file mode 100644 index 8ddbfb3916..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaTypes/IMediaTypeResolver.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.IO; - -namespace SmartStore.Services.Media -{ - public interface IMediaTypeResolver - { - /// - /// Resolves the conceptual media type for a given file extension or mime type. - /// - /// The file extension. - /// Used to map to an extension if parameter is null or empty. - /// The media type or as fallback. - MediaType Resolve(string extension, string mimeType = null); - } - - public static class IMediaTypeResolverExtensions - { - public static MediaType Resolve(this IMediaTypeResolver resolver, MediaFile file) - { - Guard.NotNull(file, nameof(file)); - return resolver.Resolve(file.Extension, file.MimeType); - } - - //public static MediaType Resolve(this IMediaTypeResolver resolver, IFile file) - //{ - // Guard.NotNull(file, nameof(file)); - // return resolver.Resolve(file.Extension?.TrimStart('.')); - //} - } -} diff --git a/src/Libraries/SmartStore.Services/Media/v4/MediaUrlGenerator.cs b/src/Libraries/SmartStore.Services/Media/v4/MediaUrlGenerator.cs deleted file mode 100644 index ee118f03d2..0000000000 --- a/src/Libraries/SmartStore.Services/Media/v4/MediaUrlGenerator.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Globalization; -using System.Web; -using System.Web.Hosting; -using SmartStore.Core; -using SmartStore.Core.Domain.Media; -using SmartStore.Services.Configuration; - -namespace SmartStore.Services.Media -{ - public partial class MediaUrlGenerator : IMediaUrlGenerator - { - private readonly HttpContextBase _httpContext; - - private readonly string _host; - private readonly string _appPath; - private readonly string _fallbackImageFileName; - - private static readonly string _processedImagesRootPath; - private static readonly string _fallbackImagesRootPath; - - static MediaUrlGenerator() - { - _processedImagesRootPath = "media4/"; // MediaFileSystem.GetMediaPublicPath(); // TODO: (mm) change - _fallbackImagesRootPath = "content/images/"; - } - - public MediaUrlGenerator( - ISettingService settingService, - MediaSettings mediaSettings, - IStoreContext storeContext, - HttpContextBase httpContext) - { - _httpContext = httpContext; - - string appPath = "/"; - - if (HostingEnvironment.IsHosted) - { - appPath = HostingEnvironment.ApplicationVirtualPath.EmptyNull(); - - var cdn = storeContext.CurrentStore.ContentDeliveryNetwork; - if (cdn.HasValue() && !_httpContext.IsDebuggingEnabled && !_httpContext.Request.IsLocal) - { - _host = cdn; - } - else if (mediaSettings.AutoGenerateAbsoluteUrls) - { - var uri = httpContext.Request.Url; - _host = "//{0}{1}".FormatInvariant(uri.Authority, appPath); - } - else - { - _host = appPath; - } - } - - _host = _host.EmptyNull().EnsureEndsWith("/"); - _appPath = appPath.EnsureEndsWith("/"); - _fallbackImageFileName = settingService.GetSettingByKey("Media.DefaultImageName", "default-image.png"); - } - - public static string FallbackImagesRootPath - { - get { return _fallbackImagesRootPath; } - } - - public virtual string GenerateUrl( - MediaFileInfo file, - ProcessImageQuery imageQuery, - string host = null, - bool doFallback = true) - { - string path; - - // Build virtual path with pattern "media/{id}/{album}/{dir}/{NameWithExt}" - if (file?.Path != null) - { - path = _processedImagesRootPath + file.Id.ToString(CultureInfo.InvariantCulture) + "/" + file.Path; - } - else if (doFallback) - { - path = _processedImagesRootPath + "0/" + _fallbackImageFileName; - } - else - { - return null; - } - - if (host == null) - { - host = _host; - } - else if (host == string.Empty) - { - host = _appPath; - } - else - { - host = host.EnsureEndsWith("/"); - } - - var url = host; - - // Strip leading "/", the host/apppath has this already - if (path[0] == '/') - { - path = path.Substring(1); - } - - // Append media path - url += path; - - // Append query - var query = imageQuery?.ToString(false); - if (query != null && query.Length > 0) - { - url += query; - } - - return url; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/CampaignService.cs b/src/Libraries/SmartStore.Services/Messages/CampaignService.cs index f3aa08846d..58a4e4360a 100644 --- a/src/Libraries/SmartStore.Services/Messages/CampaignService.cs +++ b/src/Libraries/SmartStore.Services/Messages/CampaignService.cs @@ -11,50 +11,50 @@ namespace SmartStore.Services.Messages { public partial class CampaignService : ICampaignService { - private readonly ICommonServices _services; - private readonly IRepository _campaignRepository; - private readonly IMessageTemplateService _messageTemplateService; + private readonly ICommonServices _services; + private readonly IRepository _campaignRepository; + private readonly IMessageTemplateService _messageTemplateService; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; private readonly IStoreMappingService _storeMappingService; private readonly IAclService _aclService; public CampaignService( - ICommonServices services, - IRepository campaignRepository, - IMessageTemplateService messageTemplateService, + ICommonServices services, + IRepository campaignRepository, + IMessageTemplateService messageTemplateService, INewsLetterSubscriptionService newsLetterSubscriptionService, IStoreMappingService storeMappingService, IAclService aclService) { - _services = services; - _campaignRepository = campaignRepository; - _messageTemplateService = messageTemplateService; + _services = services; + _campaignRepository = campaignRepository; + _messageTemplateService = messageTemplateService; _newsLetterSubscriptionService = newsLetterSubscriptionService; _storeMappingService = storeMappingService; _aclService = aclService; } - public Localizer T { get; set; } = NullLocalizer.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; public virtual void InsertCampaign(Campaign campaign) { - Guard.NotNull(campaign, nameof(campaign)); + Guard.NotNull(campaign, nameof(campaign)); _campaignRepository.Insert(campaign); } public virtual void UpdateCampaign(Campaign campaign) { - Guard.NotNull(campaign, nameof(campaign)); + Guard.NotNull(campaign, nameof(campaign)); - _campaignRepository.Update(campaign); + _campaignRepository.Update(campaign); } public virtual void DeleteCampaign(Campaign campaign) { - Guard.NotNull(campaign, nameof(campaign)); + Guard.NotNull(campaign, nameof(campaign)); - _campaignRepository.Delete(campaign); + _campaignRepository.Delete(campaign); } public virtual Campaign GetCampaignById(int campaignId) @@ -159,28 +159,28 @@ public virtual CreateMessageResult SendCampaign(Campaign campaign, NewsletterSub } public virtual CreateMessageResult Preview(Campaign campaign) - { - Guard.NotNull(campaign, nameof(campaign)); - - var messageContext = new MessageContext - { - MessageTemplate = GetCampaignTemplate(), - TestMode = true - }; - - var subscription =_services.MessageFactory.GetTestModels(messageContext).OfType().FirstOrDefault(); - - var message = _services.MessageFactory.CreateMessage(messageContext, false /* do NOT queue */, subscription, campaign); - return message; - } - - private MessageTemplate GetCampaignTemplate() - { - var messageTemplate = _messageTemplateService.GetMessageTemplateByName(MessageTemplateNames.SystemCampaign, _services.StoreContext.CurrentStore.Id); - if (messageTemplate == null) - throw new SmartException(T("Common.Error.NoMessageTemplate", MessageTemplateNames.SystemCampaign)); - - return messageTemplate; - } - } + { + Guard.NotNull(campaign, nameof(campaign)); + + var messageContext = new MessageContext + { + MessageTemplate = GetCampaignTemplate(), + TestMode = true + }; + + var subscription = _services.MessageFactory.GetTestModels(messageContext).OfType().FirstOrDefault(); + + var message = _services.MessageFactory.CreateMessage(messageContext, false /* do NOT queue */, subscription, campaign); + return message; + } + + private MessageTemplate GetCampaignTemplate() + { + var messageTemplate = _messageTemplateService.GetMessageTemplateByName(MessageTemplateNames.SystemCampaign, _services.StoreContext.CurrentStore.Id); + if (messageTemplate == null) + throw new SmartException(T("Common.Error.NoMessageTemplate", MessageTemplateNames.SystemCampaign)); + + return messageTemplate; + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs b/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs index b1fffacea2..d3875f252d 100644 --- a/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs +++ b/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs @@ -13,82 +13,82 @@ namespace SmartStore.Services.Messages { - public class CreateAttachmentsConsumer : IConsumer - { - private readonly PdfSettings _pdfSettings; - private readonly HttpRequestBase _httpRequest; - private readonly Lazy _fileDownloadManager; + public class CreateAttachmentsConsumer : IConsumer + { + private readonly PdfSettings _pdfSettings; + private readonly HttpRequestBase _httpRequest; + private readonly Lazy _fileDownloadManager; - public CreateAttachmentsConsumer( - PdfSettings pdfSettings, - HttpRequestBase httpRequest, - Lazy fileDownloadManager) - { - this._pdfSettings = pdfSettings; - this._httpRequest = httpRequest; - this._fileDownloadManager = fileDownloadManager; + public CreateAttachmentsConsumer( + PdfSettings pdfSettings, + HttpRequestBase httpRequest, + Lazy fileDownloadManager) + { + this._pdfSettings = pdfSettings; + this._httpRequest = httpRequest; + this._fileDownloadManager = fileDownloadManager; - Logger = NullLogger.Instance; - T = NullLocalizer.Instance; - } + Logger = NullLogger.Instance; + T = NullLocalizer.Instance; + } - public ILogger Logger { get; set; } - public Localizer T { get; set; } + public ILogger Logger { get; set; } + public Localizer T { get; set; } - public void HandleEvent(MessageQueuingEvent message) - { - var qe = message.QueuedEmail; - var ctx = message.MessageContext; - var model = message.MessageModel; + public void HandleEvent(MessageQueuingEvent message) + { + var qe = message.QueuedEmail; + var ctx = message.MessageContext; + var model = message.MessageModel; - var handledTemplates = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "OrderPlaced.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderPlacedEmail }, - { "OrderCompleted.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderCompletedEmail } - }; - - if (handledTemplates.TryGetValue(ctx.MessageTemplate.Name, out var shouldHandle) && shouldHandle) - { - if (model.Get("Order") is IDictionary order && order.Get("ID") is int orderId) - { - try - { - var qea = CreatePdfInvoiceAttachment(orderId); - qe.Attachments.Add(qea); - } - catch (Exception ex) - { - Logger.Error(ex, T("Admin.System.QueuedEmails.ErrorCreatingAttachment")); - } - } - } - } + var handledTemplates = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "OrderPlaced.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderPlacedEmail }, + { "OrderCompleted.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderCompletedEmail } + }; - private QueuedEmailAttachment CreatePdfInvoiceAttachment(int orderId) - { - var urlHelper = new UrlHelper(_httpRequest.RequestContext); - var path = urlHelper.Action("Print", "Order", new { id = orderId, pdf = true, area = "" }); + if (handledTemplates.TryGetValue(ctx.MessageTemplate.Name, out var shouldHandle) && shouldHandle) + { + if (model.Get("Order") is IDictionary order && order.Get("ID") is int orderId) + { + try + { + var qea = CreatePdfInvoiceAttachment(orderId); + qe.Attachments.Add(qea); + } + catch (Exception ex) + { + Logger.Error(ex, T("Admin.System.QueuedEmails.ErrorCreatingAttachment")); + } + } + } + } - var fileResponse = _fileDownloadManager.Value.DownloadFile(path, true, 5000); + private QueuedEmailAttachment CreatePdfInvoiceAttachment(int orderId) + { + var urlHelper = new UrlHelper(_httpRequest.RequestContext); + var path = urlHelper.Action("Print", "Order", new { id = orderId, pdf = true, area = "" }); - if (fileResponse == null) - { - throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorEmptyAttachmentResult", path)); - } + var fileResponse = _fileDownloadManager.Value.DownloadFile(path, true, 5000); - if (!fileResponse.ContentType.IsCaseInsensitiveEqual("application/pdf")) - { - throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorNoPdfAttachment")); - } + if (fileResponse == null) + { + throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorEmptyAttachmentResult", path)); + } - return new QueuedEmailAttachment - { - StorageLocation = EmailAttachmentStorageLocation.Blob, - MediaStorage = new MediaStorage { Data = fileResponse.Data }, - MimeType = fileResponse.ContentType, - Name = fileResponse.FileName - }; - } + if (!fileResponse.ContentType.IsCaseInsensitiveEqual("application/pdf")) + { + throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorNoPdfAttachment")); + } - } + return new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.Blob, + MediaStorage = new MediaStorage { Data = fileResponse.Data }, + MimeType = fileResponse.ContentType, + Name = fileResponse.FileName + }; + } + + } } diff --git a/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs b/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs index 8fafb3b117..3d8978db89 100644 --- a/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs +++ b/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs @@ -10,16 +10,16 @@ namespace SmartStore.Services.Messages { public partial class EmailAccountService : IEmailAccountService { - private readonly IRepository _emailAccountRepository; + private readonly IRepository _emailAccountRepository; private readonly EmailAccountSettings _emailAccountSettings; private readonly IEventPublisher _eventPublisher; - private EmailAccount _defaultEmailAccount; + private EmailAccount _defaultEmailAccount; public EmailAccountService( - IRepository emailAccountRepository, - EmailAccountSettings emailAccountSettings, - IEventPublisher eventPublisher) + IRepository emailAccountRepository, + EmailAccountSettings emailAccountSettings, + IEventPublisher eventPublisher) { this._emailAccountRepository = emailAccountRepository; this._emailAccountSettings = emailAccountSettings; @@ -31,11 +31,11 @@ public virtual void InsertEmailAccount(EmailAccount emailAccount) if (emailAccount == null) throw new ArgumentNullException("emailAccount"); - emailAccount.Email = emailAccount.Email.EmptyNull(); - emailAccount.DisplayName = emailAccount.DisplayName.EmptyNull(); - emailAccount.Host = emailAccount.Host.EmptyNull(); - emailAccount.Username = emailAccount.Username.EmptyNull(); - emailAccount.Password = emailAccount.Password.EmptyNull(); + emailAccount.Email = emailAccount.Email.EmptyNull(); + emailAccount.DisplayName = emailAccount.DisplayName.EmptyNull(); + emailAccount.Host = emailAccount.Host.EmptyNull(); + emailAccount.Username = emailAccount.Username.EmptyNull(); + emailAccount.Password = emailAccount.Password.EmptyNull(); emailAccount.Email = emailAccount.Email.Trim(); emailAccount.DisplayName = emailAccount.DisplayName.Trim(); @@ -43,15 +43,15 @@ public virtual void InsertEmailAccount(EmailAccount emailAccount) emailAccount.Username = emailAccount.Username.Trim(); emailAccount.Password = emailAccount.Password.Trim(); - emailAccount.Email = emailAccount.Email.Truncate(255); - emailAccount.DisplayName = emailAccount.DisplayName.Truncate(255); - emailAccount.Host = emailAccount.Host.Truncate(255); - emailAccount.Username = emailAccount.Username.Truncate(255); - emailAccount.Password = emailAccount.Password.Truncate(255); + emailAccount.Email = emailAccount.Email.Truncate(255); + emailAccount.DisplayName = emailAccount.DisplayName.Truncate(255); + emailAccount.Host = emailAccount.Host.Truncate(255); + emailAccount.Username = emailAccount.Username.Truncate(255); + emailAccount.Password = emailAccount.Password.Truncate(255); _emailAccountRepository.Insert(emailAccount); - _defaultEmailAccount = null; + _defaultEmailAccount = null; } public virtual void UpdateEmailAccount(EmailAccount emailAccount) @@ -59,11 +59,11 @@ public virtual void UpdateEmailAccount(EmailAccount emailAccount) if (emailAccount == null) throw new ArgumentNullException("emailAccount"); - emailAccount.Email = emailAccount.Email.EmptyNull(); - emailAccount.DisplayName = emailAccount.DisplayName.EmptyNull(); - emailAccount.Host = emailAccount.Host.EmptyNull(); - emailAccount.Username = emailAccount.Username.EmptyNull(); - emailAccount.Password = emailAccount.Password.EmptyNull(); + emailAccount.Email = emailAccount.Email.EmptyNull(); + emailAccount.DisplayName = emailAccount.DisplayName.EmptyNull(); + emailAccount.Host = emailAccount.Host.EmptyNull(); + emailAccount.Username = emailAccount.Username.EmptyNull(); + emailAccount.Password = emailAccount.Password.EmptyNull(); emailAccount.Email = emailAccount.Email.Trim(); emailAccount.DisplayName = emailAccount.DisplayName.Trim(); @@ -71,15 +71,15 @@ public virtual void UpdateEmailAccount(EmailAccount emailAccount) emailAccount.Username = emailAccount.Username.Trim(); emailAccount.Password = emailAccount.Password.Trim(); - emailAccount.Email = emailAccount.Email.Truncate(255); - emailAccount.DisplayName = emailAccount.DisplayName.Truncate(255); - emailAccount.Host = emailAccount.Host.Truncate(255); - emailAccount.Username = emailAccount.Username.Truncate(255); - emailAccount.Password = emailAccount.Password.Truncate(255); + emailAccount.Email = emailAccount.Email.Truncate(255); + emailAccount.DisplayName = emailAccount.DisplayName.Truncate(255); + emailAccount.Host = emailAccount.Host.Truncate(255); + emailAccount.Username = emailAccount.Username.Truncate(255); + emailAccount.Password = emailAccount.Password.Truncate(255); _emailAccountRepository.Update(emailAccount); - _defaultEmailAccount = null; + _defaultEmailAccount = null; } public virtual void DeleteEmailAccount(EmailAccount emailAccount) @@ -92,7 +92,7 @@ public virtual void DeleteEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Delete(emailAccount); - _defaultEmailAccount = null; + _defaultEmailAccount = null; } public virtual EmailAccount GetEmailAccountById(int emailAccountId) @@ -100,22 +100,22 @@ public virtual EmailAccount GetEmailAccountById(int emailAccountId) if (emailAccountId == 0) return null; - return _emailAccountRepository.GetByIdCached(emailAccountId, "db.emailaccount.id-" + emailAccountId); - } - - public virtual EmailAccount GetDefaultEmailAccount() - { - if (_defaultEmailAccount == null) - { - _defaultEmailAccount = GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); - if (_defaultEmailAccount == null) - { - _defaultEmailAccount = GetAllEmailAccounts().FirstOrDefault(); - } - } - - return _defaultEmailAccount; - } + return _emailAccountRepository.GetByIdCached(emailAccountId, "db.emailaccount.id-" + emailAccountId); + } + + public virtual EmailAccount GetDefaultEmailAccount() + { + if (_defaultEmailAccount == null) + { + _defaultEmailAccount = GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); + if (_defaultEmailAccount == null) + { + _defaultEmailAccount = GetAllEmailAccounts().FirstOrDefault(); + } + } + + return _defaultEmailAccount; + } public virtual IList GetAllEmailAccounts() { diff --git a/src/Libraries/SmartStore.Services/Messages/Events.cs b/src/Libraries/SmartStore.Services/Messages/Events.cs index 02bbf52c66..50084a7461 100644 --- a/src/Libraries/SmartStore.Services/Messages/Events.cs +++ b/src/Libraries/SmartStore.Services/Messages/Events.cs @@ -3,85 +3,85 @@ namespace SmartStore.Services.Messages { - /// - /// Published after the creation of a single message model part has been completed. - /// - /// Type of source entity - public class MessageModelPartCreatedEvent where T : class - { - public MessageModelPartCreatedEvent(T source, dynamic part) - { - Source = source; - Part = part; - } + /// + /// Published after the creation of a single message model part has been completed. + /// + /// Type of source entity + public class MessageModelPartCreatedEvent where T : class + { + public MessageModelPartCreatedEvent(T source, dynamic part) + { + Source = source; + Part = part; + } - /// - /// The source object for which the model part has been created, e.g. a Product entity. - /// - public T Source { get; private set; } + /// + /// The source object for which the model part has been created, e.g. a Product entity. + /// + public T Source { get; private set; } - /// - /// The resulting model part. - /// - public dynamic Part { get; private set; } - } + /// + /// The resulting model part. + /// + public dynamic Part { get; private set; } + } - /// - /// Published when a system mapper is missing for a particular model type (e.g. a custom entity in a plugin). - /// Implementors should subscribe to this event in order to provide a corresponding dynamic model part. - /// The result model should be assigned to the property. If this property - /// is still null, the source is used as model part instead. - /// - public class MessageModelPartMappingEvent - { - public MessageModelPartMappingEvent(object source) - { - Source = source; - } + /// + /// Published when a system mapper is missing for a particular model type (e.g. a custom entity in a plugin). + /// Implementors should subscribe to this event in order to provide a corresponding dynamic model part. + /// The result model should be assigned to the property. If this property + /// is still null, the source is used as model part instead. + /// + public class MessageModelPartMappingEvent + { + public MessageModelPartMappingEvent(object source) + { + Source = source; + } - /// - /// The source object for which a model part should be created. - /// - public object Source { get; private set; } + /// + /// The source object for which a model part should be created. + /// + public object Source { get; private set; } - /// - /// The resulting model part. - /// - public dynamic Result { get; set; } + /// + /// The resulting model part. + /// + public dynamic Result { get; set; } - /// - /// The name of the model part. If null the source's type name is used. - /// - public string ModelPartName { get; set; } - } + /// + /// The name of the model part. If null the source's type name is used. + /// + public string ModelPartName { get; set; } + } - /// - /// Published after the message model has been completely created. - /// - public class MessageModelCreatedEvent - { - public MessageModelCreatedEvent(MessageContext messageContext, TemplateModel model) - { - MessageContext = messageContext; - Model = model; - } + /// + /// Published after the message model has been completely created. + /// + public class MessageModelCreatedEvent + { + public MessageModelCreatedEvent(MessageContext messageContext, TemplateModel model) + { + MessageContext = messageContext; + Model = model; + } - public MessageContext MessageContext{ get; private set; } + public MessageContext MessageContext { get; private set; } - /// - /// The result message model. - /// - public TemplateModel Model { get; private set; } - } + /// + /// The result message model. + /// + public TemplateModel Model { get; private set; } + } - /// - /// An event message which gets published just before a new instance - /// of is persisted to the database - /// - public class MessageQueuingEvent - { - public QueuedEmail QueuedEmail { get; set; } - public MessageContext MessageContext { get; set; } - public TemplateModel MessageModel { get; set; } - } + /// + /// An event message which gets published just before a new instance + /// of is persisted to the database + /// + public class MessageQueuingEvent + { + public QueuedEmail QueuedEmail { get; set; } + public MessageContext MessageContext { get; set; } + public TemplateModel MessageModel { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs b/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs index d85ea70b89..6b51ea7a3a 100644 --- a/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs +++ b/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs @@ -35,7 +35,7 @@ public partial interface ICampaignService /// /// Campaign collection IList GetAllCampaigns(); - + /// /// Sends a campaign to all newsletter subscribers. /// @@ -57,5 +57,5 @@ public partial interface ICampaignService /// The campaign to preview /// The preview result CreateMessageResult Preview(Campaign campaign); - } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs b/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs index 6b3df6f6eb..ccc2eb4e31 100644 --- a/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs +++ b/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs @@ -30,11 +30,11 @@ public partial interface IEmailAccountService /// Email account EmailAccount GetEmailAccountById(int emailAccountId); - /// - /// Gets the default email account - /// - /// Emil account - EmailAccount GetDefaultEmailAccount(); + /// + /// Gets the default email account + /// + /// Emil account + EmailAccount GetDefaultEmailAccount(); /// /// Gets all email accounts diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs b/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs index 525e50d09c..9e1c60c210 100644 --- a/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs +++ b/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs @@ -6,104 +6,104 @@ namespace SmartStore.Services.Messages { - /// - /// Contains the result data of a call - /// - public class CreateMessageResult - { - /// - /// The queued email instance which can be saved to the database - /// - public QueuedEmail Email { get; set; } + /// + /// Contains the result data of a call + /// + public class CreateMessageResult + { + /// + /// The queued email instance which can be saved to the database + /// + public QueuedEmail Email { get; set; } - /// - /// The final model which contains all global and template specific model parts. - /// - public TemplateModel Model { get; set; } + /// + /// The final model which contains all global and template specific model parts. + /// + public TemplateModel Model { get; set; } - /// - /// The message context used to create the message. - /// - public MessageContext MessageContext { get; set; } - } + /// + /// The message context used to create the message. + /// + public MessageContext MessageContext { get; set; } + } - /// - /// Creates and optionally queues email messages - /// - public interface IMessageFactory - { - /// - /// Creates an email message - /// - /// Contains all data required for creating a message - /// If true, the created email message will automatically be queued for sending (saved in database as a ) - /// - /// All model objects that are necessary to render the template (in no particular order). - /// The passed object instances will be converted to special types which the underlying can handle. - /// is responsible for the conversion. See also . - /// - /// Contains the message creation result. - CreateMessageResult CreateMessage(MessageContext messageContext, bool queue, params object[] modelParts); + /// + /// Creates and optionally queues email messages + /// + public interface IMessageFactory + { + /// + /// Creates an email message + /// + /// Contains all data required for creating a message + /// If true, the created email message will automatically be queued for sending (saved in database as a ) + /// + /// All model objects that are necessary to render the template (in no particular order). + /// The passed object instances will be converted to special types which the underlying can handle. + /// is responsible for the conversion. See also . + /// + /// Contains the message creation result. + CreateMessageResult CreateMessage(MessageContext messageContext, bool queue, params object[] modelParts); - /// - /// Queues a message created by - /// - /// The message context used to create the message. - /// The instance of to queue, e.g. obtained from - void QueueMessage(MessageContext messageContext, QueuedEmail queuedEmail); + /// + /// Queues a message created by + /// + /// The message context used to create the message. + /// The instance of to queue, e.g. obtained from + void QueueMessage(MessageContext messageContext, QueuedEmail queuedEmail); - /// - /// Gets an array of suitable test model parts during preview mode. The message template defines - /// which model part types are required (a comma-separated type list in ). - /// The framework tries to load a random entity for each defined type from the database. If the table does not contain any records, - /// gets called internally to obtain a test model wrapper with sample data. - /// - /// The message context used to create the message. - /// An array of model parts which can be passed to - object[] GetTestModels(MessageContext messageContext); - } + /// + /// Gets an array of suitable test model parts during preview mode. The message template defines + /// which model part types are required (a comma-separated type list in ). + /// The framework tries to load a random entity for each defined type from the database. If the table does not contain any records, + /// gets called internally to obtain a test model wrapper with sample data. + /// + /// The message context used to create the message. + /// An array of model parts which can be passed to + object[] GetTestModels(MessageContext messageContext); + } - public static class IMessageFactoryExtensions - { - /// - /// Sends the "ContactUs" message to the store owner - /// - public static CreateMessageResult SendContactUsMessage(this IMessageFactory factory, Customer customer, - string senderEmail, string senderName, string subject, string message, EmailAddress senderEmailAddress, int languageId = 0) - { - var model = new NamedModelPart("Message") - { - ["Subject"] = subject.NullEmpty(), - ["Message"] = message.NullEmpty(), - ["SenderEmail"] = senderEmail.NullEmpty(), - ["SenderName"] = senderName.HasValue() ? senderName.NullEmpty() : senderEmail.NullEmpty() - }; + public static class IMessageFactoryExtensions + { + /// + /// Sends the "ContactUs" message to the store owner + /// + public static CreateMessageResult SendContactUsMessage(this IMessageFactory factory, Customer customer, + string senderEmail, string senderName, string subject, string message, EmailAddress senderEmailAddress, int languageId = 0) + { + var model = new NamedModelPart("Message") + { + ["Subject"] = subject.NullEmpty(), + ["Message"] = message.NullEmpty(), + ["SenderEmail"] = senderEmail.NullEmpty(), + ["SenderName"] = senderName.HasValue() ? senderName.NullEmpty() : senderEmail.NullEmpty() + }; - var messageContext = MessageContext.Create(MessageTemplateNames.SystemContactUs, languageId, customer: customer); - if (senderEmailAddress != null) - { - messageContext.SenderEmailAddress = senderEmailAddress; - } + var messageContext = MessageContext.Create(MessageTemplateNames.SystemContactUs, languageId, customer: customer); + if (senderEmailAddress != null) + { + messageContext.SenderEmailAddress = senderEmailAddress; + } - return factory.CreateMessage(messageContext, true, model); - } + return factory.CreateMessage(messageContext, true, model); + } - /// - /// Sends a newsletter subscription activation message - /// - public static CreateMessageResult SendNewsLetterSubscriptionActivationMessage(this IMessageFactory factory, NewsLetterSubscription subscription, int languageId = 0) - { - Guard.NotNull(subscription, nameof(subscription)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsLetterSubscriptionActivation, languageId), true, subscription); - } + /// + /// Sends a newsletter subscription activation message + /// + public static CreateMessageResult SendNewsLetterSubscriptionActivationMessage(this IMessageFactory factory, NewsLetterSubscription subscription, int languageId = 0) + { + Guard.NotNull(subscription, nameof(subscription)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsLetterSubscriptionActivation, languageId), true, subscription); + } - /// - /// Sends a newsletter subscription deactivation message - /// - public static CreateMessageResult SendNewsLetterSubscriptionDeactivationMessage(this IMessageFactory factory, NewsLetterSubscription subscription, int languageId = 0) - { - Guard.NotNull(subscription, nameof(subscription)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsLetterSubscriptionDeactivation, languageId), true, subscription); - } - } + /// + /// Sends a newsletter subscription deactivation message + /// + public static CreateMessageResult SendNewsLetterSubscriptionDeactivationMessage(this IMessageFactory factory, NewsLetterSubscription subscription, int languageId = 0) + { + Guard.NotNull(subscription, nameof(subscription)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsLetterSubscriptionDeactivation, languageId), true, subscription); + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs b/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs index 6819624dea..18d9b01c80 100644 --- a/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs @@ -13,108 +13,108 @@ namespace SmartStore.Services.Messages { - /// - /// Responsible for building the message template model - /// - public interface IMessageModelProvider - { - /// - /// Creates and adds all global model parts to the template model (): - /// - /// - /// Context (contains meta infos like template name, language etc.) - /// Customer (obtained from property) - /// Store (obtained from property) - /// Email (the ) - /// Theme (some theming variables, mostly colors) - /// - /// - /// - /// Contains all data required for building a model part and creating a message - void AddGlobalModelParts(MessageContext messageContext); + /// + /// Responsible for building the message template model + /// + public interface IMessageModelProvider + { + /// + /// Creates and adds all global model parts to the template model (): + /// + /// + /// Context (contains meta infos like template name, language etc.) + /// Customer (obtained from property) + /// Store (obtained from property) + /// Email (the ) + /// Theme (some theming variables, mostly colors) + /// + /// + /// + /// Contains all data required for building a model part and creating a message + void AddGlobalModelParts(MessageContext messageContext); - /// - /// Adds a template specific model part to the template model. - /// The passed object instance () will be converted to a special type which the underlying can handle. - /// - /// Supported types are: , , , , , - /// , , , , , - /// , , , , , - /// , - /// - /// - /// Furthermore, any object implementing or can also be passed as model part. - /// The first merges all entries within the passed object with the special Bag entry, the latter creates a whole - /// new entry using the name provided by its property. - /// - /// - /// If an unsupported object is passed, the framework will publish the event, giving - /// a subscriber the chance to provide a converted model object and a part name. - /// - /// - /// The model part instance to convert and add to the final model. - /// Contains all data required for building a model part and creating a message - /// - /// The name to use for the model part in the final model. If null, the framework tries to infer the name. - /// See also - /// - void AddModelPart(object part, MessageContext messageContext, string name = null); + /// + /// Adds a template specific model part to the template model. + /// The passed object instance () will be converted to a special type which the underlying can handle. + /// + /// Supported types are: , , , , , + /// , , , , , + /// , , , , , + /// , + /// + /// + /// Furthermore, any object implementing or can also be passed as model part. + /// The first merges all entries within the passed object with the special Bag entry, the latter creates a whole + /// new entry using the name provided by its property. + /// + /// + /// If an unsupported object is passed, the framework will publish the event, giving + /// a subscriber the chance to provide a converted model object and a part name. + /// + /// + /// The model part instance to convert and add to the final model. + /// Contains all data required for building a model part and creating a message + /// + /// The name to use for the model part in the final model. If null, the framework tries to infer the name. + /// See also + /// + void AddModelPart(object part, MessageContext messageContext, string name = null); - /// - /// Creates a serializable model object for the passed entity/object. - /// - /// Supported types are: , , , , , - /// , , , , , - /// , , , , , - /// , - /// - /// - /// Furthermore, any object implementing or can also be passed as model part. - /// The first merges all entries within the passed object with the special Bag entry, the latter creates a whole - /// new entry using the name provided by its property. - /// - /// - /// If an unsupported object is passed, null is returned - /// - /// - /// The model part instance to convert. - /// Whether members/properties with null values should be excluded from the result model. - /// Optional list of member/property names to exclude from the result model. - object CreateModelPart(object part, bool ignoreNullMembers, params string[] ignoreMemberNames); + /// + /// Creates a serializable model object for the passed entity/object. + /// + /// Supported types are: , , , , , + /// , , , , , + /// , , , , , + /// , + /// + /// + /// Furthermore, any object implementing or can also be passed as model part. + /// The first merges all entries within the passed object with the special Bag entry, the latter creates a whole + /// new entry using the name provided by its property. + /// + /// + /// If an unsupported object is passed, null is returned + /// + /// + /// The model part instance to convert. + /// Whether members/properties with null values should be excluded from the result model. + /// Optional list of member/property names to exclude from the result model. + object CreateModelPart(object part, bool ignoreNullMembers, params string[] ignoreMemberNames); - /// - /// Tries to infer the model part name by type: - /// - /// When is a plain object: type name - /// When is : ModelPartName property - /// - /// - /// The model part instance to resolve a name for. - /// The inferred name or null - string ResolveModelName(object model); + /// + /// Tries to infer the model part name by type: + /// + /// When is a plain object: type name + /// When is : ModelPartName property + /// + /// + /// The model part instance to resolve a name for. + /// The inferred name or null + string ResolveModelName(object model); - /// - /// Build a model metadata tree for a final template model. Model trees are used - /// on the client to provide autocomplete information. - /// - /// The final template model to build a model tree for. - /// A hierarchy of instances. - TreeNode BuildModelTree(TemplateModel model); + /// + /// Build a model metadata tree for a final template model. Model trees are used + /// on the client to provide autocomplete information. + /// + /// The final template model to build a model tree for. + /// A hierarchy of instances. + TreeNode BuildModelTree(TemplateModel model); - /// - /// Gets the last known model metadata tree for a particular template. - /// See also - /// - /// Name of the template to get metadata for. - /// A hierarchy of instances. - TreeNode GetLastModelTree(string messageTemplateName); + /// + /// Gets the last known model metadata tree for a particular template. + /// See also + /// + /// Name of the template to get metadata for. + /// A hierarchy of instances. + TreeNode GetLastModelTree(string messageTemplateName); - /// - /// Gets the last known model metadata tree for a particular template. - /// See also - /// - /// The template to get metadata for. - /// A hierarchy of instances. - TreeNode GetLastModelTree(MessageTemplate template); - } + /// + /// Gets the last known model metadata tree for a particular template. + /// See also + /// + /// The template to get metadata for. + /// A hierarchy of instances. + TreeNode GetLastModelTree(MessageTemplate template); + } } diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageTemplateService.cs b/src/Libraries/SmartStore.Services/Messages/IMessageTemplateService.cs index 445671bc92..ecfa00d4f9 100644 --- a/src/Libraries/SmartStore.Services/Messages/IMessageTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Messages/IMessageTemplateService.cs @@ -8,11 +8,11 @@ namespace SmartStore.Services.Messages /// public partial interface IMessageTemplateService { - /// - /// Delete a message template - /// - /// Message template - void DeleteMessageTemplate(MessageTemplate messageTemplate); + /// + /// Delete a message template + /// + /// Message template + void DeleteMessageTemplate(MessageTemplate messageTemplate); /// /// Inserts a message template @@ -48,11 +48,11 @@ public partial interface IMessageTemplateService /// Message template list IList GetAllMessageTemplates(int storeId); - /// - /// Create a copy of message template with all depended data - /// - /// Message template - /// Message template copy - MessageTemplate CopyMessageTemplate(MessageTemplate messageTemplate); + /// + /// Create a copy of message template with all depended data + /// + /// Message template + /// Message template copy + MessageTemplate CopyMessageTemplate(MessageTemplate messageTemplate); } } diff --git a/src/Libraries/SmartStore.Services/Messages/IModelPart.cs b/src/Libraries/SmartStore.Services/Messages/IModelPart.cs index 16ed798135..dd93f45a7f 100644 --- a/src/Libraries/SmartStore.Services/Messages/IModelPart.cs +++ b/src/Libraries/SmartStore.Services/Messages/IModelPart.cs @@ -4,41 +4,41 @@ namespace SmartStore.Services.Messages { - /// - /// Used to transfer miscellaneous data to the template engine - /// which is merged with the generic "Bag" part. - /// - public interface IModelPart : IDictionary - { - } - - public interface INamedModelPart : IDictionary - { - string ModelPartName { get; } - } - - - #region Impl - - public class ModelPart : HybridExpando, IModelPart - { - } - - public class NamedModelPart : HybridExpando, INamedModelPart - { - public NamedModelPart(string modelPartName) - : base(true) - { - Guard.NotEmpty(modelPartName, nameof(modelPartName)); - ModelPartName = modelPartName; - } - - public string ModelPartName - { - get; - private set; - } - } - - #endregion + /// + /// Used to transfer miscellaneous data to the template engine + /// which is merged with the generic "Bag" part. + /// + public interface IModelPart : IDictionary + { + } + + public interface INamedModelPart : IDictionary + { + string ModelPartName { get; } + } + + + #region Impl + + public class ModelPart : HybridExpando, IModelPart + { + } + + public class NamedModelPart : HybridExpando, INamedModelPart + { + public NamedModelPart(string modelPartName) + : base(true) + { + Guard.NotEmpty(modelPartName, nameof(modelPartName)); + ModelPartName = modelPartName; + } + + public string ModelPartName + { + get; + private set; + } + } + + #endregion } diff --git a/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs index a996ceea69..2dfd4fb106 100644 --- a/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs @@ -4,8 +4,8 @@ namespace SmartStore.Services.Messages { - public partial interface INewsLetterSubscriptionService - { + public partial interface INewsLetterSubscriptionService + { /// /// Inserts a newsletter subscription /// @@ -27,21 +27,21 @@ public partial interface INewsLetterSubscriptionService /// if set to true [publish subscription events]. void DeleteNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true); - /// - /// Adds or deletes a newsletter subscription - /// - /// true add subscription, false delete - /// Email address - /// Store identifier - /// true added subscription, false removed subscription, null did nothing - bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId); + /// + /// Adds or deletes a newsletter subscription + /// + /// true add subscription, false delete + /// Email address + /// Store identifier + /// true added subscription, false removed subscription, null did nothing + bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId); - /// - /// Gets a newsletter subscription by newsletter subscription identifier - /// - /// The newsletter subscription identifier - /// NewsLetter subscription - NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId); + /// + /// Gets a newsletter subscription by newsletter subscription identifier + /// + /// The newsletter subscription identifier + /// NewsLetter subscription + NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId); /// /// Gets a newsletter subscription by newsletter subscription GUID diff --git a/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs index 82a6de9bac..4a86533945 100644 --- a/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs @@ -5,7 +5,7 @@ namespace SmartStore.Services.Messages { - public partial interface IQueuedEmailService + public partial interface IQueuedEmailService { /// /// Inserts a queued email @@ -25,11 +25,11 @@ public partial interface IQueuedEmailService /// Queued email void DeleteQueuedEmail(QueuedEmail queuedEmail); - /// - /// Deletes all queued emails - /// - /// The count of deleted entries - int DeleteAllQueuedEmails(); + /// + /// Deletes all queued emails + /// + /// The count of deleted entries + int DeleteAllQueuedEmails(); /// /// Gets a queued email by identifier @@ -52,31 +52,31 @@ public partial interface IQueuedEmailService /// Email item collection IPagedList SearchEmails(SearchEmailsQuery query); - /// - /// Sends a queued email asynchronously. - /// - /// Queued email - /// Whether the operation succeeded - Task SendEmailAsync(QueuedEmail queuedEmail); + /// + /// Sends a queued email asynchronously. + /// + /// Queued email + /// Whether the operation succeeded + Task SendEmailAsync(QueuedEmail queuedEmail); - /// - /// Gets a queued email attachment by identifier - /// - /// Queued email attachment identifier - /// Queued email attachment - QueuedEmailAttachment GetQueuedEmailAttachmentById(int id); + /// + /// Gets a queued email attachment by identifier + /// + /// Queued email attachment identifier + /// Queued email attachment + QueuedEmailAttachment GetQueuedEmailAttachmentById(int id); - /// - /// Deleted a queued email attachment - /// - /// Queued email attachment - void DeleteQueuedEmailAttachment(QueuedEmailAttachment attachment); + /// + /// Deleted a queued email attachment + /// + /// Queued email attachment + void DeleteQueuedEmailAttachment(QueuedEmailAttachment attachment); - /// - /// Load binary data of a queued email attachment - /// - /// Queued email attachment - /// Binary data if attachment.StorageLocation is EmailAttachmentStorageLocation.Blob, otherwise null - byte[] LoadQueuedEmailAttachmentBinary(QueuedEmailAttachment attachment); - } + /// + /// Load binary data of a queued email attachment + /// + /// Queued email attachment + /// Binary data if attachment.StorageLocation is EmailAttachmentStorageLocation.Blob, otherwise null + byte[] LoadQueuedEmailAttachmentBinary(QueuedEmailAttachment attachment); + } } diff --git a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs index 93ba2c8044..d8cc243209 100644 --- a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs +++ b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs @@ -8,146 +8,134 @@ namespace SmartStore.Services.Messages.Importer { - public class NewsLetterSubscriptionImporter : IEntityImporter - { - private readonly IRepository _subscriptionRepository; - - public NewsLetterSubscriptionImporter(IRepository subscriptionRepository) - { - _subscriptionRepository = subscriptionRepository; - } - - public static string[] SupportedKeyFields - { - get - { - return new string[] { "Email" }; - } - } - - public static string[] DefaultKeyFields - { - get - { - return new string[] { "Email" }; - } - } - - public void Execute(ImportExecuteContext context) - { - var utcNow = DateTime.UtcNow; - var currentStoreId = context.Services.StoreContext.CurrentStore.Id; - - using (var scope = new DbContextScope(ctx: context.Services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) - { - var segmenter = context.DataSegmenter; - - context.Result.TotalRecords = segmenter.TotalRows; - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.GetCurrentBatch(); - - // Perf: detach all entities - _subscriptionRepository.Context.DetachEntities(false); - - context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); - - foreach (var row in batch) - { - try - { - var active = true; - var email = row.GetDataValue("Email"); - var storeId = row.GetDataValue("StoreId"); - - if (storeId == 0) - { - storeId = currentStoreId; - } - - if (row.HasDataValue("Active") && row.TryGetDataValue("Active", out active)) - { - } - else - { - active = true; // default - } - - if (email.IsEmpty()) - { - context.Result.AddWarning("Skipped empty email address", row.GetRowInfo(), "Email"); - continue; - } - - if (email.Length > 255) - { - context.Result.AddWarning("Skipped email address '{0}'. It exceeds the maximum allowed length of 255".FormatInvariant(email), row.GetRowInfo(), "Email"); - continue; - } - - if (!email.IsEmail()) - { - context.Result.AddWarning("Skipped invalid email address '{0}'".FormatInvariant(email), row.GetRowInfo(), "Email"); - continue; - } - - NewsLetterSubscription subscription = null; - - foreach (var keyName in context.KeyFieldNames) - { - switch (keyName) - { - case "Email": - subscription = _subscriptionRepository.Table - .OrderBy(x => x.Id) - .FirstOrDefault(x => x.Email == email && x.StoreId == storeId); - break; - } - - if (subscription != null) - break; - } - - if (subscription == null) - { - if (context.UpdateOnly) - { - ++context.Result.SkippedRecords; - continue; - } - - subscription = new NewsLetterSubscription - { - Active = active, - CreatedOnUtc = utcNow, - Email = email, - NewsLetterSubscriptionGuid = Guid.NewGuid(), - StoreId = storeId - }; - - _subscriptionRepository.Insert(subscription); - context.Result.NewRecords++; - } - else - { - subscription.Active = active; - - _subscriptionRepository.Update(subscription); - context.Result.ModifiedRecords++; - } - } - catch (Exception exception) - { - context.Result.AddError(exception.ToAllMessages(), row.GetRowInfo()); - } - } // for - - _subscriptionRepository.Context.SaveChanges(); + public class NewsLetterSubscriptionImporter : IEntityImporter + { + private readonly IRepository _subscriptionRepository; + + public NewsLetterSubscriptionImporter(IRepository subscriptionRepository) + { + _subscriptionRepository = subscriptionRepository; + } + + public static string[] SupportedKeyFields => new string[] { "Email" }; + + public static string[] DefaultKeyFields => new string[] { "Email" }; + + public void Execute(ImportExecuteContext context) + { + var utcNow = DateTime.UtcNow; + var currentStoreId = context.Services.StoreContext.CurrentStore.Id; + + using (var scope = new DbContextScope(ctx: context.Services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + var segmenter = context.DataSegmenter; + + context.Result.TotalRecords = segmenter.TotalRows; + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _subscriptionRepository.Context.DetachEntities(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + foreach (var row in batch) + { + try + { + var active = true; + var email = row.GetDataValue("Email"); + var storeId = row.GetDataValue("StoreId"); + + if (storeId == 0) + { + storeId = currentStoreId; + } + + if (row.HasDataValue("Active") && row.TryGetDataValue("Active", out active)) + { + } + else + { + active = true; // default + } + + if (email.IsEmpty()) + { + context.Result.AddWarning("Skipped empty email address", row.GetRowInfo(), "Email"); + continue; + } + + if (email.Length > 255) + { + context.Result.AddWarning("Skipped email address '{0}'. It exceeds the maximum allowed length of 255".FormatInvariant(email), row.GetRowInfo(), "Email"); + continue; + } + + if (!email.IsEmail()) + { + context.Result.AddWarning("Skipped invalid email address '{0}'".FormatInvariant(email), row.GetRowInfo(), "Email"); + continue; + } + + NewsLetterSubscription subscription = null; + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Email": + subscription = _subscriptionRepository.Table + .OrderBy(x => x.Id) + .FirstOrDefault(x => x.Email == email && x.StoreId == storeId); + break; + } + + if (subscription != null) + break; + } + + if (subscription == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + subscription = new NewsLetterSubscription + { + Active = active, + CreatedOnUtc = utcNow, + Email = email, + NewsLetterSubscriptionGuid = Guid.NewGuid(), + StoreId = storeId + }; + + _subscriptionRepository.Insert(subscription); + context.Result.NewRecords++; + } + else + { + subscription.Active = active; + + _subscriptionRepository.Update(subscription); + context.Result.ModifiedRecords++; + } + } + catch (Exception exception) + { + context.Result.AddError(exception.ToAllMessages(), row.GetRowInfo()); + } + } // for + + _subscriptionRepository.Context.SaveChanges(); context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); } // while - } - } - } + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageContext.cs b/src/Libraries/SmartStore.Services/Messages/MessageContext.cs index 056ab70126..20e93cf778 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageContext.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageContext.cs @@ -14,88 +14,85 @@ namespace SmartStore.Services.Messages /// for the creation of message templates. /// public class MessageContext - { - private IFormatProvider _formatProvider; + { + private IFormatProvider _formatProvider; - /// - /// The source message template. Required if is empty. - /// - public MessageTemplate MessageTemplate { get; set; } + /// + /// The source message template. Required if is empty. + /// + public MessageTemplate MessageTemplate { get; set; } - /// - /// The source message template name. Required if is null. - /// - public string MessageTemplateName { get; set; } + /// + /// The source message template name. Required if is null. + /// + public string MessageTemplateName { get; set; } - /// - /// If null, the email account specifies the sender. - /// - public EmailAddress SenderEmailAddress { get; set; } + /// + /// If null, the email account specifies the sender. + /// + public EmailAddress SenderEmailAddress { get; set; } - /// - /// If null, obtained from WorkContext.CurrentCustomer. - /// - public Customer Customer { get; set; } + /// + /// If null, obtained from WorkContext.CurrentCustomer. + /// + public Customer Customer { get; set; } - /// - /// If null, obtained from WorkContext.WorkingLanguage. - /// - public int? LanguageId { get; set; } + /// + /// If null, obtained from WorkContext.WorkingLanguage. + /// + public int? LanguageId { get; set; } - /// - /// If null, obtained from StoreContext.CurrentStore. - /// - public int? StoreId { get; set; } + /// + /// If null, obtained from StoreContext.CurrentStore. + /// + public int? StoreId { get; set; } - internal Language Language { get; set; } - internal Store Store { get; set; } - public EmailAccount EmailAccount { get; internal set; } + public Language Language { get; set; } + public Store Store { get; set; } + public EmailAccount EmailAccount { get; internal set; } - public bool TestMode { get; set; } + public bool TestMode { get; set; } - /// - /// If null, obtained from . - /// - public Uri BaseUri { get; set; } + /// + /// If null, obtained from . + /// + public Uri BaseUri { get; set; } - /// - /// The final template model containing all global and template specific model parts. - /// - public TemplateModel Model { get; set; } + /// + /// The final template model containing all global and template specific model parts. + /// + public TemplateModel Model { get; set; } - /// - /// If null, inferred from . - /// - public IFormatProvider FormatProvider - { - get - { - if (_formatProvider == null) - { - var culture = this.Language?.LanguageCulture; - if (culture != null && LocalizationHelper.IsValidCultureCode(culture)) - { - _formatProvider = CultureInfo.GetCultureInfo(culture); - } - } + /// + /// If null, inferred from . + /// + public IFormatProvider FormatProvider + { + get + { + if (_formatProvider == null) + { + var culture = this.Language?.LanguageCulture; + if (culture != null && LocalizationHelper.IsValidCultureCode(culture)) + { + _formatProvider = CultureInfo.GetCultureInfo(culture); + } + } - return _formatProvider ?? CultureInfo.CurrentCulture; - } - set - { - _formatProvider = value; - } - } + return _formatProvider ?? CultureInfo.CurrentCulture; + } + set => _formatProvider = value; + } - public static MessageContext Create(string messageTemplateName, int languageId, int? storeId = null, Customer customer = null) - { - return new MessageContext - { - MessageTemplateName = messageTemplateName, - LanguageId = languageId, - StoreId = storeId, - Customer = customer - }; - } - } + public static MessageContext Create(string messageTemplateName, int languageId, int? storeId = null, Customer customer = null) + { + return new MessageContext + { + MessageTemplateName = messageTemplateName, + LanguageId = languageId, + StoreId = storeId, + Customer = customer + }; + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs b/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs index 6938b55ca2..7c2ab26c7b 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs @@ -25,617 +25,640 @@ namespace SmartStore.Services.Messages { public partial class MessageFactory : IMessageFactory - { - const string LoremIpsum = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."; - - private readonly ICommonServices _services; - private readonly ITemplateEngine _templateEngine; - private readonly ITemplateManager _templateManager; - private readonly IMessageModelProvider _modelProvider; - private readonly IMessageTemplateService _messageTemplateService; - private readonly IQueuedEmailService _queuedEmailService; - private readonly ILanguageService _languageService; - private readonly IEmailAccountService _emailAccountService; - private readonly EmailAccountSettings _emailAccountSettings; - private readonly IMediaService _mediaService; - - public MessageFactory( - ICommonServices services, - ITemplateEngine templateEngine, - ITemplateManager templateManager, - IMessageModelProvider modelProvider, - IMessageTemplateService messageTemplateService, - IQueuedEmailService queuedEmailService, - ILanguageService languageService, - IEmailAccountService emailAccountService, - EmailAccountSettings emailAccountSettings, + { + const string LoremIpsum = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."; + + private readonly ICommonServices _services; + private readonly ITemplateEngine _templateEngine; + private readonly ITemplateManager _templateManager; + private readonly IMessageModelProvider _modelProvider; + private readonly IMessageTemplateService _messageTemplateService; + private readonly IQueuedEmailService _queuedEmailService; + private readonly ILanguageService _languageService; + private readonly IEmailAccountService _emailAccountService; + private readonly EmailAccountSettings _emailAccountSettings; + private readonly IMediaService _mediaService; + + public MessageFactory( + ICommonServices services, + ITemplateEngine templateEngine, + ITemplateManager templateManager, + IMessageModelProvider modelProvider, + IMessageTemplateService messageTemplateService, + IQueuedEmailService queuedEmailService, + ILanguageService languageService, + IEmailAccountService emailAccountService, + EmailAccountSettings emailAccountSettings, IMediaService mediaService) - { - _services = services; - _templateEngine = templateEngine; - _templateManager = templateManager; - _modelProvider = modelProvider; - _messageTemplateService = messageTemplateService; - _queuedEmailService = queuedEmailService; - _languageService = languageService; - _emailAccountService = emailAccountService; - _emailAccountSettings = emailAccountSettings; - _mediaService = mediaService; - } - - public Localizer T { get; set; } = NullLocalizer.Instance; - public ILogger Logger { get; set; } = NullLogger.Instance; - - public virtual CreateMessageResult CreateMessage(MessageContext messageContext, bool queue, params object[] modelParts) - { - Guard.NotNull(messageContext, nameof(messageContext)); - - modelParts = modelParts ?? new object[0]; - - // Handle TestMode - if (messageContext.TestMode && modelParts.Length == 0) - { - modelParts = GetTestModels(messageContext); - } - - ValidateMessageContext(messageContext, ref modelParts); - - // Create and assign model - var model = messageContext.Model = new TemplateModel(); - - // Do not create message if the template does not exist, is not authorized or not active. - if (messageContext.MessageTemplate == null) - { - return new CreateMessageResult { Model = model, MessageContext = messageContext }; - } - - // Add all global template model parts - _modelProvider.AddGlobalModelParts(messageContext); - - // Add specific template models for passed parts - foreach (var part in modelParts) - { - if (model != null) - { - _modelProvider.AddModelPart(part, messageContext); - } - } - - // Give implementors the chance to customize the final template model - _services.EventPublisher.Publish(new MessageModelCreatedEvent(messageContext, model)); - - var messageTemplate = messageContext.MessageTemplate; - var languageId = messageContext.Language.Id; - - // Render templates - var to = RenderEmailAddress(messageTemplate.To, messageContext); - var replyTo = RenderEmailAddress(messageTemplate.ReplyTo, messageContext, false); - var bcc = RenderTemplate(messageTemplate.GetLocalized((x) => x.BccEmailAddresses, languageId), messageContext, false); - - var subject = RenderTemplate(messageTemplate.GetLocalized((x) => x.Subject, languageId), messageContext); - ((dynamic)model).Email.Subject = subject; - - var body = RenderBodyTemplate(messageContext); - - // CSS inliner - body = InlineCss(body, model); - - // Model tree - var modelTree = _modelProvider.BuildModelTree(model); - var modelTreeJson = JsonConvert.SerializeObject(modelTree, Formatting.None); - if (modelTreeJson != messageTemplate.LastModelTree) - { - messageContext.MessageTemplate.LastModelTree = modelTreeJson; - if (!messageTemplate.IsTransientRecord()) - { - _messageTemplateService.UpdateMessageTemplate(messageContext.MessageTemplate); - } - } - - // Create queued email from template - var qe = new QueuedEmail - { - Priority = 5, - From = messageContext.SenderEmailAddress ?? messageContext.EmailAccount.ToEmailAddress(), - To = to.ToString(), - Bcc = bcc, - ReplyTo = replyTo?.ToString(), - Subject = subject, - Body = body, - CreatedOnUtc = DateTime.UtcNow, - EmailAccountId = messageContext.EmailAccount.Id, - SendManually = messageTemplate.SendManually - }; - - // Create and add attachments (if any) - CreateAttachments(qe, messageContext); - - if (queue) - { - // Put to queue - QueueMessage(messageContext, qe); - } - - return new CreateMessageResult { Email = qe, Model = model, MessageContext = messageContext }; - } - - public virtual void QueueMessage(MessageContext messageContext, QueuedEmail queuedEmail) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(queuedEmail, nameof(queuedEmail)); - - // Publish event so that integrators can add attachments, alter the email etc. - _services.EventPublisher.Publish(new MessageQueuingEvent - { - QueuedEmail = queuedEmail, - MessageContext = messageContext, - MessageModel = messageContext.Model - }); - - _queuedEmailService.InsertQueuedEmail(queuedEmail); - } - - private EmailAddress RenderEmailAddress(string email, MessageContext ctx, bool required = true) - { - string parsed = null; - - try - { - parsed = RenderTemplate(email, ctx, required); - - if (required || parsed.HasValue()) - { - return parsed.Convert(); - } - else - { - return null; - } - } - catch (Exception ex) - { - if (ctx.TestMode) - { - return new EmailAddress("john@doe.com", "John Doe"); - } - - var ex2 = new SmartException($"Failed to parse email address for variable '{email}'. Value was '{parsed.EmptyNull()}': {ex.Message}", ex); - _services.Notifier.Error(ex2.Message); - throw ex2; - } - } - - private string RenderTemplate(string template, MessageContext ctx, bool required = true) - { - if (!required && template.IsEmpty()) - { - return null; - } - - return _templateEngine.Render(template, ctx.Model, ctx.FormatProvider); - } - - private string RenderBodyTemplate(MessageContext ctx) - { - var key = BuildTemplateKey(ctx); - var source = ctx.MessageTemplate.GetLocalized((x) => x.Body, ctx.Language); - var fromCache = true; - var template = _templateManager.GetOrAdd(key, GetBodyTemplate); - - if (fromCache && template.Source != source) - { - // The template was resolved from template cache, but it has expired - // because the source text has changed. - template = _templateEngine.Compile(source); - _templateManager.Put(key, template); - } - - return template.Render(ctx.Model, ctx.FormatProvider); - - string GetBodyTemplate() - { - fromCache = false; - return source; - } - } - - private string BuildTemplateKey(MessageContext messageContext) - { - var prefix = messageContext.MessageTemplate.IsTransientRecord() ? "TransientTemplate/" : "MessageTemplate/"; - return prefix + messageContext.MessageTemplate.Name + "/" + messageContext.Language.Id + "/Body"; - } - - private string InlineCss(string html, dynamic model) - { - Uri baseUri = null; - - try - { - // 'Store' is a global model part, so we pretty can be sure it exists - baseUri = new Uri((string)model.Store.Url); - } - catch { } - - var pm = new PreMailer.Net.PreMailer(html, baseUri); - var result = pm.MoveCssInline(true, "#ignore"); - return result.Html; - } - - protected virtual void CreateAttachments(QueuedEmail queuedEmail, MessageContext messageContext) - { - var messageTemplate = messageContext.MessageTemplate; - var languageId = messageContext.Language.Id; - - // Create attachments if any. - var fileIds = (new int?[] - { - messageTemplate.GetLocalized(x => x.Attachment1FileId, languageId), - messageTemplate.GetLocalized(x => x.Attachment2FileId, languageId), - messageTemplate.GetLocalized(x => x.Attachment3FileId, languageId) - }) - .Where(x => x.HasValue) - .Select(x => x.Value) + { + _services = services; + _templateEngine = templateEngine; + _templateManager = templateManager; + _modelProvider = modelProvider; + _messageTemplateService = messageTemplateService; + _queuedEmailService = queuedEmailService; + _languageService = languageService; + _emailAccountService = emailAccountService; + _emailAccountSettings = emailAccountSettings; + _mediaService = mediaService; + } + + public Localizer T { get; set; } = NullLocalizer.Instance; + public ILogger Logger { get; set; } = NullLogger.Instance; + + public virtual CreateMessageResult CreateMessage(MessageContext messageContext, bool queue, params object[] modelParts) + { + Guard.NotNull(messageContext, nameof(messageContext)); + + modelParts = modelParts ?? new object[0]; + + // Handle TestMode + if (messageContext.TestMode && modelParts.Length == 0) + { + modelParts = GetTestModels(messageContext); + } + + ValidateMessageContext(messageContext, ref modelParts); + + // Create and assign model + var model = messageContext.Model = new TemplateModel(); + + // Do not create message if the template does not exist, is not authorized or not active. + if (messageContext.MessageTemplate == null) + { + return new CreateMessageResult { Model = model, MessageContext = messageContext }; + } + + // Add all global template model parts + _modelProvider.AddGlobalModelParts(messageContext); + + // Add specific template models for passed parts + foreach (var part in modelParts) + { + if (model != null) + { + _modelProvider.AddModelPart(part, messageContext); + } + } + + // Give implementors the chance to customize the final template model + _services.EventPublisher.Publish(new MessageModelCreatedEvent(messageContext, model)); + + var messageTemplate = messageContext.MessageTemplate; + var languageId = messageContext.Language.Id; + + // Render templates + var to = RenderEmailAddress(messageTemplate.To, messageContext); + var replyTo = RenderEmailAddress(messageTemplate.ReplyTo, messageContext, false); + var bcc = RenderTemplate(messageTemplate.GetLocalized((x) => x.BccEmailAddresses, languageId), messageContext, false); + + var subject = RenderTemplate(messageTemplate.GetLocalized((x) => x.Subject, languageId), messageContext); + ((dynamic)model).Email.Subject = subject; + + var body = RenderBodyTemplate(messageContext); + + // CSS inliner + body = InlineCss(body, model); + + // Model tree + var modelTree = _modelProvider.BuildModelTree(model); + var modelTreeJson = JsonConvert.SerializeObject(modelTree, Formatting.None); + if (modelTreeJson != messageTemplate.LastModelTree) + { + messageContext.MessageTemplate.LastModelTree = modelTreeJson; + if (!messageTemplate.IsTransientRecord()) + { + _messageTemplateService.UpdateMessageTemplate(messageContext.MessageTemplate); + } + } + + // Create queued email from template + var qe = new QueuedEmail + { + Priority = 5, + From = messageContext.SenderEmailAddress ?? messageContext.EmailAccount.ToEmailAddress(), + To = to.ToString(), + Bcc = bcc, + ReplyTo = replyTo?.ToString(), + Subject = subject, + Body = body, + CreatedOnUtc = DateTime.UtcNow, + EmailAccountId = messageContext.EmailAccount.Id, + SendManually = messageTemplate.SendManually + }; + + // Create and add attachments (if any) + CreateAttachments(qe, messageContext); + + if (queue) + { + // Put to queue + QueueMessage(messageContext, qe); + } + + return new CreateMessageResult { Email = qe, Model = model, MessageContext = messageContext }; + } + + public virtual void QueueMessage(MessageContext messageContext, QueuedEmail queuedEmail) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(queuedEmail, nameof(queuedEmail)); + + // Publish event so that integrators can add attachments, alter the email etc. + _services.EventPublisher.Publish(new MessageQueuingEvent + { + QueuedEmail = queuedEmail, + MessageContext = messageContext, + MessageModel = messageContext.Model + }); + + _queuedEmailService.InsertQueuedEmail(queuedEmail); + } + + private EmailAddress RenderEmailAddress(string email, MessageContext ctx, bool required = true) + { + string parsed = null; + + try + { + parsed = RenderTemplate(email, ctx, required); + + if (required || parsed.HasValue()) + { + return parsed.Convert(); + } + else + { + return null; + } + } + catch (Exception ex) + { + if (ctx.TestMode) + { + return new EmailAddress("john@doe.com", "John Doe"); + } + + var ex2 = new SmartException($"Failed to parse email address for variable '{email}'. Value was '{parsed.EmptyNull()}': {ex.Message}", ex); + _services.Notifier.Error(ex2.Message); + throw ex2; + } + } + + private string RenderTemplate(string template, MessageContext ctx, bool required = true) + { + if (!required && template.IsEmpty()) + { + return null; + } + + return _templateEngine.Render(template, ctx.Model, ctx.FormatProvider); + } + + private string RenderBodyTemplate(MessageContext ctx) + { + var key = BuildTemplateKey(ctx); + var source = ctx.MessageTemplate.GetLocalized((x) => x.Body, ctx.Language); + var fromCache = true; + var template = _templateManager.GetOrAdd(key, GetBodyTemplate); + + if (fromCache && template.Source != source) + { + // The template was resolved from template cache, but it has expired + // because the source text has changed. + template = _templateEngine.Compile(source); + _templateManager.Put(key, template); + } + + return template.Render(ctx.Model, ctx.FormatProvider); + + string GetBodyTemplate() + { + fromCache = false; + return source; + } + } + + private string BuildTemplateKey(MessageContext messageContext) + { + var prefix = messageContext.MessageTemplate.IsTransientRecord() ? "TransientTemplate/" : "MessageTemplate/"; + return prefix + messageContext.MessageTemplate.Name + "/" + messageContext.Language.Id + "/Body"; + } + + private string InlineCss(string html, dynamic model) + { + Uri baseUri = null; + + try + { + // 'Store' is a global model part, so we pretty can be sure it exists + baseUri = new Uri((string)model.Store.Url); + } + catch { } + + var pm = new PreMailer.Net.PreMailer(html, baseUri); + var result = pm.MoveCssInline(true, "#ignore"); + return result.Html; + } + + protected virtual void CreateAttachments(QueuedEmail queuedEmail, MessageContext messageContext) + { + var messageTemplate = messageContext.MessageTemplate; + var languageId = messageContext.Language.Id; + + // Create attachments if any. + var fileIds = (new int?[] + { + messageTemplate.GetLocalized(x => x.Attachment1FileId, languageId), + messageTemplate.GetLocalized(x => x.Attachment2FileId, languageId), + messageTemplate.GetLocalized(x => x.Attachment3FileId, languageId) + }) + .Where(x => x.HasValue) + .Select(x => x.Value) .Distinct() - .ToArray(); - - if (fileIds.Any()) - { - var files = _mediaService.GetFilesByIds(fileIds, MediaLoadFlags.WithBlob); - foreach (var file in files) - { - queuedEmail.Attachments.Add(new QueuedEmailAttachment - { - StorageLocation = EmailAttachmentStorageLocation.FileReference, - MediaFileId = file.Id, - Name = file.Name, - MimeType = file.MimeType - }); - } - } - } - - - private void ValidateMessageContext(MessageContext ctx, ref object[] modelParts) - { - var t = ctx.MessageTemplate; - if (t != null) - { - if (t.To.IsEmpty() || t.Subject.IsEmpty() || t.Name.IsEmpty()) - { - throw new InvalidOperationException("Message template validation failed, because at least one of the following properties has not been set: Name, To, Subject."); - } - } - - if (ctx.StoreId.GetValueOrDefault() == 0) - { - ctx.Store = _services.StoreContext.CurrentStore; - ctx.StoreId = ctx.Store.Id; - } - else - { - ctx.Store = _services.StoreService.GetStoreById(ctx.StoreId.Value); - } - - if (ctx.BaseUri == null) - { - ctx.BaseUri = new Uri(_services.StoreService.GetHost(ctx.Store)); - } - - if (ctx.LanguageId.GetValueOrDefault() == 0) - { - ctx.Language = _services.WorkContext.WorkingLanguage; - ctx.LanguageId = ctx.Language.Id; - } - else - { - ctx.Language = _languageService.GetLanguageById(ctx.LanguageId.Value); - } - - EnsureLanguageIsActive(ctx); - - var parts = modelParts?.AsEnumerable() ?? Enumerable.Empty(); - - if (ctx.Customer == null) - { - // Try to move Customer from parts to MessageContext - var customer = parts.OfType().FirstOrDefault(); - if (customer != null) - { - // Exclude the found customer from parts list - parts = parts.Where(x => !object.ReferenceEquals(x, customer)); - } - - ctx.Customer = customer ?? _services.WorkContext.CurrentCustomer; - } - - if (ctx.Customer.IsSystemAccount) - { - throw new ArgumentException("Cannot create messages for system customer accounts.", nameof(ctx)); - } - - if (ctx.MessageTemplate == null) - { - if (ctx.MessageTemplateName.IsEmpty()) - { - throw new ArgumentException("'MessageTemplateName' must not be empty if 'MessageTemplate' is null.", nameof(ctx)); - } - - ctx.MessageTemplate = _messageTemplateService.GetMessageTemplateByName(ctx.MessageTemplateName, ctx.Store.Id); - - if (ctx.MessageTemplate != null && !ctx.TestMode && !ctx.MessageTemplate.IsActive) - { - ctx.MessageTemplate = null; - } - } - - if (ctx.EmailAccount == null && ctx.MessageTemplate != null) - { - ctx.EmailAccount = GetEmailAccountOfMessageTemplate(ctx.MessageTemplate, ctx.Language.Id); - } - - // Sort parts: "IModelPart" instances must come first - var bagParts = parts.OfType(); - if (bagParts.Any()) - { - parts = bagParts.Concat(parts.Except(bagParts)); - } - - modelParts = parts.Where(x => x != null).ToArray(); - } - - protected EmailAccount GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, int languageId) - { - // Note that the email account to be used can be specified separately for each language, that's why we use GetLocalized here. - var accountId = messageTemplate.GetLocalized(x => x.EmailAccountId, languageId); - var account = _emailAccountService.GetEmailAccountById(accountId); - - if (account == null) - { - account = _emailAccountService.GetDefaultEmailAccount(); - } - - if (account == null) - { - throw new SmartException(T("Common.Error.NoEmailAccount")); - } - - return account; - } - - private void EnsureLanguageIsActive(MessageContext ctx) - { - var language = ctx.Language; - - if (language == null || !language.Published) - { - // Load any language from the specified store - language = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId(ctx.StoreId.Value)); - } - - if (language == null || !language.Published) - { - // Load any language - language = _languageService.GetAllLanguages().FirstOrDefault(); - } - - ctx.Language = language ?? throw new SmartException(T("Common.Error.NoActiveLanguage")); - } - - #region TestModels - - public virtual object[] GetTestModels(MessageContext messageContext) - { - var templateName = (messageContext.MessageTemplate?.Name ?? messageContext.MessageTemplateName); - - var factories = new Dictionary>(StringComparer.OrdinalIgnoreCase) - { - { "BlogComment", () => GetRandomEntity(x => true) }, - { "Product", () => GetRandomEntity(x => !x.Deleted && !x.IsSystemProduct && x.Visibility != ProductVisibility.Hidden && x.Published) }, - { "Customer", () => GetRandomEntity(x => !x.Deleted && !x.IsSystemAccount && !string.IsNullOrEmpty(x.Email)) }, - { "Order", () => GetRandomEntity(x => !x.Deleted) }, - { "Shipment", () => GetRandomEntity(x => !x.Order.Deleted) }, - { "OrderNote", () => GetRandomEntity(x => !x.Order.Deleted) }, - { "RecurringPayment", () => GetRandomEntity(x => !x.Deleted) }, - { "NewsLetterSubscription", () => GetRandomEntity(x => true) }, - { "Campaign", () => GetRandomEntity(x => true) }, - { "ReturnRequest", () => GetRandomEntity(x => true) }, - { "OrderItem", () => GetRandomEntity(x => !x.Order.Deleted) }, - { "ForumTopic", () => GetRandomEntity(x => true) }, - { "ForumPost", () => GetRandomEntity(x => true) }, - { "PrivateMessage", () => GetRandomEntity(x => true) }, - { "GiftCard", () => GetRandomEntity(x => true) }, - { "ProductReview", () => GetRandomEntity(x => !x.Product.Deleted && !x.Product.IsSystemProduct && x.Product.Visibility != ProductVisibility.Hidden && x.Product.Published) }, - { "NewsComment", () => GetRandomEntity(x => x.NewsItem.Published) }, - { "WalletHistory", () => GetRandomEntity(x => true) } - }; - - var modelNames = messageContext.MessageTemplate.ModelTypes - .SplitSafe(",") - .Select(x => x.Trim()) - .Distinct() - .ToArray(); - - var models = new Dictionary(); - var result = new List(); - - foreach (var modelName in modelNames) - { - var model = GetModelFromExpression(modelName, models, factories); - if (model != null) - { - result.Add(model); - } - } - - // Some models are special - var isTransientTemplate = messageContext.MessageTemplate != null && messageContext.MessageTemplate.IsTransientRecord(); - - if (!isTransientTemplate) - { - switch (templateName) - { - case MessageTemplateNames.SystemContactUs: - result.Add(new NamedModelPart("Message") - { - ["Subject"] = "Test subject", - ["Message"] = LoremIpsum, - ["SenderEmail"] = "jane@doe.com", - ["SenderName"] = "Jane Doe" - }); - break; - case MessageTemplateNames.ProductQuestion: - result.Add(new NamedModelPart("Message") - { - ["Message"] = LoremIpsum, - ["SenderEmail"] = "jane@doe.com", - ["SenderName"] = "Jane Doe", - ["SenderPhone"] = "123456789" - }); - break; - case MessageTemplateNames.ShareProduct: - result.Add(new NamedModelPart("Message") - { - ["Body"] = LoremIpsum, - ["From"] = "jane@doe.com", - ["To"] = "john@doe.com", - }); - break; - case MessageTemplateNames.ShareWishlist: - result.Add(new NamedModelPart("Wishlist") - { - ["PersonalMessage"] = LoremIpsum, - ["From"] = "jane@doe.com", - ["To"] = "john@doe.com", - }); - break; - case MessageTemplateNames.NewVatSubmittedStoreOwner: - result.Add(new NamedModelPart("VatValidationResult") - { - ["Name"] = "VatName", - ["Address"] = "VatAddress" - }); - break; - } - } - - return result.ToArray(); - } - - private object GetModelFromExpression(string expression, IDictionary models, IDictionary> factories) - { - object currentModel = null; - int dotIndex = 0; - int len = expression.Length; - bool bof = true; - string token = null; - - for (var i = 0; i < len; i++) - { - if (expression[i] == '.') - { - bof = false; - token = expression.Substring(0, i); - } - else if (i == len - 1) - { - // End reached - token = expression; - } - else - { - continue; - } - - if (!models.TryGetValue(token, out currentModel)) - { - if (bof) - { - // It's a simple dot-less expression where the token - // is actually the model name - currentModel = factories.Get(token)?.Invoke(); - } - else - { - // Sub-token, e.g. "Order.Customer" - // Get "Customer" part, this is our property name, NOT the model name - var propName = token.Substring(dotIndex + 1); - // Get parent model "Order" - var parentModel = models.Get(token.Substring(0, dotIndex)); - if (parentModel == null) - break; - - if (parentModel is ITestModel) - { - // When the parent model is a test model, we need to create a random instance - // instead of using the property value (which is null/void in this case) - currentModel = factories.Get(propName)?.Invoke(); - } - else - { - // Get "Customer" property of Order - var fastProp = FastProperty.GetProperty(parentModel.GetType(), propName, PropertyCachingStrategy.Uncached); - if (fastProp != null) - { - // Get "Customer" value - var propValue = fastProp.GetValue(parentModel); - if (propValue != null) - { - currentModel = propValue; - //// Resolve logical model name... - //var modelName = _modelProvider.ResolveModelName(propValue); - //if (modelName != null) - //{ - // // ...and create the value - // currentModel = factories.Get(modelName)?.Invoke(); - //} - } - } - } - } - - if (currentModel == null) - break; - - // Put it in dict as e.g. "Order.Customer" - models[token] = currentModel; - } - - if (!bof) - { - dotIndex = i; - } - } - - return currentModel; - } - - private object GetRandomEntity(Expression> predicate) where T : BaseEntity, new() - { - var dbSet = _services.DbContext.Set(); - - var query = dbSet.Where(predicate); - - // Determine how many entities match the given predicate - var count = query.Count(); - - object result; - - if (count > 0) - { - // Fetch a random one - var skip = CommonHelper.GenerateRandomInteger(0, count); - result = query.OrderBy(x => x.Id).Skip(() => skip).FirstOrDefault(); - } - else - { - // No entity macthes the predicate. Provide a fallback test entity - var entity = Activator.CreateInstance(); - result = _templateEngine.CreateTestModelFor(entity, entity.GetUnproxiedType().Name); - } - - return result; - } - - #endregion - } + .ToArray(); + + if (fileIds.Any()) + { + var files = _mediaService.GetFilesByIds(fileIds); + foreach (var file in files) + { + queuedEmail.Attachments.Add(new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.FileReference, + MediaFileId = file.Id, + Name = file.Name, + MimeType = file.MimeType + }); + } + } + } + + + private void ValidateMessageContext(MessageContext ctx, ref object[] modelParts) + { + var t = ctx.MessageTemplate; + if (t != null) + { + if (t.To.IsEmpty() || t.Subject.IsEmpty() || t.Name.IsEmpty()) + { + throw new InvalidOperationException("Message template validation failed, because at least one of the following properties has not been set: Name, To, Subject."); + } + } + + if (ctx.StoreId.GetValueOrDefault() == 0) + { + ctx.Store = _services.StoreContext.CurrentStore; + ctx.StoreId = ctx.Store.Id; + } + else + { + ctx.Store = _services.StoreService.GetStoreById(ctx.StoreId.Value); + } + + if (ctx.BaseUri == null) + { + ctx.BaseUri = new Uri(_services.StoreService.GetHost(ctx.Store)); + } + + if (ctx.LanguageId.GetValueOrDefault() == 0) + { + ctx.Language = _services.WorkContext.WorkingLanguage; + ctx.LanguageId = ctx.Language.Id; + } + else + { + ctx.Language = _languageService.GetLanguageById(ctx.LanguageId.Value); + } + + EnsureLanguageIsActive(ctx); + + var parts = modelParts?.AsEnumerable() ?? Enumerable.Empty(); + + if (ctx.Customer == null) + { + // Try to move Customer from parts to MessageContext + var customer = parts.OfType().FirstOrDefault(); + if (customer != null) + { + // Exclude the found customer from parts list + parts = parts.Where(x => !object.ReferenceEquals(x, customer)); + } + + ctx.Customer = customer ?? _services.WorkContext.CurrentCustomer; + } + + if (ctx.Customer.IsSystemAccount) + { + throw new ArgumentException("Cannot create messages for system customer accounts.", nameof(ctx)); + } + + if (ctx.MessageTemplate == null) + { + if (ctx.MessageTemplateName.IsEmpty()) + { + throw new ArgumentException("'MessageTemplateName' must not be empty if 'MessageTemplate' is null.", nameof(ctx)); + } + + ctx.MessageTemplate = _messageTemplateService.GetMessageTemplateByName(ctx.MessageTemplateName, ctx.Store.Id); + + if (ctx.MessageTemplate != null && !ctx.TestMode && !ctx.MessageTemplate.IsActive) + { + ctx.MessageTemplate = null; + } + } + + if (ctx.EmailAccount == null && ctx.MessageTemplate != null) + { + ctx.EmailAccount = GetEmailAccountOfMessageTemplate(ctx.MessageTemplate, ctx.Language.Id); + } + + // Sort parts: "IModelPart" instances must come first + var bagParts = parts.OfType(); + if (bagParts.Any()) + { + parts = bagParts.Concat(parts.Except(bagParts)); + } + + modelParts = parts.Where(x => x != null).ToArray(); + } + + protected EmailAccount GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, int languageId) + { + // Note that the email account to be used can be specified separately for each language, that's why we use GetLocalized here. + var accountId = messageTemplate.GetLocalized(x => x.EmailAccountId, languageId); + var account = _emailAccountService.GetEmailAccountById(accountId); + + if (account == null) + { + account = _emailAccountService.GetDefaultEmailAccount(); + } + + if (account == null) + { + throw new SmartException(T("Common.Error.NoEmailAccount")); + } + + return account; + } + + private void EnsureLanguageIsActive(MessageContext ctx) + { + var language = ctx.Language; + + if (language == null || !language.Published) + { + // Load any language from the specified store + language = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId(ctx.StoreId.Value)); + } + + if (language == null || !language.Published) + { + // Load any language + language = _languageService.GetAllLanguages().FirstOrDefault(); + } + + ctx.Language = language ?? throw new SmartException(T("Common.Error.NoActiveLanguage")); + } + + #region TestModels + + public virtual object[] GetTestModels(MessageContext messageContext) + { + var templateName = messageContext.MessageTemplate?.Name ?? messageContext.MessageTemplateName; + + var factories = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "BlogComment", () => GetRandomEntity(x => true) }, + { "Product", () => GetRandomEntity(x => !x.Deleted && !x.IsSystemProduct && x.Visibility != ProductVisibility.Hidden && x.Published) }, + { "Customer", () => GetRandomEntity(x => !x.Deleted && !x.IsSystemAccount && !string.IsNullOrEmpty(x.Email)) }, + { "Order", () => GetRandomEntity(x => !x.Deleted) }, + { "Shipment", () => GetRandomEntity(x => !x.Order.Deleted) }, + { "OrderNote", () => GetRandomEntity(x => !x.Order.Deleted) }, + { "RecurringPayment", () => GetRandomEntity(x => !x.Deleted) }, + { "NewsLetterSubscription", () => GetRandomEntity(x => true) }, + { "Campaign", () => GetRandomEntity(x => true) }, + { "ReturnRequest", () => GetRandomEntity(x => true) }, + { "OrderItem", () => GetRandomEntity(x => !x.Order.Deleted) }, + { "ForumTopic", () => GetRandomEntity(x => true) }, + { "ForumPost", () => GetRandomEntity(x => true) }, + { "PrivateMessage", () => GetRandomEntity(x => true) }, + { "GiftCard", () => GetRandomEntity(x => true) }, + { "ProductReview", () => GetRandomEntity(x => !x.Product.Deleted && !x.Product.IsSystemProduct && x.Product.Visibility != ProductVisibility.Hidden && x.Product.Published) }, + { "NewsComment", () => GetRandomEntity(x => x.NewsItem.Published) }, + { "WalletHistory", () => GetRandomEntity(x => true) } + }; + + var modelNames = messageContext.MessageTemplate.ModelTypes + .SplitSafe(",") + .Select(x => x.Trim()) + .Distinct() + .ToArray(); + + var models = new Dictionary(); + var result = new List(); + + foreach (var modelName in modelNames) + { + var model = GetModelFromExpression(modelName, models, factories); + if (model != null) + { + result.Add(model); + } + } + + // Some models are special. + var isTransientTemplate = messageContext.MessageTemplate != null && messageContext.MessageTemplate.IsTransientRecord(); + + if (!isTransientTemplate) + { + switch (templateName) + { + case MessageTemplateNames.SystemContactUs: + result.Add(new NamedModelPart("Message") + { + ["Subject"] = "Test subject", + ["Message"] = LoremIpsum, + ["SenderEmail"] = "jane@doe.com", + ["SenderName"] = "Jane Doe" + }); + break; + case MessageTemplateNames.ProductQuestion: + result.Add(new NamedModelPart("Message") + { + ["Message"] = LoremIpsum, + ["SenderEmail"] = "jane@doe.com", + ["SenderName"] = "Jane Doe", + ["SenderPhone"] = "123456789" + }); + break; + case MessageTemplateNames.ShareProduct: + result.Add(new NamedModelPart("Message") + { + ["Body"] = LoremIpsum, + ["From"] = "jane@doe.com", + ["To"] = "john@doe.com", + }); + break; + case MessageTemplateNames.ShareWishlist: + result.Add(new NamedModelPart("Wishlist") + { + ["PersonalMessage"] = LoremIpsum, + ["From"] = "jane@doe.com", + ["To"] = "john@doe.com", + }); + break; + case MessageTemplateNames.NewVatSubmittedStoreOwner: + result.Add(new NamedModelPart("VatValidationResult") + { + ["Name"] = "VatName", + ["Address"] = "VatAddress" + }); + break; + case MessageTemplateNames.SystemGeneric: + result.Add(new NamedModelPart("Generic") + { + ["Email"] = "john@doe.com", + ["Subject"] = "Subject", + ["Body"] = LoremIpsum + }); + break; + } + } + + return result.ToArray(); + } + + private object GetModelFromExpression(string expression, IDictionary models, IDictionary> factories) + { + object currentModel = null; + int dotIndex = 0; + int len = expression.Length; + bool bof = true; + string token = null; + + for (var i = 0; i < len; i++) + { + if (expression[i] == '.') + { + bof = false; + token = expression.Substring(0, i); + } + else if (i == len - 1) + { + // End reached + token = expression; + } + else + { + continue; + } + + if (!models.TryGetValue(token, out currentModel)) + { + if (bof) + { + // It's a simple dot-less expression where the token + // is actually the model name + currentModel = factories.Get(token)?.Invoke(); + } + else + { + // Sub-token, e.g. "Order.Customer" + // Get "Customer" part, this is our property name, NOT the model name + var propName = token.Substring(dotIndex + 1); + // Get parent model "Order" + var parentModel = models.Get(token.Substring(0, dotIndex)); + if (parentModel == null) + break; + + if (parentModel is ITestModel) + { + // When the parent model is a test model, we need to create a random instance + // instead of using the property value (which is null/void in this case) + currentModel = factories.Get(propName)?.Invoke(); + } + else + { + // Get "Customer" property of Order + var fastProp = FastProperty.GetProperty(parentModel.GetType(), propName, PropertyCachingStrategy.Uncached); + if (fastProp != null) + { + // Get "Customer" value + var propValue = fastProp.GetValue(parentModel); + if (propValue != null) + { + currentModel = propValue; + //// Resolve logical model name... + //var modelName = _modelProvider.ResolveModelName(propValue); + //if (modelName != null) + //{ + // // ...and create the value + // currentModel = factories.Get(modelName)?.Invoke(); + //} + } + } + } + } + + if (currentModel == null) + break; + + // Put it in dict as e.g. "Order.Customer" + models[token] = currentModel; + } + + if (!bof) + { + dotIndex = i; + } + } + + return currentModel; + } + + private object GetRandomEntity(Expression> predicate) where T : BaseEntity, new() + { + var dbSet = _services.DbContext.Set(); + + var query = dbSet.Where(predicate); + + // Determine how many entities match the given predicate. + var count = query.Count(); + + object result; + + if (count > 0) + { + // Fetch a random one. + var skip = CommonHelper.GenerateRandomInteger(0, count); + result = query.OrderBy(x => x.Id).Skip(() => skip).FirstOrDefault(); + } + else + { + // No entity matches the predicate. Provide a fallback test entity. + var entity = Activator.CreateInstance(); + + if (entity is NewsLetterSubscription subscription) + { + // Campaign preview requires NewsLetterSubscription entity. + subscription.NewsLetterSubscriptionGuid = Guid.NewGuid(); + subscription.Email = "john@doe.com"; + subscription.Active = true; + subscription.CreatedOnUtc = DateTime.UtcNow; + subscription.WorkingLanguageId = _services.WorkContext.WorkingLanguage.Id; + + result = entity; + } + else + { + result = _templateEngine.CreateTestModelFor(entity, entity.GetUnproxiedType().Name); + } + } + + return result; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs index f2a306fa7c..469a57cd4b 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs @@ -22,287 +22,287 @@ namespace SmartStore.Services.Messages { public partial class MessageModelProvider - { - protected virtual object CreateModelPart(Order part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var allow = new HashSet - { - nameof(part.Id), - nameof(part.OrderNumber), - nameof(part.OrderGuid), - nameof(part.StoreId), - nameof(part.OrderStatus), - nameof(part.PaymentStatus), - nameof(part.ShippingStatus), - nameof(part.CustomerTaxDisplayType), - nameof(part.TaxRatesDictionary), - nameof(part.VatNumber), - nameof(part.AffiliateId), - nameof(part.CustomerIp), - nameof(part.CardType), - nameof(part.CardName), - nameof(part.MaskedCreditCardNumber), - nameof(part.DirectDebitAccountHolder), - nameof(part.DirectDebitBankCode), // TODO: (mc) Liquid > Bank data (?) + { + protected virtual object CreateModelPart(Order part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var allow = new HashSet + { + nameof(part.Id), + nameof(part.OrderNumber), + nameof(part.OrderGuid), + nameof(part.StoreId), + nameof(part.OrderStatus), + nameof(part.PaymentStatus), + nameof(part.ShippingStatus), + nameof(part.CustomerTaxDisplayType), + nameof(part.TaxRatesDictionary), + nameof(part.VatNumber), + nameof(part.AffiliateId), + nameof(part.CustomerIp), + nameof(part.CardType), + nameof(part.CardName), + nameof(part.MaskedCreditCardNumber), + nameof(part.DirectDebitAccountHolder), + nameof(part.DirectDebitBankCode), // TODO: (mc) Liquid > Bank data (?) nameof(part.PurchaseOrderNumber), - nameof(part.ShippingMethod), - nameof(part.PaymentMethodSystemName), - nameof(part.ShippingRateComputationMethodSystemName) + nameof(part.ShippingMethod), + nameof(part.PaymentMethodSystemName), + nameof(part.ShippingRateComputationMethodSystemName) // TODO: (mc) Liquid > More whitelisting? }; - var m = new HybridExpando(part, allow, MemberOptMethod.Allow); - var d = m as dynamic; - - d.ID = part.Id; - d.Billing = CreateModelPart(part.BillingAddress, messageContext); - if (part.ShippingAddress != null) - { - d.Shipping = part.ShippingAddress.IsPostalDataEqual(part.BillingAddress) == true ? null : CreateModelPart(part.ShippingAddress, messageContext); - } - d.CustomerEmail = part.BillingAddress.Email.NullEmpty(); - d.CustomerComment = part.CustomerOrderComment.NullEmpty(); - d.Disclaimer = GetTopic("Disclaimer", messageContext); - d.ConditionsOfUse = GetTopic("ConditionsOfUse", messageContext); - d.Status = part.OrderStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id); - d.CreatedOn = ToUserDate(part.CreatedOnUtc, messageContext); - d.PaidOn = ToUserDate(part.PaidDateUtc, messageContext); - - // Payment method - var paymentMethodName = part.PaymentMethodSystemName; - var paymentMethod = _services.Resolve().GetProvider(part.PaymentMethodSystemName); - if (paymentMethod != null) - { - paymentMethodName = GetLocalizedValue(messageContext, paymentMethod.Metadata, nameof(paymentMethod.Metadata.FriendlyName), x => x.FriendlyName); - } - d.PaymentMethod = paymentMethodName.NullEmpty(); - - d.Url = part.Customer != null && !part.Customer.IsGuest() - ? BuildActionUrl("Details", "Order", new { id = part.Id, area = "" }, messageContext) - : null; - - // Overrides - m.Properties["OrderNumber"] = part.GetOrderNumber().NullEmpty(); - m.Properties["AcceptThirdPartyEmailHandOver"] = GetBoolResource(part.AcceptThirdPartyEmailHandOver, messageContext); - - // Items, Totals & Co. - d.Items = part.OrderItems.Where(x => x.Product != null).Select(x => CreateModelPart(x, messageContext)).ToList(); - d.Totals = CreateOrderTotalsPart(part, messageContext); - - // Checkout Attributes - if (part.CheckoutAttributeDescription.HasValue()) - { - d.CheckoutAttributes = HtmlUtils.ConvertPlainTextToTable(HtmlUtils.ConvertHtmlToPlainText(part.CheckoutAttributeDescription)).NullEmpty(); - } - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateOrderTotalsPart(Order order, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(order, nameof(order)); - - var language = messageContext.Language; - var currencyService = _services.Resolve(); - var paymentService = _services.Resolve(); - var priceFormatter = _services.Resolve(); - var taxSettings = _services.Settings.LoadSetting(messageContext.Store.Id); - - var taxRates = new SortedDictionary(); - Money cusTaxTotal = null; - Money cusDiscount = null; - Money cusRounding = null; - Money cusTotal = null; - - var subTotals = GetSubTotals(order, messageContext); - - // Shipping - bool dislayShipping = order.ShippingStatus != ShippingStatus.ShippingNotRequired; - - // Payment method fee - bool displayPaymentMethodFee = true; - if (order.PaymentMethodAdditionalFeeExclTax == decimal.Zero) - { - displayPaymentMethodFee = false; - } - - // Tax - bool displayTax = true; - bool displayTaxRates = true; - if (taxSettings.HideTaxInOrderSummary && order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax) - { - displayTax = false; - displayTaxRates = false; - } - else - { - if (order.OrderTax == 0 && taxSettings.HideZeroTax) - { - displayTax = false; - displayTaxRates = false; - } - else - { - taxRates = new SortedDictionary(); - foreach (var tr in order.TaxRatesDictionary) - { - taxRates.Add(tr.Key, currencyService.ConvertCurrency(tr.Value, order.CurrencyRate)); - } - - displayTaxRates = taxSettings.DisplayTaxRates && taxRates.Count > 0; - displayTax = !displayTaxRates; - - cusTaxTotal = FormatPrice(order.OrderTax, order, messageContext); - } - } - - // Discount - bool dislayDiscount = false; - if (order.OrderDiscount > decimal.Zero) - { - cusDiscount = FormatPrice(-order.OrderDiscount, order, messageContext); - dislayDiscount = true; - } - - // Total - var roundingAmount = decimal.Zero; - var orderTotal = order.GetOrderTotalInCustomerCurrency(currencyService, paymentService, out roundingAmount); - cusTotal = FormatPrice(orderTotal, order.CustomerCurrencyCode, messageContext); - - // Rounding - if (roundingAmount != decimal.Zero) - { - cusRounding = FormatPrice(roundingAmount, order.CustomerCurrencyCode, messageContext); - } - - // Model - dynamic m = new ExpandoObject(); - - m.SubTotal = subTotals.SubTotal; - m.SubTotalDiscount = subTotals.DisplaySubTotalDiscount ? subTotals.SubTotalDiscount : null; - m.Shipping = dislayShipping ? subTotals.ShippingTotal : null; - m.Payment = displayPaymentMethodFee ? subTotals.PaymentFee : null; - m.Tax = displayTax ? cusTaxTotal : null; - m.Discount = dislayDiscount ? cusDiscount : null; - m.RoundingDiff = cusRounding; - m.Total = cusTotal; - m.IsGross = order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax; - - // TaxRates - m.TaxRates = !displayTaxRates ? (object[])null : taxRates.Select(x => - { - return new - { - Rate = T("Order.TaxRateLine", language.Id, priceFormatter.FormatTaxRate(x.Key)).Text, - Value = FormatPrice(x.Value, order, messageContext) - }; - }).ToArray(); - - - // Gift Cards - m.GiftCardUsage = order.GiftCardUsageHistory.Count == 0 ? (object[])null : order.GiftCardUsageHistory.Select(x => - { - return new - { - GiftCard = T("Order.GiftCardInfo", language.Id, x.GiftCard.GiftCardCouponCode).Text, - UsedAmount = FormatPrice(-x.UsedValue, order, messageContext), - RemainingAmount = FormatPrice(x.GiftCard.GetGiftCardRemainingAmount(), order, messageContext) - }; - }).ToArray(); - - // Reward Points - m.RedeemedRewardPoints = order.RedeemedRewardPointsEntry == null ? null : new - { - Title = T("Order.RewardPoints", language.Id, -order.RedeemedRewardPointsEntry.Points).Text, - Amount = FormatPrice(-order.RedeemedRewardPointsEntry.UsedAmount, order, messageContext) - }; + var m = new HybridExpando(part, allow, MemberOptMethod.Allow); + var d = m as dynamic; + + d.ID = part.Id; + d.Billing = CreateModelPart(part.BillingAddress, messageContext); + if (part.ShippingAddress != null) + { + d.Shipping = part.ShippingAddress.IsPostalDataEqual(part.BillingAddress) == true ? null : CreateModelPart(part.ShippingAddress, messageContext); + } + d.CustomerEmail = part.BillingAddress.Email.NullEmpty(); + d.CustomerComment = part.CustomerOrderComment.NullEmpty(); + d.Disclaimer = GetTopic("Disclaimer", messageContext); + d.ConditionsOfUse = GetTopic("ConditionsOfUse", messageContext); + d.Status = part.OrderStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id); + d.CreatedOn = ToUserDate(part.CreatedOnUtc, messageContext); + d.PaidOn = ToUserDate(part.PaidDateUtc, messageContext); + + // Payment method + var paymentMethodName = part.PaymentMethodSystemName; + var paymentMethod = _services.Resolve().GetProvider(part.PaymentMethodSystemName); + if (paymentMethod != null) + { + paymentMethodName = GetLocalizedValue(messageContext, paymentMethod.Metadata, nameof(paymentMethod.Metadata.FriendlyName), x => x.FriendlyName); + } + d.PaymentMethod = paymentMethodName.NullEmpty(); + + d.Url = part.Customer != null && !part.Customer.IsGuest() + ? BuildActionUrl("Details", "Order", new { id = part.Id, area = "" }, messageContext) + : null; + + // Overrides + m.Properties["OrderNumber"] = part.GetOrderNumber().NullEmpty(); + m.Properties["AcceptThirdPartyEmailHandOver"] = GetBoolResource(part.AcceptThirdPartyEmailHandOver, messageContext); + + // Items, Totals & Co. + d.Items = part.OrderItems.Where(x => x.Product != null).Select(x => CreateModelPart(x, messageContext)).ToList(); + d.Totals = CreateOrderTotalsPart(part, messageContext); + + // Checkout Attributes + if (part.CheckoutAttributeDescription.HasValue()) + { + d.CheckoutAttributes = HtmlUtils.ConvertPlainTextToTable(HtmlUtils.ConvertHtmlToPlainText(part.CheckoutAttributeDescription)).NullEmpty(); + } + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateOrderTotalsPart(Order order, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(order, nameof(order)); + + var language = messageContext.Language; + var currencyService = _services.Resolve(); + var paymentService = _services.Resolve(); + var priceFormatter = _services.Resolve(); + var taxSettings = _services.Settings.LoadSetting(messageContext.Store.Id); + + var taxRates = new SortedDictionary(); + Money cusTaxTotal = null; + Money cusDiscount = null; + Money cusRounding = null; + Money cusTotal = null; + + var subTotals = GetSubTotals(order, messageContext); + + // Shipping + bool dislayShipping = order.ShippingStatus != ShippingStatus.ShippingNotRequired; + + // Payment method fee + bool displayPaymentMethodFee = true; + if (order.PaymentMethodAdditionalFeeExclTax == decimal.Zero) + { + displayPaymentMethodFee = false; + } + + // Tax + bool displayTax = true; + bool displayTaxRates = true; + if (taxSettings.HideTaxInOrderSummary && order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax) + { + displayTax = false; + displayTaxRates = false; + } + else + { + if (order.OrderTax == 0 && taxSettings.HideZeroTax) + { + displayTax = false; + displayTaxRates = false; + } + else + { + taxRates = new SortedDictionary(); + foreach (var tr in order.TaxRatesDictionary) + { + taxRates.Add(tr.Key, currencyService.ConvertCurrency(tr.Value, order.CurrencyRate)); + } + + displayTaxRates = taxSettings.DisplayTaxRates && taxRates.Count > 0; + displayTax = !displayTaxRates; + + cusTaxTotal = FormatPrice(order.OrderTax, order, messageContext); + } + } + + // Discount + bool dislayDiscount = false; + if (order.OrderDiscount > decimal.Zero) + { + cusDiscount = FormatPrice(-order.OrderDiscount, order, messageContext); + dislayDiscount = true; + } + + // Total + var roundingAmount = decimal.Zero; + var orderTotal = order.GetOrderTotalInCustomerCurrency(currencyService, paymentService, out roundingAmount); + cusTotal = FormatPrice(orderTotal, order.CustomerCurrencyCode, messageContext); + + // Rounding + if (roundingAmount != decimal.Zero) + { + cusRounding = FormatPrice(roundingAmount, order.CustomerCurrencyCode, messageContext); + } + + // Model + dynamic m = new ExpandoObject(); + + m.SubTotal = subTotals.SubTotal; + m.SubTotalDiscount = subTotals.DisplaySubTotalDiscount ? subTotals.SubTotalDiscount : null; + m.Shipping = dislayShipping ? subTotals.ShippingTotal : null; + m.Payment = displayPaymentMethodFee ? subTotals.PaymentFee : null; + m.Tax = displayTax ? cusTaxTotal : null; + m.Discount = dislayDiscount ? cusDiscount : null; + m.RoundingDiff = cusRounding; + m.Total = cusTotal; + m.IsGross = order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax; + + // TaxRates + m.TaxRates = !displayTaxRates ? (object[])null : taxRates.Select(x => + { + return new + { + Rate = T("Order.TaxRateLine", language.Id, priceFormatter.FormatTaxRate(x.Key)).Text, + Value = FormatPrice(x.Value, order, messageContext) + }; + }).ToArray(); - return m; - } - private (Money SubTotal, Money SubTotalDiscount, Money ShippingTotal, Money PaymentFee, bool DisplaySubTotalDiscount) GetSubTotals(Order order, MessageContext messageContext) - { - var isNet = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax; + // Gift Cards + m.GiftCardUsage = order.GiftCardUsageHistory.Count == 0 ? (object[])null : order.GiftCardUsageHistory.Select(x => + { + return new + { + GiftCard = T("Order.GiftCardInfo", language.Id, x.GiftCard.GiftCardCouponCode).Text, + UsedAmount = FormatPrice(-x.UsedValue, order, messageContext), + RemainingAmount = FormatPrice(x.GiftCard.GetGiftCardRemainingAmount(), order, messageContext) + }; + }).ToArray(); + + // Reward Points + m.RedeemedRewardPoints = order.RedeemedRewardPointsEntry == null ? null : new + { + Title = T("Order.RewardPoints", language.Id, -order.RedeemedRewardPointsEntry.Points).Text, + Amount = FormatPrice(-order.RedeemedRewardPointsEntry.UsedAmount, order, messageContext) + }; + + return m; + } + + private (Money SubTotal, Money SubTotalDiscount, Money ShippingTotal, Money PaymentFee, bool DisplaySubTotalDiscount) GetSubTotals(Order order, MessageContext messageContext) + { + var isNet = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax; - var subTotal = isNet ? order.OrderSubtotalExclTax : order.OrderSubtotalInclTax; - var subTotalDiscount = isNet ? order.OrderSubTotalDiscountExclTax : order.OrderSubTotalDiscountInclTax; - var shipping = isNet ? order.OrderShippingExclTax : order.OrderShippingInclTax; - var payment = isNet ? order.PaymentMethodAdditionalFeeExclTax : order.PaymentMethodAdditionalFeeInclTax; + var subTotal = isNet ? order.OrderSubtotalExclTax : order.OrderSubtotalInclTax; + var subTotalDiscount = isNet ? order.OrderSubTotalDiscountExclTax : order.OrderSubTotalDiscountInclTax; + var shipping = isNet ? order.OrderShippingExclTax : order.OrderShippingInclTax; + var payment = isNet ? order.PaymentMethodAdditionalFeeExclTax : order.PaymentMethodAdditionalFeeInclTax; - // Subtotal - var cusSubTotal = FormatPrice(subTotal, order, messageContext); + // Subtotal + var cusSubTotal = FormatPrice(subTotal, order, messageContext); - // Shipping - var cusShipTotal = FormatPrice(shipping, order, messageContext); + // Shipping + var cusShipTotal = FormatPrice(shipping, order, messageContext); - // Payment method additional fee - var cusPaymentMethodFee = FormatPrice(payment, order, messageContext); + // Payment method additional fee + var cusPaymentMethodFee = FormatPrice(payment, order, messageContext); - // Discount (applied to order subtotal) - Money cusSubTotalDiscount = null; - bool dislaySubTotalDiscount = false; - if (subTotalDiscount > decimal.Zero) - { - cusSubTotalDiscount = FormatPrice(-subTotalDiscount, order, messageContext); - dislaySubTotalDiscount = true; - } + // Discount (applied to order subtotal) + Money cusSubTotalDiscount = null; + bool dislaySubTotalDiscount = false; + if (subTotalDiscount > decimal.Zero) + { + cusSubTotalDiscount = FormatPrice(-subTotalDiscount, order, messageContext); + dislaySubTotalDiscount = true; + } - return (cusSubTotal, cusSubTotalDiscount, cusShipTotal, cusPaymentMethodFee, dislaySubTotalDiscount); - } + return (cusSubTotal, cusSubTotalDiscount, cusShipTotal, cusPaymentMethodFee, dislaySubTotalDiscount); + } - protected virtual object CreateModelPart(OrderItem part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); + protected virtual object CreateModelPart(OrderItem part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); - var productAttributeParser = _services.Resolve(); - var downloadService = _services.Resolve(); + var productAttributeParser = _services.Resolve(); + var downloadService = _services.Resolve(); var deliveryTimeService = _services.Resolve(); var order = part.Order; - var isNet = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax; - var product = part.Product; - product.MergeWithCombination(part.AttributesXml, productAttributeParser); - - // Bundle items. - object bundleItems = null; - if (product.ProductType == ProductType.BundledProduct && part.BundleData.HasValue()) - { - var bundleData = part.GetBundleData(); - if (bundleData.Any()) - { - var productService = _services.Resolve(); - var products = productService.GetProductsByIds(bundleData.Select(x => x.ProductId).ToArray()); - var productsDic = products.ToDictionarySafe(x => x.Id, x => x); - - bundleItems = bundleData - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - productsDic.TryGetValue(x.ProductId, out Product bundleItemProduct); - return CreateModelPart(x, part, bundleItemProduct, messageContext); - }) - .ToList(); - } - } - - var m = new Dictionary - { - { "DownloadUrl", !downloadService.IsDownloadAllowed(part) ? null : BuildActionUrl("GetDownload", "Download", new { id = part.OrderItemGuid, area = "" }, messageContext) }, - { "AttributeDescription", part.AttributeDescription.NullEmpty() }, - { "Weight", part.ItemWeight }, - { "TaxRate", part.TaxRate }, - { "Qty", part.Quantity }, - { "UnitPrice", FormatPrice(isNet ? part.UnitPriceExclTax : part.UnitPriceInclTax, part.Order, messageContext) }, - { "LineTotal", FormatPrice(isNet ? part.PriceExclTax : part.PriceInclTax, part.Order, messageContext) }, - { "Product", CreateModelPart(product, messageContext, part.AttributesXml) }, - { "BundleItems", bundleItems }, - { "IsGross", !isNet }, + var isNet = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax; + var product = part.Product; + product.MergeWithCombination(part.AttributesXml, productAttributeParser); + + // Bundle items. + object bundleItems = null; + if (product.ProductType == ProductType.BundledProduct && part.BundleData.HasValue()) + { + var bundleData = part.GetBundleData(); + if (bundleData.Any()) + { + var productService = _services.Resolve(); + var products = productService.GetProductsByIds(bundleData.Select(x => x.ProductId).ToArray()); + var productsDic = products.ToDictionarySafe(x => x.Id, x => x); + + bundleItems = bundleData + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + productsDic.TryGetValue(x.ProductId, out Product bundleItemProduct); + return CreateModelPart(x, part, bundleItemProduct, messageContext); + }) + .ToList(); + } + } + + var m = new Dictionary + { + { "DownloadUrl", !downloadService.IsDownloadAllowed(part) ? null : BuildActionUrl("GetDownload", "Download", new { id = part.OrderItemGuid, area = "" }, messageContext) }, + { "AttributeDescription", part.AttributeDescription.NullEmpty() }, + { "Weight", part.ItemWeight }, + { "TaxRate", part.TaxRate }, + { "Qty", part.Quantity }, + { "UnitPrice", FormatPrice(isNet ? part.UnitPriceExclTax : part.UnitPriceInclTax, part.Order, messageContext) }, + { "LineTotal", FormatPrice(isNet ? part.PriceExclTax : part.PriceInclTax, part.Order, messageContext) }, + { "Product", CreateModelPart(product, messageContext, part.AttributesXml) }, + { "BundleItems", bundleItems }, + { "IsGross", !isNet }, { "DisplayDeliveryTime", part.DisplayDeliveryTime }, }; @@ -318,79 +318,88 @@ protected virtual object CreateModelPart(OrderItem part, MessageContext messageC } } - PublishModelPartCreatedEvent(part, m); + PublishModelPartCreatedEvent(part, m); - return m; - } + return m; + } - protected virtual object CreateModelPart(ProductBundleItemOrderData part, OrderItem orderItem, Product product, MessageContext messageContext) - { - Guard.NotNull(part, nameof(part)); - Guard.NotNull(orderItem, nameof(orderItem)); - Guard.NotNull(product, nameof(product)); - Guard.NotNull(messageContext, nameof(messageContext)); + protected virtual object CreateModelPart(ProductBundleItemOrderData part, OrderItem orderItem, Product product, MessageContext messageContext) + { + Guard.NotNull(part, nameof(part)); + Guard.NotNull(orderItem, nameof(orderItem)); + Guard.NotNull(product, nameof(product)); + Guard.NotNull(messageContext, nameof(messageContext)); - var priceWithDiscount = FormatPrice(part.PriceWithDiscount, orderItem.Order, messageContext); + var priceWithDiscount = FormatPrice(part.PriceWithDiscount, orderItem.Order, messageContext); - var m = new Dictionary - { - { "AttributeDescription", part.AttributesInfo.NullEmpty() }, - { "Quantity", part.Quantity > 1 && part.PerItemShoppingCart ? part.Quantity.ToString() : null }, - { "PerItemShoppingCart", part.PerItemShoppingCart }, - { "PriceWithDiscount", priceWithDiscount }, - { "Product", CreateModelPart(product, messageContext, part.AttributesXml) } - }; + var m = new Dictionary + { + { "AttributeDescription", part.AttributesInfo.NullEmpty() }, + { "Quantity", part.Quantity > 1 && part.PerItemShoppingCart ? part.Quantity.ToString() : null }, + { "PerItemShoppingCart", part.PerItemShoppingCart }, + { "PriceWithDiscount", priceWithDiscount }, + { "Product", CreateModelPart(product, messageContext, part.AttributesXml) } + }; - PublishModelPartCreatedEvent(part, m); + PublishModelPartCreatedEvent(part, m); - return m; - } + return m; + } - protected virtual object CreateModelPart(OrderNote part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); + protected virtual object CreateModelPart(OrderNote part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); - var m = new Dictionary - { - { "Id", part.Id }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, - { "Text", part.FormatOrderNoteText().NullEmpty() } - }; + var m = new Dictionary + { + { "Id", part.Id }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "Text", part.FormatOrderNoteText().NullEmpty() } + }; - PublishModelPartCreatedEvent(part, m); + PublishModelPartCreatedEvent(part, m); - return m; - } + return m; + } - protected virtual object CreateModelPart(ShoppingCartItem part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); + protected virtual object CreateModelPart(ShoppingCartItem part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); - var m = new Dictionary - { - { "Id", part.Id }, - { "Quantity", part.Quantity }, - { "Product", CreateModelPart(part.Product, messageContext, part.AttributesXml) }, - }; + var m = new Dictionary + { + { "Id", part.Id }, + { "Quantity", part.Quantity }, + { "Product", CreateModelPart(part.Product, messageContext, part.AttributesXml) }, + }; - PublishModelPartCreatedEvent(part, m); + PublishModelPartCreatedEvent(part, m); - return m; - } + return m; + } - protected virtual object CreateModelPart(Shipment part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); + protected virtual object CreateModelPart(Shipment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + var itemParts = new List(); var orderService = _services.Resolve(); var orderItems = orderService.GetOrderItemsByOrderIds(new int[] { part.OrderId })[part.OrderId]; - var itemParts = orderItems - .Where(x => x.Product != null) - .Select(x => CreateModelPart(x, messageContext)) - .ToList(); + var orderItemsDic = orderItems.ToDictionarySafe(x => x.Id); + + foreach (var shipmentItem in part.ShipmentItems) + { + if (orderItemsDic.TryGetValue(shipmentItem.OrderItemId, out var orderItem) && orderItem.Product != null) + { + var itemPart = CreateModelPart(orderItem, messageContext) as Dictionary; + itemPart["Qty"] = shipmentItem.Quantity; + + itemParts.Add(itemPart); + } + } var trackingUrl = part.TrackingUrl; @@ -412,68 +421,68 @@ protected virtual object CreateModelPart(Shipment part, MessageContext messageCo } } - var m = new Dictionary - { - { "Id", part.Id }, - { "TrackingNumber", part.TrackingNumber.NullEmpty() }, + var m = new Dictionary + { + { "Id", part.Id }, + { "TrackingNumber", part.TrackingNumber.NullEmpty() }, { "TrackingUrl", trackingUrl.NullEmpty() }, { "TotalWeight", part.TotalWeight }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, - { "DeliveredOn", ToUserDate(part.DeliveryDateUtc, messageContext) }, - { "ShippedOn", ToUserDate(part.ShippedDateUtc, messageContext) }, - { "Url", BuildActionUrl("ShipmentDetails", "Order", new { id = part.Id, area = "" }, messageContext)}, - { "Items", itemParts }, - }; + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "DeliveredOn", ToUserDate(part.DeliveryDateUtc, messageContext) }, + { "ShippedOn", ToUserDate(part.ShippedDateUtc, messageContext) }, + { "Url", BuildActionUrl("ShipmentDetails", "Order", new { id = part.Id, area = "" }, messageContext)}, + { "Items", itemParts }, + }; - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(RecurringPayment part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Id", part.Id }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, - { "StartedOn", ToUserDate(part.StartDateUtc, messageContext) }, - { "NextOn", ToUserDate(part.NextPaymentDate, messageContext) }, - { "CycleLength", part.CycleLength }, - { "CyclePeriod", part.CyclePeriod.GetLocalizedEnum(_services.Localization, messageContext.Language.Id) }, - { "CyclesRemaining", part.CyclesRemaining }, - { "TotalCycles", part.TotalCycles }, - { "Url", BuildActionUrl("Edit", "RecurringPayment", new { id = part.Id, area = "admin" }, messageContext) } - }; + PublishModelPartCreatedEvent(part, m); - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(ReturnRequest part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Id", part.Id }, - { "Reason", part.ReasonForReturn.NullEmpty() }, - { "Status", part.ReturnRequestStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id) }, - { "RequestedAction", part.RequestedAction.NullEmpty() }, - { "CustomerComments", HtmlUtils.StripTags(part.CustomerComments).NullEmpty() }, - { "StaffNotes", HtmlUtils.StripTags(part.StaffNotes).NullEmpty() }, - { "Quantity", part.Quantity }, - { "RefundToWallet", part.RefundToWallet }, - { "Url", BuildActionUrl("Edit", "ReturnRequest", new { id = part.Id, area = "admin" }, messageContext) } - }; + return m; + } + + protected virtual object CreateModelPart(RecurringPayment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "StartedOn", ToUserDate(part.StartDateUtc, messageContext) }, + { "NextOn", ToUserDate(part.NextPaymentDate, messageContext) }, + { "CycleLength", part.CycleLength }, + { "CyclePeriod", part.CyclePeriod.GetLocalizedEnum(_services.Localization, messageContext.Language.Id) }, + { "CyclesRemaining", part.CyclesRemaining }, + { "TotalCycles", part.TotalCycles }, + { "Url", BuildActionUrl("Edit", "RecurringPayment", new { id = part.Id, area = "admin" }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ReturnRequest part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "Reason", part.ReasonForReturn.NullEmpty() }, + { "Status", part.ReturnRequestStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id) }, + { "RequestedAction", part.RequestedAction.NullEmpty() }, + { "CustomerComments", HtmlUtils.StripTags(part.CustomerComments).NullEmpty() }, + { "StaffNotes", HtmlUtils.StripTags(part.StaffNotes).NullEmpty() }, + { "Quantity", part.Quantity }, + { "RefundToWallet", part.RefundToWallet }, + { "Url", BuildActionUrl("Edit", "ReturnRequest", new { id = part.Id, area = "admin" }, messageContext) } + }; - PublishModelPartCreatedEvent(part, m); + PublishModelPartCreatedEvent(part, m); - return m; - } - } + return m; + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs index 06d85e6160..26d3ef994d 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs @@ -20,130 +20,130 @@ namespace SmartStore.Services.Messages { public partial class MessageModelProvider - { - private void ApplyCustomerContentPart(IDictionary model, CustomerContent content, MessageContext ctx) - { - model["CustomerId"] = content.CustomerId; - model["IpAddress"] = content.IpAddress; - model["CreatedOn"] = ToUserDate(content.CreatedOnUtc, ctx); - model["UpdatedOn"] = ToUserDate(content.UpdatedOnUtc, ctx); - } - - private string BuildUrl(string url, MessageContext ctx) - { - return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + url.EnsureStartsWith("/"); - } - - private string BuildRouteUrl(object routeValues, MessageContext ctx) - { - return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + _urlHelper.RouteUrl(routeValues); - } - - private string BuildRouteUrl(string routeName, object routeValues, MessageContext ctx) - { - return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + _urlHelper.RouteUrl(routeName, routeValues); - } - - private string BuildActionUrl(string action, string controller, object routeValues, MessageContext ctx) - { - return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + _urlHelper.Action(action, controller, routeValues); - } - - private void PublishModelPartCreatedEvent(T source, dynamic part) where T : class - { - _services.EventPublisher.Publish(new MessageModelPartCreatedEvent(source, part)); - } - - private string GetLocalizedValue(MessageContext messageContext, ProviderMetadata metadata, string propertyName, Func fallback) - { - // TODO: (mc) this actually belongs to PluginMediator, but we simply cannot add a dependency to framework from here. Refactor later! - - Guard.NotNull(metadata, nameof(metadata)); - - string systemName = metadata.SystemName; - var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); - string result = _services.Localization.GetResource(resourceName, messageContext.Language.Id, false, "", true); - - if (result.IsEmpty()) - result = fallback(metadata); - - return result; - } - - private object GetTopic(string topicSystemName, MessageContext ctx) - { - var topicService = _services.Resolve(); - - // Load by store - var topic = topicService.GetTopicBySystemName(topicSystemName, ctx.StoreId ?? 0, false); - - string body = topic?.GetLocalized(x => x.Body, ctx.Language); - if (body.HasValue()) - { - body = HtmlUtils.RelativizeFontSizes(body); - } - - return new - { - Title = topic?.GetLocalized(x => x.Title, ctx.Language).Value.NullEmpty(), - Body = body.NullEmpty() - }; - } - - private string GetDisplayNameForCustomer(Customer customer) - { - return customer.GetFullName().NullEmpty() ?? customer.Username ?? customer.FindEmail(); - } - - private string GetBoolResource(bool value, MessageContext ctx) - { - return _services.Localization.GetResource(value ? "Common.Yes" : "Common.No", ctx.Language.Id); - } - - private DateTime? ToUserDate(DateTime? utcDate, MessageContext messageContext) - { - if (utcDate == null) - return null; - - return _services.DateTimeHelper.ConvertToUserTime( - utcDate.Value, - TimeZoneInfo.Utc, - _services.DateTimeHelper.GetCustomerTimeZone(messageContext.Customer)); - } - - private Money FormatPrice(decimal price, Order order, MessageContext messageContext) - { - return FormatPrice(price, order.CustomerCurrencyCode, messageContext, order.CurrencyRate); - } - - private Money FormatPrice(decimal price, MessageContext messageContext, decimal exchangeRate = 1) - { - return FormatPrice(price, (Currency)null, messageContext, exchangeRate); - } - - private Money FormatPrice(decimal price, string currencyCode, MessageContext messageContext, decimal exchangeRate = 1) - { - return FormatPrice( - price, - _services.Resolve().GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }, - messageContext, - exchangeRate); - } - - private Money FormatPrice(decimal price, Currency currency, MessageContext messageContext, decimal exchangeRate = 1) - { - if (exchangeRate != 1) - { - price = _services.Resolve().ConvertCurrency(price, exchangeRate); - } - - if (currency == null) - { - currency = _services.Resolve().WorkingCurrency; - } - - return new Money(price, currency); - } + { + private void ApplyCustomerContentPart(IDictionary model, CustomerContent content, MessageContext ctx) + { + model["CustomerId"] = content.CustomerId; + model["IpAddress"] = content.IpAddress; + model["CreatedOn"] = ToUserDate(content.CreatedOnUtc, ctx); + model["UpdatedOn"] = ToUserDate(content.UpdatedOnUtc, ctx); + } + + private string BuildUrl(string url, MessageContext ctx) + { + return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + url.EnsureStartsWith("/"); + } + + private string BuildRouteUrl(object routeValues, MessageContext ctx) + { + return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + _urlHelper.RouteUrl(routeValues); + } + + private string BuildRouteUrl(string routeName, object routeValues, MessageContext ctx) + { + return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + _urlHelper.RouteUrl(routeName, routeValues); + } + + private string BuildActionUrl(string action, string controller, object routeValues, MessageContext ctx) + { + return ctx.BaseUri.GetLeftPart(UriPartial.Authority) + _urlHelper.Action(action, controller, routeValues); + } + + private void PublishModelPartCreatedEvent(T source, dynamic part) where T : class + { + _services.EventPublisher.Publish(new MessageModelPartCreatedEvent(source, part)); + } + + private string GetLocalizedValue(MessageContext messageContext, ProviderMetadata metadata, string propertyName, Func fallback) + { + // TODO: (mc) this actually belongs to PluginMediator, but we simply cannot add a dependency to framework from here. Refactor later! + + Guard.NotNull(metadata, nameof(metadata)); + + string systemName = metadata.SystemName; + var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); + string result = _services.Localization.GetResource(resourceName, messageContext.Language.Id, false, "", true); + + if (result.IsEmpty()) + result = fallback(metadata); + + return result; + } + + private object GetTopic(string topicSystemName, MessageContext ctx) + { + var topicService = _services.Resolve(); + + // Load by store + var topic = topicService.GetTopicBySystemName(topicSystemName, ctx.StoreId ?? 0, false); + + string body = topic?.GetLocalized(x => x.Body, ctx.Language); + if (body.HasValue()) + { + body = HtmlUtils.RelativizeFontSizes(body); + } + + return new + { + Title = topic?.GetLocalized(x => x.Title, ctx.Language).Value.NullEmpty(), + Body = body.NullEmpty() + }; + } + + private string GetDisplayNameForCustomer(Customer customer) + { + return customer.GetFullName().NullEmpty() ?? customer.Username ?? customer.FindEmail(); + } + + private string GetBoolResource(bool value, MessageContext ctx) + { + return _services.Localization.GetResource(value ? "Common.Yes" : "Common.No", ctx.Language.Id); + } + + private DateTime? ToUserDate(DateTime? utcDate, MessageContext messageContext) + { + if (utcDate == null) + return null; + + return _services.DateTimeHelper.ConvertToUserTime( + utcDate.Value, + TimeZoneInfo.Utc, + _services.DateTimeHelper.GetCustomerTimeZone(messageContext.Customer)); + } + + private Money FormatPrice(decimal price, Order order, MessageContext messageContext) + { + return FormatPrice(price, order.CustomerCurrencyCode, messageContext, order.CurrencyRate); + } + + private Money FormatPrice(decimal price, MessageContext messageContext, decimal exchangeRate = 1) + { + return FormatPrice(price, (Currency)null, messageContext, exchangeRate); + } + + private Money FormatPrice(decimal price, string currencyCode, MessageContext messageContext, decimal exchangeRate = 1) + { + return FormatPrice( + price, + _services.Resolve().GetCurrencyByCode(currencyCode) ?? new Currency { CurrencyCode = currencyCode }, + messageContext, + exchangeRate); + } + + private Money FormatPrice(decimal price, Currency currency, MessageContext messageContext, decimal exchangeRate = 1) + { + if (exchangeRate != 1) + { + price = _services.Resolve().ConvertCurrency(price, exchangeRate); + } + + if (currency == null) + { + currency = _services.Resolve().WorkingCurrency; + } + + return new Money(price, currency); + } private MediaFileInfo GetMediaFileFor(Product product, string attributesXml) @@ -162,14 +162,14 @@ private MediaFileInfo GetMediaFileFor(Product product, string attributesXml) var fileIds = combination.GetAssignedMediaIds(); if (fileIds?.Any() ?? false) { - file = _services.MediaService.GetFileById(fileIds[0]); + file = _services.MediaService.GetFileById(fileIds[0], MediaLoadFlags.AsNoTracking); } } } if (file == null) { - file = _services.MediaService.GetFileById(product.MainPictureId ?? 0); + file = _services.MediaService.GetFileById(product.MainPictureId ?? 0, MediaLoadFlags.AsNoTracking); } if (file == null && product.Visibility == ProductVisibility.Hidden && product.ParentGroupedProductId > 0) @@ -185,8 +185,8 @@ private MediaFileInfo GetMediaFileFor(Product product, string attributesXml) } private object[] Concat(params object[] values) - { - return values.Where(x => CommonHelper.IsTruthy(x)).ToArray(); - } - } + { + return values.Where(x => CommonHelper.IsTruthy(x)).ToArray(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs index febf3afa5e..37045b041f 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs @@ -41,458 +41,464 @@ namespace SmartStore.Services.Messages { public enum ModelTreeMemberKind - { - Primitive, - Complex, - Collection, - Root - } - - public class ModelTreeMember - { - public string Name { get; set; } - public ModelTreeMemberKind Kind { get; set; } - } - - public partial class MessageModelProvider : IMessageModelProvider - { - private readonly ICommonServices _services; - private readonly ITemplateEngine _templateEngine; - private readonly IMessageTemplateService _messageTemplateService; - private readonly IEmailAccountService _emailAccountService; - private readonly UrlHelper _urlHelper; - - public MessageModelProvider( - ICommonServices services, - ITemplateEngine templateEngine, - IMessageTemplateService messageTemplateService, - IEmailAccountService emailAccountService, - UrlHelper urlHelper) - { - _services = services; - _templateEngine = templateEngine; - _messageTemplateService = messageTemplateService; - _emailAccountService = emailAccountService; - _urlHelper = urlHelper; - - T = NullLocalizer.InstanceEx; - Logger = NullLogger.Instance; - } - - public LocalizerEx T { get; set; } - public ILogger Logger { get; set; } - - public virtual void AddGlobalModelParts(MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - - var model = messageContext.Model; - - model["Context"] = new Dictionary - { - { "TemplateName", messageContext.MessageTemplate.Name }, - { "LanguageId", messageContext.Language.Id }, - { "LanguageCulture", messageContext.Language.LanguageCulture }, - { "LanguageRtl", messageContext.Language.Rtl }, - { "BaseUrl", messageContext.BaseUri.ToString() } - }; - - dynamic email = new ExpandoObject(); - email.Email = messageContext.EmailAccount.Email; - email.SenderName = messageContext.EmailAccount.DisplayName; - email.DisplayName = messageContext.EmailAccount.DisplayName; // Alias - model["Email"] = email; - - model["Theme"] = CreateThemeModelPart(messageContext); - model["Customer"] = CreateModelPart(messageContext.Customer, messageContext); - model["Store"] = CreateModelPart(messageContext.Store, messageContext); - } - - public object CreateModelPart(object part, bool ignoreNullMembers, params string[] ignoreMemberNames) - { - Guard.NotNull(part, nameof(part)); - - var store = _services.StoreContext.CurrentStore; - var messageContext = new MessageContext - { - Language = _services.WorkContext.WorkingLanguage, - Store = store, - BaseUri = new Uri(_services.StoreService.GetHost(store)), - Model = new TemplateModel() - }; - - if (part is Customer x) - { - // This case is not handled in AddModelPart core method. - messageContext.Customer = x; - messageContext.Model["Part"] = CreateModelPart(x, messageContext); - } - else - { - messageContext.Customer = _services.WorkContext.CurrentCustomer; - AddModelPart(part, messageContext, "Part"); - } - - object result = null; - - if (messageContext.Model.Any()) - { - result = messageContext.Model.FirstOrDefault().Value; - - if (result is IDictionary dict) - { - SanitizeModelDictionary(dict, ignoreNullMembers, ignoreMemberNames); - } - } - - return result; - } - - private void SanitizeModelDictionary(IDictionary dict, bool ignoreNullMembers, params string[] ignoreMemberNames) - { - if (ignoreNullMembers || ignoreMemberNames.Length > 0) - { - foreach (var key in dict.Keys.ToArray()) - { - var expando = dict as HybridExpando; - var value = dict[key]; - - if ((ignoreNullMembers && value == null) || ignoreMemberNames.Contains(key)) - { - if (expando != null) - expando.Override(key, null); // INFO: we cannot remove entries from HybridExpando - else - dict.Remove(key); - continue; - } - - if (value != null && value.GetType().IsSequenceType()) - { - var ignoreMemberNames2 = ignoreMemberNames - .Where(x => x.StartsWith(key + ".", StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Substring(key.Length + 1)) - .ToArray(); - - if (value is IDictionary dict2) - { - SanitizeModelDictionary(dict2, ignoreNullMembers, ignoreMemberNames2); - } - else - { - var list = ((IEnumerable)value).OfType>(); - foreach (var dict3 in list) - { - SanitizeModelDictionary(dict3, ignoreNullMembers, ignoreMemberNames2); - } - } - } - } - } - } - - public virtual void AddModelPart(object part, MessageContext messageContext, string name = null) - { - Guard.NotNull(part, nameof(part)); - Guard.NotNull(messageContext, nameof(messageContext)); - - var model = messageContext.Model; - - name = name.NullEmpty() ?? ResolveModelName(part); - - object modelPart = null; - - switch (part) - { - case INamedModelPart x: - modelPart = x; - break; - case IModelPart x: - MergeModelBag(x, model, messageContext); - break; - case Order x: - modelPart = CreateModelPart(x, messageContext); - break; - case Product x: - modelPart = CreateModelPart(x, messageContext); - break; - case Customer x: - modelPart = CreateModelPart(x, messageContext); - break; - case Address x: - modelPart = CreateModelPart(x, messageContext); - break; - case Shipment x: - modelPart = CreateModelPart(x, messageContext); - break; - case OrderNote x: - modelPart = CreateModelPart(x, messageContext); - break; - case RecurringPayment x: - modelPart = CreateModelPart(x, messageContext); - break; - case ReturnRequest x: - modelPart = CreateModelPart(x, messageContext); - break; - case GiftCard x: - modelPart = CreateModelPart(x, messageContext); - break; - case NewsLetterSubscription x: - modelPart = CreateModelPart(x, messageContext); - break; - case Campaign x: - modelPart = CreateModelPart(x, messageContext); - break; - case ProductReview x: - modelPart = CreateModelPart(x, messageContext); - break; - case BlogComment x: - modelPart = CreateModelPart(x, messageContext); - break; - case NewsComment x: - modelPart = CreateModelPart(x, messageContext); - break; - case ForumTopic x: - modelPart = CreateModelPart(x, messageContext); - break; - case ForumPost x: - modelPart = CreateModelPart(x, messageContext); - break; + { + Primitive, + Complex, + Collection, + Root + } + + public class ModelTreeMember + { + public string Name { get; set; } + public ModelTreeMemberKind Kind { get; set; } + } + + public partial class MessageModelProvider : IMessageModelProvider + { + private readonly ICommonServices _services; + private readonly ITemplateEngine _templateEngine; + private readonly IMessageTemplateService _messageTemplateService; + private readonly IEmailAccountService _emailAccountService; + private readonly UrlHelper _urlHelper; + + public MessageModelProvider( + ICommonServices services, + ITemplateEngine templateEngine, + IMessageTemplateService messageTemplateService, + IEmailAccountService emailAccountService, + UrlHelper urlHelper) + { + _services = services; + _templateEngine = templateEngine; + _messageTemplateService = messageTemplateService; + _emailAccountService = emailAccountService; + _urlHelper = urlHelper; + + T = NullLocalizer.InstanceEx; + Logger = NullLogger.Instance; + } + + public LocalizerEx T { get; set; } + public ILogger Logger { get; set; } + + public virtual void AddGlobalModelParts(MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + + var model = messageContext.Model; + + model["Context"] = new Dictionary + { + { "TemplateName", messageContext.MessageTemplate.Name }, + { "LanguageId", messageContext.Language.Id }, + { "LanguageCulture", messageContext.Language.LanguageCulture }, + { "LanguageRtl", messageContext.Language.Rtl }, + { "BaseUrl", messageContext.BaseUri.ToString() } + }; + + dynamic email = new ExpandoObject(); + email.Email = messageContext.EmailAccount.Email; + email.SenderName = messageContext.EmailAccount.DisplayName; + email.DisplayName = messageContext.EmailAccount.DisplayName; // Alias + model["Email"] = email; + + model["Theme"] = CreateThemeModelPart(messageContext); + model["Customer"] = CreateModelPart(messageContext.Customer, messageContext); + model["Store"] = CreateModelPart(messageContext.Store, messageContext); + } + + public object CreateModelPart(object part, bool ignoreNullMembers, params string[] ignoreMemberNames) + { + Guard.NotNull(part, nameof(part)); + + var store = _services.StoreContext.CurrentStore; + var messageContext = new MessageContext + { + Language = _services.WorkContext.WorkingLanguage, + Store = store, + BaseUri = new Uri(_services.StoreService.GetHost(store)), + Model = new TemplateModel() + }; + + if (part is Customer x) + { + // This case is not handled in AddModelPart core method. + messageContext.Customer = x; + messageContext.Model["Part"] = CreateModelPart(x, messageContext); + } + else + { + messageContext.Customer = _services.WorkContext.CurrentCustomer; + AddModelPart(part, messageContext, "Part"); + } + + object result = null; + + if (messageContext.Model.Any()) + { + result = messageContext.Model.FirstOrDefault().Value; + + if (result is IDictionary dict) + { + SanitizeModelDictionary(dict, ignoreNullMembers, ignoreMemberNames); + } + } + + return result; + } + + private void SanitizeModelDictionary(IDictionary dict, bool ignoreNullMembers, params string[] ignoreMemberNames) + { + if (ignoreNullMembers || ignoreMemberNames.Length > 0) + { + foreach (var key in dict.Keys.ToArray()) + { + var expando = dict as HybridExpando; + var value = dict[key]; + + if ((ignoreNullMembers && value == null) || ignoreMemberNames.Contains(key)) + { + if (expando != null) + expando.Override(key, null); // INFO: we cannot remove entries from HybridExpando + else + dict.Remove(key); + continue; + } + + if (value != null && value.GetType().IsSequenceType()) + { + var ignoreMemberNames2 = ignoreMemberNames + .Where(x => x.StartsWith(key + ".", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Substring(key.Length + 1)) + .ToArray(); + + if (value is IDictionary dict2) + { + SanitizeModelDictionary(dict2, ignoreNullMembers, ignoreMemberNames2); + } + else + { + var list = ((IEnumerable)value).OfType>(); + foreach (var dict3 in list) + { + SanitizeModelDictionary(dict3, ignoreNullMembers, ignoreMemberNames2); + } + } + } + } + } + } + + public virtual void AddModelPart(object part, MessageContext messageContext, string name = null) + { + Guard.NotNull(part, nameof(part)); + Guard.NotNull(messageContext, nameof(messageContext)); + + var model = messageContext.Model; + + name = name.NullEmpty() ?? ResolveModelName(part); + + object modelPart = null; + + switch (part) + { + case INamedModelPart x: + modelPart = x; + break; + case IModelPart x: + MergeModelBag(x, model, messageContext); + break; + case Order x: + modelPart = CreateModelPart(x, messageContext); + break; + case Product x: + modelPart = CreateModelPart(x, messageContext); + break; + case Customer x: + modelPart = CreateModelPart(x, messageContext); + break; + case Address x: + modelPart = CreateModelPart(x, messageContext); + break; + case Shipment x: + modelPart = CreateModelPart(x, messageContext); + break; + case OrderNote x: + modelPart = CreateModelPart(x, messageContext); + break; + case RecurringPayment x: + modelPart = CreateModelPart(x, messageContext); + break; + case ReturnRequest x: + modelPart = CreateModelPart(x, messageContext); + break; + case GiftCard x: + modelPart = CreateModelPart(x, messageContext); + break; + case NewsLetterSubscription x: + modelPart = CreateModelPart(x, messageContext); + break; + case Campaign x: + modelPart = CreateModelPart(x, messageContext); + break; + case ProductReview x: + modelPart = CreateModelPart(x, messageContext); + break; + case BlogComment x: + modelPart = CreateModelPart(x, messageContext); + break; + case NewsComment x: + modelPart = CreateModelPart(x, messageContext); + break; + case ForumTopic x: + modelPart = CreateModelPart(x, messageContext); + break; + case ForumPost x: + modelPart = CreateModelPart(x, messageContext); + break; case ForumPostVote x: modelPart = CreateModelPart(x, messageContext); break; - case Forum x: - modelPart = CreateModelPart(x, messageContext); - break; - case PrivateMessage x: - modelPart = CreateModelPart(x, messageContext); - break; - case IEnumerable x: - modelPart = CreateModelPart(x, messageContext); - break; - case PollVotingRecord x: - modelPart = CreateModelPart(x, messageContext); - break; - case ProductReviewHelpfulness x: - modelPart = CreateModelPart(x, messageContext); - break; - case ForumSubscription x: - modelPart = CreateModelPart(x, messageContext); - break; - case BackInStockSubscription x: - modelPart = CreateModelPart(x, messageContext); - break; - default: - var partType = part.GetType(); - modelPart = part; - - if (partType.IsPlainObjectType() && !partType.IsAnonymous()) - { - var evt = new MessageModelPartMappingEvent(part); - _services.EventPublisher.Publish(evt); - - if (evt.Result != null && !object.ReferenceEquals(evt.Result, part)) - { - modelPart = evt.Result; - name = evt.ModelPartName.NullEmpty() ?? ResolveModelName(evt.Result) ?? name; - } - else - { - modelPart = part; - } - - modelPart = evt.Result ?? part; - name = evt.ModelPartName.NullEmpty() ?? name; - } - - break; - } - - if (modelPart != null) - { - if (name.IsEmpty()) - { - throw new SmartException($"Could not resolve a model key for part '{modelPart.GetType().Name}'. Use an instance of 'NamedModelPart' class to pass model with name."); - } - - if (model.TryGetValue(name, out var existing)) - { - // A model part with the same name exists in model already... - if (existing is IDictionary x) - { - // but it's a dictionary which we can easily merge with - x.Merge(FastProperty.ObjectToDictionary(modelPart), true); - } - else - { - // Wrap in HybridExpando and merge - var he = new HybridExpando(existing, true); - he.Merge(FastProperty.ObjectToDictionary(modelPart), true); - model[name] = he; - } - } - else - { - // Put part to model as new property - model[name] = modelPart; - } - } - } - - public string ResolveModelName(object model) - { - Guard.NotNull(model, nameof(model)); - - string name = null; - var type = model.GetType(); - - try - { - if (model is BaseEntity be) - { - name = be.GetUnproxiedType().Name; - } - else if (model is ITestModel te) - { - name = te.ModelName; - } - else if (model is INamedModelPart mp) - { - name = mp.ModelPartName; - } - else if (type.IsPlainObjectType()) - { - name = type.Name; - } - } - catch { } - - return name; - } - - #region Global model part handlers - - protected virtual object CreateThemeModelPart(MessageContext messageContext) - { - var m = new Dictionary - { - { "FontFamily", "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" }, - { "BodyBg", "#f2f4f6" }, - { "BodyColor", "#555" }, - { "TitleColor", "#2f3133" }, - { "ContentBg", "#fff" }, - { "ShadeColor", "#e2e2e2" }, - { "LinkColor", "#0066c0" }, - { "BrandPrimary", "#3f51b5" }, - { "BrandSuccess", "#4caf50" }, - { "BrandWarning", "#ff9800" }, - { "BrandDanger", "#f44336" }, - { "MutedColor", "#a5a5a5" }, - }; - - return m; - } - - protected virtual object CreateCompanyModelPart(MessageContext messageContext) - { - var settings = _services.Settings.LoadSetting(messageContext.Store.Id); - dynamic m = new HybridExpando(settings, true); - - m.NameLine = Concat(settings.Salutation, settings.Title, settings.Firstname, settings.Lastname); - m.StreetLine = Concat(settings.Street, settings.Street2); - m.CityLine = Concat(settings.ZipCode, settings.City); - m.CountryLine = Concat(settings.CountryName, settings.Region); - - PublishModelPartCreatedEvent(settings, m); - return m; - } - - protected virtual object CreateBankModelPart(MessageContext messageContext) - { - var settings = _services.Settings.LoadSetting(messageContext.Store.Id); - var m = new HybridExpando(settings, true); - PublishModelPartCreatedEvent(settings, m); - return m; - } - - protected virtual object CreateContactModelPart(MessageContext messageContext) - { - var settings = _services.Settings.LoadSetting(messageContext.Store.Id); - var contact = new HybridExpando(settings, true) as dynamic; - - // Aliases - contact.Phone = new - { - Company = settings.CompanyTelephoneNumber.NullEmpty(), - Hotline = settings.HotlineTelephoneNumber.NullEmpty(), - Mobile = settings.MobileTelephoneNumber.NullEmpty(), - Fax = settings.CompanyFaxNumber.NullEmpty() - }; - - contact.Email = new - { - Company = settings.CompanyEmailAddress.NullEmpty(), - Webmaster = settings.WebmasterEmailAddress.NullEmpty(), - Support = settings.SupportEmailAddress.NullEmpty(), - Contact = settings.ContactEmailAddress.NullEmpty() - }; - - PublishModelPartCreatedEvent(settings, contact); - - return contact; - } - - #endregion - - #region Generic model part handlers - - protected virtual void MergeModelBag(IModelPart part, IDictionary model, MessageContext messageContext) - { - if (!(model.Get("Bag") is IDictionary bag)) - { - model["Bag"] = bag = new Dictionary(); - } - - var source = part as IDictionary; - bag.Merge(source); - } - - #endregion - - #region Entity specific model part handlers - - protected virtual object CreateModelPart(Store part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var host = messageContext.BaseUri.ToString(); - var logoFile = _services.MediaService.GetFileById(messageContext.Store.LogoMediaFileId); - - // Issue: https://github.com/smartstore/SmartStoreNET/issues/1321 - - var m = new Dictionary - { - { "Email", messageContext.EmailAccount.Email }, - { "EmailName", messageContext.EmailAccount.DisplayName }, - { "Name", part.Name }, - { "Url", host }, - { "Cdn", part.ContentDeliveryNetwork }, - { "PrimaryStoreCurrency", part.PrimaryStoreCurrency?.CurrencyCode }, - { "PrimaryExchangeRateCurrency", part.PrimaryExchangeRateCurrency?.CurrencyCode }, - { "Logo", CreateModelPart(logoFile, messageContext, host, null, new Size(400, 75)) }, - { "Company", CreateCompanyModelPart(messageContext) }, - { "Contact", CreateContactModelPart(messageContext) }, - { "Bank", CreateBankModelPart(messageContext) }, - { "Copyright", T("Content.CopyrightNotice", messageContext.Language.Id, DateTime.Now.Year.ToString(), part.Name).Text } - }; - - var he = new HybridExpando(true); - he.Merge(m, true); - - PublishModelPartCreatedEvent(part, he); - - return he; - } + case Forum x: + modelPart = CreateModelPart(x, messageContext); + break; + case PrivateMessage x: + modelPart = CreateModelPart(x, messageContext); + break; + case IEnumerable x: + modelPart = CreateModelPart(x, messageContext); + break; + case PollVotingRecord x: + modelPart = CreateModelPart(x, messageContext); + break; + case ProductReviewHelpfulness x: + modelPart = CreateModelPart(x, messageContext); + break; + case ForumSubscription x: + modelPart = CreateModelPart(x, messageContext); + break; + case BackInStockSubscription x: + modelPart = CreateModelPart(x, messageContext); + break; + default: + var partType = part.GetType(); + modelPart = part; + + if (partType.IsPlainObjectType() && !partType.IsAnonymous()) + { + var evt = new MessageModelPartMappingEvent(part); + _services.EventPublisher.Publish(evt); + + if (evt.Result != null && !object.ReferenceEquals(evt.Result, part)) + { + modelPart = evt.Result; + name = evt.ModelPartName.NullEmpty() ?? ResolveModelName(evt.Result) ?? name; + } + else + { + modelPart = part; + } + + modelPart = evt.Result ?? part; + name = evt.ModelPartName.NullEmpty() ?? name; + } + + break; + } + + if (modelPart != null) + { + if (name.IsEmpty()) + { + throw new SmartException($"Could not resolve a model key for part '{modelPart.GetType().Name}'. Use an instance of 'NamedModelPart' class to pass model with name."); + } + + if (model.TryGetValue(name, out var existing)) + { + // A model part with the same name exists in model already... + if (existing is IDictionary x) + { + // but it's a dictionary which we can easily merge with + x.Merge(FastProperty.ObjectToDictionary(modelPart), true); + } + else + { + // Wrap in HybridExpando and merge + var he = new HybridExpando(existing, true); + he.Merge(FastProperty.ObjectToDictionary(modelPart), true); + model[name] = he; + } + } + else + { + // Put part to model as new property + model[name] = modelPart; + } + } + } + + public string ResolveModelName(object model) + { + Guard.NotNull(model, nameof(model)); + + string name = null; + var type = model.GetType(); + + try + { + if (model is BaseEntity be) + { + name = be.GetUnproxiedType().Name; + } + else if (model is ITestModel te) + { + name = te.ModelName; + } + else if (model is INamedModelPart mp) + { + name = mp.ModelPartName; + } + else if (type.IsPlainObjectType()) + { + name = type.Name; + } + } + catch { } + + return name; + } + + #region Global model part handlers + + protected virtual object CreateThemeModelPart(MessageContext messageContext) + { + var m = new Dictionary + { + { "FontFamily", "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" }, + { "BodyBg", "#f2f4f6" }, + { "BodyColor", "#555" }, + { "TitleColor", "#2f3133" }, + { "ContentBg", "#fff" }, + { "ShadeColor", "#e2e2e2" }, + { "LinkColor", "#0066c0" }, + { "BrandPrimary", "#3f51b5" }, + { "BrandSuccess", "#4caf50" }, + { "BrandWarning", "#ff9800" }, + { "BrandDanger", "#f44336" }, + { "MutedColor", "#a5a5a5" }, + }; + + return m; + } + + protected virtual object CreateCompanyModelPart(MessageContext messageContext) + { + var settings = _services.Settings.LoadSetting(messageContext.Store.Id); + var countryService = _services.Resolve(); + var country = countryService.GetCountryById(settings.CountryId); + dynamic m = new HybridExpando(settings, true); + + m.NameLine = Concat(settings.Salutation, settings.Title, settings.Firstname, settings.Lastname); + m.StreetLine = Concat(settings.Street, settings.Street2); + m.CityLine = Concat(settings.ZipCode, settings.City); + + if (country != null) + { + m.CountryLine = Concat(country.GetLocalized(x => x.Name), settings.Region); + } + + PublishModelPartCreatedEvent(settings, m); + return m; + } + + protected virtual object CreateBankModelPart(MessageContext messageContext) + { + var settings = _services.Settings.LoadSetting(messageContext.Store.Id); + var m = new HybridExpando(settings, true); + PublishModelPartCreatedEvent(settings, m); + return m; + } + + protected virtual object CreateContactModelPart(MessageContext messageContext) + { + var settings = _services.Settings.LoadSetting(messageContext.Store.Id); + var contact = new HybridExpando(settings, true) as dynamic; + + // Aliases + contact.Phone = new + { + Company = settings.CompanyTelephoneNumber.NullEmpty(), + Hotline = settings.HotlineTelephoneNumber.NullEmpty(), + Mobile = settings.MobileTelephoneNumber.NullEmpty(), + Fax = settings.CompanyFaxNumber.NullEmpty() + }; + + contact.Email = new + { + Company = settings.CompanyEmailAddress.NullEmpty(), + Webmaster = settings.WebmasterEmailAddress.NullEmpty(), + Support = settings.SupportEmailAddress.NullEmpty(), + Contact = settings.ContactEmailAddress.NullEmpty() + }; + + PublishModelPartCreatedEvent(settings, contact); + + return contact; + } + + #endregion + + #region Generic model part handlers + + protected virtual void MergeModelBag(IModelPart part, IDictionary model, MessageContext messageContext) + { + if (!(model.Get("Bag") is IDictionary bag)) + { + model["Bag"] = bag = new Dictionary(); + } + + var source = part as IDictionary; + bag.Merge(source); + } + + #endregion + + #region Entity specific model part handlers + + protected virtual object CreateModelPart(Store part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var host = messageContext.BaseUri.ToString(); + var logoFile = _services.MediaService.GetFileById(messageContext.Store.LogoMediaFileId, MediaLoadFlags.AsNoTracking); + + // Issue: https://github.com/smartstore/SmartStoreNET/issues/1321 + + var m = new Dictionary + { + { "Email", messageContext.EmailAccount.Email }, + { "EmailName", messageContext.EmailAccount.DisplayName }, + { "Name", part.Name }, + { "Url", host }, + { "Cdn", part.ContentDeliveryNetwork }, + { "PrimaryStoreCurrency", part.PrimaryStoreCurrency?.CurrencyCode }, + { "PrimaryExchangeRateCurrency", part.PrimaryExchangeRateCurrency?.CurrencyCode }, + { "Logo", CreateModelPart(logoFile, messageContext, host, null, new Size(400, 75)) }, + { "Company", CreateCompanyModelPart(messageContext) }, + { "Contact", CreateContactModelPart(messageContext) }, + { "Bank", CreateBankModelPart(messageContext) }, + { "Copyright", T("Content.CopyrightNotice", messageContext.Language.Id, DateTime.Now.Year.ToString(), part.Name).Text } + }; + + var he = new HybridExpando(true); + he.Merge(m, true); + + PublishModelPartCreatedEvent(part, he); + + return he; + } protected virtual object CreateModelPart( MediaFileInfo part, @@ -536,363 +542,363 @@ protected virtual object CreateModelPart( } protected virtual object CreateModelPart(Product part, MessageContext messageContext, string attributesXml = null) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var mediaSettings = _services.Resolve(); - var shoppingCartSettings = _services.Resolve(); - var catalogSettings = _services.Resolve(); - var deliveryTimeService = _services.Resolve(); - var quantityUnitService = _services.Resolve(); - var productUrlHelper = _services.Resolve(); - - var currency = _services.WorkContext.WorkingCurrency; - var additionalShippingCharge = new Money( - _services.Resolve().ConvertFromPrimaryStoreCurrency(part.AdditionalShippingCharge, currency), - currency, - true); - var url = BuildUrl(productUrlHelper.GetProductUrl(part.Id, part.GetSeName(messageContext.Language.Id), attributesXml), messageContext); + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var mediaSettings = _services.Resolve(); + var shoppingCartSettings = _services.Resolve(); + var catalogSettings = _services.Resolve(); + var deliveryTimeService = _services.Resolve(); + var quantityUnitService = _services.Resolve(); + var productUrlHelper = _services.Resolve(); + + var currency = _services.WorkContext.WorkingCurrency; + var additionalShippingCharge = new Money( + _services.Resolve().ConvertFromPrimaryStoreCurrency(part.AdditionalShippingCharge, currency), + currency, + true); + var url = BuildUrl(productUrlHelper.GetProductUrl(part.Id, part.GetSeName(messageContext.Language.Id), attributesXml), messageContext); var file = GetMediaFileFor(part, attributesXml); var name = part.GetLocalized(x => x.Name, messageContext.Language.Id).Value; - var alt = T("Media.Product.ImageAlternateTextFormat", messageContext.Language.Id, name).Text; - - var m = new Dictionary - { - { "Id", part.Id }, - { "Sku", catalogSettings.ShowProductSku ? part.Sku : null }, - { "Name", name }, - { "Description", part.GetLocalized(x => x.ShortDescription, messageContext.Language).Value.NullEmpty() }, - { "StockQuantity", part.StockQuantity }, - { "AdditionalShippingCharge", additionalShippingCharge }, - { "Url", url }, - { "Thumbnail", CreateModelPart(file, messageContext, url, mediaSettings.MessageProductThumbPictureSize, new Size(50, 50), alt) }, - { "ThumbnailLg", CreateModelPart(file, messageContext, url, mediaSettings.ProductThumbPictureSize, new Size(120, 120), alt) }, - { "DeliveryTime", null }, - { "QtyUnit", null } - }; - - if (shoppingCartSettings.ShowDeliveryTimes && part.IsShipEnabled) - { - if (deliveryTimeService.GetDeliveryTimeById(part.DeliveryTimeId ?? 0) is DeliveryTime dt) - { - m["DeliveryTime"] = new Dictionary - { - { "Color", dt.ColorHexValue }, - { "Name", dt.GetLocalized(x => x.Name, messageContext.Language).Value }, - }; - } - } - - if (quantityUnitService.GetQuantityUnitById(part.QuantityUnitId) is QuantityUnit qu) - { - m["QtyUnit"] = qu.GetLocalized(x => x.Name, messageContext.Language).Value; - } - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(Customer part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var email = part.FindEmail(); - var pwdRecoveryToken = part.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken).NullEmpty(); - var accountActivationToken = part.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken).NullEmpty(); + var alt = T("Media.Product.ImageAlternateTextFormat", messageContext.Language.Id, name).Text; + + var m = new Dictionary + { + { "Id", part.Id }, + { "Sku", catalogSettings.ShowProductSku ? part.Sku : null }, + { "Name", name }, + { "Description", part.GetLocalized(x => x.ShortDescription, messageContext.Language).Value.NullEmpty() }, + { "StockQuantity", part.StockQuantity }, + { "AdditionalShippingCharge", additionalShippingCharge }, + { "Url", url }, + { "Thumbnail", CreateModelPart(file, messageContext, url, mediaSettings.MessageProductThumbPictureSize, new Size(50, 50), alt) }, + { "ThumbnailLg", CreateModelPart(file, messageContext, url, mediaSettings.ProductThumbPictureSize, new Size(120, 120), alt) }, + { "DeliveryTime", null }, + { "QtyUnit", null } + }; + + if (part.IsShipEnabled && shoppingCartSettings.DeliveryTimesInShoppingCart != DeliveryTimesPresentation.None) + { + if (deliveryTimeService.GetDeliveryTimeById(part.DeliveryTimeId ?? 0) is DeliveryTime dt) + { + m["DeliveryTime"] = new Dictionary + { + { "Color", dt.ColorHexValue }, + { "Name", dt.GetLocalized(x => x.Name, messageContext.Language).Value }, + }; + } + } + + if (quantityUnitService.GetQuantityUnitById(part.QuantityUnitId) is QuantityUnit qu) + { + m["QtyUnit"] = qu.GetLocalized(x => x.Name, messageContext.Language).Value; + } + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Customer part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var email = part.FindEmail(); + var pwdRecoveryToken = part.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken).NullEmpty(); + var accountActivationToken = part.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken).NullEmpty(); var customerVatStatus = (VatNumberStatus)part.VatNumberStatusId; int rewardPointsBalance = part.GetRewardPointsBalance(); - decimal rewardPointsAmountBase = _services.Resolve().ConvertRewardPointsToAmount(rewardPointsBalance); - decimal rewardPointsAmount = _services.Resolve().ConvertFromPrimaryStoreCurrency(rewardPointsAmountBase, _services.WorkContext.WorkingCurrency); + decimal rewardPointsAmountBase = _services.Resolve().ConvertRewardPointsToAmount(rewardPointsBalance); + decimal rewardPointsAmount = _services.Resolve().ConvertFromPrimaryStoreCurrency(rewardPointsAmountBase, _services.WorkContext.WorkingCurrency); + + var m = new Dictionary + { + ["Id"] = part.Id, + ["CustomerGuid"] = part.CustomerGuid, + ["Username"] = part.Username, + ["Email"] = email, + ["IsTaxExempt"] = part.IsTaxExempt, + ["LastIpAddress"] = part.LastIpAddress, + ["CreatedOn"] = ToUserDate(part.CreatedOnUtc, messageContext), + ["LastLoginOn"] = ToUserDate(part.LastLoginDateUtc, messageContext), + ["LastActivityOn"] = ToUserDate(part.LastActivityDateUtc, messageContext), + + ["FullName"] = GetDisplayNameForCustomer(part).NullEmpty(), + ["VatNumber"] = part.GetAttribute(SystemCustomerAttributeNames.VatNumber).NullEmpty(), + ["VatNumberStatus"] = customerVatStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id).NullEmpty(), + ["CustomerNumber"] = part.CustomerNumber.NullEmpty(), + ["IsRegistered"] = part.IsRegistered(), + + // URLs + ["WishlistUrl"] = BuildRouteUrl("Wishlist", new { customerGuid = part.CustomerGuid }, messageContext), + ["EditUrl"] = BuildActionUrl("Edit", "Customer", new { id = part.Id, area = "admin" }, messageContext), + ["PasswordRecoveryURL"] = pwdRecoveryToken == null ? null : BuildActionUrl("passwordrecoveryconfirm", "customer", + new { token = part.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken), email, area = "" }, + messageContext), + ["AccountActivationURL"] = accountActivationToken == null ? null : BuildActionUrl("activation", "customer", + new { token = part.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken), email, area = "" }, + messageContext), + + // Addresses + ["BillingAddress"] = CreateModelPart(part.BillingAddress ?? new Address(), messageContext), + ["ShippingAddress"] = part.ShippingAddress == null ? null : CreateModelPart(part.ShippingAddress, messageContext), + + // Reward Points + ["RewardPointsAmount"] = rewardPointsAmount, + ["RewardPointsBalance"] = FormatPrice(rewardPointsAmount, messageContext), + ["RewardPointsHistory"] = part.RewardPointsHistory.Count == 0 ? null : part.RewardPointsHistory.Select(x => CreateModelPart(x, messageContext)).ToList(), + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(GiftCard part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "SenderName", part.SenderName.NullEmpty() }, + { "SenderEmail", part.SenderEmail.NullEmpty() }, + { "RecipientName", part.RecipientName.NullEmpty() }, + { "RecipientEmail", part.RecipientEmail.NullEmpty() }, + { "Amount", FormatPrice(part.Amount, messageContext) }, + { "CouponCode", part.GiftCardCouponCode.NullEmpty() } + }; + + // Message + var message = (string)null; + if (part.Message.HasValue()) + { + message = HtmlUtils.StripTags(part.Message); + } + m["Message"] = message; + + // RemainingAmount + Money remainingAmount = null; + var order = part?.PurchasedWithOrderItem?.Order; + if (order != null) + { + remainingAmount = FormatPrice(part.GetGiftCardRemainingAmount(), order, messageContext); + } + m["RemainingAmount"] = remainingAmount; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(NewsLetterSubscription part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var gid = part.NewsLetterSubscriptionGuid; + + var m = new Dictionary + { + { "Id", part.Id }, + { "Email", part.Email.NullEmpty() }, + { "ActivationUrl", gid == Guid.Empty ? null : BuildRouteUrl("NewsletterActivation", new { token = part.NewsLetterSubscriptionGuid, active = true }, messageContext) }, + { "DeactivationUrl", gid == Guid.Empty ? null : BuildRouteUrl("NewsletterActivation", new { token = part.NewsLetterSubscriptionGuid, active = false }, messageContext) } + }; + + var customer = messageContext.Customer; + if (customer != null && customer.Email.IsCaseInsensitiveEqual(part.Email.EmptyNull())) + { + // Set FullName only if a customer account exists for the subscriber's email address. + m["FullName"] = customer.GetFullName(); + } + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Campaign part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var protocol = messageContext.BaseUri.Scheme; + var host = messageContext.BaseUri.Authority + messageContext.BaseUri.AbsolutePath; + var body = HtmlUtils.RelativizeFontSizes(part.Body.EmptyNull()); + + // We must render the body separately + body = _templateEngine.Render(body, messageContext.Model, messageContext.FormatProvider); + + var m = new Dictionary + { + { "Id", part.Id }, + { "Subject", part.Subject.NullEmpty() }, + { "Body", WebHelper.MakeAllUrlsAbsolute(body, protocol, host).NullEmpty() }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ProductReview part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Title", part.Title.NullEmpty() }, + { "Text", HtmlUtils.StripTags(part.ReviewText).NullEmpty() }, + { "Rating", part.Rating } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ProductReviewHelpfulness part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "ProductReviewId", part.ProductReviewId }, + { "ReviewTitle", part.ProductReview.Title }, + { "WasHelpful", part.WasHelpful } + }; + + ApplyCustomerContentPart(m, part, messageContext); + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(PollVotingRecord part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "PollAnswerId", part.PollAnswerId }, + { "PollAnswerName", part.PollAnswer.Name }, + { "PollId", part.PollAnswer.PollId } + }; + + ApplyCustomerContentPart(m, part, messageContext); + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(PrivateMessage part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Subject", part.Subject.NullEmpty() }, + { "Text", part.FormatPrivateMessageText().NullEmpty() }, + { "FromEmail", part.FromCustomer?.FindEmail().NullEmpty() }, + { "ToEmail", part.ToCustomer?.FindEmail().NullEmpty() }, + { "FromName", part.FromCustomer?.GetFullName().NullEmpty() }, + { "ToName", part.ToCustomer?.GetFullName().NullEmpty() }, + { "Url", BuildActionUrl("View", "PrivateMessages", new { id = part.Id, area = "" }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(BlogComment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "PostTitle", part.BlogPost.GetLocalized(x => x.Title, messageContext.Language).Value.NullEmpty() }, + { "PostUrl", BuildRouteUrl("BlogPost", new { SeName = part.BlogPost.GetSeName(messageContext.Language.Id) }, messageContext) }, + { "Text", part.CommentText.NullEmpty() } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(NewsComment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "NewsTitle", part.NewsItem.GetLocalized(x => x.Title, messageContext.Language).Value.NullEmpty() }, + { "Title", part.CommentTitle.NullEmpty() }, + { "Text", HtmlUtils.StripTags(part.CommentText).NullEmpty() }, + { "NewsUrl", BuildRouteUrl("NewsItem", new { SeName = part.NewsItem.GetSeName(messageContext.Language.Id) }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ForumTopic part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var pageIndex = messageContext.Model.GetFromBag("TopicPageIndex"); + + var url = pageIndex > 0 ? + BuildRouteUrl("TopicSlugPaged", new { id = part.Id, slug = part.GetSeName(), page = pageIndex }, messageContext) : + BuildRouteUrl("TopicSlug", new { id = part.Id, slug = part.GetSeName() }, messageContext); var m = new Dictionary - { - ["Id"] = part.Id, - ["CustomerGuid"] = part.CustomerGuid, - ["Username"] = part.Username, - ["Email"] = email, - ["IsTaxExempt"] = part.IsTaxExempt, - ["LastIpAddress"] = part.LastIpAddress, - ["CreatedOn"] = ToUserDate(part.CreatedOnUtc, messageContext), - ["LastLoginOn"] = ToUserDate(part.LastLoginDateUtc, messageContext), - ["LastActivityOn"] = ToUserDate(part.LastActivityDateUtc, messageContext), - - ["FullName"] = GetDisplayNameForCustomer(part).NullEmpty(), - ["VatNumber"] = part.GetAttribute(SystemCustomerAttributeNames.VatNumber).NullEmpty(), - ["VatNumberStatus"] = customerVatStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id).NullEmpty(), - ["CustomerNumber"] = part.CustomerNumber.NullEmpty(), - ["IsRegistered"] = part.IsRegistered(), - - // URLs - ["WishlistUrl"] = BuildRouteUrl("Wishlist", new { customerGuid = part.CustomerGuid }, messageContext), - ["EditUrl"] = BuildActionUrl("Edit", "Customer", new { id = part.Id, area = "admin" }, messageContext), - ["PasswordRecoveryURL"] = pwdRecoveryToken == null ? null : BuildActionUrl("passwordrecoveryconfirm", "customer", - new { token = part.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken), email, area = "" }, - messageContext), - ["AccountActivationURL"] = accountActivationToken == null ? null : BuildActionUrl("activation", "customer", - new { token = part.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken), email, area = "" }, - messageContext), - - // Addresses - ["BillingAddress"] = CreateModelPart(part.BillingAddress ?? new Address(), messageContext), - ["ShippingAddress"] = part.ShippingAddress == null ? null : CreateModelPart(part.ShippingAddress, messageContext), - - // Reward Points - ["RewardPointsAmount"] = rewardPointsAmount, - ["RewardPointsBalance"] = FormatPrice(rewardPointsAmount, messageContext), - ["RewardPointsHistory"] = part.RewardPointsHistory.Count == 0 ? null : part.RewardPointsHistory.Select(x => CreateModelPart(x, messageContext)).ToList(), - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(GiftCard part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Id", part.Id }, - { "SenderName", part.SenderName.NullEmpty() }, - { "SenderEmail", part.SenderEmail.NullEmpty() }, - { "RecipientName", part.RecipientName.NullEmpty() }, - { "RecipientEmail", part.RecipientEmail.NullEmpty() }, - { "Amount", FormatPrice(part.Amount, messageContext) }, - { "CouponCode", part.GiftCardCouponCode.NullEmpty() } - }; - - // Message - var message = (string)null; - if (part.Message.HasValue()) - { - message = HtmlUtils.StripTags(part.Message); - } - m["Message"] = message; - - // RemainingAmount - Money remainingAmount = null; - var order = part?.PurchasedWithOrderItem?.Order; - if (order != null) - { - remainingAmount = FormatPrice(part.GetGiftCardRemainingAmount(), order, messageContext); - } - m["RemainingAmount"] = remainingAmount; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(NewsLetterSubscription part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var gid = part.NewsLetterSubscriptionGuid; - - var m = new Dictionary - { - { "Id", part.Id }, - { "Email", part.Email.NullEmpty() }, - { "ActivationUrl", gid == Guid.Empty ? null : BuildRouteUrl("NewsletterActivation", new { token = part.NewsLetterSubscriptionGuid, active = true }, messageContext) }, - { "DeactivationUrl", gid == Guid.Empty ? null : BuildRouteUrl("NewsletterActivation", new { token = part.NewsLetterSubscriptionGuid, active = false }, messageContext) } - }; - - var customer = messageContext.Customer; - if (customer != null && customer.Email.IsCaseInsensitiveEqual(part.Email.EmptyNull())) - { - // Set FullName only if a customer account exists for the subscriber's email address. - m["FullName"] = customer.GetFullName(); - } - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(Campaign part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var protocol = messageContext.BaseUri.Scheme; - var host = messageContext.BaseUri.Authority + messageContext.BaseUri.AbsolutePath; - var body = HtmlUtils.RelativizeFontSizes(part.Body.EmptyNull()); - - // We must render the body separately - body = _templateEngine.Render(body, messageContext.Model, messageContext.FormatProvider); - - var m = new Dictionary - { - { "Id", part.Id }, - { "Subject", part.Subject.NullEmpty() }, - { "Body", WebHelper.MakeAllUrlsAbsolute(body, protocol, host).NullEmpty() }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(ProductReview part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Title", part.Title.NullEmpty() }, - { "Text", HtmlUtils.StripTags(part.ReviewText).NullEmpty() }, - { "Rating", part.Rating } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(ProductReviewHelpfulness part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "ProductReviewId", part.ProductReviewId }, - { "ReviewTitle", part.ProductReview.Title }, - { "WasHelpful", part.WasHelpful } - }; - - ApplyCustomerContentPart(m, part, messageContext); - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(PollVotingRecord part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "PollAnswerId", part.PollAnswerId }, - { "PollAnswerName", part.PollAnswer.Name }, - { "PollId", part.PollAnswer.PollId } - }; - - ApplyCustomerContentPart(m, part, messageContext); - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(PrivateMessage part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Subject", part.Subject.NullEmpty() }, - { "Text", part.FormatPrivateMessageText().NullEmpty() }, - { "FromEmail", part.FromCustomer?.FindEmail().NullEmpty() }, - { "ToEmail", part.ToCustomer?.FindEmail().NullEmpty() }, - { "FromName", part.FromCustomer?.GetFullName().NullEmpty() }, - { "ToName", part.ToCustomer?.GetFullName().NullEmpty() }, - { "Url", BuildActionUrl("View", "PrivateMessages", new { id = part.Id, area = "" }, messageContext) } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(BlogComment part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "PostTitle", part.BlogPost.Title.NullEmpty() }, - { "PostUrl", BuildRouteUrl("BlogPost", new { SeName = part.BlogPost.GetSeName(part.BlogPost.LanguageId, ensureTwoPublishedLanguages: false) }, messageContext) }, - { "Text", part.CommentText.NullEmpty() } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(NewsComment part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "NewsTitle", part.NewsItem.Title.NullEmpty() }, - { "Title", part.CommentTitle.NullEmpty() }, - { "Text", HtmlUtils.StripTags(part.CommentText).NullEmpty() }, - { "NewsUrl", BuildRouteUrl("NewsItem", new { SeName = part.NewsItem.GetSeName(messageContext.Language.Id) }, messageContext) } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } + { + { "Subject", part.Subject.NullEmpty() }, + { "NumReplies", part.NumReplies }, + { "NumPosts", part.NumPosts }, + { "NumViews", part.Views }, + { "Body", part.GetFirstPost(_services.Resolve())?.FormatPostText().NullEmpty() }, + { "Url", url }, + }; - protected virtual object CreateModelPart(ForumTopic part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var pageIndex = messageContext.Model.GetFromBag("TopicPageIndex"); - - var url = pageIndex > 0 ? - BuildRouteUrl("TopicSlugPaged", new { id = part.Id, slug = part.GetSeName(), page = pageIndex }, messageContext) : - BuildRouteUrl("TopicSlug", new { id = part.Id, slug = part.GetSeName() }, messageContext); - - var m = new Dictionary - { - { "Subject", part.Subject.NullEmpty() }, - { "NumReplies", part.NumReplies }, - { "NumPosts", part.NumPosts }, - { "NumViews", part.Views }, - { "Body", part.GetFirstPost(_services.Resolve())?.FormatPostText().NullEmpty() }, - { "Url", url }, - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(ForumPost part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Author", part.Customer.FormatUserName().NullEmpty() }, - { "Body", part.FormatPostText().NullEmpty() } - }; + PublishModelPartCreatedEvent(part, m); - PublishModelPartCreatedEvent(part, m); - - return m; - } + return m; + } + + protected virtual object CreateModelPart(ForumPost part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Author", part.Customer.FormatUserName().NullEmpty() }, + { "Body", part.FormatPostText().NullEmpty() } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } protected virtual object CreateModelPart(ForumPostVote part, MessageContext messageContext) { @@ -915,271 +921,285 @@ protected virtual object CreateModelPart(ForumPostVote part, MessageContext mess } protected virtual object CreateModelPart(Forum part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Name", part.GetLocalized(x => x.Name, messageContext.Language).Value.NullEmpty() }, - { "GroupName", part.ForumGroup?.GetLocalized(x => x.Name, messageContext.Language)?.Value.NullEmpty() }, - { "NumPosts", part.NumPosts }, - { "NumTopics", part.NumTopics }, - { "Url", BuildRouteUrl("ForumSlug", new { id = part.Id, slug = part.GetSeName(messageContext.Language.Id) }, messageContext) }, - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(Address part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var settings = _services.Resolve(); - var languageId = messageContext.Language?.Id ?? messageContext.LanguageId; - - var salutation = part.Salutation.NullEmpty(); - var title = part.Title.NullEmpty(); - var company = settings.CompanyEnabled ? part.Company : null; - var firstName = part.FirstName.NullEmpty(); - var lastName = part.LastName.NullEmpty(); - var street1 = settings.StreetAddressEnabled ? part.Address1 : null; - var street2 = settings.StreetAddress2Enabled ? part.Address2 : null; - var zip = settings.ZipPostalCodeEnabled ? part.ZipPostalCode : null; - var city = settings.CityEnabled ? part.City : null; - var country = settings.CountryEnabled ? part.Country?.GetLocalized(x => x.Name, languageId ?? 0)?.Value.NullEmpty() : null; - var state = settings.StateProvinceEnabled ? part.StateProvince?.GetLocalized(x => x.Name, languageId ?? 0)?.Value.NullEmpty() : null; - - var m = new Dictionary - { - { "Title", title }, - { "Salutation", salutation }, - { "FullSalutation", part.GetFullSalutaion().NullEmpty() }, - { "FullName", part.GetFullName(false).NullEmpty() }, - { "Company", company }, - { "FirstName", firstName }, - { "LastName", lastName }, - { "Street1", street1 }, - { "Street2", street2 }, - { "Country", country }, - { "CountryId", part.Country?.Id }, - { "CountryAbbrev2", settings.CountryEnabled ? part.Country?.TwoLetterIsoCode.NullEmpty() : null }, - { "CountryAbbrev3", settings.CountryEnabled ? part.Country?.ThreeLetterIsoCode.NullEmpty() : null }, - { "State", state }, - { "StateAbbrev", settings.StateProvinceEnabled ? part.StateProvince?.Abbreviation.NullEmpty() : null }, - { "City", city }, - { "ZipCode", zip }, - { "Email", part.Email.NullEmpty() }, - { "Phone", settings.PhoneEnabled ? part.PhoneNumber : null }, - { "Fax", settings.FaxEnabled ? part.FaxNumber : null } - }; - - m["NameLine"] = Concat(salutation, title, firstName, lastName); - m["StreetLine"] = Concat(street1, street2); - m["CityLine"] = Concat(zip, city); - m["CountryLine"] = Concat(country, state); - - PublishModelPartCreatedEvent
(part, m); - - return m; - } - - protected virtual object CreateModelPart(RewardPointsHistory part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "Id", part.Id }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, - { "Message", part.Message.NullEmpty() }, - { "Points", part.Points }, - { "PointsBalance", part.PointsBalance }, - { "UsedAmount", part.UsedAmount } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(IEnumerable part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary(); - - foreach (var attr in part) - { - m[attr.Key] = attr.Value; - } - - PublishModelPartCreatedEvent>(part, m); - - return m; - } - - protected virtual object CreateModelPart(ForumSubscription part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "SubscriptionGuid", part.SubscriptionGuid }, - { "CustomerId", part.CustomerId }, - { "ForumId", part.ForumId }, - { "TopicId", part.TopicId }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - protected virtual object CreateModelPart(BackInStockSubscription part, MessageContext messageContext) - { - Guard.NotNull(messageContext, nameof(messageContext)); - Guard.NotNull(part, nameof(part)); - - var m = new Dictionary - { - { "StoreId", part.StoreId }, - { "CustomerId", part.CustomerId }, - { "ProductId", part.ProductId }, - { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } - }; - - PublishModelPartCreatedEvent(part, m); - - return m; - } - - #endregion - - #region Model Tree - - public TreeNode GetLastModelTree(string messageTemplateName) - { - Guard.NotEmpty(messageTemplateName, nameof(messageTemplateName)); - - var template = _messageTemplateService.GetMessageTemplateByName(messageTemplateName, _services.StoreContext.CurrentStore.Id); - - if (template != null) - { - return GetLastModelTree(template); - } - - return null; - } - - public TreeNode GetLastModelTree(MessageTemplate template) - { - Guard.NotNull(template, nameof(template)); - - if (template.LastModelTree.IsEmpty()) - { - return null; - } - - return Newtonsoft.Json.JsonConvert.DeserializeObject>(template.LastModelTree); - } - - public TreeNode BuildModelTree(TemplateModel model) - { - Guard.NotNull(model, nameof(model)); - - var root = new TreeNode(new ModelTreeMember { Name = "Model", Kind = ModelTreeMemberKind.Root }); - - foreach (var kvp in model) - { - root.Append(BuildModelTreePart(kvp.Key, kvp.Value)); - } - - return root; - } - - private TreeNode BuildModelTreePart(string modelName, object instance) - { - var t = instance?.GetType(); - TreeNode node = null; - - if (t == null || t.IsPredefinedType()) - { - node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Primitive }); - } - else if (t.IsSequenceType() && !(instance is IDictionary)) - { - node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Collection }); - } - else - { - node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Complex }); - - if (instance is IDictionary dict) - { - foreach (var kvp in dict) - { - node.Append(BuildModelTreePart(kvp.Key, kvp.Value)); - } - } - else if (instance is IDynamicMetaObjectProvider dyn) - { - foreach (var name in dyn.GetMetaObject(Expression.Constant(dyn)).GetDynamicMemberNames()) - { - // we don't want to go deeper in "pure" dynamic objects - node.Append(new TreeNode(new ModelTreeMember { Name = name, Kind = ModelTreeMemberKind.Primitive })); - } - } - else - { - node.AppendRange(BuildModelTreePartForClass(instance)); - } - } - - return node; - } - - private IEnumerable> BuildModelTreePartForClass(object instance) - { - var type = instance?.GetType(); - - if (type == null) - { - yield break; - } - - foreach (var prop in FastProperty.GetProperties(type).Values) - { - var pi = prop.Property; - - if (pi.PropertyType.IsPredefinedType()) - { - yield return new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Primitive }); - } - else if (typeof(IDictionary).IsAssignableFrom(pi.PropertyType)) - { - yield return BuildModelTreePart(prop.Name, prop.GetValue(instance)); - } - else if (pi.PropertyType.IsSequenceType()) - { - yield return new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Collection }); - } - else - { - var node = new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Complex }); - node.AppendRange(BuildModelTreePartForClass(prop.GetValue(instance))); - yield return node; - } - } - } - - #endregion - } + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Name", part.GetLocalized(x => x.Name, messageContext.Language).Value.NullEmpty() }, + { "GroupName", part.ForumGroup?.GetLocalized(x => x.Name, messageContext.Language)?.Value.NullEmpty() }, + { "NumPosts", part.NumPosts }, + { "NumTopics", part.NumTopics }, + { "Url", BuildRouteUrl("ForumSlug", new { id = part.Id, slug = part.GetSeName(messageContext.Language.Id) }, messageContext) }, + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Address part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var settings = _services.Resolve(); + var languageId = messageContext.Language?.Id ?? messageContext.LanguageId; + + var salutation = part.Salutation.NullEmpty(); + var title = part.Title.NullEmpty(); + var company = settings.CompanyEnabled ? part.Company : null; + var firstName = part.FirstName.NullEmpty(); + var lastName = part.LastName.NullEmpty(); + var street1 = settings.StreetAddressEnabled ? part.Address1 : null; + var street2 = settings.StreetAddress2Enabled ? part.Address2 : null; + var zip = settings.ZipPostalCodeEnabled ? part.ZipPostalCode : null; + var city = settings.CityEnabled ? part.City : null; + var country = settings.CountryEnabled ? part.Country?.GetLocalized(x => x.Name, languageId ?? 0)?.Value.NullEmpty() : null; + var state = settings.StateProvinceEnabled ? part.StateProvince?.GetLocalized(x => x.Name, languageId ?? 0)?.Value.NullEmpty() : null; + + var m = new Dictionary + { + { "Title", title }, + { "Salutation", salutation }, + { "FullSalutation", part.GetFullSalutaion().NullEmpty() }, + { "FullName", part.GetFullName(false).NullEmpty() }, + { "Company", company }, + { "FirstName", firstName }, + { "LastName", lastName }, + { "Street1", street1 }, + { "Street2", street2 }, + { "Country", country }, + { "CountryId", part.Country?.Id }, + { "CountryAbbrev2", settings.CountryEnabled ? part.Country?.TwoLetterIsoCode.NullEmpty() : null }, + { "CountryAbbrev3", settings.CountryEnabled ? part.Country?.ThreeLetterIsoCode.NullEmpty() : null }, + { "State", state }, + { "StateAbbrev", settings.StateProvinceEnabled ? part.StateProvince?.Abbreviation.NullEmpty() : null }, + { "City", city }, + { "ZipCode", zip }, + { "Email", part.Email.NullEmpty() }, + { "Phone", settings.PhoneEnabled ? part.PhoneNumber : null }, + { "Fax", settings.FaxEnabled ? part.FaxNumber : null } + }; + + m["NameLine"] = Concat(salutation, title, firstName, lastName); + m["StreetLine"] = Concat(street1, street2); + m["CityLine"] = Concat(zip, city); + m["CountryLine"] = Concat(country, state); + + PublishModelPartCreatedEvent
(part, m); + + return m; + } + + protected virtual object CreateModelPart(RewardPointsHistory part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "Message", part.Message.NullEmpty() }, + { "Points", part.Points }, + { "PointsBalance", part.PointsBalance }, + { "UsedAmount", part.UsedAmount } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(IEnumerable part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary(); + + foreach (var attr in part) + { + m[attr.Key] = attr.Value; + } + + PublishModelPartCreatedEvent>(part, m); + + return m; + } + + protected virtual object CreateModelPart(ForumSubscription part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "SubscriptionGuid", part.SubscriptionGuid }, + { "CustomerId", part.CustomerId }, + { "ForumId", part.ForumId }, + { "TopicId", part.TopicId }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(BackInStockSubscription part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "StoreId", part.StoreId }, + { "CustomerId", part.CustomerId }, + { "ProductId", part.ProductId }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + #endregion + + #region Model Tree + + public TreeNode GetLastModelTree(string messageTemplateName) + { + Guard.NotEmpty(messageTemplateName, nameof(messageTemplateName)); + + var template = _messageTemplateService.GetMessageTemplateByName(messageTemplateName, _services.StoreContext.CurrentStore.Id); + + if (template != null) + { + return GetLastModelTree(template); + } + + return null; + } + + public TreeNode GetLastModelTree(MessageTemplate template) + { + Guard.NotNull(template, nameof(template)); + + if (template.LastModelTree.IsEmpty()) + { + return null; + } + + return Newtonsoft.Json.JsonConvert.DeserializeObject>(template.LastModelTree); + } + + public TreeNode BuildModelTree(TemplateModel model) + { + Guard.NotNull(model, nameof(model)); + + var root = new TreeNode(new ModelTreeMember { Name = "Model", Kind = ModelTreeMemberKind.Root }); + + foreach (var kvp in model) + { + root.Append(BuildModelTreePart(kvp.Key, kvp.Value)); + } + + return root; + } + + private TreeNode BuildModelTreePart(string modelName, object instance) + { + var t = instance?.GetType(); + TreeNode node = null; + + if (t == null || t.IsPredefinedType()) + { + node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Primitive }); + } + else if (t.IsSequenceType() && !(instance is IDictionary)) + { + node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Collection }); + } + else + { + node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Complex }); + + if (instance is IDictionary dict) + { + foreach (var kvp in dict) + { + node.Append(BuildModelTreePart(kvp.Key, kvp.Value)); + } + } + else if (instance is IDynamicMetaObjectProvider dyn) + { + foreach (var name in dyn.GetMetaObject(Expression.Constant(dyn)).GetDynamicMemberNames()) + { + // we don't want to go deeper in "pure" dynamic objects + node.Append(new TreeNode(new ModelTreeMember { Name = name, Kind = ModelTreeMemberKind.Primitive })); + } + } + else + { + node.AppendRange(BuildModelTreePartForClass(instance)); + } + } + + return node; + } + + private IEnumerable> BuildModelTreePartForClass(object instance, HashSet instanceLookup = null) + { + var type = instance?.GetType(); + + if (type == null) + { + yield break; + } + + foreach (var prop in FastProperty.GetProperties(type).Values) + { + var pi = prop.Property; + + if (pi.PropertyType.IsPredefinedType()) + { + yield return new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Primitive }); + } + else if (typeof(IDictionary).IsAssignableFrom(pi.PropertyType)) + { + yield return BuildModelTreePart(prop.Name, prop.GetValue(instance)); + } + else if (pi.PropertyType.IsSequenceType()) + { + yield return new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Collection }); + } + else if (pi.PropertyType.IsClass) + { + if (instanceLookup == null) + { + instanceLookup = new HashSet(ReferenceEqualityComparer.Default) { instance }; + } + + var childInstance = prop.GetValue(instance); + if (childInstance != null) + { + if (!instanceLookup.Contains(childInstance)) + { + instanceLookup.Add(childInstance); + + var node = new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Complex }); + node.AppendRange(BuildModelTreePartForClass(childInstance, instanceLookup)); + yield return node; + } + } + } + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs b/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs index ede9e8df96..ee5e0689ad 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs @@ -10,62 +10,62 @@ namespace SmartStore.Services.Messages { - public partial class MessageTemplateService: IMessageTemplateService + public partial class MessageTemplateService : IMessageTemplateService { private const string MESSAGETEMPLATES_ALL_KEY = "SmartStore.messagetemplate.all-{0}"; private const string MESSAGETEMPLATES_BY_NAME_KEY = "SmartStore.messagetemplate.name-{0}-{1}"; private const string MESSAGETEMPLATES_PATTERN_KEY = "SmartStore.messagetemplate.*"; private readonly IRepository _messageTemplateRepository; - private readonly IRepository _storeMappingRepository; - private readonly ILanguageService _languageService; - private readonly IStoreMappingService _storeMappingService; - private readonly ILocalizedEntityService _localizedEntityService; + private readonly IRepository _storeMappingRepository; + private readonly ILanguageService _languageService; + private readonly IStoreMappingService _storeMappingService; + private readonly ILocalizedEntityService _localizedEntityService; private readonly IRequestCache _requestCache; public MessageTemplateService( - IRequestCache requestCache, - IRepository storeMappingRepository, - ILanguageService languageService, - ILocalizedEntityService localizedEntityService, - IStoreMappingService storeMappingService, + IRequestCache requestCache, + IRepository storeMappingRepository, + ILanguageService languageService, + ILocalizedEntityService localizedEntityService, + IStoreMappingService storeMappingService, IRepository messageTemplateRepository) { - _requestCache = requestCache; - _storeMappingRepository = storeMappingRepository; - _languageService = languageService; - _localizedEntityService = localizedEntityService; - _storeMappingService = storeMappingService; - _messageTemplateRepository = messageTemplateRepository; - - QuerySettings = DbQuerySettings.Default; - } + _requestCache = requestCache; + _storeMappingRepository = storeMappingRepository; + _languageService = languageService; + _localizedEntityService = localizedEntityService; + _storeMappingService = storeMappingService; + _messageTemplateRepository = messageTemplateRepository; + + QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } - public virtual void DeleteMessageTemplate(MessageTemplate messageTemplate) - { - Guard.NotNull(messageTemplate, nameof(messageTemplate)); + public virtual void DeleteMessageTemplate(MessageTemplate messageTemplate) + { + Guard.NotNull(messageTemplate, nameof(messageTemplate)); - _messageTemplateRepository.Delete(messageTemplate); + _messageTemplateRepository.Delete(messageTemplate); - _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); - } + _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); + } public virtual void InsertMessageTemplate(MessageTemplate messageTemplate) { - Guard.NotNull(messageTemplate, nameof(messageTemplate)); + Guard.NotNull(messageTemplate, nameof(messageTemplate)); - _messageTemplateRepository.Insert(messageTemplate); + _messageTemplateRepository.Insert(messageTemplate); _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); } public virtual void UpdateMessageTemplate(MessageTemplate messageTemplate) { - Guard.NotNull(messageTemplate, nameof(messageTemplate)); + Guard.NotNull(messageTemplate, nameof(messageTemplate)); - _messageTemplateRepository.Update(messageTemplate); + _messageTemplateRepository.Update(messageTemplate); _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); } @@ -78,109 +78,110 @@ public virtual MessageTemplate GetMessageTemplateById(int messageTemplateId) return _messageTemplateRepository.GetById(messageTemplateId); } - public virtual MessageTemplate GetMessageTemplateByName(string messageTemplateName, int storeId) + public virtual MessageTemplate GetMessageTemplateByName(string messageTemplateName, int storeId) { - Guard.NotEmpty(messageTemplateName, nameof(messageTemplateName)); + Guard.NotEmpty(messageTemplateName, nameof(messageTemplateName)); string key = string.Format(MESSAGETEMPLATES_BY_NAME_KEY, messageTemplateName, storeId); return _requestCache.Get(key, () => { - var query = _messageTemplateRepository.Table; - query = query.Where(t => t.Name == messageTemplateName); - query = query.OrderBy(t => t.Id); - var templates = query.ToList(); + var query = _messageTemplateRepository.Table; + query = query.Where(t => t.Name == messageTemplateName); + query = query.OrderBy(t => t.Id); + var templates = query.ToList(); - //store mapping - if (storeId > 0) - { - return templates.Where(t => _storeMappingService.Authorize(t, storeId)).FirstOrDefault(); - } + //store mapping + if (storeId > 0) + { + return templates.Where(t => _storeMappingService.Authorize(t, storeId)).FirstOrDefault(); + } return templates.FirstOrDefault(); }); } - public virtual IList GetAllMessageTemplates(int storeId) + public virtual IList GetAllMessageTemplates(int storeId) { - string key = string.Format(MESSAGETEMPLATES_ALL_KEY, storeId); - return _requestCache.Get(key, () => + string key = string.Format(MESSAGETEMPLATES_ALL_KEY, storeId); + return _requestCache.Get(key, () => { - var query = _messageTemplateRepository.Table; - query = query.OrderBy(t => t.Name); - - // Store mapping - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = from t in query - join sm in _storeMappingRepository.Table - on new { c1 = t.Id, c2 = "MessageTemplate" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into t_sm - from sm in t_sm.DefaultIfEmpty() - where !t.LimitedToStores || storeId == sm.StoreId - select t; - - // Only distinct items (group by ID) - query = from t in query - group t by t.Id into tGroup - orderby tGroup.Key - select tGroup.FirstOrDefault(); - query = query.OrderBy(t => t.Name); - } - - return query.ToList(); + var query = _messageTemplateRepository.Table; + query = query.OrderBy(t => t.Name); + + // Store mapping + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = from t in query + join sm in _storeMappingRepository.Table + on new { c1 = t.Id, c2 = "MessageTemplate" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into t_sm + from sm in t_sm.DefaultIfEmpty() + where !t.LimitedToStores || storeId == sm.StoreId + select t; + + // Only distinct items (group by ID) + query = from t in query + group t by t.Id into tGroup + orderby tGroup.Key + select tGroup.FirstOrDefault(); + query = query.OrderBy(t => t.Name); + } + + return query.ToList(); }); } - public virtual MessageTemplate CopyMessageTemplate(MessageTemplate messageTemplate) - { - Guard.NotNull(messageTemplate, nameof(messageTemplate)); - - var mtCopy = new MessageTemplate - { - Name = messageTemplate.Name, - To = messageTemplate.To, - ReplyTo = messageTemplate.ReplyTo, - BccEmailAddresses = messageTemplate.BccEmailAddresses, - Subject = messageTemplate.Subject, - Body = messageTemplate.Body, - IsActive = messageTemplate.IsActive, - EmailAccountId = messageTemplate.EmailAccountId, - LimitedToStores = messageTemplate.LimitedToStores - // INFO: we do not copy attachments - }; - - InsertMessageTemplate(mtCopy); - - var languages = _languageService.GetAllLanguages(true); - - // localization - foreach (var lang in languages) - { - string bccEmailAddresses = messageTemplate.GetLocalized(x => x.BccEmailAddresses, lang, false, false); - if (bccEmailAddresses.HasValue()) - _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.BccEmailAddresses, bccEmailAddresses, lang.Id); - - string subject = messageTemplate.GetLocalized(x => x.Subject, lang, false, false); - if (subject.HasValue()) - _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.Subject, subject, lang.Id); - - string body = messageTemplate.GetLocalized(x => x.Body, lang, false, false); - if (body.HasValue()) - _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.Body, subject, lang.Id); - - int emailAccountId = messageTemplate.GetLocalized(x => x.EmailAccountId, lang, false, false); - if (emailAccountId > 0) - _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.EmailAccountId, emailAccountId, lang.Id); - } - - // store mapping - var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(messageTemplate); - foreach (var id in selectedStoreIds) - { - _storeMappingService.InsertStoreMapping(mtCopy, id); - } - - return mtCopy; - } + public virtual MessageTemplate CopyMessageTemplate(MessageTemplate messageTemplate) + { + Guard.NotNull(messageTemplate, nameof(messageTemplate)); + + var mtCopy = new MessageTemplate + { + Name = messageTemplate.Name, + To = messageTemplate.To, + ReplyTo = messageTemplate.ReplyTo, + BccEmailAddresses = messageTemplate.BccEmailAddresses, + Subject = messageTemplate.Subject, + Body = messageTemplate.Body, + IsActive = messageTemplate.IsActive, + EmailAccountId = messageTemplate.EmailAccountId, + LimitedToStores = messageTemplate.LimitedToStores, + ModelTypes = messageTemplate.ModelTypes + // INFO: we do not copy attachments + }; + + InsertMessageTemplate(mtCopy); + + var languages = _languageService.GetAllLanguages(true); + + // localization + foreach (var lang in languages) + { + string bccEmailAddresses = messageTemplate.GetLocalized(x => x.BccEmailAddresses, lang, false, false); + if (bccEmailAddresses.HasValue()) + _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.BccEmailAddresses, bccEmailAddresses, lang.Id); + + string subject = messageTemplate.GetLocalized(x => x.Subject, lang, false, false); + if (subject.HasValue()) + _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.Subject, subject, lang.Id); + + string body = messageTemplate.GetLocalized(x => x.Body, lang, false, false); + if (body.HasValue()) + _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.Body, subject, lang.Id); + + int emailAccountId = messageTemplate.GetLocalized(x => x.EmailAccountId, lang, false, false); + if (emailAccountId > 0) + _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.EmailAccountId, emailAccountId, lang.Id); + } + + // store mapping + var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(messageTemplate); + foreach (var id in selectedStoreIds) + { + _storeMappingService.InsertStoreMapping(mtCopy, id); + } + + return mtCopy; + } } } diff --git a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs index 6802877bc9..b116b8c3ac 100644 --- a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs @@ -13,24 +13,24 @@ public class NewsLetterSubscriptionService : INewsLetterSubscriptionService private readonly IRepository _customerRepository; private readonly ICommonServices _services; - public NewsLetterSubscriptionService( + public NewsLetterSubscriptionService( IRepository subscriptionRepository, IRepository customerRepository, ICommonServices services) { _subscriptionRepository = subscriptionRepository; _customerRepository = customerRepository; - _services = services; - } + _services = services; + } public void InsertNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true) { - Guard.NotNull(newsLetterSubscription, nameof(newsLetterSubscription)); + Guard.NotNull(newsLetterSubscription, nameof(newsLetterSubscription)); - if (newsLetterSubscription.StoreId == 0) - { - throw new SmartException("News letter subscription must be assigned to a valid store."); - } + if (newsLetterSubscription.StoreId == 0) + { + throw new SmartException("News letter subscription must be assigned to a valid store."); + } // Handle e-mail newsLetterSubscription.Email = EnsureSubscriberEmailOrThrow(newsLetterSubscription.Email); @@ -47,12 +47,12 @@ public void InsertNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr public void UpdateNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true) { - Guard.NotNull(newsLetterSubscription, nameof(newsLetterSubscription)); + Guard.NotNull(newsLetterSubscription, nameof(newsLetterSubscription)); - if (newsLetterSubscription.StoreId == 0) - { - throw new SmartException("News letter subscription must be assigned to a valid store."); - } + if (newsLetterSubscription.StoreId == 0) + { + throw new SmartException("News letter subscription must be assigned to a valid store."); + } // Handle e-mail newsLetterSubscription.Email = EnsureSubscriberEmailOrThrow(newsLetterSubscription.Email); @@ -70,8 +70,8 @@ public void UpdateNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr // If the previous entry was false, but this one is true, publish a subscribe. PublishSubscriptionEvent(newsLetterSubscription.Email, true, publishSubscriptionEvents); } - - if ((originalSubscription.Active && newsLetterSubscription.Active) && + + if ((originalSubscription.Active && newsLetterSubscription.Active) && (originalSubscription.Email != newsLetterSubscription.Email)) { // If the two emails are different publish an unsubscribe. @@ -87,7 +87,7 @@ public void UpdateNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true) { - Guard.NotNull(newsLetterSubscription, nameof(newsLetterSubscription)); + Guard.NotNull(newsLetterSubscription, nameof(newsLetterSubscription)); _subscriptionRepository.Delete(newsLetterSubscription); @@ -95,55 +95,55 @@ public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLett PublishSubscriptionEvent(newsLetterSubscription.Email, false, publishSubscriptionEvents); } - public virtual bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId) - { - bool? result = null; - - if (email.IsEmail()) - { - var newsletter = GetNewsLetterSubscriptionByEmail(email, storeId); - if (newsletter != null) - { - if (add) - { - if (!newsletter.Active) - { - _services.MessageFactory.SendNewsLetterSubscriptionActivationMessage(newsletter, _services.WorkContext.WorkingLanguage.Id); - } - UpdateNewsLetterSubscription(newsletter); - result = true; - } - else - { - DeleteNewsLetterSubscription(newsletter); - result = false; - } - } - else - { - if (add) - { - newsletter = new NewsLetterSubscription - { - NewsLetterSubscriptionGuid = Guid.NewGuid(), - Email = email, - Active = false, - CreatedOnUtc = DateTime.UtcNow, - StoreId = storeId, + public virtual bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId) + { + bool? result = null; + + if (email.IsEmail()) + { + var newsletter = GetNewsLetterSubscriptionByEmail(email, storeId); + if (newsletter != null) + { + if (add) + { + if (!newsletter.Active) + { + _services.MessageFactory.SendNewsLetterSubscriptionActivationMessage(newsletter, _services.WorkContext.WorkingLanguage.Id); + } + UpdateNewsLetterSubscription(newsletter); + result = true; + } + else + { + DeleteNewsLetterSubscription(newsletter); + result = false; + } + } + else + { + if (add) + { + newsletter = new NewsLetterSubscription + { + NewsLetterSubscriptionGuid = Guid.NewGuid(), + Email = email, + Active = false, + CreatedOnUtc = DateTime.UtcNow, + StoreId = storeId, WorkingLanguageId = _services.WorkContext.WorkingLanguage.Id - }; - InsertNewsLetterSubscription(newsletter); + }; + InsertNewsLetterSubscription(newsletter); - _services.MessageFactory.SendNewsLetterSubscriptionActivationMessage(newsletter, _services.WorkContext.WorkingLanguage.Id); + _services.MessageFactory.SendNewsLetterSubscriptionActivationMessage(newsletter, _services.WorkContext.WorkingLanguage.Id); - result = true; - } - } - } - return result; - } + result = true; + } + } + } + return result; + } - public virtual NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId) + public virtual NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId) { if (newsLetterSubscriptionId == 0) return null; @@ -165,20 +165,20 @@ orderby nls.Id public virtual NewsLetterSubscription GetNewsLetterSubscriptionByEmail(string email, int storeId = 0) { - if (!email.IsEmail()) - return null; + if (!email.IsEmail()) + return null; email = email.Trim(); - var query = _subscriptionRepository.Table - .Where(x => x.Email == email); + var query = _subscriptionRepository.Table + .Where(x => x.Email == email); - if (storeId > 0) - query = query.Where(x => x.StoreId == storeId); + if (storeId > 0) + query = query.Where(x => x.StoreId == storeId); - var subscription = query.OrderBy(x => x.Id).FirstOrDefault(); + var subscription = query.OrderBy(x => x.Id).FirstOrDefault(); - return subscription; + return subscription; } public virtual IPagedList GetAllNewsLetterSubscriptions( @@ -237,27 +237,27 @@ private void PublishSubscriptionEvent(string email, bool isSubscribe, bool publi { if (publishSubscriptionEvents) { - if (isSubscribe) + if (isSubscribe) { - _services.EventPublisher.Publish(new EmailSubscribedEvent(email)); - } + _services.EventPublisher.Publish(new EmailSubscribedEvent(email)); + } else { - _services.EventPublisher.Publish(new EmailUnsubscribedEvent(email)); - } + _services.EventPublisher.Publish(new EmailUnsubscribedEvent(email)); + } } } - private static string EnsureSubscriberEmailOrThrow(string email) - { - string output = email.EmptyNull().Trim().Truncate(255); + private static string EnsureSubscriberEmailOrThrow(string email) + { + string output = email.EmptyNull().Trim().Truncate(255); - if (!output.IsEmail()) - { - throw Error.ArgumentOutOfRange("email", "Email is not valid", email); - } + if (!output.IsEmail()) + { + throw Error.ArgumentOutOfRange("email", "Email is not valid", email); + } - return output; - } - } + return output; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs index b24defdee0..8e0a9846fc 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs @@ -20,55 +20,55 @@ namespace SmartStore.Services.Messages { - public partial class QueuedEmailService : IQueuedEmailService + public partial class QueuedEmailService : IQueuedEmailService { private readonly IRepository _queuedEmailRepository; - private readonly IRepository _queuedEmailAttachmentRepository; - private readonly IEmailSender _emailSender; - private readonly ICommonServices _services; - - public QueuedEmailService( - IRepository queuedEmailRepository, - IRepository queuedEmailAttachmentRepository, - IEmailSender emailSender, - ICommonServices services) + private readonly IRepository _queuedEmailAttachmentRepository; + private readonly IEmailSender _emailSender; + private readonly ICommonServices _services; + + public QueuedEmailService( + IRepository queuedEmailRepository, + IRepository queuedEmailAttachmentRepository, + IEmailSender emailSender, + ICommonServices services) { _queuedEmailRepository = queuedEmailRepository; - _queuedEmailAttachmentRepository = queuedEmailAttachmentRepository; - _emailSender = emailSender; - _services = services; - } + _queuedEmailAttachmentRepository = queuedEmailAttachmentRepository; + _emailSender = emailSender; + _services = services; + } - public Localizer T { get; set; } = NullLocalizer.Instance; - public ILogger Logger { get; set; } = NullLogger.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; + public ILogger Logger { get; set; } = NullLogger.Instance; - public virtual void InsertQueuedEmail(QueuedEmail queuedEmail) + public virtual void InsertQueuedEmail(QueuedEmail queuedEmail) { - Guard.NotNull(queuedEmail, nameof(queuedEmail)); + Guard.NotNull(queuedEmail, nameof(queuedEmail)); - _queuedEmailRepository.Insert(queuedEmail); + _queuedEmailRepository.Insert(queuedEmail); } public virtual void UpdateQueuedEmail(QueuedEmail queuedEmail) { - Guard.NotNull(queuedEmail, nameof(queuedEmail)); + Guard.NotNull(queuedEmail, nameof(queuedEmail)); - _queuedEmailRepository.Update(queuedEmail); + _queuedEmailRepository.Update(queuedEmail); } public virtual void DeleteQueuedEmail(QueuedEmail queuedEmail) { - Guard.NotNull(queuedEmail, nameof(queuedEmail)); + Guard.NotNull(queuedEmail, nameof(queuedEmail)); _queuedEmailRepository.Delete(queuedEmail); } - public virtual int DeleteAllQueuedEmails() - { - // do not delete e-mails which are about to be sent - return _queuedEmailRepository.DeleteAll(x => x.SentOnUtc.HasValue || x.SentTries >= 3); - } + public virtual int DeleteAllQueuedEmails() + { + // do not delete e-mails which are about to be sent + return _queuedEmailRepository.DeleteAll(x => x.SentOnUtc.HasValue || x.SentTries >= 3); + } public virtual QueuedEmail GetQueuedEmailById(int queuedEmailId) { @@ -76,7 +76,7 @@ public virtual QueuedEmail GetQueuedEmailById(int queuedEmailId) return null; var queuedEmail = _queuedEmailRepository.GetById(queuedEmailId); - return queuedEmail; + return queuedEmail; } public virtual IList GetQueuedEmailsByIds(int[] queuedEmailIds) @@ -90,29 +90,29 @@ where queuedEmailIds.Contains(qe.Id) var queuedEmails = query.ToList(); - // sort by passed identifier sequence - return queuedEmails.OrderBySequence(queuedEmailIds).ToList(); - } + // sort by passed identifier sequence + return queuedEmails.OrderBySequence(queuedEmailIds).ToList(); + } public virtual IPagedList SearchEmails(SearchEmailsQuery query) { - Guard.NotNull(query, nameof(query)); - + Guard.NotNull(query, nameof(query)); + var q = _queuedEmailRepository.Table; - if (query.Expand.HasValue()) - { - var expands = query.Expand.Split(','); - foreach (var expand in expands) - { - q = q.Expand(expand.Trim()); - } - } + if (query.Expand.HasValue()) + { + var expands = query.Expand.Split(','); + foreach (var expand in expands) + { + q = q.Expand(expand.Trim()); + } + } if (query.From.HasValue()) - q = q.Where(qe => qe.From.Contains(query.From.Trim())); + q = q.Where(qe => qe.From.Contains(query.From.Trim())); - if (query.To.HasValue()) + if (query.To.HasValue()) q = q.Where(qe => qe.To.Contains(query.To.Trim())); if (query.StartTime.HasValue) @@ -124,14 +124,14 @@ public virtual IPagedList SearchEmails(SearchEmailsQuery query) if (query.UnsentOnly) q = q.Where(qe => !qe.SentOnUtc.HasValue); - if (query.SendManually.HasValue) - q = q.Where(qe => qe.SendManually == query.SendManually.Value); + if (query.SendManually.HasValue) + q = q.Where(qe => qe.SendManually == query.SendManually.Value); q = q.Where(qe => qe.SentTries < query.MaxSendTries); - - q = q.OrderByDescending(qe => qe.Priority); - q = query.OrderByLatest ? + q = q.OrderByDescending(qe => qe.Priority); + + q = query.OrderByLatest ? ((IOrderedQueryable)q).ThenByDescending(qe => qe.CreatedOnUtc) : ((IOrderedQueryable)q).ThenBy(qe => qe.CreatedOnUtc); @@ -139,164 +139,164 @@ public virtual IPagedList SearchEmails(SearchEmailsQuery query) return queuedEmails; } - public virtual async Task SendEmailAsync(QueuedEmail queuedEmail) - { - var result = false; - - try - { - var smtpContext = new SmtpContext(queuedEmail.EmailAccount); - var msg = ConvertEmail(queuedEmail); - - await _emailSender.SendEmailAsync(smtpContext, msg); - - queuedEmail.SentOnUtc = DateTime.UtcNow; - result = true; - } - catch (Exception ex) - { - Logger.Error(ex, string.Concat(T("Admin.Common.ErrorSendingEmail"), ": ", ex.Message)); - } - finally - { - queuedEmail.SentTries += 1; - UpdateQueuedEmail(queuedEmail); - } - - return result; - } - - private void AddEmailAddresses(string addresses, ICollection target) - { - var arr = addresses.IsEmpty() ? null : addresses.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - if (arr != null && arr.Length > 0) - { - target.AddRange(arr.Where(x => x.Trim().HasValue()).Select(x => new EmailAddress(x))); - } - } - - internal EmailMessage ConvertEmail(QueuedEmail qe) - { - // 'internal' for testing purposes - - var msg = new EmailMessage( - new EmailAddress(qe.To), - qe.Subject.Replace("\r\n", string.Empty), - qe.Body, - new EmailAddress(qe.From)); - - if (qe.ReplyTo.HasValue()) - { - msg.ReplyTo.Add(new EmailAddress(qe.ReplyTo)); - } - - AddEmailAddresses(qe.CC, msg.Cc); - AddEmailAddresses(qe.Bcc, msg.Bcc); - - if (qe.Attachments != null && qe.Attachments.Count > 0) - { - foreach (var qea in qe.Attachments) - { - Attachment attachment = null; - - if (qea.StorageLocation == EmailAttachmentStorageLocation.Blob) - { - var data = qea.MediaStorage?.Data; - - if (data != null && data.LongLength > 0) - { - attachment = new Attachment(data.ToStream(), qea.Name, qea.MimeType); - } - } - else if (qea.StorageLocation == EmailAttachmentStorageLocation.Path) - { - var path = qea.Path; - if (path.HasValue()) - { - if (path[0] == '~' || path[0] == '/') - { - path = CommonHelper.MapPath(VirtualPathUtility.ToAppRelative(path), false); - } - if (File.Exists(path)) - { - attachment = new Attachment(path, qea.MimeType) { Name = qea.Name }; - } - } - } - else if (qea.StorageLocation == EmailAttachmentStorageLocation.FileReference) - { - var file = qea.MediaFile; - if (file != null) - { + public virtual async Task SendEmailAsync(QueuedEmail queuedEmail) + { + var result = false; + + try + { + var smtpContext = new SmtpContext(queuedEmail.EmailAccount); + var msg = ConvertEmail(queuedEmail); + + await _emailSender.SendEmailAsync(smtpContext, msg); + + queuedEmail.SentOnUtc = DateTime.UtcNow; + result = true; + } + catch (Exception ex) + { + Logger.Error(ex, string.Concat(T("Admin.Common.ErrorSendingEmail"), ": ", ex.Message)); + } + finally + { + queuedEmail.SentTries += 1; + UpdateQueuedEmail(queuedEmail); + } + + return result; + } + + private void AddEmailAddresses(string addresses, ICollection target) + { + var arr = addresses.IsEmpty() ? null : addresses.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + if (arr != null && arr.Length > 0) + { + target.AddRange(arr.Where(x => x.Trim().HasValue()).Select(x => new EmailAddress(x))); + } + } + + internal EmailMessage ConvertEmail(QueuedEmail qe) + { + // 'internal' for testing purposes + + var msg = new EmailMessage( + new EmailAddress(qe.To), + qe.Subject.Replace("\r\n", string.Empty), + qe.Body, + new EmailAddress(qe.From)); + + if (qe.ReplyTo.HasValue()) + { + msg.ReplyTo.Add(new EmailAddress(qe.ReplyTo)); + } + + AddEmailAddresses(qe.CC, msg.Cc); + AddEmailAddresses(qe.Bcc, msg.Bcc); + + if (qe.Attachments != null && qe.Attachments.Count > 0) + { + foreach (var qea in qe.Attachments) + { + Attachment attachment = null; + + if (qea.StorageLocation == EmailAttachmentStorageLocation.Blob) + { + var data = qea.MediaStorage?.Data; + + if (data != null && data.LongLength > 0) + { + attachment = new Attachment(data.ToStream(), qea.Name, qea.MimeType); + } + } + else if (qea.StorageLocation == EmailAttachmentStorageLocation.Path) + { + var path = qea.Path; + if (path.HasValue()) + { + if (path[0] == '~' || path[0] == '/') + { + path = CommonHelper.MapPath(VirtualPathUtility.ToAppRelative(path), false); + } + if (File.Exists(path)) + { + attachment = new Attachment(path, qea.MimeType) { Name = qea.Name }; + } + } + } + else if (qea.StorageLocation == EmailAttachmentStorageLocation.FileReference) + { + var file = qea.MediaFile; + if (file != null) + { var mediaFile = _services.MediaService.ConvertMediaFile(file); - var stream = mediaFile.OpenRead(); + var stream = mediaFile.OpenRead(); if (stream != null) { attachment = new Attachment(stream, file.Name, file.MimeType); } } - } - - if (attachment != null) - { - msg.Attachments.Add(attachment); - } - } - } - - return msg; - } - - #region Attachments - - public virtual QueuedEmailAttachment GetQueuedEmailAttachmentById(int id) - { - if (id == 0) - return null; - - var qea = _queuedEmailAttachmentRepository.GetById(id); - return qea; - } - - public virtual void DeleteQueuedEmailAttachment(QueuedEmailAttachment attachment) - { - Guard.NotNull(attachment, nameof(attachment)); - - _queuedEmailAttachmentRepository.Delete(attachment); - } - - public virtual byte[] LoadQueuedEmailAttachmentBinary(QueuedEmailAttachment attachment) - { - Guard.NotNull(attachment, nameof(attachment)); - - if (attachment.StorageLocation == EmailAttachmentStorageLocation.Blob) - { - return attachment.MediaStorage?.Data ?? new byte[0]; - } - - return null; - } - - #endregion - } - - public class SearchEmailsQuery - { - public string From { get; set; } - public string To { get; set; } - public DateTime? StartTime { get; set; } - public DateTime? EndTime { get; set; } - public bool UnsentOnly { get; set; } - public int MaxSendTries { get; set; } - public bool OrderByLatest { get; set; } - public int PageIndex { get; set; } - public int PageSize { get; set; } - public bool? SendManually { get; set; } - - /// - /// Navigation properties to eager load (comma separataed) - /// - public string Expand { get; set; } - } + } + + if (attachment != null) + { + msg.Attachments.Add(attachment); + } + } + } + + return msg; + } + + #region Attachments + + public virtual QueuedEmailAttachment GetQueuedEmailAttachmentById(int id) + { + if (id == 0) + return null; + + var qea = _queuedEmailAttachmentRepository.GetById(id); + return qea; + } + + public virtual void DeleteQueuedEmailAttachment(QueuedEmailAttachment attachment) + { + Guard.NotNull(attachment, nameof(attachment)); + + _queuedEmailAttachmentRepository.Delete(attachment); + } + + public virtual byte[] LoadQueuedEmailAttachmentBinary(QueuedEmailAttachment attachment) + { + Guard.NotNull(attachment, nameof(attachment)); + + if (attachment.StorageLocation == EmailAttachmentStorageLocation.Blob) + { + return attachment.MediaStorage?.Data ?? new byte[0]; + } + + return null; + } + + #endregion + } + + public class SearchEmailsQuery + { + public string From { get; set; } + public string To { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public bool UnsentOnly { get; set; } + public int MaxSendTries { get; set; } + public bool OrderByLatest { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } + public bool? SendManually { get; set; } + + /// + /// Navigation properties to eager load (comma separataed) + /// + public string Expand { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs index 1af83916e8..6c2bae3d97 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs @@ -19,16 +19,16 @@ public QueuedMessagesClearTask( IRepository qeRepository, CommonSettings commonSettings) { - _qeRepository = qeRepository; + _qeRepository = qeRepository; _commonSettings = commonSettings; } - public override async Task ExecuteAsync(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { - var olderThan = DateTime.UtcNow.AddDays(-Math.Abs(_commonSettings.MaxQueuedMessagesAgeInDays)); - await _qeRepository.DeleteAllAsync(x => x.SentOnUtc.HasValue && x.CreatedOnUtc < olderThan); + var olderThan = DateTime.UtcNow.AddDays(-Math.Abs(_commonSettings.MaxQueuedMessagesAgeInDays)); + await _qeRepository.DeleteAllAsync(x => x.SentOnUtc.HasValue && x.CreatedOnUtc < olderThan); - _qeRepository.Context.ShrinkDatabase(); + _qeRepository.Context.ShrinkDatabase(); } } } diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs index abcd1c2315..eccd83e7ab 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs @@ -16,33 +16,33 @@ public QueuedMessagesSendTask(IQueuedEmailService queuedEmailService) _queuedEmailService = queuedEmailService; } - public override async Task ExecuteAsync(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { - const int pageSize = 1000; - const int maxTries = 3; + const int pageSize = 1000; + const int maxTries = 3; - for (int i = 0; i < 9999999; ++i) - { - var q = new SearchEmailsQuery - { - MaxSendTries = maxTries, - PageIndex = i, - PageSize = pageSize, - Expand = "Attachments", - UnsentOnly = true, - SendManually = false - }; + for (int i = 0; i < 9999999; ++i) + { + var q = new SearchEmailsQuery + { + MaxSendTries = maxTries, + PageIndex = i, + PageSize = pageSize, + Expand = "Attachments", + UnsentOnly = true, + SendManually = false + }; - var queuedEmails = await _queuedEmailService.SearchEmails(q).LoadAsync(); + var queuedEmails = await _queuedEmailService.SearchEmails(q).LoadAsync(); - foreach (var queuedEmail in queuedEmails) - { - await _queuedEmailService.SendEmailAsync(queuedEmail); - } + foreach (var queuedEmail in queuedEmails) + { + await _queuedEmailService.SendEmailAsync(queuedEmail); + } - if (!queuedEmails.HasNextPage) - break; - } + if (!queuedEmails.HasNextPage) + break; + } } } } diff --git a/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs b/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs index 61037c1e01..c7e92c361a 100644 --- a/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs +++ b/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs @@ -4,21 +4,21 @@ namespace SmartStore.Services.Messages { - public class TemplateModel : HybridExpando - { - public T GetFromBag(string key) - { - Guard.NotEmpty(key, nameof(key)); + public class TemplateModel : HybridExpando + { + public T GetFromBag(string key) + { + Guard.NotEmpty(key, nameof(key)); - if (base.Contains("Bag") && base["Bag"] is IDictionary bag) - { - if (bag.TryGetValue(key, out var value) && value is T result) - { - return result; - } - } + if (base.Contains("Bag") && base["Bag"] is IDictionary bag) + { + if (bag.TryGetValue(key, out var value) && value is T result) + { + return result; + } + } - return default(T); - } - } + return default(T); + } + } } diff --git a/src/Libraries/SmartStore.Services/News/INewsService.cs b/src/Libraries/SmartStore.Services/News/INewsService.cs index d25a8d8afa..76652ed2ce 100644 --- a/src/Libraries/SmartStore.Services/News/INewsService.cs +++ b/src/Libraries/SmartStore.Services/News/INewsService.cs @@ -6,58 +6,72 @@ namespace SmartStore.Services.News { /// - /// News service interface + /// News service interface. /// public partial interface INewsService { /// - /// Deletes a news + /// Inserts a news item. /// - /// News item - void DeleteNews(NewsItem newsItem); + /// News item. + void InsertNews(NewsItem news); /// - /// Gets a news + /// Updates the news item. /// - /// The news identifier - /// News - NewsItem GetNewsById(int newsId); + /// News item. + void UpdateNews(NewsItem news); - /// - /// Get news by identifiers - /// - /// News identifiers - /// News query - IQueryable GetNewsByIds(int[] newsIds); + /// + /// Deletes a news. + /// + /// News item. + void DeleteNews(NewsItem news); /// - /// Gets all news + /// Update news item comment totals. /// - /// Language identifier; 0 if you want to get all records - /// Store identifier; 0 if you want to get all records - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// The maximum age of returned news - /// News items - IPagedList GetAllNews(int languageId, int storeId, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null); + /// News item. + void UpdateCommentTotals(NewsItem news); /// - /// Inserts a news item + /// Gets a news. /// - /// News item - void InsertNews(NewsItem news); + /// The news identifier. + /// News item. + NewsItem GetNewsById(int newsId); /// - /// Updates the news item + /// Get news by identifiers. /// - /// News item - void UpdateNews(NewsItem news); + /// News identifiers. + /// News query. + IQueryable GetNewsByIds(int[] newsIds); /// - /// Update news item comment totals + /// Gets all news. /// - /// News item - void UpdateCommentTotals(NewsItem newsItem); + /// Store identifier. 0 to get all news items. + /// Page index. + /// Page size. + /// Filter by a language identifier. 0 to get all news items. + /// A value indicating whether to show hidden records. + /// The maximum age of returned news. + /// Search for a term or phrase in the title. + /// Search for a term or phrase in the intro. + /// Search for a term or phrase in the full description. + /// Indicates whether to load entities tracked or untracked. + /// News items. + IPagedList GetAllNews( + int storeId, + int pageIndex, + int pageSize, + int languageId = 0, + bool showHidden = false, + DateTime? maxAge = null, + string title = "", + string intro = "", + string full = "", + bool untracked = true); } } diff --git a/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs index be7b2f8284..5138eddb03 100644 --- a/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs @@ -5,15 +5,15 @@ namespace SmartStore.Services.News { - public static class NewsMessageFactoryExtensions - { - /// - /// Sends a news comment notification message to a store owner - /// - public static CreateMessageResult SendNewsCommentNotificationMessage(this IMessageFactory factory, NewsComment newsComment, int languageId = 0) - { - Guard.NotNull(newsComment, nameof(newsComment)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsCommentStoreOwner, languageId, customer: newsComment.Customer), true, newsComment); - } - } + public static class NewsMessageFactoryExtensions + { + /// + /// Sends a news comment notification message to a store owner + /// + public static CreateMessageResult SendNewsCommentNotificationMessage(this IMessageFactory factory, NewsComment newsComment, int languageId = 0) + { + Guard.NotNull(newsComment, nameof(newsComment)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsCommentStoreOwner, languageId, customer: newsComment.Customer), true, newsComment); + } + } } diff --git a/src/Libraries/SmartStore.Services/News/NewsService.cs b/src/Libraries/SmartStore.Services/News/NewsService.cs index 3d80841cb7..f4c50f8865 100644 --- a/src/Libraries/SmartStore.Services/News/NewsService.cs +++ b/src/Libraries/SmartStore.Services/News/NewsService.cs @@ -13,61 +13,125 @@ namespace SmartStore.Services.News public partial class NewsService : INewsService, IXmlSitemapPublisher { private readonly IRepository _newsItemRepository; - private readonly IRepository _storeMappingRepository; + private readonly IRepository _storeMappingRepository; - public NewsService(IRepository newsItemRepository, - IRepository storeMappingRepository) + public NewsService( + IRepository newsItemRepository, + IRepository storeMappingRepository) { _newsItemRepository = newsItemRepository; - _storeMappingRepository = storeMappingRepository; + _storeMappingRepository = storeMappingRepository; + } - this.QuerySettings = DbQuerySettings.Default; - } + public DbQuerySettings QuerySettings { get; set; } = DbQuerySettings.Default; - public DbQuerySettings QuerySettings { get; set; } + public virtual void InsertNews(NewsItem news) + { + Guard.NotNull(news, nameof(news)); + + _newsItemRepository.Insert(news); + } - public virtual void DeleteNews(NewsItem newsItem) + public virtual void UpdateNews(NewsItem news) { - if (newsItem == null) - throw new ArgumentNullException("newsItem"); + Guard.NotNull(news, nameof(news)); + + _newsItemRepository.Update(news); + } - _newsItemRepository.Delete(newsItem); + public virtual void DeleteNews(NewsItem news) + { + if (news != null) + { + _newsItemRepository.Delete(news); + } + } + + public virtual void UpdateCommentTotals(NewsItem news) + { + Guard.NotNull(news, nameof(news)); + + var approvedCommentCount = 0; + var notApprovedCommentCount = 0; + var newsComments = news.NewsComments; + + foreach (var nc in newsComments) + { + if (nc.IsApproved) + { + approvedCommentCount++; + } + else + { + notApprovedCommentCount++; + } + } + + news.ApprovedCommentCount = approvedCommentCount; + news.NotApprovedCommentCount = notApprovedCommentCount; + UpdateNews(news); } public virtual NewsItem GetNewsById(int newsId) { if (newsId == 0) + { return null; + } return _newsItemRepository.GetById(newsId); } - public virtual IQueryable GetNewsByIds(int[] newsIds) - { - if (newsIds == null || newsIds.Length == 0) - return null; + public virtual IQueryable GetNewsByIds(int[] newsIds) + { + if (newsIds == null || newsIds.Length == 0) + { + return null; + } - var query = - from x in _newsItemRepository.Table - where newsIds.Contains(x.Id) - select x; + var query = + from x in _newsItemRepository.Table + where newsIds.Contains(x.Id) + select x; - return query; - } + return query; + } - public virtual IPagedList GetAllNews(int languageId, int storeId, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null) + public virtual IPagedList GetAllNews( + int storeId, + int pageIndex, + int pageSize, + int languageId = 0, + bool showHidden = false, + DateTime? maxAge = null, + string title = "", + string intro = "", + string full = "", + bool untracked = true) { - var query = _newsItemRepository.Table; + var query = untracked ? _newsItemRepository.TableUntracked : _newsItemRepository.Table; - if (languageId > 0) - { - query = query.Where(n => languageId == n.LanguageId); - } + if (maxAge.HasValue) + { + query = query.Where(n => n.CreatedOnUtc >= maxAge.Value); + } + if (title.HasValue()) + { + query = query.Where(n => n.Title.Contains(title)); + } + if (intro.HasValue()) + { + query = query.Where(n => n.Short.Contains(intro)); + } + if (full.HasValue()) + { + query = query.Where(n => n.Full.Contains(full)); + } - if (maxAge.HasValue) - { - query = query.Where(n => n.CreatedOnUtc >= maxAge.Value); - } + if (languageId != 0) + { + query = query.Where(n => !n.LanguageId.HasValue || n.LanguageId == languageId); + } if (!showHidden) { @@ -77,66 +141,28 @@ public virtual IPagedList GetAllNews(int languageId, int storeId, int query = query.Where(n => !n.EndDateUtc.HasValue || n.EndDateUtc >= utcNow); } - query = query.OrderByDescending(n => n.CreatedOnUtc); - - //Store mapping - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = from n in query - join sm in _storeMappingRepository.Table - on new { c1 = n.Id, c2 = "NewsItem" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into n_sm - from sm in n_sm.DefaultIfEmpty() - where !n.LimitedToStores || storeId == sm.StoreId - select n; - - //only distinct items (group by ID) - query = from n in query - group n by n.Id into nGroup - orderby nGroup.Key - select nGroup.FirstOrDefault(); - - query = query.OrderByDescending(n => n.CreatedOnUtc); - } - - var news = new PagedList(query, pageIndex, pageSize); - return news; - } - - public virtual void InsertNews(NewsItem news) - { - if (news == null) - throw new ArgumentNullException("news"); - - _newsItemRepository.Insert(news); - } - - public virtual void UpdateNews(NewsItem news) - { - if (news == null) - throw new ArgumentNullException("news"); + query = query.OrderByDescending(n => n.CreatedOnUtc); - _newsItemRepository.Update(news); - } - - public virtual void UpdateCommentTotals(NewsItem newsItem) - { - if (newsItem == null) - throw new ArgumentNullException("newsItem"); - - int approvedCommentCount = 0; - int notApprovedCommentCount = 0; - var newsComments = newsItem.NewsComments; - foreach (var nc in newsComments) + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) { - if (nc.IsApproved) - approvedCommentCount++; - else - notApprovedCommentCount++; + query = from n in query + join sm in _storeMappingRepository.Table + on new { c1 = n.Id, c2 = "NewsItem" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into n_sm + from sm in n_sm.DefaultIfEmpty() + where !n.LimitedToStores || storeId == sm.StoreId + select n; + + // Only distinct items (group by ID). + query = from n in query + group n by n.Id into nGroup + orderby nGroup.Key + select nGroup.FirstOrDefault(); + + query = query.OrderByDescending(n => n.CreatedOnUtc); } - newsItem.ApprovedCommentCount = approvedCommentCount; - newsItem.NotApprovedCommentCount = notApprovedCommentCount; - UpdateNews(newsItem); + var news = new PagedList(query, pageIndex, pageSize); + return news; } #region XML Sitemap @@ -144,9 +170,11 @@ public virtual void UpdateCommentTotals(NewsItem newsItem) public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) { if (!context.LoadSetting().XmlSitemapIncludesNews || !context.LoadSetting().Enabled) + { return null; + } - var query = GetAllNews(0, context.RequestStoreId, 0, int.MaxValue).SourceQuery; + var query = GetAllNews(context.RequestStoreId, 0, int.MaxValue).SourceQuery; return new NewsXmlSitemapResult { Query = query }; } @@ -161,8 +189,9 @@ public override int GetTotalCount() public override IEnumerable Enlist() { - var topics = Query.Select(x => new { x.Id, x.CreatedOnUtc, x.LanguageId }).ToList(); - foreach (var x in topics) + var newsItems = Query.Select(x => new { x.Id, x.CreatedOnUtc, x.LanguageId }).ToList(); + + foreach (var x in newsItems) { yield return new NamedEntity { EntityName = "NewsItem", Id = x.Id, LastMod = x.CreatedOnUtc, LanguageId = x.LanguageId }; } diff --git a/src/Libraries/SmartStore.Services/Orders/AddToCartContext.cs b/src/Libraries/SmartStore.Services/Orders/AddToCartContext.cs index e747b73fa3..e409494c69 100644 --- a/src/Libraries/SmartStore.Services/Orders/AddToCartContext.cs +++ b/src/Libraries/SmartStore.Services/Orders/AddToCartContext.cs @@ -6,37 +6,31 @@ namespace SmartStore.Services.Orders { - public class AddToCartContext - { - public AddToCartContext() - { - Warnings = new List(); - CustomerEnteredPrice = decimal.Zero; - ChildItems = new List(); - } + public class AddToCartContext + { + public AddToCartContext() + { + Warnings = new List(); + CustomerEnteredPrice = decimal.Zero; + ChildItems = new List(); + } - public List Warnings { get; set; } + public List Warnings { get; set; } - public ShoppingCartItem Item { get; set; } - public List ChildItems { get; set; } - public ProductBundleItem BundleItem { get; set; } + public ShoppingCartItem Item { get; set; } + public List ChildItems { get; set; } + public ProductBundleItem BundleItem { get; set; } - public Customer Customer { get; set; } - public Product Product { get; set; } - public ShoppingCartType CartType { get; set; } - public ProductVariantQuery VariantQuery { get; set; } - public string AttributesXml { get; set; } - public decimal CustomerEnteredPrice { get; set; } - public int Quantity { get; set; } - public bool AddRequiredProducts { get; set; } - public int? StoreId { get; set; } + public Customer Customer { get; set; } + public Product Product { get; set; } + public ShoppingCartType CartType { get; set; } + public ProductVariantQuery VariantQuery { get; set; } + public string AttributesXml { get; set; } + public decimal CustomerEnteredPrice { get; set; } + public int Quantity { get; set; } + public bool AddRequiredProducts { get; set; } + public int? StoreId { get; set; } - public int BundleItemId - { - get - { - return (BundleItem == null ? 0 : BundleItem.Id); - } - } - } + public int BundleItemId => (BundleItem == null ? 0 : BundleItem.Id); + } } diff --git a/src/Libraries/SmartStore.Services/Orders/AutoUpdateOrderItemContext.cs b/src/Libraries/SmartStore.Services/Orders/AutoUpdateOrderItemContext.cs index 38528c835c..007306cb6c 100644 --- a/src/Libraries/SmartStore.Services/Orders/AutoUpdateOrderItemContext.cs +++ b/src/Libraries/SmartStore.Services/Orders/AutoUpdateOrderItemContext.cs @@ -4,101 +4,108 @@ namespace SmartStore.Services.Orders { - public class AutoUpdateOrderItemContext - { - /// - /// [IN] Whether order item is new - /// - public bool IsNewOrderItem { get; set; } - - /// - /// [IN] Order item - /// - public OrderItem OrderItem { get; set; } - - /// - /// [IN] Whether to adjust the inventory - /// - public bool AdjustInventory { get; set; } - - /// - /// [IN] Whether to update order totals if order is in pending state - /// - public bool UpdateTotals { get; set; } - - /// - /// [IN] Whether to update reward points - /// - public bool UpdateRewardPoints { get; set; } - - /// - /// [IN] Quantity old - /// - public int QuantityOld { get; set; } - - /// - /// [IN] Quantity new - /// - public int QuantityNew { get; set; } - - /// - /// [OUT] Inventory changes - /// - public AdjustInventoryResult Inventory { get; set; } - - /// - /// [OUT] Reward points old - /// - public int RewardPointsOld { get; set; } - - /// - /// [OUT] Reward points new - /// - public int RewardPointsNew { get; set; } - - public static string InfoKey { get { return "UpdateOrderItemContextInfo"; } } - - /// - /// The value to which the quantity amount has changed - /// - public int QuantityDelta - { - get - { - return QuantityNew - QuantityOld; - } - } - - public decimal QuantityChangeFactor - { - get - { - if (QuantityOld != 0) - return (decimal)QuantityNew / (decimal)QuantityOld; - - return 1.0M; - } - } - - public string ToString(ILocalizationService localizationService) - { - if (Inventory == null && RewardPointsOld == 0 && RewardPointsNew == 0) - return ""; - - string stockOld = null; - string stockNew = null; - - if (Inventory != null && Inventory.HasClearStockQuantityResult) - { - stockOld = Inventory.StockQuantityOld.ToString(); - stockNew = Inventory.StockQuantityNew.ToString(); - } - - string result = localizationService.GetResource("Admin.Orders.OrderItem.Update.Info").FormatWith( - stockOld.NaIfEmpty(), stockNew.NaIfEmpty(), RewardPointsOld, RewardPointsNew - ); - - return result; - } - } + /// + /// Contains data that is used to recalculated details of an order. + /// + public class AutoUpdateOrderItemContext + { + /// + /// [IN] Whether order item is new + /// + public bool IsNewOrderItem { get; set; } + + /// + /// [IN] Order item + /// + public OrderItem OrderItem { get; set; } + + /// + /// [IN] Whether to adjust the inventory + /// + public bool AdjustInventory { get; set; } + + /// + /// [IN] Whether to update order totals if order is in pending state + /// + public bool UpdateTotals { get; set; } + + /// + /// [IN] Whether to update reward points + /// + public bool UpdateRewardPoints { get; set; } + + /// + /// [IN] Quantity old + /// + public int QuantityOld { get; set; } + + /// + /// [IN] Quantity new + /// + public int QuantityNew { get; set; } + + /// + /// [IN] Old price incl. tax. + /// + public decimal? PriceInclTaxOld { get; set; } + + /// + /// [IN] Old price excl. tax. + /// + public decimal? PriceExclTaxOld { get; set; } + + /// + /// [OUT] Inventory changes + /// + public AdjustInventoryResult Inventory { get; set; } + + /// + /// [OUT] Reward points old + /// + public int RewardPointsOld { get; set; } + + /// + /// [OUT] Reward points new + /// + public int RewardPointsNew { get; set; } + + public static string InfoKey => "UpdateOrderItemContextInfo"; + + /// + /// The value to which the quantity amount has changed + /// + public int QuantityDelta => QuantityNew - QuantityOld; + + public decimal QuantityChangeFactor + { + get + { + if (QuantityOld != 0) + return (decimal)QuantityNew / (decimal)QuantityOld; + + return 1.0M; + } + } + + public string ToString(ILocalizationService localizationService) + { + if (Inventory == null && RewardPointsOld == 0 && RewardPointsNew == 0) + return ""; + + string stockOld = null; + string stockNew = null; + + if (Inventory != null && Inventory.HasClearStockQuantityResult) + { + stockOld = Inventory.StockQuantityOld.ToString(); + stockNew = Inventory.StockQuantityNew.ToString(); + } + + string result = localizationService.GetResource("Admin.Orders.OrderItem.Update.Info").FormatWith( + stockOld.NaIfEmpty(), stockNew.NaIfEmpty(), RewardPointsOld, RewardPointsNew + ); + + return result; + } + } } diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeExtensions.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeExtensions.cs index ceda7c8b9d..1529de1509 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeExtensions.cs @@ -26,7 +26,7 @@ public static bool ShouldHaveValues(this CheckoutAttribute checkoutAttribute) checkoutAttribute.AttributeControlType == AttributeControlType.Datepicker || checkoutAttribute.AttributeControlType == AttributeControlType.FileUpload) return false; - + //other attribute controle types support values return true; } diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeFormatter.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeFormatter.cs index 377cace034..84fd3b046f 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeFormatter.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeFormatter.cs @@ -68,9 +68,9 @@ public string FormatAttributes(string attributes) /// A value indicating whether to HTML hyperink tags could be rendered (if required) /// Attributes public string FormatAttributes(string attributes, - Customer customer, - string serapator = "
", - bool htmlEncode = true, + Customer customer, + string serapator = "
", + bool htmlEncode = true, bool renderPrices = true, bool allowHyperlinks = true) { @@ -101,8 +101,8 @@ public string FormatAttributes(string attributes, if (htmlEncode) attributeName = HttpUtility.HtmlEncode(attributeName); - caAttribute = string.Format("{0}: {1}", attributeName, - HtmlUtils.ConvertPlainTextToHtml(valueStr.EmptyNull().Replace(":", "").HtmlEncode())); + caAttribute = string.Format("{0}: {1}", attributeName, + HtmlUtils.ConvertPlainTextToHtml(valueStr.EmptyNull().Replace(":", "").HtmlEncode())); //we never encode multiline textbox input } @@ -184,6 +184,6 @@ public string FormatAttributes(string attributes, return result.ToString(); } - + } } diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs index ed51878fd0..4393805397 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs @@ -91,39 +91,39 @@ public IList ParseValues(string attributes, int checkoutAttributeId) { var selectedCheckoutAttributeValues = new List(); - if (attributes.HasValue()) - { - try - { - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(attributes); - - var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/CheckoutAttribute"); - foreach (XmlNode node1 in nodeList1) - { - if (node1.Attributes != null && node1.Attributes["ID"] != null) - { - var str1 = node1.Attributes["ID"].InnerText.Trim(); - if (int.TryParse(str1, out var id)) - { - if (id == checkoutAttributeId) - { - var nodeList2 = node1.SelectNodes(@"CheckoutAttributeValue/Value"); - foreach (XmlNode node2 in nodeList2) - { - string value = node2.InnerText.Trim(); - selectedCheckoutAttributeValues.Add(value); - } - } - } - } - } - } - catch (Exception ex) - { - Debug.Write(ex.ToString()); - } - } + if (attributes.HasValue()) + { + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(attributes); + + var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/CheckoutAttribute"); + foreach (XmlNode node1 in nodeList1) + { + if (node1.Attributes != null && node1.Attributes["ID"] != null) + { + var str1 = node1.Attributes["ID"].InnerText.Trim(); + if (int.TryParse(str1, out var id)) + { + if (id == checkoutAttributeId) + { + var nodeList2 = node1.SelectNodes(@"CheckoutAttributeValue/Value"); + foreach (XmlNode node2 in nodeList2) + { + string value = node2.InnerText.Trim(); + selectedCheckoutAttributeValues.Add(value); + } + } + } + } + } + } + catch (Exception ex) + { + Debug.Write(ex.ToString()); + } + } return selectedCheckoutAttributeValues; } diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs index 63a0d65b12..afa40cda5f 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs @@ -9,32 +9,32 @@ namespace SmartStore.Services.Orders { - public partial class CheckoutAttributeService : ICheckoutAttributeService + public partial class CheckoutAttributeService : ICheckoutAttributeService { private readonly IRepository _checkoutAttributeRepository; private readonly IRepository _checkoutAttributeValueRepository; - private readonly IRepository _storeMappingRepository; - private readonly IEventPublisher _eventPublisher; + private readonly IRepository _storeMappingRepository; + private readonly IEventPublisher _eventPublisher; public CheckoutAttributeService( IRepository checkoutAttributeRepository, IRepository checkoutAttributeValueRepository, - IRepository storeMappingRepository, - IEventPublisher eventPublisher) + IRepository storeMappingRepository, + IEventPublisher eventPublisher) { _checkoutAttributeRepository = checkoutAttributeRepository; _checkoutAttributeValueRepository = checkoutAttributeValueRepository; - _storeMappingRepository = storeMappingRepository; + _storeMappingRepository = storeMappingRepository; _eventPublisher = eventPublisher; - this.QuerySettings = DbQuerySettings.Default; - } + this.QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } - #region Checkout attributes + #region Checkout attributes - public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) + public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) { if (checkoutAttribute == null) throw new ArgumentNullException("checkoutAttribute"); @@ -42,39 +42,39 @@ public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) _checkoutAttributeRepository.Delete(checkoutAttribute); } - public virtual IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false) - { - var query = _checkoutAttributeRepository.Table; - - if (!showHidden) - query = query.Where(x => x.IsActive); - - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = - from x in query - join sm in _storeMappingRepository.Table on new { c1 = x.Id, c2 = "CheckoutAttribute" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm - from sm in x_sm.DefaultIfEmpty() - where !x.LimitedToStores || storeId == sm.StoreId - select x; - - query = - from x in query - group x by x.Id into grp - orderby grp.Key - select grp.FirstOrDefault(); - } - - query = query.OrderBy(x => x.DisplayOrder); - - return query; - } + public virtual IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false) + { + var query = _checkoutAttributeRepository.Table; + + if (!showHidden) + query = query.Where(x => x.IsActive); + + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = + from x in query + join sm in _storeMappingRepository.Table on new { c1 = x.Id, c2 = "CheckoutAttribute" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm + from sm in x_sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == sm.StoreId + select x; + + query = + from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + } + + query = query.OrderBy(x => x.DisplayOrder); + + return query; + } - public virtual IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false) + public virtual IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false) { - var query = GetCheckoutAttributes(storeId, showHidden); - return query.ToListCached("db.checkoutattrs.{0}.{1}".FormatInvariant(storeId, showHidden)); - } + var query = GetCheckoutAttributes(storeId, showHidden); + return query.ToListCached("db.checkoutattrs.{0}.{1}".FormatInvariant(storeId, showHidden)); + } /// /// Gets a checkout attribute @@ -86,8 +86,8 @@ public virtual CheckoutAttribute GetCheckoutAttributeById(int checkoutAttributeI if (checkoutAttributeId == 0) return null; - return _checkoutAttributeRepository.GetByIdCached(checkoutAttributeId, "db.checkoutattr.id-" + checkoutAttributeId); - } + return _checkoutAttributeRepository.GetByIdCached(checkoutAttributeId, "db.checkoutattr.id-" + checkoutAttributeId); + } public virtual void InsertCheckoutAttribute(CheckoutAttribute checkoutAttribute) { @@ -119,21 +119,21 @@ public virtual void DeleteCheckoutAttributeValue(CheckoutAttributeValue checkout public virtual IList GetCheckoutAttributeValues(int checkoutAttributeId) { - var query = from cav in _checkoutAttributeValueRepository.Table - orderby cav.DisplayOrder - where cav.CheckoutAttributeId == checkoutAttributeId - select cav; - var checkoutAttributeValues = query.ToListCached(); - return checkoutAttributeValues; - } - + var query = from cav in _checkoutAttributeValueRepository.Table + orderby cav.DisplayOrder + where cav.CheckoutAttributeId == checkoutAttributeId + select cav; + var checkoutAttributeValues = query.ToListCached(); + return checkoutAttributeValues; + } + public virtual CheckoutAttributeValue GetCheckoutAttributeValueById(int checkoutAttributeValueId) { if (checkoutAttributeValueId == 0) return null; - return _checkoutAttributeValueRepository.GetByIdCached(checkoutAttributeValueId, "db.checkoutattrval.id-" + checkoutAttributeValueId); - } + return _checkoutAttributeValueRepository.GetByIdCached(checkoutAttributeValueId, "db.checkoutattrval.id-" + checkoutAttributeValueId); + } public virtual void InsertCheckoutAttributeValue(CheckoutAttributeValue checkoutAttributeValue) { @@ -150,7 +150,7 @@ public virtual void UpdateCheckoutAttributeValue(CheckoutAttributeValue checkout _checkoutAttributeValueRepository.Update(checkoutAttributeValue); } - + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutState.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutState.cs index a2da7e33e2..103405a4a7 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutState.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutState.cs @@ -3,35 +3,35 @@ namespace SmartStore.Services.Orders { - [Serializable] - public partial class CheckoutState - { - public CheckoutState() - { - CustomProperties = new Dictionary(); - PaymentData = new Dictionary(); - } + [Serializable] + public partial class CheckoutState + { + public CheckoutState() + { + CustomProperties = new Dictionary(); + PaymentData = new Dictionary(); + } - public static string CheckoutStateSessionKey { get { return "SmCheckoutState"; } } + public static string CheckoutStateSessionKey => "SmCheckoutState"; - /// - /// The payment summary as displayed on the checkout confirmation page - /// - public string PaymentSummary { get; set; } + /// + /// The payment summary as displayed on the checkout confirmation page + /// + public string PaymentSummary { get; set; } - /// - /// Indicates whether the payment method selection page was skipped - /// - public bool IsPaymentSelectionSkipped { get; set; } + /// + /// Indicates whether the payment method selection page was skipped + /// + public bool IsPaymentSelectionSkipped { get; set; } - /// - /// Use this dictionary for any custom data required along checkout flow - /// - public IDictionary CustomProperties { get; set; } + /// + /// Use this dictionary for any custom data required along checkout flow + /// + public IDictionary CustomProperties { get; set; } - /// - /// Payment data entered on payment method selection page - /// - public IDictionary PaymentData { get; set; } - } + /// + /// Payment data entered on payment method selection page + /// + public IDictionary PaymentData { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Orders/EventPublisherExtensions.cs b/src/Libraries/SmartStore.Services/Orders/EventPublisherExtensions.cs index 00bab7dad1..8e5bd4f491 100644 --- a/src/Libraries/SmartStore.Services/Orders/EventPublisherExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/EventPublisherExtensions.cs @@ -26,28 +26,28 @@ public static void PublishOrderPlaced(this IEventPublisher eventPublisher, Order eventPublisher.Publish(new OrderPlacedEvent(order)); } - /// - /// Publishes the order updated event. - /// - /// The event publisher. - /// The order. - public static void PublishOrderUpdated(this IEventPublisher eventPublisher, Order order) - { - if (order != null) - eventPublisher.Publish(new OrderUpdatedEvent(order)); - } + /// + /// Publishes the order updated event. + /// + /// The event publisher. + /// The order. + public static void PublishOrderUpdated(this IEventPublisher eventPublisher, Order order) + { + if (order != null) + eventPublisher.Publish(new OrderUpdatedEvent(order)); + } - /// - /// Publishes the migrate shopping cart event. - /// - /// The event publisher. - /// The source customer entity. - /// The destination customer entity. - /// Store identifier. - public static void PublishMigrateShoppingCart(this IEventPublisher eventPublisher, Customer fromCustomer, Customer toCustomer, int storeId) - { - if (fromCustomer != null && toCustomer != null) - eventPublisher.Publish(new MigrateShoppingCartEvent(fromCustomer, toCustomer, storeId)); - } + /// + /// Publishes the migrate shopping cart event. + /// + /// The event publisher. + /// The source customer entity. + /// The destination customer entity. + /// Store identifier. + public static void PublishMigrateShoppingCart(this IEventPublisher eventPublisher, Customer fromCustomer, Customer toCustomer, int storeId) + { + if (fromCustomer != null && toCustomer != null) + eventPublisher.Publish(new MigrateShoppingCartEvent(fromCustomer, toCustomer, storeId)); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs b/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs index d89896397d..752dfc3a22 100644 --- a/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs +++ b/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs @@ -15,7 +15,7 @@ namespace SmartStore.Services.Orders public partial class GiftCardService : IGiftCardService { #region Fields - + private readonly IRepository _giftCardRepository; private readonly IEventPublisher _eventPublisher; @@ -35,7 +35,7 @@ public GiftCardService(IRepository giftCardRepository, IEventPublisher } #endregion - + #region Methods /// @@ -74,7 +74,7 @@ public virtual GiftCard GetGiftCardById(int giftCardId) /// Value indicating whether gift card is activated; null to load all records /// Gift card coupon code; null or string.empty to load all records /// Gift cards - public virtual IList GetAllGiftCards(int? purchasedWithOrderId, + public virtual IList GetAllGiftCards(int? purchasedWithOrderId, DateTime? startTime = null, DateTime? endTime = null, bool? isGiftCardActivated = null, string giftCardCouponCode = "") { diff --git a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeFormatter.cs b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeFormatter.cs index 7394315ccd..86735a79ba 100644 --- a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeFormatter.cs +++ b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeFormatter.cs @@ -27,8 +27,8 @@ public partial interface ICheckoutAttributeFormatter /// A value indicating whether to HTML hyperink tags could be rendered (if required) /// Attributes string FormatAttributes(string attributes, - Customer customer, - string serapator = "
", + Customer customer, + string serapator = "
", bool htmlEncode = true, bool renderPrices = true, bool allowHyperlinks = true); diff --git a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs index 9a520ca28b..e7c70c9c93 100644 --- a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs @@ -4,10 +4,10 @@ namespace SmartStore.Services.Orders { - /// - /// Checkout attribute service - /// - public partial interface ICheckoutAttributeService + /// + /// Checkout attribute service + /// + public partial interface ICheckoutAttributeService { #region Checkout attributes @@ -17,21 +17,21 @@ public partial interface ICheckoutAttributeService /// Checkout attribute void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute); - /// - /// Gets checkout attributes - /// - /// Whether to filter result by store identifier - /// A value indicating whether to show hidden records - /// Checkout attributes query - IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false); + /// + /// Gets checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attributes query + IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false); - /// - /// Gets all checkout attributes - /// - /// Whether to filter result by store identifier - /// A value indicating whether to show hidden records - /// Checkout attribute collection - IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false); + /// + /// Gets all checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attribute collection + IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false); /// /// Gets a checkout attribute @@ -68,7 +68,7 @@ public partial interface ICheckoutAttributeService /// The checkout attribute identifier /// Checkout attribute value collection IList GetCheckoutAttributeValues(int checkoutAttributeId); - + /// /// Gets a checkout attribute value /// @@ -87,7 +87,7 @@ public partial interface ICheckoutAttributeService /// /// Checkout attribute value void UpdateCheckoutAttributeValue(CheckoutAttributeValue checkoutAttributeValue); - + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Orders/IGiftCardService.cs b/src/Libraries/SmartStore.Services/Orders/IGiftCardService.cs index 6c7255852b..fa819a1e39 100644 --- a/src/Libraries/SmartStore.Services/Orders/IGiftCardService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IGiftCardService.cs @@ -53,12 +53,12 @@ IList GetAllGiftCards(int? purchasedWithOrderId, DateTime? startTime = /// Purchased with order item identifier /// Gift card entries IList GetGiftCardsByPurchasedWithOrderItemId(int purchasedWithOrderItemId); - + /// /// Get active gift cards that are applied by a customer /// /// Customer - /// Store identifier + /// Store identifier /// Active gift cards IList GetActiveGiftCardsAppliedByCustomer(Customer customer, int storeId); diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs index ea9d733dc5..0801067fd1 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs @@ -59,7 +59,6 @@ IList GetOrderPlacementWarnings( /// The order void DeleteOrder(Order order); - /// /// Process next recurring psayment /// @@ -80,17 +79,13 @@ IList GetOrderPlacementWarnings( /// value indicating whether a customer can cancel recurring payment bool CanCancelRecurringPayment(Customer customerToValidate, RecurringPayment recurringPayment); - - - - /// /// Send a shipment /// /// Shipment /// True to notify customer void Ship(Shipment shipment, bool notifyCustomer); - + /// /// Marks a shipment as delivered /// @@ -98,8 +93,6 @@ IList GetOrderPlacementWarnings( /// True to notify customer void Deliver(Shipment shipment, bool notifyCustomer); - - /// /// Gets a value indicating whether cancel is allowed /// @@ -114,12 +107,11 @@ IList GetOrderPlacementWarnings( /// True to notify customer void CancelOrder(Order order, bool notifyCustomer); - /// - /// Auto update order details - /// - /// Context parameters - void AutoUpdateOrderDetails(AutoUpdateOrderItemContext context); - + /// + /// Auto update order details + /// + /// Context parameters + void AutoUpdateOrderDetails(AutoUpdateOrderItemContext context); /// /// Gets a value indicating whether order can be marked as authorized @@ -134,20 +126,18 @@ IList GetOrderPlacementWarnings( /// Order void MarkAsAuthorized(Order order); + /// + /// Gets a value indicating whether the order can be marked as completed + /// + /// Order + /// A value indicating whether the order can be marked as completed + bool CanCompleteOrder(Order order); - /// - /// Gets a value indicating whether the order can be marked as completed - /// - /// Order - /// A value indicating whether the order can be marked as completed - bool CanCompleteOrder(Order order); - - /// - /// Marks the order as completed - /// - /// Order - void CompleteOrder(Order order); - + /// + /// Marks the order as completed + /// + /// Order + void CompleteOrder(Order order); /// /// Gets a value indicating whether capture from admin panel is allowed @@ -176,8 +166,6 @@ IList GetOrderPlacementWarnings( /// Order void MarkOrderAsPaid(Order order); - - /// /// Gets a value indicating whether refund from admin panel is allowed /// @@ -236,8 +224,6 @@ IList GetOrderPlacementWarnings( /// Amount to refund void PartiallyRefundOffline(Order order, decimal amountToRefund); - - /// /// Gets a value indicating whether void from admin panel is allowed /// @@ -265,15 +251,12 @@ IList GetOrderPlacementWarnings( /// Order void VoidOffline(Order order); - - - /// /// Place order items in current user shopping cart. /// /// The order void ReOrder(Order order); - + /// /// Check whether return request is allowed /// @@ -281,30 +264,33 @@ IList GetOrderPlacementWarnings( /// Result bool IsReturnRequestAllowed(Order order); - /// - /// Valdiate minimum order sub-total amount + /// Valdiate minimum order amount. + /// Gets min order amount from customer role. + /// When no min order amount is defined in customer role, default order settings are used as fallback if present. /// /// Shopping cart - /// true - OK; false - minimum order sub-total amount is not reached - bool ValidateMinOrderSubtotalAmount(IList cart); + /// true - OK; false - minimum order amount is not reached + (bool valid, decimal orderTotalMinimum) IsAboveOrderTotalMinimum(IList cart, int[] customerRoleIds); /// - /// Valdiate minimum order total amount + /// Valdiate maximum order amount. + /// Gets max order amount from customer role. + /// When no max order amount is defined in customer role, default order settings are used as fallback if present. /// - /// Shopping cart - /// true - OK; false - minimum order total amount is not reached - bool ValidateMinOrderTotalAmount(IList cart); - - /// - /// Adds a shipment to an order. - /// - /// Order. - /// Tracking number. + /// Shopping cart, customer role ids + /// true - OK; false - maximum order amount is exceeded + (bool valid, decimal orderTotalMaximum) IsBelowOrderTotalMaximum(IList cart, int[] customerRoleIds); + + /// + /// Adds a shipment to an order. + /// + /// Order. + /// Tracking number. /// Tracking URL. - /// Quantities by order item identifiers. null to use the remaining total number of products for each order item. - /// New shipment, null if no shipment was added. - Shipment AddShipment( + /// Quantities by order item identifiers. null to use the remaining total number of products for each order item. + /// New shipment, null if no shipment was added. + Shipment AddShipment( Order order, string trackingNumber, string trackingUrl, diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderReportService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderReportService.cs index 0f176c9efc..4894bf6549 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderReportService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderReportService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; @@ -10,6 +9,14 @@ namespace SmartStore.Services.Orders { + public enum ReportSorting + { + ByQuantityAsc = 0, + ByQuantityDesc, + ByAmountAsc, + ByAmountDesc + } + /// /// Order report service interface /// @@ -47,34 +54,36 @@ OrderAverageReportLine GetOrderAverageReportLine(int storeId, /// Get order average report. /// /// Store identifier - /// Order status + /// Order status /// Order average report. - OrderAverageReportLineSummary OrderAverageReport(int storeId, OrderStatus os); + OrderAverageReportLineSummary OrderAverageReport(int storeId, OrderStatus orderStatus); /// - /// Get best sellers report + /// Get best sellers report. /// /// Store identifier /// Order start time; null to load all /// Order end time; null to load all - /// Order status; null to load all records - /// Order payment status; null to load all records - /// Shipping status; null to load all records + /// Order status; null to load all records + /// Order payment status; null to load all records + /// Shipping status; null to load all records /// Billing country identifier; 0 to load all records - /// Records to return - /// 1 - order by quantity, 2 - order by total amount + /// Page index. + /// Page size. + /// Sorting of report items. /// A value indicating whether to show hidden records - /// Result - IList BestSellersReport( + /// Best selling products. + IPagedList BestSellersReport( int storeId, DateTime? startTime, DateTime? endTime, - OrderStatus? os, - PaymentStatus? ps, - ShippingStatus? ss, + OrderStatus? orderStatus, + PaymentStatus? paymentStatus, + ShippingStatus? shippingStatus, int billingCountryId = 0, - int recordsToReturn = 5, - int orderBy = 1, + int pageIndex = 0, + int pageSize = int.MaxValue, + ReportSorting sorting = ReportSorting.ByQuantityDesc, bool showHidden = false); /// diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs index e0d8dddfc4..51e4d43746 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs @@ -48,21 +48,21 @@ public partial interface IOrderService /// Order Order GetOrderByGuid(Guid orderGuid); - /// - /// Get order by payment authorization data - /// - /// System name of the payment method - /// Authorization transaction Id - /// Order entity - Order GetOrderByPaymentAuthorization(string paymentMethodSystemName, string authorizationTransactionId); - - /// - /// Get order by payment capture data - /// - /// System name of the payment method - /// Capture transaction Id - /// Order entity - Order GetOrderByPaymentCapture(string paymentMethodSystemName, string captureTransactionId); + /// + /// Get order by payment authorization data + /// + /// System name of the payment method + /// Authorization transaction Id + /// Order entity + Order GetOrderByPaymentAuthorization(string paymentMethodSystemName, string authorizationTransactionId); + + /// + /// Get order by payment capture data + /// + /// System name of the payment method + /// Capture transaction Id + /// Order entity + Order GetOrderByPaymentCapture(string paymentMethodSystemName, string captureTransactionId); /// /// Deletes an order @@ -70,32 +70,32 @@ public partial interface IOrderService /// The order void DeleteOrder(Order order); - /// - /// Get orders - /// - /// Store identifier; null to load all orders - /// Customer identifier; null to load all orders - /// Order start time; null to load all orders - /// Order end time; null to load all orders - /// Filter by order status - /// Filter by payment status - /// Filter by shipping status - /// Billing email. Leave empty to load all records. - /// Filter by order number - /// Billing name. Leave empty to load all records. + /// + /// Get orders + /// + /// Store identifier; null to load all orders + /// Customer identifier; null to load all orders + /// Order start time; null to load all orders + /// Order end time; null to load all orders + /// Filter by order status + /// Filter by payment status + /// Filter by shipping status + /// Billing email. Leave empty to load all records. + /// Filter by order number + /// Billing name. Leave empty to load all records. /// Filter by payment method system names. - /// Order query - IQueryable GetOrders( - int storeId, - int customerId, - DateTime? startTime, - DateTime? endTime, - int[] orderStatusIds, - int[] paymentStatusIds, - int[] shippingStatusIds, - string billingEmail, - string orderNumber, - string billingName = null, + /// Order query + IQueryable GetOrders( + int storeId, + int customerId, + DateTime? startTime, + DateTime? endTime, + int[] orderStatusIds, + int[] paymentStatusIds, + int[] shippingStatusIds, + string billingEmail, + string orderNumber, + string billingName = null, string[] paymentMethods = null); /// @@ -117,8 +117,8 @@ IQueryable GetOrders( /// Filter by payment method system names. /// Order collection IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, DateTime? endTime, - int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, - string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null, + int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, + string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null, string[] paymentMethods = null); /// @@ -163,24 +163,24 @@ IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, /// Order Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName); - /// - /// Shortcut to add an order - /// - /// Order - /// Order note - /// Whether to display the note to the customer - void AddOrderNote(Order order, string note, bool displayToCustomer = false); + /// + /// Shortcut to add an order + /// + /// Order + /// Order note + /// Whether to display the note to the customer + void AddOrderNote(Order order, string note, bool displayToCustomer = false); - #endregion + #endregion - #region Orders items + #region Orders items - /// - /// Gets an order item - /// - /// Order item identifier - /// Order item - OrderItem GetOrderItemById(int orderItemId); + /// + /// Gets an order item + /// + /// Order item identifier + /// Order item + OrderItem GetOrderItemById(int orderItemId); /// /// Gets an order item @@ -206,12 +206,12 @@ IList GetAllOrderItems(int? orderId, OrderStatus? os, PaymentStatus? ps, ShippingStatus? ss, bool loadDownloableProductsOnly = false); - /// - /// Get order items by order identifiers - /// - /// Order identifiers - /// Order items - Multimap GetOrderItemsByOrderIds(int[] orderIds); + /// + /// Get order items by order identifiers + /// + /// Order identifiers + /// Order items + Multimap GetOrderItemsByOrderIds(int[] orderIds); /// /// Delete an order item @@ -252,13 +252,21 @@ IList GetAllOrderItems(int? orderId, /// Search recurring payments /// /// The customer identifier; 0 to load all records - /// The store identifier; 0 to load all records + /// The store identifier; 0 to load all records /// The initial order identifier; 0 to load all records /// Initial order status identifier; null to load all records /// A value indicating whether to show hidden records + /// Page index + /// Page size /// Recurring payment collection - IList SearchRecurringPayments(int storeId, - int customerId, int initialOrderId, OrderStatus? initialOrderStatus, bool showHidden = false); + IPagedList SearchRecurringPayments( + int storeId, + int customerId, + int initialOrderId, + OrderStatus? initialOrderStatus, + bool showHidden = false, + int pageIndex = 0, + int pageSize = int.MaxValue); #endregion @@ -276,20 +284,20 @@ IList SearchRecurringPayments(int storeId, /// Return request identifier /// Return request ReturnRequest GetReturnRequestById(int returnRequestId); - + /// /// Search return requests /// - /// Store identifier; 0 to load all entries + /// Store identifier; 0 to load all entries /// Customer identifier; null to load all entries /// Order item identifier; null to load all entries /// Return request status; null to load all entries - /// Page index - /// Page size - /// Return Request Id + /// Page index + /// Page size + /// Return Request Id /// Return requests - IPagedList SearchReturnRequests(int storeId, int customerId, int orderItemId, ReturnRequestStatus? rs, int pageIndex, int pageSize, int id = 0); - + IPagedList SearchReturnRequests(int storeId, int customerId, int orderItemId, ReturnRequestStatus? rs, int pageIndex, int pageSize, int id = 0); + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs index 3f5ee14bb5..627723b438 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs @@ -72,7 +72,7 @@ decimal GetOrderSubtotalDiscount(Customer customer, /// Applied discount /// Adjusted shipping rate decimal AdjustShippingRate(decimal shippingRate, IList cart, - ShippingOption shippingOption, IList shippingMethods, out Discount appliedDiscount); + ShippingOption shippingOption, IList shippingMethods, out Discount appliedDiscount); /// /// Gets shopping cart additional shipping charge @@ -114,14 +114,14 @@ decimal AdjustShippingRate(decimal shippingRate, IList cart, bool includingTax, out decimal taxRate, out Discount appliedDiscount); - /// - /// Gets a shipping discount - /// - /// Customer - /// Shipping total - /// Applied discount - /// Shipping discount - decimal GetShippingDiscount(Customer customer, decimal shippingTotal, out Discount appliedDiscount); + /// + /// Gets a shipping discount + /// + /// Customer + /// Shipping total + /// Applied discount + /// Shipping discount + decimal GetShippingDiscount(Customer customer, decimal shippingTotal, out Discount appliedDiscount); @@ -161,7 +161,7 @@ ShoppingCartTotal GetShoppingCartTotal( IList cart, bool ignoreRewardPoints = false, bool usePaymentMethodAdditionalFee = true, - bool ignoreCreditBalance = false); + bool ignoreCreditBalance = false); /// diff --git a/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs index 65db9f38a9..924a63437b 100644 --- a/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs @@ -12,42 +12,42 @@ namespace SmartStore.Services.Orders /// public partial interface IShoppingCartService { - /// - /// Gets the shopping cart items count - /// - /// Customer. Cannot be null. - /// Type of cart to get items count for - /// Store id - /// Sum of all item quantities - int CountItems(Customer customer, ShoppingCartType cartType, int? storeId = null); + /// + /// Gets the shopping cart items count + /// + /// Customer. Cannot be null. + /// Type of cart to get items count for + /// Store id + /// Sum of all item quantities + int CountItems(Customer customer, ShoppingCartType cartType, int? storeId = null); - /// - /// Gets the shopping cart items - /// - /// Customer. Cannot be null. - /// Type of cart to get items for - /// Store id - /// All cart items - List GetCartItems(Customer customer, ShoppingCartType cartType, int? storeId = null); + /// + /// Gets the shopping cart items + /// + /// Customer. Cannot be null. + /// Type of cart to get items for + /// Store id + /// All cart items + List GetCartItems(Customer customer, ShoppingCartType cartType, int? storeId = null); - /// - /// Delete shopping cart item - /// - /// Shopping cart item - /// A value indicating whether to reset checkout data - /// A value indicating whether to ensure that only active checkout attributes are attached to the current customer - /// A value indicating whether to delete child cart items - void DeleteShoppingCartItem( - ShoppingCartItem shoppingCartItem, - bool resetCheckoutData = true, - bool ensureOnlyActiveCheckoutAttributes = false, - bool deleteChildCartItems = true); + /// + /// Delete shopping cart item + /// + /// Shopping cart item + /// A value indicating whether to reset checkout data + /// A value indicating whether to ensure that only active checkout attributes are attached to the current customer + /// A value indicating whether to delete child cart items + void DeleteShoppingCartItem( + ShoppingCartItem shoppingCartItem, + bool resetCheckoutData = true, + bool ensureOnlyActiveCheckoutAttributes = false, + bool deleteChildCartItems = true); - void DeleteShoppingCartItem( - int shoppingCartItemId, - bool resetCheckoutData = true, - bool ensureOnlyActiveCheckoutAttributes = false, - bool deleteChildCartItems = true); + void DeleteShoppingCartItem( + int shoppingCartItemId, + bool resetCheckoutData = true, + bool ensureOnlyActiveCheckoutAttributes = false, + bool deleteChildCartItems = true); /// /// Deletes expired shopping cart items @@ -67,11 +67,11 @@ void DeleteShoppingCartItem( /// Automatically add required products if enabled /// Warnings IList GetRequiredProductWarnings( - Customer customer, - ShoppingCartType shoppingCartType, - Product product, - int storeId, - bool automaticallyAddRequiredProductsIfEnabled); + Customer customer, + ShoppingCartType shoppingCartType, + Product product, + int storeId, + bool automaticallyAddRequiredProductsIfEnabled); /// /// Validates a product for standard properties @@ -84,32 +84,33 @@ IList GetRequiredProductWarnings( /// Quantity /// Warnings IList GetStandardWarnings( - Customer customer, - ShoppingCartType shoppingCartType, - Product product, - string selectedAttributes, - decimal customerEnteredPrice, - int quantity); + Customer customer, + ShoppingCartType shoppingCartType, + Product product, + string selectedAttributes, + decimal customerEnteredPrice, + int quantity, + int storeId = 0); - /// - /// Validates shopping cart item attributes - /// - /// The customer - /// Shopping cart type - /// Product - /// Selected attributes - /// The product variant attribute combination instance (reduces database roundtrips) - /// Quantity - /// Product bundle item - /// Warnings - IList GetShoppingCartItemAttributeWarnings( - Customer customer, - ShoppingCartType shoppingCartType, - Product product, - string selectedAttributes, - int quantity = 1, - ProductBundleItem bundleItem = null, - ProductVariantAttributeCombination combination = null); + /// + /// Validates shopping cart item attributes + /// + /// The customer + /// Shopping cart type + /// Product + /// Selected attributes + /// The product variant attribute combination instance (reduces database roundtrips) + /// Quantity + /// Product bundle item + /// Warnings + IList GetShoppingCartItemAttributeWarnings( + Customer customer, + ShoppingCartType shoppingCartType, + Product product, + string selectedAttributes, + int quantity = 1, + ProductBundleItem bundleItem = null, + ProductVariantAttributeCombination combination = null); /// /// Validates shopping cart item (gift card) @@ -120,14 +121,14 @@ IList GetShoppingCartItemAttributeWarnings( /// Warnings IList GetShoppingCartItemGiftCardWarnings(ShoppingCartType shoppingCartType, Product product, string selectedAttributes); - /// - /// Validates bundle items - /// - /// Shopping cart type - /// Product bundle item - /// Warnings - IList GetBundleItemWarnings(ProductBundleItem bundleItem); - IList GetBundleItemWarnings(IList cartItems); + /// + /// Validates bundle items + /// + /// Shopping cart type + /// Product bundle item + /// Warnings + IList GetBundleItemWarnings(ProductBundleItem bundleItem); + IList GetBundleItemWarnings(IList cartItems); /// /// Validates shopping cart item @@ -149,21 +150,21 @@ IList GetShoppingCartItemAttributeWarnings( /// Child cart items to validate bundle items /// Warnings IList GetShoppingCartItemWarnings( - Customer customer, - ShoppingCartType shoppingCartType, - Product product, - int storeId, - string selectedAttributes, - decimal customerEnteredPrice, - int quantity, - bool automaticallyAddRequiredProductsIfEnabled, - bool getStandardWarnings = true, - bool getAttributesWarnings = true, - bool getGiftCardWarnings = true, - bool getRequiredProductWarnings = true, - bool getBundleWarnings = true, - ProductBundleItem bundleItem = null, - IList childItems = null); + Customer customer, + ShoppingCartType shoppingCartType, + Product product, + int storeId, + string selectedAttributes, + decimal customerEnteredPrice, + int quantity, + bool automaticallyAddRequiredProductsIfEnabled, + bool getStandardWarnings = true, + bool getAttributesWarnings = true, + bool getGiftCardWarnings = true, + bool getRequiredProductWarnings = true, + bool getBundleWarnings = true, + ProductBundleItem bundleItem = null, + IList childItems = null); /// /// Validates whether this shopping cart is valid @@ -184,47 +185,47 @@ IList GetShoppingCartItemWarnings( /// Price entered by a customer /// Found shopping cart item OrganizedShoppingCartItem FindShoppingCartItemInTheCart( - IList shoppingCart, + IList shoppingCart, ShoppingCartType shoppingCartType, - Product product, + Product product, string selectedAttributes = "", decimal customerEnteredPrice = decimal.Zero); - /// - /// Add product to cart - /// - /// The customer - /// The product - /// Cart type - /// Store identifier - /// Selected attributes - /// Price entered by customer - /// Quantity - /// Whether to add required products - /// Add to cart context - /// List with warnings - List AddToCart( - Customer customer, - Product product, - ShoppingCartType cartType, - int storeId, - string selectedAttributes, - decimal customerEnteredPrice, - int quantity, - bool automaticallyAddRequiredProductsIfEnabled, - AddToCartContext ctx = null); + /// + /// Add product to cart + /// + /// The customer + /// The product + /// Cart type + /// Store identifier + /// Selected attributes + /// Price entered by customer + /// Quantity + /// Whether to add required products + /// Add to cart context + /// List with warnings + List AddToCart( + Customer customer, + Product product, + ShoppingCartType cartType, + int storeId, + string selectedAttributes, + decimal customerEnteredPrice, + int quantity, + bool automaticallyAddRequiredProductsIfEnabled, + AddToCartContext ctx = null); - /// - /// Add product to cart - /// - /// Add to cart context - void AddToCart(AddToCartContext ctx); + /// + /// Add product to cart + /// + /// Add to cart context + void AddToCart(AddToCartContext ctx); - /// - /// Stores the shopping card items in the database - /// - /// Add to cart context - void AddToCartStoring(AddToCartContext ctx); + /// + /// Stores the shopping card items in the database + /// + /// Add to cart context + void AddToCartStoring(AddToCartContext ctx); /// /// Validates if all required attributes are selected @@ -251,16 +252,16 @@ List AddToCart( /// To customer void MigrateShoppingCart(Customer fromCustomer, Customer toCustomer); - /// - /// Copies a shopping cart item. - /// - /// Shopping cart item - /// The customer - /// Shopping cart type - /// Store Id - /// Add required products if enabled - /// List with add-to-cart warnings. - IList Copy(OrganizedShoppingCartItem sci, Customer customer, ShoppingCartType cartType, int storeId, bool addRequiredProductsIfEnabled); + /// + /// Copies a shopping cart item. + /// + /// Shopping cart item + /// The customer + /// Shopping cart type + /// Store Id + /// Add required products if enabled + /// List with add-to-cart warnings. + IList Copy(OrganizedShoppingCartItem sci, Customer customer, ShoppingCartType cartType, int storeId, bool addRequiredProductsIfEnabled); /// /// Gets the subtotal of cart items for the current user @@ -279,23 +280,23 @@ List AddToCart( /// /// Formatted subtotal of cart items for the current user string GetFormattedCurrentCartSubTotal(); - + /// /// Gets the formatted subtotal of cart items for the current user /// /// Formatted subtotal of cart items for the current user string GetFormattedCurrentCartSubTotal(IList cart); - /// - /// Get open carts subtotal - /// - /// subtotal - decimal GetAllOpenCartSubTotal(); + /// + /// Get open carts subtotal + /// + /// subtotal + decimal GetAllOpenCartSubTotal(); - /// - /// Get open wishlists subtotal - /// - /// subtotal - decimal GetAllOpenWishlistSubTotal(); - } + /// + /// Get open wishlists subtotal + /// + /// subtotal + decimal GetAllOpenWishlistSubTotal(); + } } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs b/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs index 5a5803d728..2c032bd403 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs @@ -10,23 +10,23 @@ namespace SmartStore.Services.Orders { - public static class OrderExtensions + public static class OrderExtensions { - private static int AggregateQuantity(IEnumerable shipments, OrderItem orderItem) - { - var result = 0; - - foreach (var shipment in shipments) - { - var item = shipment.ShipmentItems.FirstOrDefault(x => x.OrderItemId == orderItem.Id); - if (item != null) - { - result += item.Quantity; - } - } - - return result; - } + private static int AggregateQuantity(IEnumerable shipments, OrderItem orderItem) + { + var result = 0; + + foreach (var shipment in shipments) + { + var item = shipment.ShipmentItems.FirstOrDefault(x => x.OrderItemId == orderItem.Id); + if (item != null) + { + result += item.Quantity; + } + } + + return result; + } /// /// Formats the order note text @@ -35,7 +35,7 @@ private static int AggregateQuantity(IEnumerable shipments, OrderItem /// Formatted text public static string FormatOrderNoteText(this OrderNote orderNote) { - Guard.NotNull(orderNote, nameof(orderNote)); + Guard.NotNull(orderNote, nameof(orderNote)); if (orderNote.Note.IsEmpty()) return string.Empty; @@ -43,25 +43,25 @@ public static string FormatOrderNoteText(this OrderNote orderNote) return HtmlUtils.ConvertPlainTextToHtml(orderNote.Note); } - public static List GetBundleData(this OrderItem orderItem) - { - if (orderItem != null && orderItem.BundleData.HasValue()) - { - var data = orderItem.BundleData.Convert>(); - return data; - } - return new List(); - } + public static List GetBundleData(this OrderItem orderItem) + { + if (orderItem != null && orderItem.BundleData.HasValue()) + { + var data = orderItem.BundleData.Convert>(); + return data; + } + return new List(); + } - public static void SetBundleData(this OrderItem orderItem, List bundleData) - { - string rawData = null; + public static void SetBundleData(this OrderItem orderItem, List bundleData) + { + string rawData = null; - if (bundleData != null && bundleData.Count > 0) - rawData = bundleData.Convert(); + if (bundleData != null && bundleData.Count > 0) + rawData = bundleData.Convert(); - orderItem.BundleData = rawData; - } + orderItem.BundleData = rawData; + } /// /// Get the order total in the currency of the customer @@ -107,143 +107,143 @@ public static decimal GetOrderTotalInCustomerCurrency( /// Order /// A value indicating whether an order has items to dispatch public static bool HasItemsToDispatch(this Order order) - { - Guard.NotNull(order, nameof(order)); - - foreach (var orderItem in order.OrderItems.Where(x => x.Product.IsShipEnabled)) - { - var notDispatchedItems = orderItem.GetNotDispatchedItemsCount(); - if (notDispatchedItems <= 0) - continue; - - // yes, we have at least one item to ship - return true; - } - return false; - } - - /// - /// Gets a value indicating whether an order has items to deliver - /// - /// Order - /// A value indicating whether an order has items to deliver - public static bool HasItemsToDeliver(this Order order) - { - Guard.NotNull(order, nameof(order)); - - foreach (var orderItem in order.OrderItems.Where(x => x.Product.IsShipEnabled)) - { - var dispatchedItems = orderItem.GetDispatchedItemsCount(); - var deliveredItems = orderItem.GetDeliveredItemsCount(); - - if (dispatchedItems <= deliveredItems) - continue; - - // yes, we have at least one item to deliver - return true; - } - return false; - } - - /// - /// Gets a value indicating whether an order has items to be added to a shipment - /// - /// Order - /// A value indicating whether an order has items to be added to a shipment - public static bool CanAddItemsToShipment(this Order order) - { - Guard.NotNull(order, nameof(order)); - - foreach (var orderItem in order.OrderItems.Where(x => x.Product.IsShipEnabled)) - { - var canBeAddedToShipment = orderItem.GetItemsCanBeAddedToShipmentCount(); - if (canBeAddedToShipment <= 0) - continue; - - // yes, we have at least one item to create a new shipment - return true; - } - return false; - } - - - /// - /// Gets the total number of items which can be added to new shipments - /// - /// Order item - /// Total number of items which can be added to new shipments - public static int GetItemsCanBeAddedToShipmentCount(this OrderItem orderItem) - { - Guard.NotNull(orderItem, nameof(orderItem)); - - var itemsCount = orderItem.GetShipmentItemsCount(); - - return Math.Max(orderItem.Quantity - itemsCount, 0); - } - - /// - /// Gets the total number of items in all shipments - /// - /// Order item - /// Total number of items in all shipmentss - public static int GetShipmentItemsCount(this OrderItem orderItem) { - Guard.NotNull(orderItem, nameof(orderItem)); + Guard.NotNull(order, nameof(order)); + + foreach (var orderItem in order.OrderItems.Where(x => x.Product.IsShipEnabled)) + { + var notDispatchedItems = orderItem.GetNotDispatchedItemsCount(); + if (notDispatchedItems <= 0) + continue; + + // yes, we have at least one item to ship + return true; + } + return false; + } + + /// + /// Gets a value indicating whether an order has items to deliver + /// + /// Order + /// A value indicating whether an order has items to deliver + public static bool HasItemsToDeliver(this Order order) + { + Guard.NotNull(order, nameof(order)); + + foreach (var orderItem in order.OrderItems.Where(x => x.Product.IsShipEnabled)) + { + var dispatchedItems = orderItem.GetDispatchedItemsCount(); + var deliveredItems = orderItem.GetDeliveredItemsCount(); + + if (dispatchedItems <= deliveredItems) + continue; + + // yes, we have at least one item to deliver + return true; + } + return false; + } + + /// + /// Gets a value indicating whether an order has items to be added to a shipment + /// + /// Order + /// A value indicating whether an order has items to be added to a shipment + public static bool CanAddItemsToShipment(this Order order) + { + Guard.NotNull(order, nameof(order)); + + foreach (var orderItem in order.OrderItems.Where(x => x.Product.IsShipEnabled)) + { + var canBeAddedToShipment = orderItem.GetItemsCanBeAddedToShipmentCount(); + if (canBeAddedToShipment <= 0) + continue; + + // yes, we have at least one item to create a new shipment + return true; + } + return false; + } + + + /// + /// Gets the total number of items which can be added to new shipments + /// + /// Order item + /// Total number of items which can be added to new shipments + public static int GetItemsCanBeAddedToShipmentCount(this OrderItem orderItem) + { + Guard.NotNull(orderItem, nameof(orderItem)); + + var itemsCount = orderItem.GetShipmentItemsCount(); + + return Math.Max(orderItem.Quantity - itemsCount, 0); + } + + /// + /// Gets the total number of items in all shipments + /// + /// Order item + /// Total number of items in all shipmentss + public static int GetShipmentItemsCount(this OrderItem orderItem) + { + Guard.NotNull(orderItem, nameof(orderItem)); + + var shipments = orderItem.Order.Shipments.ToList(); + return AggregateQuantity(shipments, orderItem); + } + + /// + /// Gets the total number of dispatched items + /// + /// Order item + /// Total number of dispatched items + public static int GetDispatchedItemsCount(this OrderItem orderItem) + { + Guard.NotNull(orderItem, nameof(orderItem)); var shipments = orderItem.Order.Shipments.ToList(); - return AggregateQuantity(shipments, orderItem); + return AggregateQuantity(shipments.Where(x => x.ShippedDateUtc.HasValue), orderItem); } - /// - /// Gets the total number of dispatched items - /// - /// Order item - /// Total number of dispatched items - public static int GetDispatchedItemsCount(this OrderItem orderItem) - { - Guard.NotNull(orderItem, nameof(orderItem)); - - var shipments = orderItem.Order.Shipments.ToList(); - return AggregateQuantity(shipments.Where(x => x.ShippedDateUtc.HasValue), orderItem); - } - - /// - /// Gets the total number of not dispatched items - /// - /// Order item - /// Total number of not dispatched items - public static int GetNotDispatchedItemsCount(this OrderItem orderItem) + /// + /// Gets the total number of not dispatched items + /// + /// Order item + /// Total number of not dispatched items + public static int GetNotDispatchedItemsCount(this OrderItem orderItem) { - Guard.NotNull(orderItem, nameof(orderItem)); + Guard.NotNull(orderItem, nameof(orderItem)); var shipments = orderItem.Order.Shipments.ToList(); - return AggregateQuantity(shipments.Where(x => !x.ShippedDateUtc.HasValue), orderItem); + return AggregateQuantity(shipments.Where(x => !x.ShippedDateUtc.HasValue), orderItem); } - /// - /// Gets the total number of already delivered items - /// - /// Order item - /// Total number of already delivered items - public static int GetDeliveredItemsCount(this OrderItem orderItem) - { - Guard.NotNull(orderItem, nameof(orderItem)); - - var shipments = orderItem.Order.Shipments.ToList(); - return AggregateQuantity(shipments.Where(x => x.DeliveryDateUtc.HasValue), orderItem); - } - - /// - /// Gets the total number of not delivered items - /// - /// Order item - /// Total number of already delivered items - public static int GetNotDeliveredItemsCount(this OrderItem orderItem) - { - Guard.NotNull(orderItem, nameof(orderItem)); - - var shipments = orderItem.Order.Shipments.ToList(); - return AggregateQuantity(shipments.Where(x => !x.DeliveryDateUtc.HasValue), orderItem); - } - } + /// + /// Gets the total number of already delivered items + /// + /// Order item + /// Total number of already delivered items + public static int GetDeliveredItemsCount(this OrderItem orderItem) + { + Guard.NotNull(orderItem, nameof(orderItem)); + + var shipments = orderItem.Order.Shipments.ToList(); + return AggregateQuantity(shipments.Where(x => x.DeliveryDateUtc.HasValue), orderItem); + } + + /// + /// Gets the total number of not delivered items + /// + /// Order item + /// Total number of already delivered items + public static int GetNotDeliveredItemsCount(this OrderItem orderItem) + { + Guard.NotNull(orderItem, nameof(orderItem)); + + var shipments = orderItem.Order.Shipments.ToList(); + return AggregateQuantity(shipments.Where(x => !x.DeliveryDateUtc.HasValue), orderItem); + } + } } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs index 50bb45426e..e640cf85d1 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs @@ -6,121 +6,121 @@ namespace SmartStore.Services.Orders { - public static class OrderMessageFactoryExtensions - { - /// - /// Sends an order placed notification to a store owner - /// - public static CreateMessageResult SendOrderPlacedStoreOwnerNotification(this IMessageFactory factory, Order order, int languageId = 0) - { - Guard.NotNull(order, nameof(order)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderPlacedStoreOwner, languageId, order.StoreId), true, order, order.Customer); - } - - /// - /// Sends an order placed notification to a customer - /// - public static CreateMessageResult SendOrderPlacedCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) - { - Guard.NotNull(order, nameof(order)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderPlacedCustomer, languageId, order.StoreId, order.Customer), true, order); - } - - /// - /// Sends a shipment sent notification to a customer - /// - public static CreateMessageResult SendShipmentSentCustomerNotification(this IMessageFactory factory, Shipment shipment, int languageId = 0) - { - Guard.NotNull(shipment, nameof(shipment)); - Guard.NotNull(shipment.Order, nameof(shipment.Order)); - - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShipmentSentCustomer, languageId, shipment.Order.StoreId, shipment.Order.Customer), true, shipment, shipment.Order); - } - - /// - /// Sends a shipment delivered notification to a customer - /// - public static CreateMessageResult SendShipmentDeliveredCustomerNotification(this IMessageFactory factory, Shipment shipment, int languageId = 0) - { - Guard.NotNull(shipment, nameof(shipment)); - Guard.NotNull(shipment.Order, nameof(shipment.Order)); - - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShipmentDeliveredCustomer, languageId, shipment.Order.StoreId, shipment.Order.Customer), true, shipment, shipment.Order); - } - - /// - /// Sends an order completed notification to a customer - /// - public static CreateMessageResult SendOrderCompletedCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) - { - Guard.NotNull(order, nameof(order)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderCompletedCustomer, languageId, order.StoreId, order.Customer), true, order); - } - - /// - /// Sends an order cancelled notification to a customer - /// - public static CreateMessageResult SendOrderCancelledCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) - { - Guard.NotNull(order, nameof(order)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderCancelledCustomer, languageId, order.StoreId, order.Customer), true, order); - } - - /// - /// Sends a new order note added notification to a customer - /// - public static CreateMessageResult SendNewOrderNoteAddedCustomerNotification(this IMessageFactory factory, OrderNote orderNote, int languageId = 0) - { - Guard.NotNull(orderNote, nameof(orderNote)); - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderNoteAddedCustomer, languageId, orderNote.Order?.StoreId, orderNote.Order?.Customer), true, orderNote, orderNote.Order); - } - - /// - /// Sends a "Recurring payment cancelled" notification to a store owner - /// - public static CreateMessageResult SendRecurringPaymentCancelledStoreOwnerNotification(this IMessageFactory factory, RecurringPayment recurringPayment, int languageId = 0) - { - Guard.NotNull(recurringPayment, nameof(recurringPayment)); - - var order = recurringPayment.InitialOrder; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.RecurringPaymentCancelledStoreOwner, languageId, order?.StoreId, order?.Customer), true, - recurringPayment, order); - } - - /// - /// Sends 'New Return Request' message to a store owner - /// - public static CreateMessageResult SendNewReturnRequestStoreOwnerNotification(this IMessageFactory factory, ReturnRequest returnRequest, OrderItem orderItem, int languageId = 0) - { - Guard.NotNull(returnRequest, nameof(returnRequest)); - Guard.NotNull(orderItem, nameof(orderItem)); - - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewReturnRequestStoreOwner, languageId, orderItem.Order?.StoreId, returnRequest.Customer), true, - returnRequest, orderItem, orderItem.Order, orderItem.Product); - } - - /// - /// Sends 'Return Request status changed' message to a customer - /// - public static CreateMessageResult SendReturnRequestStatusChangedCustomerNotification(this IMessageFactory factory, ReturnRequest returnRequest, OrderItem orderItem, int languageId = 0) - { - Guard.NotNull(returnRequest, nameof(returnRequest)); - Guard.NotNull(orderItem, nameof(orderItem)); - - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ReturnRequestStatusChangedCustomer, languageId, orderItem.Order?.StoreId, returnRequest.Customer), true, returnRequest); - } - - /// - /// Sends a gift card notification - /// - public static CreateMessageResult SendGiftCardNotification(this IMessageFactory factory, GiftCard giftCard, int languageId = 0) - { - Guard.NotNull(giftCard, nameof(giftCard)); - - var orderItem = giftCard.PurchasedWithOrderItem; - var customer = orderItem?.Order?.Customer; - var storeId = orderItem?.Order?.StoreId; - return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.GiftCardCustomer, languageId, storeId, customer), true, giftCard, orderItem, orderItem?.Product); - } - } + public static class OrderMessageFactoryExtensions + { + /// + /// Sends an order placed notification to a store owner + /// + public static CreateMessageResult SendOrderPlacedStoreOwnerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderPlacedStoreOwner, languageId, order.StoreId), true, order, order.Customer); + } + + /// + /// Sends an order placed notification to a customer + /// + public static CreateMessageResult SendOrderPlacedCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderPlacedCustomer, languageId, order.StoreId, order.Customer), true, order); + } + + /// + /// Sends a shipment sent notification to a customer + /// + public static CreateMessageResult SendShipmentSentCustomerNotification(this IMessageFactory factory, Shipment shipment, int languageId = 0) + { + Guard.NotNull(shipment, nameof(shipment)); + Guard.NotNull(shipment.Order, nameof(shipment.Order)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShipmentSentCustomer, languageId, shipment.Order.StoreId, shipment.Order.Customer), true, shipment, shipment.Order); + } + + /// + /// Sends a shipment delivered notification to a customer + /// + public static CreateMessageResult SendShipmentDeliveredCustomerNotification(this IMessageFactory factory, Shipment shipment, int languageId = 0) + { + Guard.NotNull(shipment, nameof(shipment)); + Guard.NotNull(shipment.Order, nameof(shipment.Order)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShipmentDeliveredCustomer, languageId, shipment.Order.StoreId, shipment.Order.Customer), true, shipment, shipment.Order); + } + + /// + /// Sends an order completed notification to a customer + /// + public static CreateMessageResult SendOrderCompletedCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderCompletedCustomer, languageId, order.StoreId, order.Customer), true, order); + } + + /// + /// Sends an order cancelled notification to a customer + /// + public static CreateMessageResult SendOrderCancelledCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderCancelledCustomer, languageId, order.StoreId, order.Customer), true, order); + } + + /// + /// Sends a new order note added notification to a customer + /// + public static CreateMessageResult SendNewOrderNoteAddedCustomerNotification(this IMessageFactory factory, OrderNote orderNote, int languageId = 0) + { + Guard.NotNull(orderNote, nameof(orderNote)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderNoteAddedCustomer, languageId, orderNote.Order?.StoreId, orderNote.Order?.Customer), true, orderNote, orderNote.Order); + } + + /// + /// Sends a "Recurring payment cancelled" notification to a store owner + /// + public static CreateMessageResult SendRecurringPaymentCancelledStoreOwnerNotification(this IMessageFactory factory, RecurringPayment recurringPayment, int languageId = 0) + { + Guard.NotNull(recurringPayment, nameof(recurringPayment)); + + var order = recurringPayment.InitialOrder; + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.RecurringPaymentCancelledStoreOwner, languageId, order?.StoreId, order?.Customer), true, + recurringPayment, order); + } + + /// + /// Sends 'New Return Request' message to a store owner + /// + public static CreateMessageResult SendNewReturnRequestStoreOwnerNotification(this IMessageFactory factory, ReturnRequest returnRequest, OrderItem orderItem, int languageId = 0) + { + Guard.NotNull(returnRequest, nameof(returnRequest)); + Guard.NotNull(orderItem, nameof(orderItem)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewReturnRequestStoreOwner, languageId, orderItem.Order?.StoreId, returnRequest.Customer), true, + returnRequest, orderItem, orderItem.Order, orderItem.Product); + } + + /// + /// Sends 'Return Request status changed' message to a customer + /// + public static CreateMessageResult SendReturnRequestStatusChangedCustomerNotification(this IMessageFactory factory, ReturnRequest returnRequest, OrderItem orderItem, int languageId = 0) + { + Guard.NotNull(returnRequest, nameof(returnRequest)); + Guard.NotNull(orderItem, nameof(orderItem)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ReturnRequestStatusChangedCustomer, languageId, orderItem.Order?.StoreId, returnRequest.Customer), true, returnRequest); + } + + /// + /// Sends a gift card notification + /// + public static CreateMessageResult SendGiftCardNotification(this IMessageFactory factory, GiftCard giftCard, int languageId = 0) + { + Guard.NotNull(giftCard, nameof(giftCard)); + + var orderItem = giftCard.PurchasedWithOrderItem; + var customer = orderItem?.Order?.Customer; + var storeId = orderItem?.Order?.StoreId; + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.GiftCardCustomer, languageId, storeId, customer), true, giftCard, orderItem, orderItem?.Product); + } + } } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index f9589dc0e8..bc58b098d2 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Orders; @@ -24,6 +25,7 @@ using SmartStore.Services.Directory; using SmartStore.Services.Discounts; using SmartStore.Services.Localization; +using SmartStore.Services.Media; using SmartStore.Services.Messages; using SmartStore.Services.Payments; using SmartStore.Services.Security; @@ -35,7 +37,7 @@ namespace SmartStore.Services.Orders public partial class OrderProcessingService : IOrderProcessingService { #region Fields - + private readonly IOrderService _orderService; private readonly IWebHelper _webHelper; private readonly ILocalizationService _localizationService; @@ -50,6 +52,7 @@ public partial class OrderProcessingService : IOrderProcessingService private readonly IGiftCardService _giftCardService; private readonly IShoppingCartService _shoppingCartService; private readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter; + private readonly ICheckoutAttributeParser _checkoutAttributeParser; private readonly IShippingService _shippingService; private readonly IShipmentService _shipmentService; private readonly ITaxService _taxService; @@ -57,21 +60,21 @@ public partial class OrderProcessingService : IOrderProcessingService private readonly IDiscountService _discountService; private readonly IEncryptionService _encryptionService; private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; + private readonly IStoreContext _storeContext; private readonly IMessageFactory _messageFactory; private readonly ICustomerActivityService _customerActivityService; private readonly ICurrencyService _currencyService; - private readonly IAffiliateService _affiliateService; + private readonly IAffiliateService _affiliateService; private readonly IEventPublisher _eventPublisher; - private readonly IGenericAttributeService _genericAttributeService; - private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; + private readonly IGenericAttributeService _genericAttributeService; + private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; + private readonly IDownloadService _downloadService; - private readonly PaymentSettings _paymentSettings; private readonly RewardPointsSettings _rewardPointsSettings; private readonly OrderSettings _orderSettings; private readonly TaxSettings _taxSettings; private readonly LocalizationSettings _localizationSettings; - private readonly ShoppingCartSettings _shoppingCartSettings; + private readonly ShoppingCartSettings _shoppingCartSettings; private readonly CatalogSettings _catalogSettings; #endregion @@ -79,7 +82,7 @@ public partial class OrderProcessingService : IOrderProcessingService #region Ctor public OrderProcessingService( - IOrderService orderService, + IOrderService orderService, IWebHelper webHelper, ILocalizationService localizationService, ILanguageService languageService, @@ -93,6 +96,7 @@ public OrderProcessingService( IGiftCardService giftCardService, IShoppingCartService shoppingCartService, ICheckoutAttributeFormatter checkoutAttributeFormatter, + ICheckoutAttributeParser checkoutAttributeParser, IShippingService shippingService, IShipmentService shipmentService, ITaxService taxService, @@ -100,20 +104,20 @@ public OrderProcessingService( IDiscountService discountService, IEncryptionService encryptionService, IWorkContext workContext, - IStoreContext storeContext, + IStoreContext storeContext, IMessageFactory messageFactory, ICustomerActivityService customerActivityService, ICurrencyService currencyService, - IAffiliateService affiliateService, + IAffiliateService affiliateService, IEventPublisher eventPublisher, - IGenericAttributeService genericAttributeService, - INewsLetterSubscriptionService newsLetterSubscriptionService, - PaymentSettings paymentSettings, + IGenericAttributeService genericAttributeService, + INewsLetterSubscriptionService newsLetterSubscriptionService, + IDownloadService downloadService, RewardPointsSettings rewardPointsSettings, OrderSettings orderSettings, TaxSettings taxSettings, LocalizationSettings localizationSettings, - ShoppingCartSettings shoppingCartSettings, + ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings) { _orderService = orderService; @@ -130,8 +134,9 @@ public OrderProcessingService( _giftCardService = giftCardService; _shoppingCartService = shoppingCartService; _checkoutAttributeFormatter = checkoutAttributeFormatter; + _checkoutAttributeParser = checkoutAttributeParser; _workContext = workContext; - _storeContext = storeContext; + _storeContext = storeContext; _messageFactory = messageFactory; _shippingService = shippingService; _shipmentService = shipmentService; @@ -141,53 +146,50 @@ public OrderProcessingService( _encryptionService = encryptionService; _customerActivityService = customerActivityService; _currencyService = currencyService; - _affiliateService = affiliateService; + _affiliateService = affiliateService; _eventPublisher = eventPublisher; - _genericAttributeService = genericAttributeService; - _newsLetterSubscriptionService = newsLetterSubscriptionService; - _paymentSettings = paymentSettings; + _genericAttributeService = genericAttributeService; + _newsLetterSubscriptionService = newsLetterSubscriptionService; + _downloadService = downloadService; _rewardPointsSettings = rewardPointsSettings; _orderSettings = orderSettings; _taxSettings = taxSettings; _localizationSettings = localizationSettings; - _shoppingCartSettings = shoppingCartSettings; + _shoppingCartSettings = shoppingCartSettings; _catalogSettings = catalogSettings; + } - T = NullLocalizer.Instance; - Logger = NullLogger.Instance; - } + public Localizer T { get; set; } = NullLocalizer.Instance; + public ILogger Logger { get; set; } = NullLogger.Instance; - public Localizer T { get; set; } - public ILogger Logger { get; set; } + #endregion - #endregion + #region Utilities - #region Utilities + protected string FormatTaxRates(SortedDictionary taxRates) + { + var result = string.Empty; - protected string FormatTaxRates(SortedDictionary taxRates) - { - var result = string.Empty; + foreach (var rate in taxRates) + { + result += "{0}:{1}; ".FormatInvariant( + rate.Key.ToString(CultureInfo.InvariantCulture), + rate.Value.ToString(CultureInfo.InvariantCulture)); + } - foreach (var rate in taxRates) - { - result += "{0}:{1}; ".FormatInvariant( - rate.Key.ToString(CultureInfo.InvariantCulture), - rate.Value.ToString(CultureInfo.InvariantCulture)); - } - - return result; - } + return result; + } - private void ProcessErrors(Order order, IList errors, string messageKey) - { - if (errors.Any()) - { - var msg = string.Concat(T(messageKey, order.GetOrderNumber()), " ", string.Join(" ", errors)); + private void ProcessErrors(Order order, IList errors, string messageKey) + { + if (errors.Any()) + { + var msg = string.Concat(T(messageKey, order.GetOrderNumber()), " ", string.Join(" ", errors)); - _orderService.AddOrderNote(order, msg); - Logger.Error(msg); - } - } + _orderService.AddOrderNote(order, msg); + Logger.Error(msg); + } + } /// /// Award reward points @@ -206,12 +208,12 @@ protected void AwardRewardPoints(Order order, decimal? amount = null) if (order.Customer == null || order.Customer.IsGuest()) return; - //Ensure that reward points were not added before. We should not add reward points if they were already earned for this order - if (order.RewardPointsWereAdded) - return; + //Ensure that reward points were not added before. We should not add reward points if they were already earned for this order + if (order.RewardPointsWereAdded) + return; - // Truncate same as Floor for positive amounts - var points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + // Truncate same as Floor for positive amounts + var points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); if (points == 0) return; @@ -235,32 +237,32 @@ protected void ReduceRewardPoints(Order order, decimal? amount = null) if (_rewardPointsSettings.PointsForPurchases_Amount <= decimal.Zero) return; - //ensure that reward points were already earned for this order before - if (!order.RewardPointsWereAdded) - return; + //ensure that reward points were already earned for this order before + if (!order.RewardPointsWereAdded) + return; //Ensure that reward points are applied only to registered users if (order.Customer == null || order.Customer.IsGuest()) return; - // Truncate increases the risk of inaccuracy of rounding - //int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + // Truncate increases the risk of inaccuracy of rounding + //int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); - int points = (int)Math.Round((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + int points = (int)Math.Round((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); - if (order.RewardPointsRemaining.HasValue && order.RewardPointsRemaining.Value < points) - points = order.RewardPointsRemaining.Value; + if (order.RewardPointsRemaining.HasValue && order.RewardPointsRemaining.Value < points) + points = order.RewardPointsRemaining.Value; - if (points == 0) - return; + if (points == 0) + return; //reduce reward points order.Customer.AddRewardPointsHistoryEntry(-points, T("RewardPoints.Message.ReducedForOrder", order.GetOrderNumber())); - if (!order.RewardPointsRemaining.HasValue) - order.RewardPointsRemaining = (int)Math.Round(order.OrderTotal / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + if (!order.RewardPointsRemaining.HasValue) + order.RewardPointsRemaining = (int)Math.Round(order.OrderTotal / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); - order.RewardPointsRemaining = Math.Max(order.RewardPointsRemaining.Value - points, 0); + order.RewardPointsRemaining = Math.Max(order.RewardPointsRemaining.Value - points, 0); _orderService.UpdateOrder(order); } @@ -291,7 +293,7 @@ protected void SetActivatedValueForPurchasedGiftCards(Order order, bool activate customerLang = _languageService.GetAllLanguages().FirstOrDefault(); var qe = _messageFactory.SendGiftCardNotification(gc, customerLang.Id); - isRecipientNotified = qe?.Email.Id != null; + isRecipientNotified = qe?.Email.Id != null; } } @@ -329,7 +331,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) order.OrderStatusId = (int)os; _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderStatusChanged", os.GetLocalizedEnum(_localizationService))); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderStatusChanged", os.GetLocalizedEnum(_localizationService))); if (prevOrderStatus != OrderStatus.Complete && os == OrderStatus.Complete && notifyCustomer) { @@ -337,7 +339,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) var msg = _messageFactory.SendOrderCompletedCustomerNotification(order, order.CustomerLanguageId); if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", msg.Email.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", msg.Email.Id)); } } @@ -347,7 +349,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) var msg = _messageFactory.SendOrderCancelledCustomerNotification(order, order.CustomerLanguageId); if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", msg.Email.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", msg.Email.Id)); } } @@ -523,24 +525,34 @@ public virtual IList GetOrderPlacementWarnings( } } - // Min totals validation. - var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); - if (!minOrderSubtotalAmountOk) + // Order total minimum validation + var customerRoleIds = customer.GetRoleIds(); + var (isAboveOrderTotalMinimum, orderTotalMinimum) = IsAboveOrderTotalMinimum(cart, customerRoleIds); + if (!isAboveOrderTotalMinimum) { - var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); - warnings.Add(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); + orderTotalMinimum = _currencyService.ConvertFromPrimaryStoreCurrency( + orderTotalMinimum, + _workContext.WorkingCurrency); + + var msg = T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(orderTotalMinimum, true, false)); + warnings.Add(msg); return warnings; } - var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); - if (!minOrderTotalAmountOk) + // Order total maximum validation + var (isBelowOrderTotalMaximum, orderTotalMaximum) = IsBelowOrderTotalMaximum(cart, customerRoleIds); + if (isAboveOrderTotalMinimum && !isBelowOrderTotalMaximum) { - var minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); - warnings.Add(T("Checkout.MinOrderTotalAmount", _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); + orderTotalMaximum = _currencyService.ConvertFromPrimaryStoreCurrency( + orderTotalMaximum, + _workContext.WorkingCurrency); + + var msg = T("Checkout.MaxOrderSubtotalAmount", _priceFormatter.FormatPrice(orderTotalMaximum, true, false)); + warnings.Add(msg); return warnings; } - // Total validations. + // Total validations var orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out var orderShippingTaxRate, out var shippingTotalDiscount); var orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); @@ -629,12 +641,8 @@ public virtual IList GetOrderPlacementWarnings( // Payment. if (!warnings.Any() && !skipPaymentWorkflow) { - var paymentMethod = _paymentService.LoadPaymentMethodBySystemName(paymentMethodSystemName); - if (paymentMethod == null) - { - warnings.Add(T("Payment.CouldNotLoadMethod")); - } - else if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) + var isPaymentMethodActive = _paymentService.IsPaymentMethodActive(paymentMethodSystemName, customer, cart, processPaymentRequest.StoreId); + if (!isPaymentMethodActive) { warnings.Add(T("Payment.MethodNotAvailable")); } @@ -703,7 +711,7 @@ public virtual IList GetOrderPlacementWarnings( } public virtual PlaceOrderResult PlaceOrder( - ProcessPaymentRequest processPaymentRequest, + ProcessPaymentRequest processPaymentRequest, Dictionary extraData) { // Think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method. @@ -715,7 +723,7 @@ public virtual PlaceOrderResult PlaceOrder( } var result = new PlaceOrderResult(); - var utcNow = DateTime.UtcNow; + var utcNow = DateTime.UtcNow; try { @@ -737,13 +745,13 @@ public virtual PlaceOrderResult PlaceOrder( processPaymentRequest.PaymentMethodSystemName = initialOrder.PaymentMethodSystemName; } - // Affilites. - var affiliateId = 0; - var affiliate = _affiliateService.GetAffiliateById(customer.AffiliateId); - if (affiliate != null && affiliate.Active && !affiliate.Deleted) - { - affiliateId = affiliate.Id; - } + // Affilites. + var affiliateId = 0; + var affiliate = _affiliateService.GetAffiliateById(customer.AffiliateId); + if (affiliate != null && affiliate.Active && !affiliate.Deleted) + { + affiliateId = affiliate.Id; + } // Customer currency. var customerCurrencyCode = string.Empty; @@ -766,24 +774,24 @@ public virtual PlaceOrderResult PlaceOrder( // Customer language. Language customerLanguage = null; if (!processPaymentRequest.IsRecurringPayment) - { + { customerLanguage = _languageService.GetLanguageById(customer.GetAttribute(SystemCustomerAttributeNames.LanguageId, processPaymentRequest.StoreId)); } - else - { - customerLanguage = _languageService.GetLanguageById(initialOrder.CustomerLanguageId); - } + else + { + customerLanguage = _languageService.GetLanguageById(initialOrder.CustomerLanguageId); + } + + if (customerLanguage == null || !customerLanguage.Published) + { + customerLanguage = _workContext.WorkingLanguage; + } - if (customerLanguage == null || !customerLanguage.Published) - { - customerLanguage = _workContext.WorkingLanguage; - } - // Tax display type. var customerTaxDisplayType = TaxDisplayType.IncludingTax; if (!processPaymentRequest.IsRecurringPayment) { - customerTaxDisplayType = _workContext.GetTaxDisplayTypeFor(customer, processPaymentRequest.StoreId); + customerTaxDisplayType = _workContext.GetTaxDisplayTypeFor(customer, processPaymentRequest.StoreId); } else { @@ -794,12 +802,12 @@ public virtual PlaceOrderResult PlaceOrder( string checkoutAttributeDescription, checkoutAttributesXml; if (!processPaymentRequest.IsRecurringPayment) { - checkoutAttributesXml = customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes); - checkoutAttributeDescription = _checkoutAttributeFormatter.FormatAttributes(checkoutAttributesXml, customer); + checkoutAttributesXml = customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes); + checkoutAttributeDescription = _checkoutAttributeFormatter.FormatAttributes(checkoutAttributesXml, customer); } else { - checkoutAttributesXml = initialOrder.CheckoutAttributesXml; + checkoutAttributesXml = initialOrder.CheckoutAttributesXml; checkoutAttributeDescription = initialOrder.CheckoutAttributeDescription; } @@ -868,7 +876,7 @@ public virtual PlaceOrderResult PlaceOrder( // Shipping total. decimal? orderShippingTotalInclTax, orderShippingTotalExclTax = null; - decimal orderShippingTaxRate = decimal.Zero; + decimal orderShippingTaxRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) { orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out var shippingTotalDiscount); @@ -883,11 +891,11 @@ public virtual PlaceOrderResult PlaceOrder( { orderShippingTotalInclTax = initialOrder.OrderShippingInclTax; orderShippingTotalExclTax = initialOrder.OrderShippingExclTax; - orderShippingTaxRate = initialOrder.OrderShippingTaxRate; + orderShippingTaxRate = initialOrder.OrderShippingTaxRate; } // Payment total. - decimal paymentAdditionalFeeInclTax, paymentAdditionalFeeExclTax, paymentAdditionalFeeTaxRate; + decimal paymentAdditionalFeeInclTax, paymentAdditionalFeeExclTax, paymentAdditionalFeeTaxRate; if (!processPaymentRequest.IsRecurringPayment) { var paymentAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, processPaymentRequest.PaymentMethodSystemName); @@ -898,7 +906,7 @@ public virtual PlaceOrderResult PlaceOrder( { paymentAdditionalFeeInclTax = initialOrder.PaymentMethodAdditionalFeeInclTax; paymentAdditionalFeeExclTax = initialOrder.PaymentMethodAdditionalFeeExclTax; - paymentAdditionalFeeTaxRate = initialOrder.PaymentMethodAdditionalFeeTaxRate; + paymentAdditionalFeeTaxRate = initialOrder.PaymentMethodAdditionalFeeTaxRate; } // Tax total. @@ -910,13 +918,13 @@ public virtual PlaceOrderResult PlaceOrder( orderTaxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out var taxRatesDictionary); // VAT number. - var customerVatStatus = (VatNumberStatus)customer.VatNumberStatusId; - if (_taxSettings.EuVatEnabled && customerVatStatus == VatNumberStatus.Valid) - { - vatNumber = customer.GetAttribute(SystemCustomerAttributeNames.VatNumber); - } + var customerVatStatus = (VatNumberStatus)customer.VatNumberStatusId; + if (_taxSettings.EuVatEnabled && customerVatStatus == VatNumberStatus.Valid) + { + vatNumber = customer.GetAttribute(SystemCustomerAttributeNames.VatNumber); + } - taxRates = FormatTaxRates(taxRatesDictionary); + taxRates = FormatTaxRates(taxRatesDictionary); } else { @@ -924,7 +932,7 @@ public virtual PlaceOrderResult PlaceOrder( vatNumber = initialOrder.VatNumber; } - processPaymentRequest.OrderTax = orderTaxTotal; + processPaymentRequest.OrderTax = orderTaxTotal; // Order total (and applied discounts, gift cards, reward points). ShoppingCartTotal cartTotal = null; @@ -933,11 +941,11 @@ public virtual PlaceOrderResult PlaceOrder( { cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); - // Discount history. - if (cartTotal.AppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == cartTotal.AppliedDiscount.Id)) - { - appliedDiscounts.Add(cartTotal.AppliedDiscount); - } + // Discount history. + if (cartTotal.AppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == cartTotal.AppliedDiscount.Id)) + { + appliedDiscounts.Add(cartTotal.AppliedDiscount); + } } else { @@ -949,66 +957,66 @@ public virtual PlaceOrderResult PlaceOrder( #endregion - #region Addresses & pre-payment workflow - - // Give payment processor the opportunity to fullfill billing address. - var preProcessPaymentResult = _paymentService.PreProcessPayment(processPaymentRequest); + #region Addresses & pre-payment workflow - if (!preProcessPaymentResult.Success) - { - result.Errors.AddRange(preProcessPaymentResult.Errors); - result.Errors.Add(T("Common.Error.PreProcessPayment")); - return result; - } + // Give payment processor the opportunity to fullfill billing address. + var preProcessPaymentResult = _paymentService.PreProcessPayment(processPaymentRequest); + + if (!preProcessPaymentResult.Success) + { + result.Errors.AddRange(preProcessPaymentResult.Errors); + result.Errors.Add(T("Common.Error.PreProcessPayment")); + return result; + } var billingAddress = !processPaymentRequest.IsRecurringPayment ? (Address)customer.BillingAddress.Clone() : (Address)initialOrder.BillingAddress.Clone(); - Address shippingAddress = null; - if (shoppingCartRequiresShipping) - { + Address shippingAddress = null; + if (shoppingCartRequiresShipping) + { shippingAddress = !processPaymentRequest.IsRecurringPayment ? (Address)customer.ShippingAddress.Clone() : (Address)initialOrder.ShippingAddress.Clone(); - } + } - #endregion + #endregion - #region Payment workflow + #region Payment workflow - // Skip payment workflow if order total equals zero. + // Skip payment workflow if order total equals zero. var skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; // Payment workflow. Provider paymentMethod = null; - if (!skipPaymentWorkflow) - { - paymentMethod = _paymentService.LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); - } - else - { - processPaymentRequest.PaymentMethodSystemName = ""; - } + if (!skipPaymentWorkflow) + { + paymentMethod = _paymentService.LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); + } + else + { + processPaymentRequest.PaymentMethodSystemName = ""; + } // Recurring or standard shopping cart? var isRecurringShoppingCart = false; - if (!processPaymentRequest.IsRecurringPayment) - { - isRecurringShoppingCart = cart.IsRecurring(); - if (isRecurringShoppingCart) - { - var unused = cart.GetRecurringCycleInfo(_localizationService, out var recurringCycleLength, out var recurringCyclePeriod, out var recurringTotalCycles); - - processPaymentRequest.RecurringCycleLength = recurringCycleLength; - processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod; - processPaymentRequest.RecurringTotalCycles = recurringTotalCycles; - } - } - else - { - isRecurringShoppingCart = true; - } + if (!processPaymentRequest.IsRecurringPayment) + { + isRecurringShoppingCart = cart.IsRecurring(); + if (isRecurringShoppingCart) + { + var unused = cart.GetRecurringCycleInfo(_localizationService, out var recurringCycleLength, out var recurringCyclePeriod, out var recurringTotalCycles); + + processPaymentRequest.RecurringCycleLength = recurringCycleLength; + processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod; + processPaymentRequest.RecurringTotalCycles = recurringTotalCycles; + } + } + else + { + isRecurringShoppingCart = true; + } // Process payment. ProcessPaymentResult processPaymentResult = null; @@ -1054,7 +1062,7 @@ public virtual PlaceOrderResult PlaceOrder( processPaymentRequest.CreditCardExpireMonth = initialOrder.AllowStoringCreditCardNumber ? Convert.ToInt32(_encryptionService.DecryptText(initialOrder.CardExpirationMonth)) : 0; processPaymentRequest.CreditCardExpireYear = initialOrder.AllowStoringCreditCardNumber ? Convert.ToInt32(_encryptionService.DecryptText(initialOrder.CardExpirationYear)) : 0; } - catch {} + catch { } var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) @@ -1069,8 +1077,8 @@ public virtual PlaceOrderResult PlaceOrder( processPaymentResult = new ProcessPaymentResult(); break; default: - throw new SmartException(T("Payment.RecurringPaymentTypeUnknown")); - } + throw new SmartException(T("Payment.RecurringPaymentTypeUnknown")); + } } else { @@ -1098,14 +1106,14 @@ public virtual PlaceOrderResult PlaceOrder( #region Save order details var shippingStatus = ShippingStatus.NotYetShipped; - if (!shoppingCartRequiresShipping) - { - shippingStatus = ShippingStatus.ShippingNotRequired; - } - + if (!shoppingCartRequiresShipping) + { + shippingStatus = ShippingStatus.ShippingNotRequired; + } + var order = new Order { - StoreId = processPaymentRequest.StoreId, + StoreId = processPaymentRequest.StoreId, OrderGuid = processPaymentRequest.OrderGuid, CustomerId = customer.Id, CustomerLanguageId = customerLanguage.Id, @@ -1117,22 +1125,22 @@ public virtual PlaceOrderResult PlaceOrder( OrderSubTotalDiscountExclTax = orderSubTotalDiscountExclTax, OrderShippingInclTax = orderShippingTotalInclTax.Value, OrderShippingExclTax = orderShippingTotalExclTax.Value, - OrderShippingTaxRate = orderShippingTaxRate, + OrderShippingTaxRate = orderShippingTaxRate, PaymentMethodAdditionalFeeInclTax = paymentAdditionalFeeInclTax, PaymentMethodAdditionalFeeExclTax = paymentAdditionalFeeExclTax, - PaymentMethodAdditionalFeeTaxRate = paymentAdditionalFeeTaxRate, + PaymentMethodAdditionalFeeTaxRate = paymentAdditionalFeeTaxRate, TaxRates = taxRates, OrderTax = orderTaxTotal, OrderTotalRounding = cartTotal.RoundingAmount, OrderTotal = cartTotal.TotalAmount.Value, RefundedAmount = decimal.Zero, OrderDiscount = cartTotal.DiscountAmount, - CreditBalance = cartTotal.CreditBalance, + CreditBalance = cartTotal.CreditBalance, CheckoutAttributeDescription = checkoutAttributeDescription, CheckoutAttributesXml = checkoutAttributesXml, CustomerCurrencyCode = customerCurrencyCode, CurrencyRate = customerCurrencyRate, - AffiliateId = affiliateId, + AffiliateId = affiliateId, OrderStatus = OrderStatus.Pending, AllowStoringCreditCardNumber = processPaymentResult.AllowStoringCreditCardNumber, CardType = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardType) : string.Empty, @@ -1169,10 +1177,10 @@ public virtual PlaceOrderResult PlaceOrder( CustomerOrderComment = extraData.ContainsKey("CustomerComment") ? extraData["CustomerComment"] : "" }; - if (extraData.ContainsKey("AcceptThirdPartyEmailHandOver") && _shoppingCartSettings.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None) - { - order.AcceptThirdPartyEmailHandOver = extraData["AcceptThirdPartyEmailHandOver"].ToBool(); - } + if (extraData.ContainsKey("AcceptThirdPartyEmailHandOver") && _shoppingCartSettings.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None) + { + order.AcceptThirdPartyEmailHandOver = extraData["AcceptThirdPartyEmailHandOver"].ToBool(); + } _orderService.InsertOrder(order); @@ -1183,14 +1191,14 @@ public virtual PlaceOrderResult PlaceOrder( // Move shopping cart items to order products. foreach (var sc in cart) { - sc.Item.Product.MergeWithCombination(sc.Item.AttributesXml); + sc.Item.Product.MergeWithCombination(sc.Item.AttributesXml); // Prices. decimal taxRate = decimal.Zero; - decimal unitPriceTaxRate = decimal.Zero; + decimal unitPriceTaxRate = decimal.Zero; decimal scUnitPrice = _priceCalculationService.GetUnitPrice(sc, true); decimal scSubTotal = _priceCalculationService.GetSubTotal(sc, true); - decimal scUnitPriceInclTax = _taxService.GetProductPrice(sc.Item.Product, scUnitPrice, true, customer, out unitPriceTaxRate); + decimal scUnitPriceInclTax = _taxService.GetProductPrice(sc.Item.Product, scUnitPrice, true, customer, out unitPriceTaxRate); decimal scUnitPriceExclTax = _taxService.GetProductPrice(sc.Item.Product, scUnitPrice, false, customer, out taxRate); decimal scSubTotalInclTax = _taxService.GetProductPrice(sc.Item.Product, scSubTotal, true, customer, out taxRate); decimal scSubTotalExclTax = _taxService.GetProductPrice(sc.Item.Product, scSubTotal, false, customer, out taxRate); @@ -1201,15 +1209,15 @@ public virtual PlaceOrderResult PlaceOrder( decimal discountAmountInclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, true, customer, out taxRate); decimal discountAmountExclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, false, customer, out taxRate); - if (scDiscount != null && !appliedDiscounts.Any(x => x.Id == scDiscount.Id)) - { - appliedDiscounts.Add(scDiscount); - } + if (scDiscount != null && !appliedDiscounts.Any(x => x.Id == scDiscount.Id)) + { + appliedDiscounts.Add(scDiscount); + } var attributeDescription = _productAttributeFormatter.FormatAttributes(sc.Item.Product, sc.Item.AttributesXml, customer); var itemWeight = _shippingService.GetShoppingCartItemWeight(sc); - var displayDeliveryTime = - _shoppingCartSettings.ShowDeliveryTimes && + var displayDeliveryTime = + _shoppingCartSettings.DeliveryTimesInShoppingCart != DeliveryTimesPresentation.None && sc.Item.Product.DeliveryTimeId.HasValue && sc.Item.Product.IsShipEnabled && sc.Item.Product.DisplayDeliveryTimeAccordingToStock(_catalogSettings); @@ -1224,7 +1232,7 @@ public virtual PlaceOrderResult PlaceOrder( UnitPriceExclTax = scUnitPriceExclTax, PriceInclTax = scSubTotalInclTax, PriceExclTax = scSubTotalExclTax, - TaxRate = unitPriceTaxRate, + TaxRate = unitPriceTaxRate, AttributeDescription = attributeDescription, AttributesXml = sc.Item.AttributesXml, Quantity = sc.Item.Quantity, @@ -1234,27 +1242,27 @@ public virtual PlaceOrderResult PlaceOrder( IsDownloadActivated = false, LicenseDownloadId = 0, ItemWeight = itemWeight, - ProductCost = _priceCalculationService.GetProductCost(sc.Item.Product, sc.Item.AttributesXml), + ProductCost = _priceCalculationService.GetProductCost(sc.Item.Product, sc.Item.AttributesXml), DeliveryTimeId = sc.Item.Product.GetDeliveryTimeIdAccordingToStock(_catalogSettings), DisplayDeliveryTime = displayDeliveryTime }; if (sc.Item.Product.ProductType == ProductType.BundledProduct && sc.ChildItems != null) - { - var listBundleData = new List(); + { + var listBundleData = new List(); - foreach (var childItem in sc.ChildItems) - { - var bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); + foreach (var childItem in sc.ChildItems) + { + var bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); - var attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, - renderPrices: false, allowHyperlinks: true); + var attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, + renderPrices: false, allowHyperlinks: true); - childItem.BundleItemData.ToOrderData(listBundleData, bundleItemSubTotal, childItem.Item.AttributesXml, attributesInfo); - } + childItem.BundleItemData.ToOrderData(listBundleData, bundleItemSubTotal, childItem.Item.AttributesXml, attributesInfo); + } - orderItem.SetBundleData(listBundleData); - } + orderItem.SetBundleData(listBundleData); + } order.OrderItems.Add(orderItem); _orderService.UpdateOrder(order); @@ -1263,12 +1271,12 @@ public virtual PlaceOrderResult PlaceOrder( if (sc.Item.Product.IsGiftCard) { _productAttributeParser.GetGiftCardAttribute( - sc.Item.AttributesXml, + sc.Item.AttributesXml, out var giftCardRecipientName, - out var giftCardRecipientEmail, - out var giftCardSenderName, - out var giftCardSenderEmail, - out var giftCardMessage); + out var giftCardRecipientEmail, + out var giftCardSenderName, + out var giftCardSenderEmail, + out var giftCardMessage); for (int i = 0; i < sc.Item.Quantity; i++) { @@ -1285,20 +1293,20 @@ public virtual PlaceOrderResult PlaceOrder( SenderEmail = giftCardSenderEmail, Message = giftCardMessage, IsRecipientNotified = false, - CreatedOnUtc = utcNow + CreatedOnUtc = utcNow }; _giftCardService.InsertGiftCard(gc); } } - _productService.AdjustInventory(sc, true); + _productService.AdjustInventory(sc, true); } - // Clear shopping cart. - if (!processPaymentRequest.IsMultiOrder) - { - cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); - } + // Clear shopping cart. + if (!processPaymentRequest.IsMultiOrder) + { + cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); + } } else { @@ -1316,7 +1324,7 @@ public virtual PlaceOrderResult PlaceOrder( UnitPriceExclTax = orderItem.UnitPriceExclTax, PriceInclTax = orderItem.PriceInclTax, PriceExclTax = orderItem.PriceExclTax, - TaxRate = orderItem.TaxRate, + TaxRate = orderItem.TaxRate, AttributeDescription = orderItem.AttributeDescription, AttributesXml = orderItem.AttributesXml, Quantity = orderItem.Quantity, @@ -1326,8 +1334,8 @@ public virtual PlaceOrderResult PlaceOrder( IsDownloadActivated = false, LicenseDownloadId = 0, ItemWeight = orderItem.ItemWeight, - BundleData = orderItem.BundleData, - ProductCost = orderItem.ProductCost, + BundleData = orderItem.BundleData, + ProductCost = orderItem.ProductCost, DeliveryTimeId = orderItem.DeliveryTimeId, DisplayDeliveryTime = orderItem.DisplayDeliveryTime }; @@ -1338,12 +1346,12 @@ public virtual PlaceOrderResult PlaceOrder( if (orderItem.Product.IsGiftCard) { _productAttributeParser.GetGiftCardAttribute( - orderItem.AttributesXml, - out var giftCardRecipientName, - out var giftCardRecipientEmail, - out var giftCardSenderName, - out var giftCardSenderEmail, - out var giftCardMessage); + orderItem.AttributesXml, + out var giftCardRecipientName, + out var giftCardRecipientEmail, + out var giftCardSenderName, + out var giftCardSenderEmail, + out var giftCardMessage); for (int i = 0; i < orderItem.Quantity; i++) { @@ -1360,54 +1368,54 @@ public virtual PlaceOrderResult PlaceOrder( SenderEmail = giftCardSenderEmail, Message = giftCardMessage, IsRecipientNotified = false, - CreatedOnUtc = utcNow + CreatedOnUtc = utcNow }; _giftCardService.InsertGiftCard(gc); } } - _productService.AdjustInventory(orderItem, true, orderItem.Quantity); + _productService.AdjustInventory(orderItem, true, orderItem.Quantity); } } // Discount usage history. - if (!processPaymentRequest.IsRecurringPayment) - { - foreach (var discount in appliedDiscounts) - { - var duh = new DiscountUsageHistory - { - Discount = discount, - Order = order, - CreatedOnUtc = utcNow - }; - _discountService.InsertDiscountUsageHistory(duh); - } - } + if (!processPaymentRequest.IsRecurringPayment) + { + foreach (var discount in appliedDiscounts) + { + var duh = new DiscountUsageHistory + { + Discount = discount, + Order = order, + CreatedOnUtc = utcNow + }; + _discountService.InsertDiscountUsageHistory(duh); + } + } // Gift card usage history. - if (!processPaymentRequest.IsRecurringPayment && cartTotal.AppliedGiftCards != null) - { - foreach (var agc in cartTotal.AppliedGiftCards) - { - var amountUsed = agc.AmountCanBeUsed; - var gcuh = new GiftCardUsageHistory - { - GiftCard = agc.GiftCard, - UsedWithOrder = order, - UsedValue = amountUsed, - CreatedOnUtc = utcNow - }; - agc.GiftCard.GiftCardUsageHistory.Add(gcuh); - _giftCardService.UpdateGiftCard(agc.GiftCard); - } - } + if (!processPaymentRequest.IsRecurringPayment && cartTotal.AppliedGiftCards != null) + { + foreach (var agc in cartTotal.AppliedGiftCards) + { + var amountUsed = agc.AmountCanBeUsed; + var gcuh = new GiftCardUsageHistory + { + GiftCard = agc.GiftCard, + UsedWithOrder = order, + UsedValue = amountUsed, + CreatedOnUtc = utcNow + }; + agc.GiftCard.GiftCardUsageHistory.Add(gcuh); + _giftCardService.UpdateGiftCard(agc.GiftCard); + } + } // Reward points history. if (cartTotal.RedeemedRewardPointsAmount > decimal.Zero) { customer.AddRewardPointsHistoryEntry(-cartTotal.RedeemedRewardPoints, - _localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId).FormatInvariant(order.GetOrderNumber()), + _localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId).FormatInvariant(order.GetOrderNumber()), order, cartTotal.RedeemedRewardPointsAmount); @@ -1425,7 +1433,7 @@ public virtual PlaceOrderResult PlaceOrder( TotalCycles = processPaymentRequest.RecurringTotalCycles, StartDateUtc = utcNow, IsActive = true, - CreatedOnUtc = utcNow, + CreatedOnUtc = utcNow, InitialOrder = order, }; _orderService.InsertRecurringPayment(rp); @@ -1457,40 +1465,64 @@ public virtual PlaceOrderResult PlaceOrder( } } - #endregion + #endregion + + #region Notifications, notes and attributes - #region Notifications, notes and attributes + // Notes, messages. + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); - // Notes, messages. - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); - // Send email notifications. var msg = _messageFactory.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.MerchantEmailQueued", msg.Email.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.MerchantEmailQueued", msg.Email.Id)); } - msg = _messageFactory.SendOrderPlacedCustomerNotification(order, order.CustomerLanguageId); + msg = _messageFactory.SendOrderPlacedCustomerNotification(order, order.CustomerLanguageId); if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", msg.Email.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", msg.Email.Id)); } // Check order status. CheckOrderStatus(order); - // Reset checkout data. + // Reset checkout data. if (!processPaymentRequest.IsRecurringPayment && !processPaymentRequest.IsMultiOrder) - { - _customerService.ResetCheckoutData(customer, processPaymentRequest.StoreId, true, true, true, clearCreditBalance: true); - } + { + _customerService.ResetCheckoutData(customer, processPaymentRequest.StoreId, true, true, true, clearCreditBalance: true); + } + + // Check for generic attributes to be inserted automatically. + foreach (var customProperty in processPaymentRequest.CustomProperties.Where(x => x.Key.HasValue() && x.Value.AutoCreateGenericAttribute)) + { + _genericAttributeService.SaveAttribute(order, customProperty.Key, customProperty.Value.Value, order.StoreId); + } - // Check for generic attributes to be inserted automatically. - foreach (var customProperty in processPaymentRequest.CustomProperties.Where(x => x.Key.HasValue() && x.Value.AutoCreateGenericAttribute)) - { - _genericAttributeService.SaveAttribute(order, customProperty.Key, customProperty.Value.Value, order.StoreId); - } + // Handle transiancy of uploaded files for checkout attributes + if (order.CheckoutAttributesXml.HasValue()) + { + var checkoutAttrs = _checkoutAttributeParser.ParseCheckoutAttributes(order.CheckoutAttributesXml); + + foreach (var attr in checkoutAttrs) + { + if (attr.AttributeControlType == AttributeControlType.FileUpload) + { + var value = _checkoutAttributeParser.ParseValues(order.CheckoutAttributesXml, attr.Id).FirstOrDefault(); + Guid.TryParse(value, out var downloadGuid); + if (downloadGuid != null) + { + var download = _downloadService.GetDownloadByGuid(downloadGuid); + if (download != null) + { + download.IsTransient = false; + _downloadService.UpdateDownload(download); + } + } + } + } + } // Uncomment this line to support transactions. //scope.Complete(); @@ -1502,51 +1534,51 @@ public virtual PlaceOrderResult PlaceOrder( { _customerActivityService.InsertActivity("PublicStore.PlaceOrder", T("ActivityLog.PublicStore.PlaceOrder", order.GetOrderNumber())); } - + if (order.PaymentStatus == PaymentStatus.Paid) { _eventPublisher.PublishOrderPaid(order); } - #endregion + #endregion - #region Newsletter subscription + #region Newsletter subscription - if (extraData.ContainsKey("SubscribeToNewsLetter") && _shoppingCartSettings.NewsLetterSubscription != CheckoutNewsLetterSubscription.None) - { - var addSubscription = extraData["SubscribeToNewsLetter"].ToBool(); + if (extraData.ContainsKey("SubscribeToNewsLetter") && _shoppingCartSettings.NewsLetterSubscription != CheckoutNewsLetterSubscription.None) + { + var addSubscription = extraData["SubscribeToNewsLetter"].ToBool(); - bool? nsResult = _newsLetterSubscriptionService.AddNewsLetterSubscriptionFor(addSubscription, customer.Email, order.StoreId); + bool? nsResult = _newsLetterSubscriptionService.AddNewsLetterSubscriptionFor(addSubscription, customer.Email, order.StoreId); - if (nsResult.HasValue) - { + if (nsResult.HasValue) + { _orderService.AddOrderNote(order, T(nsResult.Value ? "Admin.OrderNotice.NewsLetterSubscriptionAdded" : "Admin.OrderNotice.NewsLetterSubscriptionRemoved")); - } - } + } + } - #endregion - } - } + #endregion + } + } else { - result.AddError(T("Payment.PayingFailed")); + result.AddError(T("Payment.PayingFailed")); - foreach (var paymentError in processPaymentResult.Errors) - { - result.AddError(paymentError); - } + foreach (var paymentError in processPaymentResult.Errors) + { + result.AddError(paymentError); + } } } catch (Exception ex) { - Logger.Error(ex); + Logger.Error(ex); result.AddError(ex.Message); } - if (result.Errors.Any()) - { - Logger.Error(string.Join(" ", result.Errors)); - } + if (result.Errors.Any()) + { + Logger.Error(string.Join(" ", result.Errors)); + } return result; } @@ -1611,7 +1643,7 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen var paymentInfo = new ProcessPaymentRequest { - StoreId = initialOrder.StoreId, + StoreId = initialOrder.StoreId, CustomerId = customer.Id, OrderGuid = Guid.NewGuid(), IsRecurringPayment = true, @@ -1640,13 +1672,13 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen _orderService.UpdateRecurringPayment(recurringPayment); } else if (result.Errors.Count > 0) - { + { throw new SmartException(string.Join(" ", result.Errors)); } } catch (Exception exception) { - Logger.ErrorsAll(exception); + Logger.ErrorsAll(exception); throw; } } @@ -1671,7 +1703,7 @@ public virtual IList CancelRecurringPayment(RecurringPayment recurringPa { request.Order = initialOrder; - result = _paymentService.CancelRecurringPayment(request); + result = _paymentService.CancelRecurringPayment(request); if (result.Success) { @@ -1679,7 +1711,7 @@ public virtual IList CancelRecurringPayment(RecurringPayment recurringPa recurringPayment.IsActive = false; _orderService.UpdateRecurringPayment(recurringPayment); - _orderService.AddOrderNote(initialOrder, T("Admin.OrderNotice.RecurringPaymentCancelled")); + _orderService.AddOrderNote(initialOrder, T("Admin.OrderNotice.RecurringPaymentCancelled")); //notify a store owner _messageFactory.SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, _localizationSettings.DefaultAdminLanguageId); @@ -1687,17 +1719,17 @@ public virtual IList CancelRecurringPayment(RecurringPayment recurringPa } catch (Exception exception) { - if (result == null) - { - result = new CancelRecurringPaymentResult(); - } + if (result == null) + { + result = new CancelRecurringPaymentResult(); + } result.AddError(exception.ToAllMessages()); } - ProcessErrors(initialOrder, result.Errors, "Admin.OrderNotice.RecurringPaymentCancellationError"); + ProcessErrors(initialOrder, result.Errors, "Admin.OrderNotice.RecurringPaymentCancellationError"); - return result.Errors; + return result.Errors; } /// @@ -1767,7 +1799,7 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentSent", shipment.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentSent", shipment.Id)); if (notifyCustomer) { @@ -1775,7 +1807,7 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) var msg = _messageFactory.SendShipmentSentCustomerNotification(shipment, order.CustomerLanguageId); if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", msg.Email.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", msg.Email.Id)); } } @@ -1795,22 +1827,22 @@ public virtual void Deliver(Shipment shipment, bool notifyCustomer) var order = shipment.Order; if (order == null) - throw new SmartException(T("Order.NotFound", shipment.OrderId)); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); - if (shipment.DeliveryDateUtc.HasValue) + if (shipment.DeliveryDateUtc.HasValue) throw new SmartException(T("Shipment.AlreadyDelivered")); shipment.DeliveryDateUtc = DateTime.UtcNow; _shipmentService.UpdateShipment(shipment); - if (!order.CanAddItemsToShipment() && !order.HasItemsToDispatch() && !order.HasItemsToDeliver()) - { - order.ShippingStatusId = (int)ShippingStatus.Delivered; - } + if (!order.CanAddItemsToShipment() && !order.HasItemsToDispatch() && !order.HasItemsToDeliver()) + { + order.ShippingStatusId = (int)ShippingStatus.Delivered; + } _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentDelivered", shipment.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentDelivered", shipment.Id)); if (notifyCustomer) { @@ -1818,7 +1850,7 @@ public virtual void Deliver(Shipment shipment, bool notifyCustomer) var msg = _messageFactory.SendShipmentDeliveredCustomerNotification(shipment, order.CustomerLanguageId); if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", msg.Email.Id)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", msg.Email.Id)); } } @@ -1861,7 +1893,7 @@ public virtual void CancelOrder(Order order, bool notifyCustomer) // Cancel order. SetOrderStatus(order, OrderStatus.Cancelled, notifyCustomer); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCancelled")); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCancelled")); // Cancel recurring payments. var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); @@ -1871,88 +1903,93 @@ public virtual void CancelOrder(Order order, bool notifyCustomer) } // Adjust inventory. - foreach (var orderItem in order.OrderItems) - { - _productService.AdjustInventory(orderItem, false, orderItem.Quantity); - } + foreach (var orderItem in order.OrderItems) + { + _productService.AdjustInventory(orderItem, false, orderItem.Quantity); + } } - /// - /// Auto update order details - /// - /// Context parameters - public virtual void AutoUpdateOrderDetails(AutoUpdateOrderItemContext context) - { - var oi = context.OrderItem; + /// + /// Auto update order details + /// + /// Context parameters + public virtual void AutoUpdateOrderDetails(AutoUpdateOrderItemContext context) + { + var oi = context.OrderItem; - context.RewardPointsOld = context.RewardPointsNew = oi.Order.Customer.GetRewardPointsBalance(); + context.RewardPointsOld = context.RewardPointsNew = oi.Order.Customer.GetRewardPointsBalance(); - if (context.UpdateTotals && oi.Order.OrderStatusId <= (int)OrderStatus.Pending) - { - var currency = _currencyService.GetCurrencyByCode(oi.Order.CustomerCurrencyCode); + if (context.UpdateTotals && oi.Order.OrderStatusId <= (int)OrderStatus.Pending) + { + var currency = _currencyService.GetCurrencyByCode(oi.Order.CustomerCurrencyCode); - decimal priceInclTax = (context.QuantityNew * oi.UnitPriceInclTax).RoundIfEnabledFor(currency); - decimal priceExclTax = (context.QuantityNew * oi.UnitPriceExclTax).RoundIfEnabledFor(currency); + decimal priceInclTax = (context.QuantityNew * oi.UnitPriceInclTax).RoundIfEnabledFor(currency); + decimal priceExclTax = (context.QuantityNew * oi.UnitPriceExclTax).RoundIfEnabledFor(currency); - decimal deltaPriceInclTax = priceInclTax - (context.IsNewOrderItem ? decimal.Zero : oi.PriceInclTax); - decimal deltaPriceExclTax = priceExclTax - (context.IsNewOrderItem ? decimal.Zero : oi.PriceExclTax); + decimal deltaPriceInclTax = context.IsNewOrderItem + ? priceInclTax + : priceInclTax - (context.PriceInclTaxOld ?? oi.PriceInclTax); - oi.Quantity = context.QuantityNew; - oi.PriceInclTax = priceInclTax.RoundIfEnabledFor(currency); - oi.PriceExclTax = priceExclTax.RoundIfEnabledFor(currency); + decimal deltaPriceExclTax = context.IsNewOrderItem + ? priceExclTax + : priceExclTax - (context.PriceExclTaxOld ?? oi.PriceExclTax); - decimal subtotalInclTax = oi.Order.OrderSubtotalInclTax + deltaPriceInclTax; - decimal subtotalExclTax = oi.Order.OrderSubtotalExclTax + deltaPriceExclTax; + oi.Quantity = context.QuantityNew; + oi.PriceInclTax = priceInclTax.RoundIfEnabledFor(currency); + oi.PriceExclTax = priceExclTax.RoundIfEnabledFor(currency); - oi.Order.OrderSubtotalInclTax = subtotalInclTax.RoundIfEnabledFor(currency); - oi.Order.OrderSubtotalExclTax = subtotalExclTax.RoundIfEnabledFor(currency); + decimal subtotalInclTax = oi.Order.OrderSubtotalInclTax + deltaPriceInclTax; + decimal subtotalExclTax = oi.Order.OrderSubtotalExclTax + deltaPriceExclTax; - decimal discountInclTax = oi.DiscountAmountInclTax * context.QuantityChangeFactor; - decimal discountExclTax = oi.DiscountAmountExclTax * context.QuantityChangeFactor; + oi.Order.OrderSubtotalInclTax = subtotalInclTax.RoundIfEnabledFor(currency); + oi.Order.OrderSubtotalExclTax = subtotalExclTax.RoundIfEnabledFor(currency); - decimal deltaDiscountInclTax = discountInclTax - oi.DiscountAmountInclTax; - decimal deltaDiscountExclTax = discountExclTax - oi.DiscountAmountExclTax; + decimal discountInclTax = oi.DiscountAmountInclTax * context.QuantityChangeFactor; + decimal discountExclTax = oi.DiscountAmountExclTax * context.QuantityChangeFactor; - oi.DiscountAmountInclTax = discountInclTax.RoundIfEnabledFor(currency); - oi.DiscountAmountExclTax = discountExclTax.RoundIfEnabledFor(currency); + //decimal deltaDiscountInclTax = discountInclTax - oi.DiscountAmountInclTax; + //decimal deltaDiscountExclTax = discountExclTax - oi.DiscountAmountExclTax; - decimal total = Math.Max(oi.Order.OrderTotal + deltaPriceInclTax, 0); - decimal tax = Math.Max(oi.Order.OrderTax + (deltaPriceInclTax - deltaPriceExclTax), 0); + oi.DiscountAmountInclTax = discountInclTax.RoundIfEnabledFor(currency); + oi.DiscountAmountExclTax = discountExclTax.RoundIfEnabledFor(currency); - oi.Order.OrderTotal = total.RoundIfEnabledFor(currency); - oi.Order.OrderTax = tax.RoundIfEnabledFor(currency); + decimal total = Math.Max(oi.Order.OrderTotal + deltaPriceInclTax, 0); + decimal tax = Math.Max(oi.Order.OrderTax + (deltaPriceInclTax - deltaPriceExclTax), 0); - // Update tax rate value. - var deltaTax = deltaPriceInclTax - deltaPriceExclTax; - if (deltaTax != decimal.Zero) - { - var taxRates = oi.Order.TaxRatesDictionary; + oi.Order.OrderTotal = total.RoundIfEnabledFor(currency); + oi.Order.OrderTax = tax.RoundIfEnabledFor(currency); - taxRates[oi.TaxRate] = taxRates.ContainsKey(oi.TaxRate) - ? Math.Max(taxRates[oi.TaxRate] + deltaTax, 0) - : Math.Max(deltaTax, 0); + // Update tax rate value. + var deltaTax = deltaPriceInclTax - deltaPriceExclTax; + if (deltaTax != decimal.Zero) + { + var taxRates = oi.Order.TaxRatesDictionary; - oi.Order.TaxRates = FormatTaxRates(taxRates); - } + taxRates[oi.TaxRate] = taxRates.ContainsKey(oi.TaxRate) + ? Math.Max(taxRates[oi.TaxRate] + deltaTax, 0) + : Math.Max(deltaTax, 0); - _orderService.UpdateOrder(oi.Order); - } + oi.Order.TaxRates = FormatTaxRates(taxRates); + } - if (context.AdjustInventory && context.QuantityDelta != 0) - { - context.Inventory = _productService.AdjustInventory(oi, context.QuantityDelta > 0, Math.Abs(context.QuantityDelta)); - } + _orderService.UpdateOrder(oi.Order); + } + + if (context.AdjustInventory && context.QuantityDelta != 0) + { + context.Inventory = _productService.AdjustInventory(oi, context.QuantityDelta > 0, Math.Abs(context.QuantityDelta)); + } - if (context.UpdateRewardPoints && context.QuantityDelta < 0) - { - // we reduce but we do not award points subsequently. they can be awarded once per order anyway (see Order.RewardPointsWereAdded). - // UpdateRewardPoints only visible for unpending orders (see RewardPointsSettingsValidator). - // note: reducing can of cource only work if oi.UnitPriceExclTax has not been changed! - decimal reduceAmount = Math.Abs(context.QuantityDelta) * oi.UnitPriceInclTax; - ReduceRewardPoints(oi.Order, reduceAmount); - context.RewardPointsNew = oi.Order.Customer.GetRewardPointsBalance(); - } - } + if (context.UpdateRewardPoints && context.QuantityDelta < 0) + { + // We reduce but we do not award points subsequently. They can be awarded once per order anyway (see Order.RewardPointsWereAdded). + // UpdateRewardPoints only visible for unpending orders (see RewardPointsSettingsValidator). + // Note: reducing can of cource only work if oi.UnitPriceExclTax has not been changed! + decimal reduceAmount = Math.Abs(context.QuantityDelta) * oi.UnitPriceInclTax; + ReduceRewardPoints(oi.Order, reduceAmount); + context.RewardPointsNew = oi.Order.Customer.GetRewardPointsBalance(); + } + } /// @@ -1986,49 +2023,49 @@ public virtual void MarkAsAuthorized(Order order) order.PaymentStatusId = (int)PaymentStatus.Authorized; _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsAuthorized")); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsAuthorized")); //check order status CheckOrderStatus(order); } - /// - /// Gets a value indicating whether the order can be marked as completed - /// - /// Order - /// A value indicating whether the order can be marked as completed - public virtual bool CanCompleteOrder(Order order) - { - if (order == null) - throw new ArgumentNullException("order"); - - return (order.OrderStatus != OrderStatus.Complete && order.OrderStatus != OrderStatus.Cancelled); - } - - /// - /// Marks the order as completed - /// - /// Order - public virtual void CompleteOrder(Order order) - { + /// + /// Gets a value indicating whether the order can be marked as completed + /// + /// Order + /// A value indicating whether the order can be marked as completed + public virtual bool CanCompleteOrder(Order order) + { + if (order == null) + throw new ArgumentNullException("order"); + + return (order.OrderStatus != OrderStatus.Complete && order.OrderStatus != OrderStatus.Cancelled); + } + + /// + /// Marks the order as completed + /// + /// Order + public virtual void CompleteOrder(Order order) + { if (!CanCompleteOrder(order)) throw new SmartException(T("Order.CannotMarkCompleted")); - if (CanMarkOrderAsPaid(order)) - { - MarkOrderAsPaid(order); - } + if (CanMarkOrderAsPaid(order)) + { + MarkOrderAsPaid(order); + } - if (order.ShippingStatus != ShippingStatus.ShippingNotRequired) - { - order.ShippingStatusId = (int)ShippingStatus.Delivered; - } + if (order.ShippingStatus != ShippingStatus.ShippingNotRequired) + { + order.ShippingStatusId = (int)ShippingStatus.Delivered; + } - _orderService.UpdateOrder(order); + _orderService.UpdateOrder(order); - CheckOrderStatus(order); - } + CheckOrderStatus(order); + } /// @@ -2074,10 +2111,10 @@ public virtual IList Capture(Order order) if (result.Success) { var paidDate = order.PaidDateUtc; - if (result.NewPaymentStatus == PaymentStatus.Paid) - { - paidDate = DateTime.UtcNow; - } + if (result.NewPaymentStatus == PaymentStatus.Paid) + { + paidDate = DateTime.UtcNow; + } order.CaptureTransactionId = result.CaptureTransactionId; order.CaptureTransactionResult = result.CaptureTransactionResult; @@ -2086,7 +2123,7 @@ public virtual IList Capture(Order order) _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCaptured")); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCaptured")); CheckOrderStatus(order); @@ -2099,17 +2136,17 @@ public virtual IList Capture(Order order) } catch (Exception exception) { - if (result == null) - { - result = new CapturePaymentResult(); - } + if (result == null) + { + result = new CapturePaymentResult(); + } - result.AddError(exception.ToAllMessages()); - } + result.AddError(exception.ToAllMessages()); + } - ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderCaptureError"); + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderCaptureError"); - return result.Errors; + return result.Errors; } /// @@ -2148,9 +2185,9 @@ public virtual void MarkOrderAsPaid(Order order) order.PaymentStatusId = (int)PaymentStatus.Paid; order.PaidDateUtc = DateTime.UtcNow; - _orderService.UpdateOrder(order); + _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPaid")); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPaid")); CheckOrderStatus(order); @@ -2176,20 +2213,20 @@ public virtual bool CanRefund(Order order) if (order.OrderTotal == decimal.Zero) return false; - // Only partial refunds allowed if already refunded. - if (order.RefundedAmount > decimal.Zero) - return false; + // Only partial refunds allowed if already refunded. + if (order.RefundedAmount > decimal.Zero) + return false; - // Uncomment the lines below in order to allow this operation for cancelled orders. - //if (order.OrderStatus == OrderStatus.Cancelled) - // return false; + // Uncomment the lines below in order to allow this operation for cancelled orders. + //if (order.OrderStatus == OrderStatus.Cancelled) + // return false; - if (order.PaymentStatus == PaymentStatus.Paid && _paymentService.SupportRefund(order.PaymentMethodSystemName)) + if (order.PaymentStatus == PaymentStatus.Paid && _paymentService.SupportRefund(order.PaymentMethodSystemName)) return true; return false; } - + /// /// Refunds an order (from admin panel) /// @@ -2211,7 +2248,7 @@ public virtual IList Refund(Order order) request.AmountToRefund = order.OrderTotal; request.IsPartialRefund = false; - result = _paymentService.Refund(request); + result = _paymentService.Refund(request); if (result.Success) { @@ -2224,7 +2261,7 @@ public virtual IList Refund(Order order) _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderRefunded", _priceFormatter.FormatPrice(request.AmountToRefund, true, false))); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderRefunded", _priceFormatter.FormatPrice(request.AmountToRefund, true, false))); CheckOrderStatus(order); } @@ -2232,17 +2269,17 @@ public virtual IList Refund(Order order) } catch (Exception exception) { - if (result == null) - { - result = new RefundPaymentResult(); - } + if (result == null) + { + result = new RefundPaymentResult(); + } - result.AddError(exception.ToAllMessages()); + result.AddError(exception.ToAllMessages()); } - ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderRefundError"); + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderRefundError"); - return result.Errors; + return result.Errors; } /// @@ -2258,15 +2295,15 @@ public virtual bool CanRefundOffline(Order order) if (order.OrderTotal == decimal.Zero) return false; - // Only partial refunds allowed if already refunded. - if (order.RefundedAmount > decimal.Zero) - return false; + // Only partial refunds allowed if already refunded. + if (order.RefundedAmount > decimal.Zero) + return false; - // Uncomment the lines below in order to allow this operation for cancelled orders. - //if (order.OrderStatus == OrderStatus.Cancelled) - // return false; + // Uncomment the lines below in order to allow this operation for cancelled orders. + //if (order.OrderStatus == OrderStatus.Cancelled) + // return false; - if (order.PaymentStatus == PaymentStatus.Paid) + if (order.PaymentStatus == PaymentStatus.Paid) return true; return false; @@ -2296,7 +2333,7 @@ public virtual void RefundOffline(Order order) _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); CheckOrderStatus(order); } @@ -2370,7 +2407,7 @@ public virtual IList PartiallyRefund(Order order, decimal amountToRefund _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); //check order status CheckOrderStatus(order); @@ -2378,17 +2415,17 @@ public virtual IList PartiallyRefund(Order order, decimal amountToRefund } catch (Exception exception) { - if (result == null) - { - result = new RefundPaymentResult(); - } + if (result == null) + { + result = new RefundPaymentResult(); + } - result.AddError(exception.ToAllMessages()); - } + result.AddError(exception.ToAllMessages()); + } - ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderPartiallyRefundError"); + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderPartiallyRefundError"); - return result.Errors; + return result.Errors; } /// @@ -2432,7 +2469,7 @@ public virtual void PartiallyRefundOffline(Order order, decimal amountToRefund) { if (order == null) throw new ArgumentNullException("order"); - + if (!CanPartiallyRefundOffline(order, amountToRefund)) throw new SmartException(T("Order.CannotPartialRefund")); @@ -2446,7 +2483,7 @@ public virtual void PartiallyRefundOffline(Order order, decimal amountToRefund) _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); //check order status CheckOrderStatus(order); @@ -2506,7 +2543,7 @@ public virtual IList Void(Order order) _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderVoided")); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderVoided")); //check order status CheckOrderStatus(order); @@ -2514,15 +2551,15 @@ public virtual IList Void(Order order) } catch (Exception exception) { - if (result == null) - { - result = new VoidPaymentResult(); - } + if (result == null) + { + result = new VoidPaymentResult(); + } - result.AddError(exception.ToAllMessages()); - } + result.AddError(exception.ToAllMessages()); + } - ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderVoidError"); + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderVoidError"); return result.Errors; } @@ -2560,13 +2597,13 @@ public virtual void VoidOffline(Order order) throw new ArgumentNullException("order"); if (!CanVoidOffline(order)) - throw new SmartException(T("Order.CannotVoid")); + throw new SmartException(T("Order.CannotVoid")); - order.PaymentStatusId = (int)PaymentStatus.Voided; + order.PaymentStatusId = (int)PaymentStatus.Voided; _orderService.UpdateOrder(order); - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsVoided")); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsVoided")); //check orer status CheckOrderStatus(order); @@ -2585,34 +2622,29 @@ public virtual void ReOrder(Order order) foreach (var orderItem in order.OrderItems) { - var isBundle = (orderItem.Product.ProductType == ProductType.BundledProduct); + var isBundle = (orderItem.Product.ProductType == ProductType.BundledProduct); - var addToCartContext = new AddToCartContext(); + var addToCartContext = new AddToCartContext(); - addToCartContext.Warnings = _shoppingCartService.AddToCart(order.Customer, orderItem.Product, ShoppingCartType.ShoppingCart, order.StoreId, - orderItem.AttributesXml, isBundle ? decimal.Zero : orderItem.UnitPriceExclTax, orderItem.Quantity, false, addToCartContext); + addToCartContext.Warnings = _shoppingCartService.AddToCart(order.Customer, orderItem.Product, ShoppingCartType.ShoppingCart, order.StoreId, + orderItem.AttributesXml, isBundle ? decimal.Zero : orderItem.UnitPriceExclTax, orderItem.Quantity, false, addToCartContext); - if (isBundle && orderItem.BundleData.HasValue() && addToCartContext.Warnings.Count == 0) - { - foreach (var bundleData in orderItem.GetBundleData()) - { - var bundleItem = _productService.GetBundleItemById(bundleData.BundleItemId); - addToCartContext.BundleItem = bundleItem; + if (isBundle && orderItem.BundleData.HasValue() && addToCartContext.Warnings.Count == 0) + { + foreach (var bundleData in orderItem.GetBundleData()) + { + var bundleItem = _productService.GetBundleItemById(bundleData.BundleItemId); + addToCartContext.BundleItem = bundleItem; - addToCartContext.Warnings = _shoppingCartService.AddToCart(order.Customer, bundleItem.Product, ShoppingCartType.ShoppingCart, order.StoreId, - bundleData.AttributesXml, decimal.Zero, bundleData.Quantity, false, addToCartContext); - } - } + addToCartContext.Warnings = _shoppingCartService.AddToCart(order.Customer, bundleItem.Product, ShoppingCartType.ShoppingCart, order.StoreId, + bundleData.AttributesXml, decimal.Zero, bundleData.Quantity, false, addToCartContext); + } + } - _shoppingCartService.AddToCartStoring(addToCartContext); + _shoppingCartService.AddToCartStoring(addToCartContext); } } - - /// - /// Check whether return request is allowed - /// - /// Order - /// Result + public virtual bool IsReturnRequestAllowed(Order order) { if (!_orderSettings.ReturnRequestsEnabled) @@ -2639,134 +2671,158 @@ public virtual bool IsReturnRequestAllowed(Order order) return numberOfDaysReturnRequestAvailableValid; } + public virtual (bool valid, decimal orderTotalMinimum) IsAboveOrderTotalMinimum(IList cart, int[] customerRoleIds) + { + var query = _customerService.GetAllCustomerRoles().SourceQuery + .Where(x => x.OrderTotalMinimum > decimal.Zero && customerRoleIds.Contains(x.Id)); + + var customerRole = _orderSettings.MultipleOrderTotalRestrictionsExpandRange + ? query.OrderBy(x => x.OrderTotalMinimum).FirstOrDefault() + : query.OrderByDescending(x => x.OrderTotalMinimum).FirstOrDefault(); + + var minimumOrderTotal = customerRole == null + ? _orderSettings.OrderTotalMinimum : customerRole.OrderTotalMinimum; + + return IsAboveOrderTotalMinimum(cart, minimumOrderTotal ?? decimal.Zero); + } /// - /// Valdiate minimum order sub-total amount + /// Validate order total minimum. /// - /// Shopping cart - /// true - OK; false - minimum order sub-total amount is not reached - public virtual bool ValidateMinOrderSubtotalAmount(IList cart) + /// Shopping cart, minimum order total + /// true - OK; false - minimum order total is not reached + private (bool valid, decimal orderTotalMinimum) IsAboveOrderTotalMinimum(IList cart, decimal minimumOrderTotal) { if (cart == null) throw new ArgumentNullException("cart"); - //min order amount sub-total validation - if (cart.Count > 0 && _orderSettings.MinOrderSubtotalAmount > decimal.Zero) + if (cart.Count > 0 && minimumOrderTotal > decimal.Zero) { - decimal orderSubTotalDiscountAmountBase = decimal.Zero; - Discount orderSubTotalAppliedDiscount = null; - decimal subTotalWithoutDiscountBase = decimal.Zero; - decimal subTotalWithDiscountBase = decimal.Zero; - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - out orderSubTotalDiscountAmountBase, out orderSubTotalAppliedDiscount, out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); + _orderTotalCalculationService.GetShoppingCartSubTotal( + cart, out _, out _, out var cartSubtotal, out _); - if (subTotalWithoutDiscountBase < _orderSettings.MinOrderSubtotalAmount) - return false; + if (cartSubtotal < minimumOrderTotal) + return (false, minimumOrderTotal); } - return true; + return (true, minimumOrderTotal); + } + + public virtual (bool valid, decimal orderTotalMaximum) IsBelowOrderTotalMaximum(IList cart, int[] customerRoleIds) + { + var query = _customerService.GetAllCustomerRoles().SourceQuery + .Where(x => x.OrderTotalMaximum > decimal.Zero && customerRoleIds.Contains(x.Id)); + + var customerRole = _orderSettings.MultipleOrderTotalRestrictionsExpandRange + ? query.OrderByDescending(x => x.OrderTotalMaximum).FirstOrDefault() + : query.OrderBy(x => x.OrderTotalMaximum).FirstOrDefault(); + + var maximumOrderTotal = customerRole == null + ? _orderSettings.OrderTotalMaximum : customerRole.OrderTotalMaximum; + + return IsBelowOrderTotalMaximum(cart, maximumOrderTotal ?? decimal.Zero); } /// - /// Valdiate minimum order total amount + /// Validate order total maximum. /// - /// Shopping cart - /// true - OK; false - minimum order total amount is not reached - public virtual bool ValidateMinOrderTotalAmount(IList cart) + /// Shopping cart, maximum order total + /// true - OK; false - maximum order total is exceeded + private (bool valid, decimal orderTotalMaximum) IsBelowOrderTotalMaximum(IList cart, decimal maximumOrderTotal) { if (cart == null) throw new ArgumentNullException("cart"); - if (cart.Count > 0 && _orderSettings.MinOrderTotalAmount > decimal.Zero) + if (cart.Count > 0 && maximumOrderTotal > decimal.Zero) { - decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); + _orderTotalCalculationService.GetShoppingCartSubTotal( + cart, out _, out _, out var cartSubtotal, out _); - if (shoppingCartTotalBase.HasValue && shoppingCartTotalBase.Value < _orderSettings.MinOrderTotalAmount) - return false; + if (cartSubtotal > maximumOrderTotal) + return (false, maximumOrderTotal); } - return true; + return (true, maximumOrderTotal); } - public virtual Shipment AddShipment( + public virtual Shipment AddShipment( Order order, string trackingNumber, string trackingUrl, Dictionary quantities) - { - Guard.NotNull(order, nameof(order)); - - Shipment shipment = null; - decimal? totalWeight = null; - - foreach (var orderItem in order.OrderItems) - { - if (!orderItem.Product.IsShipEnabled) - continue; - - // Ensure that this product can be shipped (have at least one item to ship). - var maxQtyToAdd = orderItem.GetItemsCanBeAddedToShipmentCount(); - if (maxQtyToAdd <= 0) - continue; - - var qtyToAdd = 0; - - if (quantities != null && quantities.ContainsKey(orderItem.Id)) - qtyToAdd = quantities[orderItem.Id]; - else if (quantities == null) - qtyToAdd = maxQtyToAdd; - - if (qtyToAdd <= 0) - continue; - - if (qtyToAdd > maxQtyToAdd) - qtyToAdd = maxQtyToAdd; - - var orderItemTotalWeight = orderItem.ItemWeight.HasValue ? orderItem.ItemWeight * qtyToAdd : null; - if (orderItemTotalWeight.HasValue) - { - if (!totalWeight.HasValue) - totalWeight = 0; - - totalWeight += orderItemTotalWeight.Value; - } - - if (shipment == null) - { - shipment = new Shipment - { - OrderId = order.Id, - Order = order, // Otherwise order updated event would not be fired during InsertShipment. - TrackingNumber = trackingNumber, + { + Guard.NotNull(order, nameof(order)); + + Shipment shipment = null; + decimal? totalWeight = null; + + foreach (var orderItem in order.OrderItems) + { + if (!orderItem.Product.IsShipEnabled) + continue; + + // Ensure that this product can be shipped (have at least one item to ship). + var maxQtyToAdd = orderItem.GetItemsCanBeAddedToShipmentCount(); + if (maxQtyToAdd <= 0) + continue; + + var qtyToAdd = 0; + + if (quantities != null && quantities.ContainsKey(orderItem.Id)) + qtyToAdd = quantities[orderItem.Id]; + else if (quantities == null) + qtyToAdd = maxQtyToAdd; + + if (qtyToAdd <= 0) + continue; + + if (qtyToAdd > maxQtyToAdd) + qtyToAdd = maxQtyToAdd; + + var orderItemTotalWeight = orderItem.ItemWeight.HasValue ? orderItem.ItemWeight * qtyToAdd : null; + if (orderItemTotalWeight.HasValue) + { + if (!totalWeight.HasValue) + totalWeight = 0; + + totalWeight += orderItemTotalWeight.Value; + } + + if (shipment == null) + { + shipment = new Shipment + { + OrderId = order.Id, + Order = order, // Otherwise order updated event would not be fired during InsertShipment. + TrackingNumber = trackingNumber, TrackingUrl = trackingUrl, - TotalWeight = null, - ShippedDateUtc = null, - DeliveryDateUtc = null, - CreatedOnUtc = DateTime.UtcNow, - }; - } + TotalWeight = null, + ShippedDateUtc = null, + DeliveryDateUtc = null, + CreatedOnUtc = DateTime.UtcNow, + }; + } - var shipmentItem = new ShipmentItem - { - OrderItemId = orderItem.Id, - Quantity = qtyToAdd - }; + var shipmentItem = new ShipmentItem + { + OrderItemId = orderItem.Id, + Quantity = qtyToAdd + }; - shipment.ShipmentItems.Add(shipmentItem); - } + shipment.ShipmentItems.Add(shipmentItem); + } - if (shipment != null && shipment.ShipmentItems.Count > 0) - { - shipment.TotalWeight = totalWeight; + if (shipment != null && shipment.ShipmentItems.Count > 0) + { + shipment.TotalWeight = totalWeight; - _shipmentService.InsertShipment(shipment); + _shipmentService.InsertShipment(shipment); - return shipment; - } + return shipment; + } - return null; - } + return null; + } #endregion } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderReportService.cs b/src/Libraries/SmartStore.Services/Orders/OrderReportService.cs index 59527880f6..a3e30eea97 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderReportService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderReportService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data.Entity; using System.Globalization; using System.Linq; @@ -21,7 +20,8 @@ public partial class OrderReportService : IOrderReportService private readonly IRepository _productRepository; private readonly IDateTimeHelper _dateTimeHelper; - public OrderReportService(IRepository orderRepository, + public OrderReportService( + IRepository orderRepository, IRepository orderItemRepository, IRepository productRepository, IDateTimeHelper dateTimeHelper) @@ -181,87 +181,68 @@ public virtual OrderAverageReportLineSummary OrderAverageReport(int storeId, Ord return item; } - public virtual IList BestSellersReport( + public virtual IPagedList BestSellersReport( int storeId, DateTime? startTime, DateTime? endTime, - OrderStatus? os, - PaymentStatus? ps, - ShippingStatus? ss, + OrderStatus? orderStatus, + PaymentStatus? paymentStatus, + ShippingStatus? shippingStatus, int billingCountryId = 0, - int recordsToReturn = 5, - int orderBy = 1, + int pageIndex = 0, + int pageSize = int.MaxValue, + ReportSorting sorting = ReportSorting.ByQuantityDesc, bool showHidden = false) { - int? orderStatusId = null; - if (os.HasValue) - orderStatusId = (int)os.Value; + var orderStatusId = orderStatus.HasValue ? (int)orderStatus.Value : (int?)null; + var paymentStatusId = paymentStatus.HasValue ? (int)paymentStatus.Value : (int?)null; + var shippingStatusId = shippingStatus.HasValue ? (int)shippingStatus : (int?)null; - int? paymentStatusId = null; - if (ps.HasValue) - paymentStatusId = (int)ps.Value; - - int? shippingStatusId = null; - if (ss.HasValue) - shippingStatusId = (int)ss.Value; - - - var query1 = from orderItem in _orderItemRepository.Table - join o in _orderRepository.Table on orderItem.OrderId equals o.Id - join p in _productRepository.Table on orderItem.ProductId equals p.Id - where (storeId == 0 || storeId == o.StoreId) && - (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && - (!endTime.HasValue || endTime.Value >= o.CreatedOnUtc) && - (!orderStatusId.HasValue || orderStatusId == o.OrderStatusId) && - (!paymentStatusId.HasValue || paymentStatusId == o.PaymentStatusId) && - (!shippingStatusId.HasValue || shippingStatusId == o.ShippingStatusId) && - (!o.Deleted) && - (!p.Deleted) && (!p.IsSystemProduct) && - (billingCountryId == 0 || o.BillingAddress.CountryId == billingCountryId) && - (showHidden || p.Published) - select orderItem; + var query = + from orderItem in _orderItemRepository.TableUntracked + join o in _orderRepository.TableUntracked on orderItem.OrderId equals o.Id + join p in _productRepository.TableUntracked on orderItem.ProductId equals p.Id + where (storeId == 0 || storeId == o.StoreId) && + (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && + (!endTime.HasValue || endTime.Value >= o.CreatedOnUtc) && + (!orderStatusId.HasValue || orderStatusId == o.OrderStatusId) && + (!paymentStatusId.HasValue || paymentStatusId == o.PaymentStatusId) && + (!shippingStatusId.HasValue || shippingStatusId == o.ShippingStatusId) && + (!o.Deleted) && + (!p.Deleted) && (!p.IsSystemProduct) && + (billingCountryId == 0 || o.BillingAddress.CountryId == billingCountryId) && + (showHidden || p.Published) + select orderItem; - var query2 = - //group by products - from orderItem in query1 + // Group by product ID. + var groupedQuery = + from orderItem in query group orderItem by orderItem.ProductId into g - select new + select new BestsellersReportLine { - EntityId = g.Key, + ProductId = g.Key, TotalAmount = g.Sum(x => x.PriceExclTax), - TotalQuantity = g.Sum(x => x.Quantity), + TotalQuantity = g.Sum(x => x.Quantity) }; - switch (orderBy) + switch (sorting) { - case 1: - { - query2 = query2.OrderByDescending(x => x.TotalQuantity).ThenByDescending(x => x.TotalAmount); - } + case ReportSorting.ByAmountAsc: + groupedQuery = groupedQuery.OrderBy(x => x.TotalAmount); + break; + case ReportSorting.ByAmountDesc: + groupedQuery = groupedQuery.OrderByDescending(x => x.TotalAmount); break; - case 2: - { - query2 = query2.OrderByDescending(x => x.TotalAmount); - } + case ReportSorting.ByQuantityAsc: + groupedQuery = groupedQuery.OrderBy(x => x.TotalQuantity).ThenByDescending(x => x.TotalAmount); break; + case ReportSorting.ByQuantityDesc: default: - throw new ArgumentException("Wrong orderBy parameter", "orderBy"); + groupedQuery = groupedQuery.OrderByDescending(x => x.TotalQuantity).ThenByDescending(x => x.TotalAmount); + break; } - if (recordsToReturn != 0 && recordsToReturn != int.MaxValue) - query2 = query2.Take(() => recordsToReturn); - - var result = query2.ToList().Select(x => - { - var reportLine = new BestsellersReportLine() - { - ProductId = x.EntityId, - TotalAmount = x.TotalAmount, - TotalQuantity = x.TotalQuantity - }; - return reportLine; - }).ToList(); - + var result = new PagedList(groupedQuery, pageIndex, pageSize); return result; } @@ -332,19 +313,18 @@ public virtual IPagedList ProductsNeverSold(DateTime? startTime, DateTi { var groupedProductId = (int)ProductType.GroupedProduct; - // This inner query should retrieve all purchased order product varint identifiers. - var query1 = (from orderItem in _orderItemRepository.Table - join o in _orderRepository.Table on orderItem.OrderId equals o.Id - where (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && - (!endTime.HasValue || endTime.Value >= o.CreatedOnUtc) && - (!o.Deleted) + var query1 = (from orderItem in _orderItemRepository.TableUntracked + join o in _orderRepository.TableUntracked on orderItem.OrderId equals o.Id + where !o.Deleted && + (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && + (!endTime.HasValue || endTime.Value >= o.CreatedOnUtc) select orderItem.ProductId).Distinct(); - var query2 = from p in _productRepository.Table + var query2 = from p in _productRepository.TableUntracked where !query1.Contains(p.Id) && - p.ProductTypeId != groupedProductId && - !p.Deleted && - (showHidden || p.Published) + !p.Deleted && !p.IsSystemProduct && + (showHidden || p.Published) && + p.ProductTypeId != groupedProductId orderby p.Name select p; @@ -387,8 +367,6 @@ public virtual IPagedList GetIncompleteOrders(int storeId, DateT query = query.Where(x => x.ShippingStatusId == (int)ShippingStatus.NotYetShipped || x.PaymentStatusId == (int)PaymentStatus.Pending - || x.OrderStatusId == (int)OrderStatus.Pending - || x.OrderStatusId == (int)OrderStatus.Processing ); var dataPoints = query.Select(x => new OrderDataPoint { @@ -424,7 +402,7 @@ public virtual IPagedList GetOrdersDashboardData(int storeId, Da { CreatedOn = x.CreatedOnUtc, OrderTotal = x.OrderTotal, - OrderStatusId = x.OrderStatusId + OrderStatusId = x.OrderStatusId }); return new PagedList(dataPoints, pageIndex, pageSize); } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index 0cb641c385..92ec02f77f 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -19,7 +19,7 @@ public partial class OrderService : IOrderService private readonly IRepository _orderRepository; private readonly IRepository _orderItemRepository; private readonly IRepository _orderNoteRepository; - private readonly IRepository _productRepository; + private readonly IRepository _productRepository; private readonly IRepository _recurringPaymentRepository; private readonly IRepository _customerRepository; private readonly IRepository _returnRequestRepository; @@ -28,16 +28,16 @@ public partial class OrderService : IOrderService public OrderService(IRepository orderRepository, IRepository orderItemRepository, IRepository orderNoteRepository, - IRepository productRepository, + IRepository productRepository, IRepository recurringPaymentRepository, - IRepository customerRepository, + IRepository customerRepository, IRepository returnRequestRepository, IEventPublisher eventPublisher) { _orderRepository = orderRepository; _orderItemRepository = orderItemRepository; _orderNoteRepository = orderNoteRepository; - _productRepository = productRepository; + _productRepository = productRepository; _recurringPaymentRepository = recurringPaymentRepository; _customerRepository = customerRepository; _returnRequestRepository = returnRequestRepository; @@ -62,9 +62,9 @@ where orderIds.Contains(o.Id) select o; var orders = query.ToList(); - // sort by passed identifier sequence - return orders.OrderBySequence(orderIds).ToList(); - } + // sort by passed identifier sequence + return orders.OrderBySequence(orderIds).ToList(); + } public virtual Order GetOrderByNumber(string orderNumber) { @@ -99,114 +99,114 @@ public virtual Order GetOrderByGuid(Guid orderGuid) return order; } - public virtual Order GetOrderByPaymentAuthorization(string paymentMethodSystemName, string authorizationTransactionId) - { - if (paymentMethodSystemName.IsEmpty() || authorizationTransactionId.IsEmpty()) - return null; - - var query = - from o in _orderRepository.Table - where o.PaymentMethodSystemName == paymentMethodSystemName && o.AuthorizationTransactionId == authorizationTransactionId - select o; - - return query.FirstOrDefault(); - } - - public virtual Order GetOrderByPaymentCapture(string paymentMethodSystemName, string captureTransactionId) - { - if (paymentMethodSystemName.IsEmpty() || captureTransactionId.IsEmpty()) - return null; - - var query = - from o in _orderRepository.Table - where o.PaymentMethodSystemName == paymentMethodSystemName && o.CaptureTransactionId == captureTransactionId - select o; - - return query.FirstOrDefault(); - } - - public virtual IQueryable GetOrders( - int storeId, - int customerId, - DateTime? startTime, - DateTime? endTime, - int[] orderStatusIds, - int[] paymentStatusIds, - int[] shippingStatusIds, - string billingEmail, - string orderNumber, - string billingName = null, + public virtual Order GetOrderByPaymentAuthorization(string paymentMethodSystemName, string authorizationTransactionId) + { + if (paymentMethodSystemName.IsEmpty() || authorizationTransactionId.IsEmpty()) + return null; + + var query = + from o in _orderRepository.Table + where o.PaymentMethodSystemName == paymentMethodSystemName && o.AuthorizationTransactionId == authorizationTransactionId + select o; + + return query.FirstOrDefault(); + } + + public virtual Order GetOrderByPaymentCapture(string paymentMethodSystemName, string captureTransactionId) + { + if (paymentMethodSystemName.IsEmpty() || captureTransactionId.IsEmpty()) + return null; + + var query = + from o in _orderRepository.Table + where o.PaymentMethodSystemName == paymentMethodSystemName && o.CaptureTransactionId == captureTransactionId + select o; + + return query.FirstOrDefault(); + } + + public virtual IQueryable GetOrders( + int storeId, + int customerId, + DateTime? startTime, + DateTime? endTime, + int[] orderStatusIds, + int[] paymentStatusIds, + int[] shippingStatusIds, + string billingEmail, + string orderNumber, + string billingName = null, string[] paymentMethods = null) - { - var query = _orderRepository.Table; + { + var query = _orderRepository.Table; if (customerId > 0) query = query.Where(x => x.CustomerId == customerId); if (storeId > 0) - query = query.Where(x => x.StoreId == storeId); + query = query.Where(x => x.StoreId == storeId); - if (startTime.HasValue) - query = query.Where(x => startTime.Value <= x.CreatedOnUtc); + if (startTime.HasValue) + query = query.Where(x => startTime.Value <= x.CreatedOnUtc); - if (endTime.HasValue) - query = query.Where(x => endTime.Value >= x.CreatedOnUtc); + if (endTime.HasValue) + query = query.Where(x => endTime.Value >= x.CreatedOnUtc); - if (billingEmail.HasValue()) - query = query.Where(x => x.BillingAddress != null && !String.IsNullOrEmpty(x.BillingAddress.Email) && x.BillingAddress.Email.Contains(billingEmail)); + if (billingEmail.HasValue()) + query = query.Where(x => x.BillingAddress != null && !String.IsNullOrEmpty(x.BillingAddress.Email) && x.BillingAddress.Email.Contains(billingEmail)); - if (billingName.HasValue()) - { - query = query.Where(x => x.BillingAddress != null && ( - (!String.IsNullOrEmpty(x.BillingAddress.LastName) && x.BillingAddress.LastName.Contains(billingName)) || - (!String.IsNullOrEmpty(x.BillingAddress.FirstName) && x.BillingAddress.FirstName.Contains(billingName)) - )); - } + if (billingName.HasValue()) + { + query = query.Where(x => x.BillingAddress != null && ( + (!String.IsNullOrEmpty(x.BillingAddress.LastName) && x.BillingAddress.LastName.Contains(billingName)) || + (!String.IsNullOrEmpty(x.BillingAddress.FirstName) && x.BillingAddress.FirstName.Contains(billingName)) + )); + } - if (orderNumber.HasValue()) - query = query.Where(x => x.OrderNumber.ToLower().Contains(orderNumber.ToLower())); + if (orderNumber.HasValue()) + query = query.Where(x => x.OrderNumber.ToLower().Contains(orderNumber.ToLower())); - if (orderStatusIds != null && orderStatusIds.Count() > 0) - query = query.Where(x => orderStatusIds.Contains(x.OrderStatusId)); + if (orderStatusIds != null && orderStatusIds.Count() > 0) + query = query.Where(x => orderStatusIds.Contains(x.OrderStatusId)); - if (paymentStatusIds != null && paymentStatusIds.Count() > 0) - query = query.Where(x => paymentStatusIds.Contains(x.PaymentStatusId)); + if (paymentStatusIds != null && paymentStatusIds.Count() > 0) + query = query.Where(x => paymentStatusIds.Contains(x.PaymentStatusId)); - if (shippingStatusIds != null && shippingStatusIds.Count() > 0) - query = query.Where(x => shippingStatusIds.Contains(x.ShippingStatusId)); + if (shippingStatusIds != null && shippingStatusIds.Count() > 0) + query = query.Where(x => shippingStatusIds.Contains(x.ShippingStatusId)); if (paymentMethods != null && paymentMethods.Any()) { query = query.Where(x => paymentMethods.Contains(x.PaymentMethodSystemName)); } - query = query.Where(x => !x.Deleted); + query = query.Where(x => !x.Deleted); - return query; - } + return query; + } - public virtual IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, DateTime? endTime, - int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, - string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null, + public virtual IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, DateTime? endTime, + int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, + string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null, string[] paymentMethods = null) { - var query = GetOrders(storeId, customerId, startTime, endTime, orderStatusIds, paymentStatusIds, shippingStatusIds, - billingEmail, orderNumber, billingName, paymentMethods); - - query = query.OrderByDescending(x => x.CreatedOnUtc); - - if (orderGuid.HasValue()) - { - // Filter by GUID. Filter in BLL because EF doesn't support casting of GUID to string - var orders = query.ToList(); - orders = orders.FindAll(x => x.OrderGuid.ToString().ToLowerInvariant().Contains(orderGuid.ToLowerInvariant())); - - return new PagedList(orders, pageIndex, pageSize); - } - else - { - return new PagedList(query, pageIndex, pageSize); - } + var query = GetOrders(storeId, customerId, startTime, endTime, orderStatusIds, paymentStatusIds, shippingStatusIds, + billingEmail, orderNumber, billingName, paymentMethods); + + query = query.OrderByDescending(x => x.CreatedOnUtc); + + if (orderGuid.HasValue()) + { + // Filter by GUID. Filter in BLL because EF doesn't support casting of GUID to string + var orders = query.ToList(); + orders = orders.FindAll(x => x.OrderGuid.ToString().ToLowerInvariant().Contains(orderGuid.ToLowerInvariant())); + + return new PagedList(orders, pageIndex, pageSize); + } + else + { + return new PagedList(query, pageIndex, pageSize); + } } public virtual IPagedList GetAllOrders(int affiliateId, int pageIndex, int pageSize) @@ -245,63 +245,63 @@ public virtual void UpdateOrder(Order order) _orderRepository.Update(order); - _eventPublisher.PublishOrderUpdated(order); + _eventPublisher.PublishOrderUpdated(order); } - public virtual void DeleteOrder(Order order) - { - if (order == null) - throw new ArgumentNullException("order"); + public virtual void DeleteOrder(Order order) + { + if (order == null) + throw new ArgumentNullException("order"); - order.Deleted = true; - UpdateOrder(order); - } + order.Deleted = true; + UpdateOrder(order); + } public virtual void DeleteOrderNote(OrderNote orderNote) { if (orderNote == null) throw new ArgumentNullException("orderNote"); - int orderId = orderNote.OrderId; + int orderId = orderNote.OrderId; _orderNoteRepository.Delete(orderNote); - var order = GetOrderById(orderId); - _eventPublisher.PublishOrderUpdated(order); + var order = GetOrderById(orderId); + _eventPublisher.PublishOrderUpdated(order); } public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName) - { + { var query = _orderRepository.Table; if (!String.IsNullOrWhiteSpace(authorizationTransactionId)) query = query.Where(o => o.AuthorizationTransactionId == authorizationTransactionId); - + if (!String.IsNullOrWhiteSpace(paymentMethodSystemName)) query = query.Where(o => o.PaymentMethodSystemName == paymentMethodSystemName); - + query = query.OrderByDescending(o => o.CreatedOnUtc); var order = query.FirstOrDefault(); return order; } - public virtual void AddOrderNote(Order order, string note, bool displayToCustomer = false) - { - if (order != null && note.HasValue()) - { - order.OrderNotes.Add(new OrderNote - { - Note = note, - DisplayToCustomer = displayToCustomer, - CreatedOnUtc = DateTime.UtcNow - }); - - UpdateOrder(order); - } - } + public virtual void AddOrderNote(Order order, string note, bool displayToCustomer = false) + { + if (order != null && note.HasValue()) + { + order.OrderNotes.Add(new OrderNote + { + Note = note, + DisplayToCustomer = displayToCustomer, + CreatedOnUtc = DateTime.UtcNow + }); + + UpdateOrder(order); + } + } - #region Order items + #region Order items - public virtual OrderItem GetOrderItemById(int orderItemId) + public virtual OrderItem GetOrderItemById(int orderItemId) { if (orderItemId == 0) return null; @@ -313,7 +313,7 @@ public virtual OrderItem GetOrderItemByGuid(Guid orderItemGuid) { if (orderItemGuid == Guid.Empty) return null; - + var query = from orderItem in _orderItemRepository.Table where orderItem.OrderItemGuid == orderItemGuid select orderItem; @@ -337,11 +337,11 @@ public virtual IList GetAllOrderItems(int? orderId, int? shippingStatusId = null; if (ss.HasValue) shippingStatusId = (int)ss.Value; - + var query = from orderItem in _orderItemRepository.Table join o in _orderRepository.Table on orderItem.OrderId equals o.Id - join p in _productRepository.Table on orderItem.ProductId equals p.Id + join p in _productRepository.Table on orderItem.ProductId equals p.Id where (!orderId.HasValue || orderId.Value == 0 || orderId == o.Id) && (!customerId.HasValue || customerId.Value == 0 || customerId == o.CustomerId) && (!startTime.HasValue || startTime.Value <= o.CreatedOnUtc) && @@ -358,38 +358,38 @@ join p in _productRepository.Table on orderItem.ProductId equals p.Id return orderItems; } - public virtual Multimap GetOrderItemsByOrderIds(int[] orderIds) - { - Guard.NotNull(orderIds, nameof(orderIds)); + public virtual Multimap GetOrderItemsByOrderIds(int[] orderIds) + { + Guard.NotNull(orderIds, nameof(orderIds)); - var query = - from x in _orderItemRepository.TableUntracked.Expand(x => x.Product) - where orderIds.Contains(x.OrderId) - select x; + var query = + from x in _orderItemRepository.TableUntracked.Expand(x => x.Product) + where orderIds.Contains(x.OrderId) + select x; - var map = query - .OrderBy(x => x.OrderId) - .ToList() - .ToMultimap(x => x.OrderId, x => x); + var map = query + .OrderBy(x => x.OrderId) + .ToList() + .ToMultimap(x => x.OrderId, x => x); - return map; - } + return map; + } public virtual void DeleteOrderItem(OrderItem orderItem) { if (orderItem == null) throw new ArgumentNullException("orderItem"); - int orderId = orderItem.OrderId; + int orderId = orderItem.OrderId; _orderItemRepository.Delete(orderItem); - var order = GetOrderById(orderId); - _eventPublisher.PublishOrderUpdated(order); + var order = GetOrderById(orderId); + _eventPublisher.PublishOrderUpdated(order); } #endregion - + #region Recurring payments public virtual void DeleteRecurringPayment(RecurringPayment recurringPayment) @@ -406,7 +406,7 @@ public virtual RecurringPayment GetRecurringPaymentById(int recurringPaymentId) if (recurringPaymentId == 0) return null; - return _recurringPaymentRepository.GetById(recurringPaymentId); + return _recurringPaymentRepository.GetById(recurringPaymentId); } public virtual void InsertRecurringPayment(RecurringPayment recurringPayment) @@ -416,7 +416,7 @@ public virtual void InsertRecurringPayment(RecurringPayment recurringPayment) _recurringPaymentRepository.Insert(recurringPayment); - _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); + _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); } public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment) @@ -426,16 +426,24 @@ public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment) _recurringPaymentRepository.Update(recurringPayment); - _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); + _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); } - public virtual IList SearchRecurringPayments(int storeId, - int customerId, int initialOrderId, OrderStatus? initialOrderStatus, - bool showHidden = false) + public virtual IPagedList SearchRecurringPayments( + int storeId, + int customerId, + int initialOrderId, + OrderStatus? initialOrderStatus, + bool showHidden = false, + int pageIndex = 0, + int pageSize = int.MaxValue) { int? initialOrderStatusId = null; + if (initialOrderStatus.HasValue) + { initialOrderStatusId = (int)initialOrderStatus.Value; + } var query1 = from rp in _recurringPaymentRepository.Table join c in _customerRepository.Table on rp.InitialOrder.CustomerId equals c.Id @@ -445,7 +453,7 @@ join c in _customerRepository.Table on rp.InitialOrder.CustomerId equals c.Id (showHidden || !c.Deleted) && (showHidden || rp.IsActive) && (customerId == 0 || rp.InitialOrder.CustomerId == customerId) && - (storeId == 0 || rp.InitialOrder.StoreId == storeId) && + (storeId == 0 || rp.InitialOrder.StoreId == storeId) && (initialOrderId == 0 || rp.InitialOrder.Id == initialOrderId) && (!initialOrderStatusId.HasValue || initialOrderStatusId.Value == 0 || rp.InitialOrder.OrderStatusId == initialOrderStatusId.Value) select rp.Id; @@ -454,9 +462,8 @@ join c in _customerRepository.Table on rp.InitialOrder.CustomerId equals c.Id where query1.Contains(rp.Id) orderby rp.StartDateUtc, rp.Id select rp; - - var recurringPayments = query2.ToList(); - return recurringPayments; + + return new PagedList(query2, pageIndex, pageSize); } #endregion @@ -468,12 +475,12 @@ public virtual void DeleteReturnRequest(ReturnRequest returnRequest) if (returnRequest == null) throw new ArgumentNullException("returnRequest"); - int orderItemId = returnRequest.OrderItemId; + int orderItemId = returnRequest.OrderItemId; _returnRequestRepository.Delete(returnRequest); - var orderItem = GetOrderItemById(orderItemId); - _eventPublisher.PublishOrderUpdated(orderItem.Order); + var orderItem = GetOrderItemById(orderItemId); + _eventPublisher.PublishOrderUpdated(orderItem.Order); } public virtual ReturnRequest GetReturnRequestById(int returnRequestId) @@ -484,21 +491,21 @@ public virtual ReturnRequest GetReturnRequestById(int returnRequestId) return _returnRequestRepository.GetById(returnRequestId); } - public virtual IPagedList SearchReturnRequests(int storeId, int customerId, int orderItemId, ReturnRequestStatus? rs, int pageIndex, int pageSize, int id = 0) + public virtual IPagedList SearchReturnRequests(int storeId, int customerId, int orderItemId, ReturnRequestStatus? rs, int pageIndex, int pageSize, int id = 0) { var query = _returnRequestRepository.Table; - if (storeId > 0) - query = query.Where(rr => storeId == rr.StoreId); - - if (customerId > 0) + if (storeId > 0) + query = query.Where(rr => storeId == rr.StoreId); + + if (customerId > 0) query = query.Where(rr => customerId == rr.CustomerId); - if (orderItemId > 0) - query = query.Where(rr => rr.OrderItemId == orderItemId); + if (orderItemId > 0) + query = query.Where(rr => rr.OrderItemId == orderItemId); - if (id != 0) - query = query.Where(rr => rr.Id == id); + if (id != 0) + query = query.Where(rr => rr.Id == id); if (rs.HasValue) { @@ -508,7 +515,7 @@ public virtual IPagedList SearchReturnRequests(int storeId, int c query = query.OrderByDescending(rr => rr.CreatedOnUtc).ThenByDescending(rr => rr.Id); - var returnRequests = new PagedList(query, pageIndex, pageSize); + var returnRequests = new PagedList(query, pageIndex, pageSize); return returnRequests; } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs index 12131dac95..9a0aa20a08 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs @@ -27,16 +27,16 @@ namespace SmartStore.Services.Orders /// public partial class OrderTotalCalculationService : IOrderTotalCalculationService { - private const string CART_TAXING_INFO_KEY = "CartTaxingInfos"; + private const string CART_TAXING_INFO_KEY = "CartTaxingInfos"; - #region Fields + #region Fields - private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; + private readonly IWorkContext _workContext; + private readonly IStoreContext _storeContext; private readonly IPriceCalculationService _priceCalculationService; private readonly ITaxService _taxService; private readonly IShippingService _shippingService; - private readonly IProviderManager _providerManager; + private readonly IProviderManager _providerManager; private readonly ICheckoutAttributeParser _checkoutAttributeParser; private readonly IDiscountService _discountService; private readonly IGiftCardService _giftCardService; @@ -74,11 +74,11 @@ public partial class OrderTotalCalculationService : IOrderTotalCalculationServic /// Catalog settings public OrderTotalCalculationService( IWorkContext workContext, - IStoreContext storeContext, + IStoreContext storeContext, IPriceCalculationService priceCalculationService, ITaxService taxService, IShippingService shippingService, - IProviderManager providerManager, + IProviderManager providerManager, ICheckoutAttributeParser checkoutAttributeParser, IDiscountService discountService, IGiftCardService giftCardService, @@ -93,210 +93,210 @@ public OrderTotalCalculationService( CatalogSettings catalogSettings) { _workContext = workContext; - _storeContext = storeContext; + _storeContext = storeContext; _priceCalculationService = priceCalculationService; _taxService = taxService; _shippingService = shippingService; - _providerManager = providerManager; + _providerManager = providerManager; _checkoutAttributeParser = checkoutAttributeParser; _discountService = discountService; _giftCardService = giftCardService; _genericAttributeService = genericAttributeService; _paymentService = paymentService; _currencyService = currencyService; - _productAttributeParser = productAttributeParser; + _productAttributeParser = productAttributeParser; _taxSettings = taxSettings; _rewardPointsSettings = rewardPointsSettings; _shippingSettings = shippingSettings; _shoppingCartSettings = shoppingCartSettings; _catalogSettings = catalogSettings; - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - #endregion - - #region Utilities - - private Func GetTaxingInfo = cartItem => (CartTaxingInfo)cartItem.CustomProperties[CART_TAXING_INFO_KEY]; - - protected virtual void PrepareAuxiliaryServicesTaxingInfos(IList cart) - { - // no additional infos required - if (!cart.Any() || _taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.SpecifiedTaxCategory) - return; - - // additional infos already collected - if (cart.First().CustomProperties.ContainsKey(CART_TAXING_INFO_KEY)) - return; - - // instance taxing info objects - cart.Each(x => x.CustomProperties[CART_TAXING_INFO_KEY] = new CartTaxingInfo()); - - // collect infos - if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) - { - // calculate all subtotals - cart.Each(x => GetTaxingInfo(x).SubTotalWithoutDiscount = _priceCalculationService.GetSubTotal(x, false)); - - // items with the highest subtotal - var highestAmountItems = cart - .GroupBy(x => x.Item.Product.TaxCategoryId) - .OrderByDescending(x => x.Sum(y => GetTaxingInfo(y).SubTotalWithoutDiscount)) - .First(); - - // mark items - highestAmountItems.Each(x => GetTaxingInfo(x).HasHighestCartAmount = true); - } - else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) - { - var customer = cart.GetCustomer(); - var maxTaxRate = decimal.Zero; - var maxTaxCategoryId = 0; - - // get tax category id with the highest rate - cart.Each(x => - { - var taxRate = _taxService.GetTaxRate(x.Item.Product, x.Item.Product.TaxCategoryId, customer); - if (taxRate > maxTaxRate) - { - maxTaxRate = taxRate; - maxTaxCategoryId = x.Item.Product.TaxCategoryId; - } - }); - - // mark items - cart.Where(x => x.Item.Product.TaxCategoryId == maxTaxCategoryId) - .Each(x => GetTaxingInfo(x).HasHighestTaxRate = true); - } - //else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) - //{ - // // calculate all subtotals - // cart.Each(x => GetTaxingInfo(x).SubTotalWithoutDiscount = _priceCalculationService.GetSubTotal(x, false)); - - // // sum over all subtotals - // var subTotalSum = cart.Sum(x => GetTaxingInfo(x).SubTotalWithoutDiscount); - - // // calculate pro rata weightings - // cart.Each(x => - // { - // var taxingInfo = GetTaxingInfo(x); - // taxingInfo.ProRataWeighting = taxingInfo.SubTotalWithoutDiscount / subTotalSum; - // }); - //} - } - - protected virtual decimal? GetAdjustedShippingTotal(IList cart, out Discount appliedDiscount) - { - appliedDiscount = null; - - decimal? shippingTotal = null; - ShippingOption shippingOption = null; - var customer = cart.GetCustomer(); - var storeId = _storeContext.CurrentStore.Id; - - if (customer != null) - { - shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, _genericAttributeService, storeId); - } - - if (shippingOption != null) - { - // use last shipping option (get from cache) - var shippingMethods = _shippingService.GetAllShippingMethods(null, storeId); - shippingTotal = AdjustShippingRate(shippingOption.Rate, cart, shippingOption, shippingMethods, out appliedDiscount); - } - else - { - // use fixed rate (if possible) - Address shippingAddress = null; - if (customer != null) - shippingAddress = customer.ShippingAddress; - - var shippingRateComputationMethods = _shippingService.LoadActiveShippingRateComputationMethods(storeId); - if (!shippingRateComputationMethods.Any()) - throw new SmartException(T("Shipping.CouldNotLoadMethod")); - - if (shippingRateComputationMethods.Count() == 1) - { - var shippingRateComputationMethod = shippingRateComputationMethods.First(); - var getShippingOptionRequest = _shippingService.CreateShippingOptionRequest(cart, shippingAddress, storeId); - var fixedRate = shippingRateComputationMethod.Value.GetFixedRate(getShippingOptionRequest); - - if (fixedRate.HasValue) - { - shippingTotal = AdjustShippingRate(fixedRate.Value, cart, null, null, out appliedDiscount); - } - } - } - - return shippingTotal; - } - - protected virtual decimal GetShippingTaxAmount( - decimal shipping, - Customer customer, - int taxCategoryId, - SortedDictionary taxRates) - { - var taxRate = decimal.Zero; - var shippingExclTax = _taxService.GetShippingPrice(shipping, false, customer, taxCategoryId, out taxRate); - var shippingInclTax = _taxService.GetShippingPrice(shipping, true, customer, taxCategoryId, out taxRate); - - var shippingTax = shippingInclTax - shippingExclTax; - - if (shippingTax < decimal.Zero) - shippingTax = decimal.Zero; - - if (taxRate > decimal.Zero && shippingTax > decimal.Zero) - { - if (taxRates.ContainsKey(taxRate)) - taxRates[taxRate] = taxRates[taxRate] + shippingTax; - else - taxRates.Add(taxRate, shippingTax); - } - - return shippingTax; - } - - protected virtual decimal GetPaymentFeeTaxAmount( - decimal paymentFee, - Customer customer, - int taxCategoryId, - SortedDictionary taxRates) - { - var taxRate = decimal.Zero; - var paymentFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentFee, false, customer, taxCategoryId, out taxRate); - var paymentFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentFee, true, customer, taxCategoryId, out taxRate); - - var paymentFeeTax = paymentFeeInclTax - paymentFeeExclTax; - - if (taxRate > decimal.Zero && paymentFeeTax != decimal.Zero) - { - if (taxRates.ContainsKey(taxRate)) - taxRates[taxRate] = taxRates[taxRate] + paymentFeeTax; - else - taxRates.Add(taxRate, paymentFeeTax); - } - - return paymentFeeTax; - } - - #endregion - - #region Methods - - /// - /// Gets shopping cart subtotal - /// - /// Cart - /// Applied discount amount - /// Applied discount - /// Sub total (without discount) - /// Sub total (with discount) - public virtual void GetShoppingCartSubTotal(IList cart, + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion + + #region Utilities + + private Func GetTaxingInfo = cartItem => (CartTaxingInfo)cartItem.CustomProperties[CART_TAXING_INFO_KEY]; + + protected virtual void PrepareAuxiliaryServicesTaxingInfos(IList cart) + { + // no additional infos required + if (!cart.Any() || _taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.SpecifiedTaxCategory) + return; + + // additional infos already collected + if (cart.First().CustomProperties.ContainsKey(CART_TAXING_INFO_KEY)) + return; + + // instance taxing info objects + cart.Each(x => x.CustomProperties[CART_TAXING_INFO_KEY] = new CartTaxingInfo()); + + // collect infos + if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) + { + // calculate all subtotals + cart.Each(x => GetTaxingInfo(x).SubTotalWithoutDiscount = _priceCalculationService.GetSubTotal(x, false)); + + // items with the highest subtotal + var highestAmountItems = cart + .GroupBy(x => x.Item.Product.TaxCategoryId) + .OrderByDescending(x => x.Sum(y => GetTaxingInfo(y).SubTotalWithoutDiscount)) + .First(); + + // mark items + highestAmountItems.Each(x => GetTaxingInfo(x).HasHighestCartAmount = true); + } + else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) + { + var customer = cart.GetCustomer(); + var maxTaxRate = decimal.Zero; + var maxTaxCategoryId = 0; + + // get tax category id with the highest rate + cart.Each(x => + { + var taxRate = _taxService.GetTaxRate(x.Item.Product, x.Item.Product.TaxCategoryId, customer); + if (taxRate > maxTaxRate) + { + maxTaxRate = taxRate; + maxTaxCategoryId = x.Item.Product.TaxCategoryId; + } + }); + + // mark items + cart.Where(x => x.Item.Product.TaxCategoryId == maxTaxCategoryId) + .Each(x => GetTaxingInfo(x).HasHighestTaxRate = true); + } + //else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) + //{ + // // calculate all subtotals + // cart.Each(x => GetTaxingInfo(x).SubTotalWithoutDiscount = _priceCalculationService.GetSubTotal(x, false)); + + // // sum over all subtotals + // var subTotalSum = cart.Sum(x => GetTaxingInfo(x).SubTotalWithoutDiscount); + + // // calculate pro rata weightings + // cart.Each(x => + // { + // var taxingInfo = GetTaxingInfo(x); + // taxingInfo.ProRataWeighting = taxingInfo.SubTotalWithoutDiscount / subTotalSum; + // }); + //} + } + + protected virtual decimal? GetAdjustedShippingTotal(IList cart, out Discount appliedDiscount) + { + appliedDiscount = null; + + decimal? shippingTotal = null; + ShippingOption shippingOption = null; + var customer = cart.GetCustomer(); + var storeId = _storeContext.CurrentStore.Id; + + if (customer != null) + { + shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, _genericAttributeService, storeId); + } + + if (shippingOption != null) + { + // use last shipping option (get from cache) + var shippingMethods = _shippingService.GetAllShippingMethods(null, storeId); + shippingTotal = AdjustShippingRate(shippingOption.Rate, cart, shippingOption, shippingMethods, out appliedDiscount); + } + else + { + // use fixed rate (if possible) + Address shippingAddress = null; + if (customer != null) + shippingAddress = customer.ShippingAddress; + + var shippingRateComputationMethods = _shippingService.LoadActiveShippingRateComputationMethods(storeId); + if (!shippingRateComputationMethods.Any()) + throw new SmartException(T("Shipping.CouldNotLoadMethod")); + + if (shippingRateComputationMethods.Count() == 1) + { + var shippingRateComputationMethod = shippingRateComputationMethods.First(); + var getShippingOptionRequest = _shippingService.CreateShippingOptionRequest(cart, shippingAddress, storeId); + var fixedRate = shippingRateComputationMethod.Value.GetFixedRate(getShippingOptionRequest); + + if (fixedRate.HasValue) + { + shippingTotal = AdjustShippingRate(fixedRate.Value, cart, null, null, out appliedDiscount); + } + } + } + + return shippingTotal; + } + + protected virtual decimal GetShippingTaxAmount( + decimal shipping, + Customer customer, + int taxCategoryId, + SortedDictionary taxRates) + { + var taxRate = decimal.Zero; + var shippingExclTax = _taxService.GetShippingPrice(shipping, false, customer, taxCategoryId, out taxRate); + var shippingInclTax = _taxService.GetShippingPrice(shipping, true, customer, taxCategoryId, out taxRate); + + var shippingTax = shippingInclTax - shippingExclTax; + + if (shippingTax < decimal.Zero) + shippingTax = decimal.Zero; + + if (taxRate > decimal.Zero && shippingTax > decimal.Zero) + { + if (taxRates.ContainsKey(taxRate)) + taxRates[taxRate] = taxRates[taxRate] + shippingTax; + else + taxRates.Add(taxRate, shippingTax); + } + + return shippingTax; + } + + protected virtual decimal GetPaymentFeeTaxAmount( + decimal paymentFee, + Customer customer, + int taxCategoryId, + SortedDictionary taxRates) + { + var taxRate = decimal.Zero; + var paymentFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentFee, false, customer, taxCategoryId, out taxRate); + var paymentFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentFee, true, customer, taxCategoryId, out taxRate); + + var paymentFeeTax = paymentFeeInclTax - paymentFeeExclTax; + + if (taxRate > decimal.Zero && paymentFeeTax != decimal.Zero) + { + if (taxRates.ContainsKey(taxRate)) + taxRates[taxRate] = taxRates[taxRate] + paymentFeeTax; + else + taxRates.Add(taxRate, paymentFeeTax); + } + + return paymentFeeTax; + } + + #endregion + + #region Methods + + /// + /// Gets shopping cart subtotal + /// + /// Cart + /// Applied discount amount + /// Applied discount + /// Sub total (without discount) + /// Sub total (with discount) + public virtual void GetShoppingCartSubTotal(IList cart, out decimal discountAmount, out Discount appliedDiscount, out decimal subTotalWithoutDiscount, out decimal subTotalWithDiscount) { @@ -312,12 +312,12 @@ public virtual void GetShoppingCartSubTotal(IList car break; } - GetShoppingCartSubTotal(cart, - includingTax, - out discountAmount, - out appliedDiscount, - out subTotalWithoutDiscount, - out subTotalWithDiscount); + GetShoppingCartSubTotal(cart, + includingTax, + out discountAmount, + out appliedDiscount, + out subTotalWithoutDiscount, + out subTotalWithDiscount); } /// @@ -336,13 +336,13 @@ public virtual void GetShoppingCartSubTotal(IList car { SortedDictionary taxRates = null; - GetShoppingCartSubTotal(cart, - includingTax, - out discountAmount, - out appliedDiscount, - out subTotalWithoutDiscount, - out subTotalWithDiscount, - out taxRates); + GetShoppingCartSubTotal(cart, + includingTax, + out discountAmount, + out appliedDiscount, + out subTotalWithoutDiscount, + out subTotalWithDiscount, + out taxRates); } /// @@ -357,10 +357,10 @@ public virtual void GetShoppingCartSubTotal(IList car /// Tax rates (of order sub total) public virtual void GetShoppingCartSubTotal(IList cart, bool includingTax, - out decimal discountAmount, - out Discount appliedDiscount, - out decimal subTotalWithoutDiscount, - out decimal subTotalWithDiscount, + out decimal discountAmount, + out Discount appliedDiscount, + out decimal subTotalWithoutDiscount, + out decimal subTotalWithDiscount, out SortedDictionary taxRates) { discountAmount = decimal.Zero; @@ -374,42 +374,42 @@ public virtual void GetShoppingCartSubTotal(IList car // get the customer var customer = cart.GetCustomer(); - var currency = _workContext.WorkingCurrency; + var currency = _workContext.WorkingCurrency; - // sub totals - decimal subTotalExclTaxWithoutDiscount = decimal.Zero; + // sub totals + decimal subTotalExclTaxWithoutDiscount = decimal.Zero; decimal subTotalInclTaxWithoutDiscount = decimal.Zero; foreach (var shoppingCartItem in cart) - { - if (shoppingCartItem.Item.Product == null) - continue; - - decimal taxRate, sciSubTotal, sciExclTax, sciInclTax = decimal.Zero; - - shoppingCartItem.Item.Product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); - - if (currency.RoundOrderItemsEnabled) - { - // Gross > Net RoundFix - int qty = shoppingCartItem.Item.Quantity; - - sciSubTotal = _priceCalculationService.GetUnitPrice(shoppingCartItem, true); - - // Adaption to eliminate rounding issues - sciExclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, false, customer, out taxRate); - sciExclTax = sciExclTax.RoundIfEnabledFor(currency) * qty; - sciInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, true, customer, out taxRate); - sciInclTax = sciInclTax.RoundIfEnabledFor(currency) * qty; - } - else - { - sciSubTotal = _priceCalculationService.GetSubTotal(shoppingCartItem, true); - sciExclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, false, customer, out taxRate); - sciInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, true, customer, out taxRate); - } - - subTotalExclTaxWithoutDiscount += sciExclTax; + { + if (shoppingCartItem.Item.Product == null) + continue; + + decimal taxRate, sciSubTotal, sciExclTax, sciInclTax = decimal.Zero; + + shoppingCartItem.Item.Product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); + + if (currency.RoundOrderItemsEnabled) + { + // Gross > Net RoundFix + int qty = shoppingCartItem.Item.Quantity; + + sciSubTotal = _priceCalculationService.GetUnitPrice(shoppingCartItem, true); + + // Adaption to eliminate rounding issues + sciExclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, false, customer, out taxRate); + sciExclTax = sciExclTax.RoundIfEnabledFor(currency) * qty; + sciInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, true, customer, out taxRate); + sciInclTax = sciInclTax.RoundIfEnabledFor(currency) * qty; + } + else + { + sciSubTotal = _priceCalculationService.GetSubTotal(shoppingCartItem, true); + sciExclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, false, customer, out taxRate); + sciInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, true, customer, out taxRate); + } + + subTotalExclTaxWithoutDiscount += sciExclTax; subTotalInclTaxWithoutDiscount += sciInclTax; // tax rates @@ -430,8 +430,8 @@ public virtual void GetShoppingCartSubTotal(IList car // checkout attributes if (customer != null) { - var checkoutAttributesXml = customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); - var caValues = _checkoutAttributeParser.ParseCheckoutAttributeValues(checkoutAttributesXml); + var checkoutAttributesXml = customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); + var caValues = _checkoutAttributeParser.ParseCheckoutAttributeValues(checkoutAttributesXml); if (caValues != null) { foreach (var caValue in caValues) @@ -460,20 +460,20 @@ public virtual void GetShoppingCartSubTotal(IList car } } - //subtotal without discount - if (includingTax) - { - subTotalWithoutDiscount = subTotalInclTaxWithoutDiscount; - } - else - { - subTotalWithoutDiscount = subTotalExclTaxWithoutDiscount; - } - - if (subTotalWithoutDiscount < decimal.Zero) - { - subTotalWithoutDiscount = decimal.Zero; - } + //subtotal without discount + if (includingTax) + { + subTotalWithoutDiscount = subTotalInclTaxWithoutDiscount; + } + else + { + subTotalWithoutDiscount = subTotalExclTaxWithoutDiscount; + } + + if (subTotalWithoutDiscount < decimal.Zero) + { + subTotalWithoutDiscount = decimal.Zero; + } subTotalWithoutDiscount = subTotalWithoutDiscount.RoundIfEnabledFor(currency); @@ -526,11 +526,11 @@ public virtual void GetShoppingCartSubTotal(IList car discountAmount = discountAmountExclTax; } - //round - if (subTotalWithDiscount < decimal.Zero) - { - subTotalWithDiscount = decimal.Zero; - } + //round + if (subTotalWithDiscount < decimal.Zero) + { + subTotalWithDiscount = decimal.Zero; + } subTotalWithDiscount = subTotalWithDiscount.RoundIfEnabledFor(currency); } @@ -542,8 +542,7 @@ public virtual void GetShoppingCartSubTotal(IList car /// Order subtotal /// Applied discount /// Order discount - public virtual decimal GetOrderSubtotalDiscount(Customer customer, - decimal orderSubTotal, out Discount appliedDiscount) + public virtual decimal GetOrderSubtotalDiscount(Customer customer, decimal orderSubTotal, out Discount appliedDiscount) { appliedDiscount = null; decimal discountAmount = decimal.Zero; @@ -552,16 +551,16 @@ public virtual decimal GetOrderSubtotalDiscount(Customer customer, var allDiscounts = _discountService.GetAllDiscounts(DiscountType.AssignedToOrderSubTotal); var allowedDiscounts = new List(); - if (allDiscounts != null) - { - foreach (var discount in allDiscounts) - { - if (discount.DiscountType == DiscountType.AssignedToOrderSubTotal && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) - { - allowedDiscounts.Add(discount); - } - } - } + if (allDiscounts != null) + { + foreach (var discount in allDiscounts) + { + if (discount.DiscountType == DiscountType.AssignedToOrderSubTotal && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) + { + allowedDiscounts.Add(discount); + } + } + } appliedDiscount = allowedDiscounts.GetPreferredDiscount(orderSubTotal); if (appliedDiscount != null) @@ -573,21 +572,21 @@ public virtual decimal GetOrderSubtotalDiscount(Customer customer, return discountAmount; } - + /// /// Gets shopping cart additional shipping charge /// /// Cart /// Additional shipping charge - public virtual decimal GetShoppingCartAdditionalShippingCharge(IList cart) + public virtual decimal GetShoppingCartAdditionalShippingCharge(IList cart) { if (IsFreeShipping(cart)) return decimal.Zero; - decimal additionalShippingCharge = decimal.Zero; + decimal additionalShippingCharge = decimal.Zero; - foreach (var sci in cart) - { + foreach (var sci in cart) + { if (_shippingSettings.ChargeOnlyHighestProductShippingSurcharge) { @@ -596,7 +595,7 @@ public virtual decimal GetShoppingCartAdditionalShippingCharge(IList cart) /// Cart /// Applied discount /// Adjusted shipping rate - public virtual decimal AdjustShippingRate(decimal shippingRate, IList cart, - ShippingOption shippingOption, IList shippingMethods, out Discount appliedDiscount) + public virtual decimal AdjustShippingRate(decimal shippingRate, IList cart, + ShippingOption shippingOption, IList shippingMethods, out Discount appliedDiscount) { appliedDiscount = null; - if (IsFreeShipping(cart)) - { - return decimal.Zero; - } - - var adjustedRate = decimal.Zero; - var bundlePerItemShipping = decimal.Zero; - var ignoreAdditionalShippingCharge = false; - - foreach (var sci in cart) - { - if (sci.Item.Product != null && sci.Item.Product.ProductType == ProductType.BundledProduct && sci.Item.Product.BundlePerItemShipping) - { - if (sci.ChildItems != null) - { - foreach (var childItem in sci.ChildItems.Where(x => x.Item.IsShipEnabled && !x.Item.IsFreeShipping)) - { - bundlePerItemShipping += shippingRate; - } - } - } - else if (adjustedRate == decimal.Zero) - { - adjustedRate = shippingRate; - } - } - - adjustedRate += bundlePerItemShipping; - - if (shippingOption != null && shippingMethods != null) - { - var shippingMethod = shippingMethods.FirstOrDefault(x => x.Id == shippingOption.ShippingMethodId); - if (shippingMethod != null) - { - ignoreAdditionalShippingCharge = shippingMethod.IgnoreCharges; - } - } - - // Additional shipping charges. - if (!ignoreAdditionalShippingCharge) - { - decimal additionalShippingCharge = GetShoppingCartAdditionalShippingCharge(cart); - adjustedRate += additionalShippingCharge; - } + if (IsFreeShipping(cart)) + { + return decimal.Zero; + } + + var adjustedRate = decimal.Zero; + var bundlePerItemShipping = decimal.Zero; + var ignoreAdditionalShippingCharge = false; + + foreach (var sci in cart) + { + if (sci.Item.Product != null && sci.Item.Product.ProductType == ProductType.BundledProduct && sci.Item.Product.BundlePerItemShipping) + { + if (sci.ChildItems != null) + { + foreach (var childItem in sci.ChildItems.Where(x => x.Item.IsShipEnabled && !x.Item.IsFreeShipping)) + { + bundlePerItemShipping += shippingRate; + } + } + } + else if (adjustedRate == decimal.Zero) + { + adjustedRate = shippingRate; + } + } + + adjustedRate += bundlePerItemShipping; + + if (shippingOption != null && shippingMethods != null) + { + var shippingMethod = shippingMethods.FirstOrDefault(x => x.Id == shippingOption.ShippingMethodId); + if (shippingMethod != null) + { + ignoreAdditionalShippingCharge = shippingMethod.IgnoreCharges; + } + } + + // Additional shipping charges. + if (!ignoreAdditionalShippingCharge) + { + decimal additionalShippingCharge = GetShoppingCartAdditionalShippingCharge(cart); + adjustedRate += additionalShippingCharge; + } // Discount. var customer = cart.GetCustomer(); var discountAmount = GetShippingDiscount(customer, adjustedRate, out appliedDiscount); adjustedRate = adjustedRate - discountAmount; - if (adjustedRate < decimal.Zero) - { - adjustedRate = decimal.Zero; - } + if (adjustedRate < decimal.Zero) + { + adjustedRate = decimal.Zero; + } adjustedRate = adjustedRate.RoundIfEnabledFor(_workContext.WorkingCurrency); return adjustedRate; @@ -774,9 +773,9 @@ public virtual decimal AdjustShippingRate(decimal shippingRate, IList cart, bool includingTax) { var taxRate = decimal.Zero; - Discount appliedDiscount = null; + Discount appliedDiscount = null; - return GetShoppingCartShippingTotal(cart, includingTax, out taxRate, out appliedDiscount); + return GetShoppingCartShippingTotal(cart, includingTax, out taxRate, out appliedDiscount); } /// @@ -788,93 +787,93 @@ public virtual decimal AdjustShippingRate(decimal shippingRate, IListApplied discount /// Shipping total public virtual decimal? GetShoppingCartShippingTotal( - IList cart, - bool includingTax, + IList cart, + bool includingTax, out decimal taxRate, - out Discount appliedDiscount) + out Discount appliedDiscount) { - appliedDiscount = null; - taxRate = decimal.Zero; + appliedDiscount = null; + taxRate = decimal.Zero; - if (IsFreeShipping(cart)) - return decimal.Zero; + if (IsFreeShipping(cart)) + return decimal.Zero; - decimal? shippingTotalTaxed = null; - var taxCategoryId = 0; - var customer = cart.GetCustomer(); - var currency = _workContext.WorkingCurrency; + decimal? shippingTotalTaxed = null; + var taxCategoryId = 0; + var customer = cart.GetCustomer(); + var currency = _workContext.WorkingCurrency; - var shippingTotal = GetAdjustedShippingTotal(cart, out appliedDiscount); + var shippingTotal = GetAdjustedShippingTotal(cart, out appliedDiscount); - if (!shippingTotal.HasValue) - return null; + if (!shippingTotal.HasValue) + return null; - if (shippingTotal.Value < decimal.Zero) - { - shippingTotal = decimal.Zero; - } + if (shippingTotal.Value < decimal.Zero) + { + shippingTotal = decimal.Zero; + } shippingTotal = shippingTotal.Value.RoundIfEnabledFor(currency); - PrepareAuxiliaryServicesTaxingInfos(cart); - - // commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate - //if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) - //{ - // // calculate proRataShipping: get product weightings for cart and multiply them with the shipping amount - // shippingTotalTaxed = decimal.Zero; - - // var tmpTaxRate = decimal.Zero; - // var taxRates = new List(); - - // foreach (var item in cart) - // { - // var proRataShipping = shippingTotal.Value * GetTaxingInfo(item).ProRataWeighting; - // shippingTotalTaxed += _taxService.GetShippingPrice(proRataShipping, includingTax, customer, item.Item.Product.TaxCategoryId, out tmpTaxRate); - - // taxRates.Add(tmpTaxRate); - // } - - // // a tax rate is only defined if all rates are equal. return zero tax rate in all other cases. - // if (taxRates.Any() && taxRates.Distinct().Count() == 1) - // { - // taxRate = taxRates.First(); - // } - //} - //else - //{ - - if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) - { - var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestCartAmount); - taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); - } - else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) - { - var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestTaxRate); - taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); - } - - // fallback to setting - if (taxCategoryId == 0) - { - taxCategoryId = _taxSettings.ShippingTaxClassId; - } - - shippingTotalTaxed = _taxService.GetShippingPrice(shippingTotal.Value, includingTax, customer, taxCategoryId, out taxRate); + PrepareAuxiliaryServicesTaxingInfos(cart); + + // commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate + //if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) + //{ + // // calculate proRataShipping: get product weightings for cart and multiply them with the shipping amount + // shippingTotalTaxed = decimal.Zero; + + // var tmpTaxRate = decimal.Zero; + // var taxRates = new List(); + + // foreach (var item in cart) + // { + // var proRataShipping = shippingTotal.Value * GetTaxingInfo(item).ProRataWeighting; + // shippingTotalTaxed += _taxService.GetShippingPrice(proRataShipping, includingTax, customer, item.Item.Product.TaxCategoryId, out tmpTaxRate); + + // taxRates.Add(tmpTaxRate); + // } + + // // a tax rate is only defined if all rates are equal. return zero tax rate in all other cases. + // if (taxRates.Any() && taxRates.Distinct().Count() == 1) + // { + // taxRate = taxRates.First(); + // } + //} + //else + //{ + + if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) + { + var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestCartAmount); + taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); + } + else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) + { + var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestTaxRate); + taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); + } + + // fallback to setting + if (taxCategoryId == 0) + { + taxCategoryId = _taxSettings.ShippingTaxClassId; + } + + shippingTotalTaxed = _taxService.GetShippingPrice(shippingTotal.Value, includingTax, customer, taxCategoryId, out taxRate); shippingTotalTaxed = shippingTotalTaxed.Value.RoundIfEnabledFor(currency); - return shippingTotalTaxed; + return shippingTotalTaxed; } - /// - /// Gets a shipping discount - /// - /// Customer - /// Shipping total - /// Applied discount - /// Shipping discount - public virtual decimal GetShippingDiscount(Customer customer, decimal shippingTotal, out Discount appliedDiscount) + /// + /// Gets a shipping discount + /// + /// Customer + /// Shipping total + /// Applied discount + /// Shipping discount + public virtual decimal GetShippingDiscount(Customer customer, decimal shippingTotal, out Discount appliedDiscount) { appliedDiscount = null; decimal shippingDiscountAmount = decimal.Zero; @@ -884,16 +883,16 @@ public virtual decimal GetShippingDiscount(Customer customer, decimal shippingTo var allDiscounts = _discountService.GetAllDiscounts(DiscountType.AssignedToShipping); var allowedDiscounts = new List(); - if (allDiscounts != null) - { - foreach (var discount in allDiscounts) - { - if (discount.DiscountType == DiscountType.AssignedToShipping && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) - { - allowedDiscounts.Add(discount); - } - } - } + if (allDiscounts != null) + { + foreach (var discount in allDiscounts) + { + if (discount.DiscountType == DiscountType.AssignedToShipping && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) + { + allowedDiscounts.Add(discount); + } + } + } appliedDiscount = allowedDiscounts.GetPreferredDiscount(shippingTotal); if (appliedDiscount != null) @@ -901,10 +900,10 @@ public virtual decimal GetShippingDiscount(Customer customer, decimal shippingTo shippingDiscountAmount = appliedDiscount.GetDiscountAmount(shippingTotal); } - if (shippingDiscountAmount < decimal.Zero) - { - shippingDiscountAmount = decimal.Zero; - } + if (shippingDiscountAmount < decimal.Zero) + { + shippingDiscountAmount = decimal.Zero; + } shippingDiscountAmount = shippingDiscountAmount.RoundIfEnabledFor(_workContext.WorkingCurrency); return shippingDiscountAmount; @@ -943,27 +942,27 @@ public virtual decimal GetTaxTotal(IList cart, out So taxRates = new SortedDictionary(); - var subTotalTax = decimal.Zero; - var shippingTax = decimal.Zero; - var paymentFeeTax = decimal.Zero; - var customer = cart.GetCustomer(); - var currency = _workContext.WorkingCurrency; + var subTotalTax = decimal.Zero; + var shippingTax = decimal.Zero; + var paymentFeeTax = decimal.Zero; + var customer = cart.GetCustomer(); + var currency = _workContext.WorkingCurrency; - //// (VATFIX) - if (_taxService.IsVatExempt(null, customer)) - { - taxRates.Add(decimal.Zero, decimal.Zero); - return decimal.Zero; - } - //// (VATFIX) + //// (VATFIX) + if (_taxService.IsVatExempt(null, customer)) + { + taxRates.Add(decimal.Zero, decimal.Zero); + return decimal.Zero; + } + //// (VATFIX) - #region order sub total (items + checkout attributes) + #region order sub total (items + checkout attributes) - var orderSubTotalDiscountAmount = decimal.Zero; + var orderSubTotalDiscountAmount = decimal.Zero; var subTotalWithoutDiscountBase = decimal.Zero; var subTotalWithDiscountBase = decimal.Zero; - Discount appliedDiscount = null; - SortedDictionary orderSubTotalTaxRates = null; + Discount appliedDiscount = null; + SortedDictionary orderSubTotalTaxRates = null; GetShoppingCartSubTotal(cart, false, out orderSubTotalDiscountAmount, out appliedDiscount, @@ -985,114 +984,114 @@ public virtual decimal GetTaxTotal(IList cart, out So } } - #endregion + #endregion - #region shipping tax amount + #region shipping tax amount - if (_taxSettings.ShippingIsTaxable && !IsFreeShipping(cart)) - { - var taxCategoryId = 0; - var shippingTotal = GetAdjustedShippingTotal(cart, out appliedDiscount); + if (_taxSettings.ShippingIsTaxable && !IsFreeShipping(cart)) + { + var taxCategoryId = 0; + var shippingTotal = GetAdjustedShippingTotal(cart, out appliedDiscount); - if (shippingTotal.HasValue) - { - if (shippingTotal.Value < decimal.Zero) - { - shippingTotal = decimal.Zero; - } + if (shippingTotal.HasValue) + { + if (shippingTotal.Value < decimal.Zero) + { + shippingTotal = decimal.Zero; + } shippingTotal = shippingTotal.Value.RoundIfEnabledFor(currency); - PrepareAuxiliaryServicesTaxingInfos(cart); - - // commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate - //if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) - //{ - // // calculate proRataShipping: get product weightings for cart and multiply them with the shipping amount - // foreach (var item in cart) - // { - // var proRataShipping = shippingTotal.Value * GetTaxingInfo(item).ProRataWeighting; - // shippingTax += GetShippingTaxAmount(proRataShipping, customer, item.Item.Product.TaxCategoryId, taxRates); - // } - //} - //else - //{ - - if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) - { - var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestCartAmount); - taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); - } - else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) - { - var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestTaxRate); - taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); - } - - // fallback to setting - if (taxCategoryId == 0) - { - taxCategoryId = _taxSettings.ShippingTaxClassId; - } - - shippingTax = GetShippingTaxAmount(shippingTotal.Value, customer, taxCategoryId, taxRates); + PrepareAuxiliaryServicesTaxingInfos(cart); + + // commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate + //if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) + //{ + // // calculate proRataShipping: get product weightings for cart and multiply them with the shipping amount + // foreach (var item in cart) + // { + // var proRataShipping = shippingTotal.Value * GetTaxingInfo(item).ProRataWeighting; + // shippingTax += GetShippingTaxAmount(proRataShipping, customer, item.Item.Product.TaxCategoryId, taxRates); + // } + //} + //else + //{ + + if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) + { + var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestCartAmount); + taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); + } + else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) + { + var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestTaxRate); + taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); + } + + // fallback to setting + if (taxCategoryId == 0) + { + taxCategoryId = _taxSettings.ShippingTaxClassId; + } + + shippingTax = GetShippingTaxAmount(shippingTotal.Value, customer, taxCategoryId, taxRates); shippingTax = shippingTax.RoundIfEnabledFor(currency); - } - } + } + } - #endregion + #endregion - #region payment fee tax amount + #region payment fee tax amount - if (usePaymentMethodAdditionalFee && _taxSettings.PaymentMethodAdditionalFeeIsTaxable && customer != null) - { - var paymentMethodSystemName = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, _storeContext.CurrentStore.Id); - var provider = _providerManager.GetProvider(paymentMethodSystemName); + if (usePaymentMethodAdditionalFee && _taxSettings.PaymentMethodAdditionalFeeIsTaxable && customer != null) + { + var paymentMethodSystemName = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, _storeContext.CurrentStore.Id); + var provider = _providerManager.GetProvider(paymentMethodSystemName); - if (provider != null) - { - var taxCategoryId = 0; - var paymentFee = provider.Value.GetAdditionalHandlingFee(cart); + if (provider != null) + { + var taxCategoryId = 0; + var paymentFee = provider.Value.GetAdditionalHandlingFee(cart); paymentFee = paymentFee.RoundIfEnabledFor(currency); - PrepareAuxiliaryServicesTaxingInfos(cart); - - // commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate - //if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) - //{ - // // calculate proRataShipping: get product weightings for cart and multiply them with the shipping amount - // foreach (var item in cart) - // { - // var proRataPaymentFees = paymentFee * GetTaxingInfo(item).ProRataWeighting; - // paymentFeeTax += GetPaymentFeeTaxAmount(proRataPaymentFees, customer, item.Item.Product.TaxCategoryId, taxRates); - // } - //} - //else - //{ - - if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) - { - var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestCartAmount); - taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); - } - else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) - { - var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestTaxRate); - taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); - } - - // fallback to setting - if (taxCategoryId == 0) - taxCategoryId = _taxSettings.PaymentMethodAdditionalFeeTaxClassId; - - paymentFeeTax = GetPaymentFeeTaxAmount(paymentFee, customer, taxCategoryId, taxRates); - } - } - - #endregion - - // add at least one tax rate (0%) - if (taxRates.Count == 0) + PrepareAuxiliaryServicesTaxingInfos(cart); + + // commented out cause requires several plugins to be updated and migration of Order.OrderShippingTaxRate and Order.PaymentMethodAdditionalFeeTaxRate + //if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.ProRata) + //{ + // // calculate proRataShipping: get product weightings for cart and multiply them with the shipping amount + // foreach (var item in cart) + // { + // var proRataPaymentFees = paymentFee * GetTaxingInfo(item).ProRataWeighting; + // paymentFeeTax += GetPaymentFeeTaxAmount(proRataPaymentFees, customer, item.Item.Product.TaxCategoryId, taxRates); + // } + //} + //else + //{ + + if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestCartAmount) + { + var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestCartAmount); + taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); + } + else if (_taxSettings.AuxiliaryServicesTaxingType == AuxiliaryServicesTaxType.HighestTaxRate) + { + var cartItem = cart.FirstOrDefault(x => GetTaxingInfo(x).HasHighestTaxRate); + taxCategoryId = (cartItem != null ? cartItem.Item.Product.TaxCategoryId : 0); + } + + // fallback to setting + if (taxCategoryId == 0) + taxCategoryId = _taxSettings.PaymentMethodAdditionalFeeTaxClassId; + + paymentFeeTax = GetPaymentFeeTaxAmount(paymentFee, customer, taxCategoryId, taxRates); + } + } + + #endregion + + // add at least one tax rate (0%) + if (taxRates.Count == 0) taxRates.Add(decimal.Zero, decimal.Zero); // summarize taxes @@ -1115,7 +1114,7 @@ public virtual ShoppingCartTotal GetShoppingCartTotal( IList cart, bool ignoreRewardPoints = false, bool usePaymentMethodAdditionalFee = true, - bool ignoreCreditBalance = false) + bool ignoreCreditBalance = false) { var customer = cart.GetCustomer(); var store = _storeContext.CurrentStore; @@ -1248,47 +1247,47 @@ public virtual ShoppingCartTotal GetShoppingCartTotal( } } - #endregion + #endregion - if (resultTemp < decimal.Zero) + if (resultTemp < decimal.Zero) { resultTemp = decimal.Zero; } resultTemp = resultTemp.RoundIfEnabledFor(currency); - var roundingAmount = decimal.Zero; + var roundingAmount = decimal.Zero; var roundingAmountConverted = decimal.Zero; // Return null if we have errors: var orderTotal = shoppingCartShipping.HasValue ? resultTemp : (decimal?)null; var orderTotalConverted = orderTotal; - var appliedCreditBalance = decimal.Zero; + var appliedCreditBalance = decimal.Zero; - if (orderTotal.HasValue) + if (orderTotal.HasValue) { orderTotal = orderTotal.Value - redeemedRewardPointsAmount; - // Credit balance. - if (!ignoreCreditBalance && customer != null && orderTotal > decimal.Zero) - { - var creditBalance = customer.GetAttribute(SystemCustomerAttributeNames.UseCreditBalanceDuringCheckout, _genericAttributeService, store.Id); - if (creditBalance > decimal.Zero) - { - if (creditBalance > orderTotal) - { - // Normalize used amount. - appliedCreditBalance = orderTotal.Value; - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.UseCreditBalanceDuringCheckout, orderTotal.Value, store.Id); - } - else - { - appliedCreditBalance = creditBalance; - } - } - } - - orderTotal = orderTotal.Value - appliedCreditBalance; - orderTotal = orderTotal.Value.RoundIfEnabledFor(currency); + // Credit balance. + if (!ignoreCreditBalance && customer != null && orderTotal > decimal.Zero) + { + var creditBalance = customer.GetAttribute(SystemCustomerAttributeNames.UseCreditBalanceDuringCheckout, _genericAttributeService, store.Id); + if (creditBalance > decimal.Zero) + { + if (creditBalance > orderTotal) + { + // Normalize used amount. + appliedCreditBalance = orderTotal.Value; + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.UseCreditBalanceDuringCheckout, orderTotal.Value, store.Id); + } + else + { + appliedCreditBalance = creditBalance; + } + } + } + + orderTotal = orderTotal.Value - appliedCreditBalance; + orderTotal = orderTotal.Value.RoundIfEnabledFor(currency); orderTotalConverted = _currencyService.ConvertFromPrimaryStoreCurrency(orderTotal.Value, currency, store); @@ -1311,7 +1310,7 @@ public virtual ShoppingCartTotal GetShoppingCartTotal( result.AppliedGiftCards = appliedGiftCards; result.RedeemedRewardPoints = redeemedRewardPoints; result.RedeemedRewardPointsAmount = redeemedRewardPointsAmount; - result.CreditBalance = appliedCreditBalance; + result.CreditBalance = appliedCreditBalance; result.ConvertedFromPrimaryStoreCurrency.TotalAmount = orderTotalConverted; result.ConvertedFromPrimaryStoreCurrency.RoundingAmount = roundingAmountConverted; @@ -1336,16 +1335,16 @@ public virtual decimal GetOrderTotalDiscount(Customer customer, decimal orderTot var allDiscounts = _discountService.GetAllDiscounts(DiscountType.AssignedToOrderTotal); var allowedDiscounts = new List(); - if (allDiscounts != null) - { - foreach (var discount in allDiscounts) - { - if (discount.DiscountType == DiscountType.AssignedToOrderTotal && !allowedDiscounts.Any(x => x.Id == discount.Id) &&_discountService.IsDiscountValid(discount, customer)) - { - allowedDiscounts.Add(discount); - } - } - } + if (allDiscounts != null) + { + foreach (var discount in allDiscounts) + { + if (discount.DiscountType == DiscountType.AssignedToOrderTotal && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) + { + allowedDiscounts.Add(discount); + } + } + } appliedDiscount = allowedDiscounts.GetPreferredDiscount(orderTotal); if (appliedDiscount != null) @@ -1390,13 +1389,13 @@ public virtual int ConvertAmountToRewardPoints(decimal amount) if (amount <= 0) return 0; - if (_rewardPointsSettings.ExchangeRate > 0) - { - if (_rewardPointsSettings.RoundDownRewardPoints) - result = (int)Math.Floor(amount / _rewardPointsSettings.ExchangeRate); - else - result = (int)Math.Ceiling(amount / _rewardPointsSettings.ExchangeRate); - } + if (_rewardPointsSettings.ExchangeRate > 0) + { + if (_rewardPointsSettings.RoundDownRewardPoints) + result = (int)Math.Floor(amount / _rewardPointsSettings.ExchangeRate); + else + result = (int)Math.Ceiling(amount / _rewardPointsSettings.ExchangeRate); + } return result; } @@ -1405,16 +1404,16 @@ public virtual int ConvertAmountToRewardPoints(decimal amount) } - internal class CartTaxingInfo - { - internal CartTaxingInfo() - { - ProRataWeighting = decimal.Zero; - } + internal class CartTaxingInfo + { + internal CartTaxingInfo() + { + ProRataWeighting = decimal.Zero; + } - public decimal SubTotalWithoutDiscount { get; internal set; } - public bool HasHighestCartAmount { get; internal set; } - public bool HasHighestTaxRate { get; internal set; } - public decimal ProRataWeighting { get; internal set; } - } + public decimal SubTotalWithoutDiscount { get; internal set; } + public bool HasHighestCartAmount { get; internal set; } + public bool HasHighestTaxRate { get; internal set; } + public decimal ProRataWeighting { get; internal set; } + } } diff --git a/src/Libraries/SmartStore.Services/Orders/PlaceOrderResult.cs b/src/Libraries/SmartStore.Services/Orders/PlaceOrderResult.cs index 2ed059da87..780352bef5 100644 --- a/src/Libraries/SmartStore.Services/Orders/PlaceOrderResult.cs +++ b/src/Libraries/SmartStore.Services/Orders/PlaceOrderResult.cs @@ -10,22 +10,19 @@ public partial class PlaceOrderResult { public IList Errors { get; set; } - public PlaceOrderResult() + public PlaceOrderResult() { this.Errors = new List(); } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); public void AddError(string error) { this.Errors.Add(error); } - + /// /// Gets or sets the placed order /// diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs index 1f6c7970dd..3e9653b209 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs @@ -61,11 +61,11 @@ public static bool IsRecurring(this IList shoppingCar /// Cycle period /// Total cycles /// Error (if exists); otherwise, empty string - public static string GetRecurringCycleInfo(this IList shoppingCart, - ILocalizationService localizationService, - out int cycleLength, - out RecurringProductCyclePeriod cyclePeriod, - out int totalCycles) + public static string GetRecurringCycleInfo(this IList shoppingCart, + ILocalizationService localizationService, + out int cycleLength, + out RecurringProductCyclePeriod cyclePeriod, + out int totalCycles) { string error = ""; @@ -85,7 +85,7 @@ public static string GetRecurringCycleInfo(this IList throw new SmartException(string.Format("Product (Id={0}) cannot be loaded", sci.Item.ProductId)); } - string conflictError = localizationService.GetResource("ShoppingCart.ConflictingShipmentSchedules"); + string conflictError = localizationService.GetResource("ShoppingCart.ConflictingShipmentSchedules"); if (product.IsRecurring) { // cycle length @@ -123,7 +123,7 @@ public static string GetRecurringCycleInfo(this IList } } - if (_cycleLength.HasValue && _cyclePeriod.HasValue && _totalCycles.HasValue) + if (_cycleLength.HasValue && _cyclePeriod.HasValue && _totalCycles.HasValue) { cycleLength = _cycleLength.Value; cyclePeriod = _cyclePeriod.Value; @@ -146,14 +146,14 @@ public static Customer GetCustomer(this IList shoppin return shoppingCart[0].Item.Customer; } - public static IEnumerable Filter(this IEnumerable shoppingCart, ShoppingCartType type, int? storeId = null) - { - var enumerable = shoppingCart.Where(x => x.ShoppingCartType == type); + public static IEnumerable Filter(this IEnumerable shoppingCart, ShoppingCartType type, int? storeId = null) + { + var enumerable = shoppingCart.Where(x => x.ShoppingCartType == type); - if (storeId.HasValue) - enumerable = enumerable.Where(x => x.StoreId == storeId.Value); + if (storeId.HasValue) + enumerable = enumerable.Where(x => x.StoreId == storeId.Value); - return enumerable; - } + return enumerable; + } } } diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index 2c697f414e..66041200a3 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -361,11 +361,17 @@ public virtual IList GetStandardWarnings( Product product, string selectedAttributes, decimal customerEnteredPrice, - int quantity) + int quantity, + int storeId = 0) { Guard.NotNull(customer, nameof(customer)); Guard.NotNull(product, nameof(product)); + if (storeId == 0) + { + storeId = _storeContext.CurrentStore.Id; + } + var warnings = new List(); // deleted? @@ -401,7 +407,7 @@ public virtual IList GetStandardWarnings( } // Store mapping - if (!_storeMappingService.Authorize(product, _storeContext.CurrentStore.Id)) + if (!_storeMappingService.Authorize(product, storeId)) { warnings.Add(T("ShoppingCart.ProductUnpublished")); } @@ -471,7 +477,7 @@ public virtual IList GetStandardWarnings( break; case ManageInventoryMethod.ManageStock: { - if ((BackorderMode)product.BackorderMode == BackorderMode.NoBackorders) + if (product.BackorderMode == BackorderMode.NoBackorders) { if (product.StockQuantity < quantity) { @@ -833,7 +839,7 @@ public virtual IList GetShoppingCartItemWarnings( // standard properties if (getStandardWarnings) - warnings.AddRange(GetStandardWarnings(customer, shoppingCartType, product, selectedAttributes, customerEnteredPrice, quantity)); + warnings.AddRange(GetStandardWarnings(customer, shoppingCartType, product, selectedAttributes, customerEnteredPrice, quantity, storeId)); // selected attributes if (getAttributesWarnings) @@ -1093,7 +1099,7 @@ public virtual List AddToCart( existingCartItem = FindShoppingCartItemInTheCart(cart, cartType, product, selectedAttributes, customerEnteredPrice); } - if (existingCartItem != null) + if (existingCartItem != null && !_shoppingCartSettings.AddProductsToBasketInSinglePositions) { // update existing shopping cart item int newQuantity = existingCartItem.Item.Quantity + quantity; diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs index 65155d6db7..65e170e814 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs @@ -56,12 +56,12 @@ public static implicit operator ShoppingCartTotal(decimal? obj) /// public decimal RedeemedRewardPointsAmount { get; set; } - /// - /// Credit balance. - /// - public decimal CreditBalance { get; set; } + /// + /// Credit balance. + /// + public decimal CreditBalance { get; set; } - public ConvertedAmounts ConvertedFromPrimaryStoreCurrency { get; set; } + public ConvertedAmounts ConvertedFromPrimaryStoreCurrency { get; set; } public override string ToString() { diff --git a/src/Libraries/SmartStore.Services/Orders/ValidatingCartEventConsumer.cs b/src/Libraries/SmartStore.Services/Orders/ValidatingCartEventConsumer.cs new file mode 100644 index 0000000000..9dfcbc0f81 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Orders/ValidatingCartEventConsumer.cs @@ -0,0 +1,62 @@ +using SmartStore.Core; +using SmartStore.Core.Events; +using SmartStore.Services.Cart; +using SmartStore.Services.Catalog; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; + +namespace SmartStore.Services.Orders +{ + public class ValidatingCartEventConsumer : IConsumer + { + private readonly IOrderProcessingService _orderProcessingService; + private readonly ILocalizationService _localizationService; + private readonly ICurrencyService _currencyService; + private readonly IPriceFormatter _priceFormatter; + private readonly IWorkContext _workContext; + + public ValidatingCartEventConsumer( + IOrderProcessingService orderProcessingService, + ILocalizationService localizationService, + ICurrencyService currencyService, + IPriceFormatter priceFormatter, + IWorkContext workContext) + { + _orderProcessingService = orderProcessingService; + _localizationService = localizationService; + _currencyService = currencyService; + _priceFormatter = priceFormatter; + _workContext = workContext; + } + + public void HandleEvent(ValidatingCartEvent message) + { + // Default Order Totals restriction + var roleIds = _workContext.OriginalCustomerIfImpersonated?.GetRoleIds() ?? message.Customer.GetRoleIds(); + + // Minimum order totals validation + var (isAboveMin, min) = _orderProcessingService.IsAboveOrderTotalMinimum(message.Cart, roleIds); + if (!isAboveMin) + { + min = _currencyService.ConvertFromPrimaryStoreCurrency(min, _workContext.WorkingCurrency); + message.Warnings.Add(string.Format( + _localizationService.GetResource("Checkout.MinOrderSubtotalAmount"), + _priceFormatter.FormatPrice(min, true, false)) + ); + + return; + } + + // Maximum order totals validation + var (isBelowMax, max) = _orderProcessingService.IsBelowOrderTotalMaximum(message.Cart, roleIds); + if (!isBelowMax) + { + max = _currencyService.ConvertFromPrimaryStoreCurrency(max, _workContext.WorkingCurrency); + message.Warnings.Add(string.Format( + _localizationService.GetResource("Checkout.MaxOrderSubtotalAmount"), + _priceFormatter.FormatPrice(max, true, false)) + ); + } + } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Payments/CancelRecurringPaymentResult.cs b/src/Libraries/SmartStore.Services/Payments/CancelRecurringPaymentResult.cs index 02478591cc..b98e608271 100644 --- a/src/Libraries/SmartStore.Services/Payments/CancelRecurringPaymentResult.cs +++ b/src/Libraries/SmartStore.Services/Payments/CancelRecurringPaymentResult.cs @@ -9,17 +9,14 @@ public partial class CancelRecurringPaymentResult { public IList Errors { get; set; } - public CancelRecurringPaymentResult() + public CancelRecurringPaymentResult() { this.Errors = new List(); } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); - public void AddError(string error) + public void AddError(string error) { this.Errors.Add(error); } diff --git a/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs b/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs index 1b30b02387..7cb73dbdd4 100644 --- a/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs +++ b/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs @@ -9,84 +9,81 @@ namespace SmartStore.Services.Payments { - public class CapturePaymentHook : DbSaveHook - { - private readonly Lazy _services; - private readonly Lazy _orderProcessingService; - private readonly HashSet _toCapture = new HashSet(); + public class CapturePaymentHook : DbSaveHook + { + private readonly Lazy _services; + private readonly Lazy _orderProcessingService; + private readonly HashSet _toCapture = new HashSet(); - public CapturePaymentHook( - Lazy services, - Lazy orderProcessingService) - { - _services = services; - _orderProcessingService = orderProcessingService; - } + public CapturePaymentHook( + Lazy services, + Lazy orderProcessingService) + { + _services = services; + _orderProcessingService = orderProcessingService; + } - private bool IsStatusPropertyModifiedTo(IHookedEntity entry, string propertyName, int statusId) - { - var prop = entry.Entry.Property(propertyName); + private bool IsStatusPropertyModifiedTo(IHookedEntity entry, string propertyName, int statusId) + { + var prop = entry.Entry.Property(propertyName); - if (prop != null && prop.CurrentValue != null) - { - if (!prop.CurrentValue.Equals(prop.OriginalValue)) - { - return (int)prop.CurrentValue == statusId; - } - } + if (prop != null && prop.CurrentValue != null) + { + if (!prop.CurrentValue.Equals(prop.OriginalValue)) + { + return (int)prop.CurrentValue == statusId; + } + } - return false; - } + return false; + } - protected override void OnUpdating(Order entity, IHookedEntity entry) - { - if (entry.State == Core.Data.EntityState.Modified) - { - var isShipped = IsStatusPropertyModifiedTo(entry, nameof(entity.ShippingStatusId), (int)ShippingStatus.Shipped); - var isDelivered = IsStatusPropertyModifiedTo(entry, nameof(entity.ShippingStatusId), (int)ShippingStatus.Delivered); + protected override void OnUpdating(Order entity, IHookedEntity entry) + { + var isShipped = IsStatusPropertyModifiedTo(entry, nameof(entity.ShippingStatusId), (int)ShippingStatus.Shipped); + var isDelivered = IsStatusPropertyModifiedTo(entry, nameof(entity.ShippingStatusId), (int)ShippingStatus.Delivered); - if (isShipped || isDelivered) - { - var settings = _services.Value.Settings.LoadSetting(entity.StoreId); - if (settings.CapturePaymentReason.HasValue) - { - if (isShipped && settings.CapturePaymentReason.Value == CapturePaymentReason.OrderShipped) - { - _toCapture.Add(entity); - } - else if (isDelivered && settings.CapturePaymentReason.Value == CapturePaymentReason.OrderDelivered) - { - _toCapture.Add(entity); - } - } - } + if (isShipped || isDelivered) + { + var settings = _services.Value.Settings.LoadSetting(entity.StoreId); + if (settings.CapturePaymentReason.HasValue) + { + if (isShipped && settings.CapturePaymentReason.Value == CapturePaymentReason.OrderShipped) + { + _toCapture.Add(entity); + } + else if (isDelivered && settings.CapturePaymentReason.Value == CapturePaymentReason.OrderDelivered) + { + _toCapture.Add(entity); + } + } + } - //if (IsStatusPropertyModifiedTo(entry, nameof(entity.OrderStatusId), (int)OrderStatus.Complete)) - //{ - // That's too late. The payment is already marked as paid and the capture process would never be executed. - //} - } - } + //if (IsStatusPropertyModifiedTo(entry, nameof(entity.OrderStatusId), (int)OrderStatus.Complete)) + //{ + // That's too late. The payment is already marked as paid and the capture process would never be executed. + //} + } - public override void OnAfterSave(IHookedEntity entry) - { - // Do not remove. - } + public override void OnAfterSave(IHookedEntity entry) + { + // Do not remove. + } - public override void OnAfterSaveCompleted() - { - if (_toCapture.Any()) - { - foreach (var order in _toCapture) - { - if (_orderProcessingService.Value.CanCapture(order)) - { - _orderProcessingService.Value.Capture(order); - } - } + public override void OnAfterSaveCompleted() + { + if (_toCapture.Any()) + { + foreach (var order in _toCapture) + { + if (_orderProcessingService.Value.CanCapture(order)) + { + _orderProcessingService.Value.Capture(order); + } + } - _toCapture.Clear(); - } - } - } + _toCapture.Clear(); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Payments/CapturePaymentResult.cs b/src/Libraries/SmartStore.Services/Payments/CapturePaymentResult.cs index dd7d95be5a..dba210f775 100644 --- a/src/Libraries/SmartStore.Services/Payments/CapturePaymentResult.cs +++ b/src/Libraries/SmartStore.Services/Payments/CapturePaymentResult.cs @@ -11,21 +11,18 @@ public partial class CapturePaymentResult private PaymentStatus _newPaymentStatus = PaymentStatus.Pending; public IList Errors { get; set; } - public CapturePaymentResult() + public CapturePaymentResult() { this.Errors = new List(); } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); public void AddError(string error) { this.Errors.Add(error); } - + /// /// Gets or sets the capture transaction identifier /// @@ -41,14 +38,8 @@ public void AddError(string error) /// public PaymentStatus NewPaymentStatus { - get - { - return _newPaymentStatus; - } - set - { - _newPaymentStatus = value; - } + get => _newPaymentStatus; + set => _newPaymentStatus = value; } } } diff --git a/src/Libraries/SmartStore.Services/Payments/IPaymentMethod.cs b/src/Libraries/SmartStore.Services/Payments/IPaymentMethod.cs index 9dc48a1b85..2b439f7b26 100644 --- a/src/Libraries/SmartStore.Services/Payments/IPaymentMethod.cs +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentMethod.cs @@ -13,12 +13,12 @@ public partial interface IPaymentMethod : IProvider, IUserEditable { #region Methods - /// - /// Pre process payment - /// - /// Payment info required for an order processing - /// Pre process payment result - PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest); + /// + /// Pre process payment + /// + /// Payment info required for an order processing + /// Pre process payment result + PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest); /// /// Process a payment @@ -97,16 +97,16 @@ public partial interface IPaymentMethod : IProvider, IUserEditable #region Properties - /// - /// Gets a value indicating whether the payment method is active and should be offered to customers - /// - bool IsActive { get; } + /// + /// Gets a value indicating whether the payment method is active and should be offered to customers + /// + bool IsActive { get; } - /// - /// Gets a value indicating whether the payment method requires user input - /// before proceeding (e.g. CreditCard, DirectDebit etc.) - /// - bool RequiresInteraction { get; } + /// + /// Gets a value indicating whether the payment method requires user input + /// before proceeding (e.g. CreditCard, DirectDebit etc.) + /// + bool RequiresInteraction { get; } /// /// Gets a value indicating whether capture is supported diff --git a/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs b/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs index f70985ef08..b04726c2b8 100644 --- a/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs @@ -9,36 +9,36 @@ namespace SmartStore.Services.Payments /// Allows to filter out payment methods. /// public partial interface IPaymentMethodFilter - { - /// - /// Gets a value indicating whether a payment method should be filtered out. - /// - /// Payment filter request. - /// true filter out method, false do not filter out method. - bool IsExcluded(PaymentFilterRequest request); - } + { + /// + /// Gets a value indicating whether a payment method should be filtered out. + /// + /// Payment filter request. + /// true filter out method, false do not filter out method. + bool IsExcluded(PaymentFilterRequest request); + } - public partial class PaymentFilterRequest - { - /// - /// The payment method to be checked - /// - public Provider PaymentMethod { get; set; } + public partial class PaymentFilterRequest + { + /// + /// The payment method to be checked + /// + public Provider PaymentMethod { get; set; } - /// - /// The context shopping cart - /// - public IList Cart { get; set; } + /// + /// The context shopping cart + /// + public IList Cart { get; set; } - /// - /// The context store identifier - /// - public int StoreId { get; set; } + /// + /// The context store identifier + /// + public int StoreId { get; set; } - /// - /// The context customer - /// - public Customer Customer { get; set; } - } + /// + /// The context customer + /// + public Customer Customer { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs b/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs index 5cd9fcee05..e699eb5323 100644 --- a/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs @@ -33,20 +33,20 @@ bool IsPaymentMethodActive( /// Provide a fallback payment method if there is no match. /// Filtered payment methods. IEnumerable> LoadActivePaymentMethods( - Customer customer = null, - IList cart = null, - int storeId = 0, - PaymentMethodType[] types = null, - bool provideFallbackMethod = true); - - /// - /// Load payment provider by system name - /// - /// System name - /// true to load only active provider - /// Load records allowed only in specified store; pass 0 to load all records - /// Found payment provider - Provider LoadPaymentMethodBySystemName(string systemName, bool onlyWhenActive = false, int storeId = 0); + Customer customer = null, + IList cart = null, + int storeId = 0, + PaymentMethodType[] types = null, + bool provideFallbackMethod = true); + + /// + /// Load payment provider by system name + /// + /// System name + /// true to load only active provider + /// Load records allowed only in specified store; pass 0 to load all records + /// Found payment provider + Provider LoadPaymentMethodBySystemName(string systemName, bool onlyWhenActive = false, int storeId = 0); /// /// Load all payment providers @@ -62,38 +62,38 @@ IEnumerable> LoadActivePaymentMethods( /// Dictionary of payment methods. Key is the payment method system name. IDictionary GetAllPaymentMethods(int storeId = 0); - /// - /// Gets payment method extra data by system name - /// - /// Provider system name - /// Payment method entity - PaymentMethod GetPaymentMethodBySystemName(string systemName); + /// + /// Gets payment method extra data by system name + /// + /// Provider system name + /// Payment method entity + PaymentMethod GetPaymentMethodBySystemName(string systemName); - /// - /// Insert payment method extra data - /// - /// Payment method - void InsertPaymentMethod(PaymentMethod paymentMethod); + /// + /// Insert payment method extra data + /// + /// Payment method + void InsertPaymentMethod(PaymentMethod paymentMethod); - /// - /// Updates payment method extra data - /// - /// Payment method - void UpdatePaymentMethod(PaymentMethod paymentMethod); + /// + /// Updates payment method extra data + /// + /// Payment method + void UpdatePaymentMethod(PaymentMethod paymentMethod); - /// - /// Delete payment method extra data - /// - /// Payment method - void DeletePaymentMethod(PaymentMethod paymentMethod); + /// + /// Delete payment method extra data + /// + /// Payment method + void DeletePaymentMethod(PaymentMethod paymentMethod); - /// - /// Pre process a payment - /// - /// Payment info required for an order processing - /// Pre process payment result - PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest); + /// + /// Pre process a payment + /// + /// Payment info required for an order processing + /// Pre process payment result + PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest); /// /// Process a payment @@ -218,10 +218,10 @@ IEnumerable> LoadActivePaymentMethods( /// Masked credit card number string GetMaskedCreditCardNumber(string creditCardNumber); - /// - /// Gets all payment filters - /// - /// List of payment filters - IList GetAllPaymentMethodFilters(); + /// + /// Gets all payment filters + /// + /// List of payment filters + IList GetAllPaymentMethodFilters(); } } diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs index 85504dad98..f157ef74d9 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs @@ -9,7 +9,7 @@ namespace SmartStore.Services.Payments { - public static class PaymentExtentions + public static class PaymentExtentions { /// /// Is payment method active? @@ -28,10 +28,10 @@ public static bool IsPaymentMethodActive(this Provider paymentMe if (paymentSettings.ActivePaymentMethodSystemNames == null) return false; - if (!paymentMethod.Value.IsActive) - return false; + if (!paymentMethod.Value.IsActive) + return false; - return paymentSettings.ActivePaymentMethodSystemNames.Contains(paymentMethod.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); + return paymentSettings.ActivePaymentMethodSystemNames.Contains(paymentMethod.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); } /// @@ -44,10 +44,10 @@ public static bool IsPaymentMethodActive(this Provider paymentMe /// Is fee amount specified as percentage or fixed value? /// Result public static decimal CalculateAdditionalFee(this IPaymentMethod paymentMethod, - IOrderTotalCalculationService orderTotalCalculationService, - IList cart, - decimal fee, - bool usePercentage) + IOrderTotalCalculationService orderTotalCalculationService, + IList cart, + decimal fee, + bool usePercentage) { if (paymentMethod == null) throw new ArgumentNullException("paymentMethod"); @@ -70,43 +70,43 @@ public static decimal CalculateAdditionalFee(this IPaymentMethod paymentMethod, return result; } - public static RouteInfo GetConfigurationRoute(this IPaymentMethod method) - { - Guard.NotNull(method, nameof(method)); - - string action; - string controller; - RouteValueDictionary routeValues; - - var configurable = method as IConfigurable; - - if (configurable != null) - { - configurable.GetConfigurationRoute(out action, out controller, out routeValues); - if (action.HasValue()) - { - return new RouteInfo(action, controller, routeValues); - } - } - - return null; - } - - public static RouteInfo GetPaymentInfoRoute(this IPaymentMethod method) - { - Guard.NotNull(method, nameof(method)); - - string action; - string controller; - RouteValueDictionary routeValues; - - method.GetPaymentInfoRoute(out action, out controller, out routeValues); - if (action.HasValue()) - { - return new RouteInfo(action, controller, routeValues); - } - - return null; - } + public static RouteInfo GetConfigurationRoute(this IPaymentMethod method) + { + Guard.NotNull(method, nameof(method)); + + string action; + string controller; + RouteValueDictionary routeValues; + + var configurable = method as IConfigurable; + + if (configurable != null) + { + configurable.GetConfigurationRoute(out action, out controller, out routeValues); + if (action.HasValue()) + { + return new RouteInfo(action, controller, routeValues); + } + } + + return null; + } + + public static RouteInfo GetPaymentInfoRoute(this IPaymentMethod method) + { + Guard.NotNull(method, nameof(method)); + + string action; + string controller; + RouteValueDictionary routeValues; + + method.GetPaymentInfoRoute(out action, out controller, out routeValues); + if (action.HasValue()) + { + return new RouteInfo(action, controller, routeValues); + } + + return null; + } } } diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentMethodBase.cs b/src/Libraries/SmartStore.Services/Payments/PaymentMethodBase.cs index 3510b71b2a..bfa3fd2948 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentMethodBase.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentMethodBase.cs @@ -9,254 +9,206 @@ namespace SmartStore.Services.Payments { - /// - /// Base class for payment methods - /// - public abstract class PaymentMethodBase : IPaymentMethod - { - - protected PaymentMethodBase() - { - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - #region Methods - - /// - /// Pre process payment - /// - /// Payment info required for an order processing - /// Pre process payment result - public virtual PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = new PreProcessPaymentResult(); - return result; - } - - /// - /// Process payment - /// - /// Payment info required for an order processing - /// Process payment result - public abstract ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest); - - /// - /// Post process payment (used by payment gateways that require redirecting to a third-party URL) - /// - /// Payment info required for an order processing - public virtual void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) - { - // NoImpl - } - - /// - /// Gets additional handling fee - /// - /// Additional handling fee - public virtual decimal GetAdditionalHandlingFee(IList cart) - { - return 0; - } - - /// - /// Captures payment - /// - /// Capture payment request - /// Capture payment result - public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) - { - var result = new CapturePaymentResult(); - result.AddError(T("Common.Payment.NoCaptureSupport")); - return result; - } - - /// - /// Refunds a payment - /// - /// Request - /// Result - public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequest) - { - var result = new RefundPaymentResult(); - result.AddError(T("Common.Payment.NoRefundSupport")); - return result; - } - - /// - /// Voids a payment - /// - /// Request - /// Result - public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) - { - var result = new VoidPaymentResult(); - result.AddError(T("Common.Payment.NoVoidSupport")); - return result; - } - - /// - /// Process recurring payment - /// - /// Payment info required for an order processing - /// Process payment result - public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = new ProcessPaymentResult(); - result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); - return result; - } - - /// - /// Cancels a recurring payment - /// - /// Request - /// Result - public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurringPaymentRequest cancelPaymentRequest) - { - var result = new CancelRecurringPaymentResult(); - result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); - return result; - } - - /// - /// Gets a value indicating whether customers can complete a payment after order is placed but not completed (for redirection payment methods) - /// - /// Order - /// Result - public virtual bool CanRePostProcessPayment(Order order) - { - return false; - } - - /// - /// Gets a route for provider configuration - /// - /// Action name - /// Controller name - /// Route values - public abstract void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues); - - /// - /// Gets a route for payment info - /// - /// Action name - /// Controller name - /// Route values - public abstract void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues); - - /// - /// Gets a route for the payment info handler controller action - /// - /// Order - /// Result - /// - /// The defined route is being redirected to during the checkout process > PaymentInfo page. - /// Implementors should return null if no redirection occurs. - /// - public virtual RouteInfo GetPaymentInfoHandlerRoute() - { - return null; - } - - public abstract Type GetControllerType(); - - #endregion - - #region Properties - - /// - /// Gets a value indicating whether the payment method is active and should be offered to customers - /// - public virtual bool IsActive - { - get - { - return true; - } - } - - /// - /// Gets a value indicating whether the payment method requires user input - /// before proceeding (e.g. CreditCard, DirectDebit etc.) - /// - public virtual bool RequiresInteraction - { - get - { - return false; - } - } - - /// - /// Gets a value indicating whether capture is supported - /// - public virtual bool SupportCapture - { - get - { - return false; - } - } - - /// - /// Gets a value indicating whether partial refund is supported - /// - public virtual bool SupportPartiallyRefund - { - get - { - return false; - } - } - - /// - /// Gets a value indicating whether refund is supported - /// - public virtual bool SupportRefund - { - get - { - return false; - } - } - - /// - /// Gets a value indicating whether void is supported - /// - public virtual bool SupportVoid - { - get - { - return false; - } - } - - /// - /// Gets a recurring payment type of payment method - /// - public virtual RecurringPaymentType RecurringPaymentType - { - get - { - return RecurringPaymentType.NotSupported; - } - } - - /// - /// Gets a payment method type - /// - public virtual PaymentMethodType PaymentMethodType - { - get - { - return PaymentMethodType.Unknown; - } - } - - #endregion - - } + /// + /// Base class for payment methods + /// + public abstract class PaymentMethodBase : IPaymentMethod + { + + protected PaymentMethodBase() + { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #region Methods + + /// + /// Pre process payment + /// + /// Payment info required for an order processing + /// Pre process payment result + public virtual PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new PreProcessPaymentResult(); + return result; + } + + /// + /// Process payment + /// + /// Payment info required for an order processing + /// Process payment result + public abstract ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest); + + /// + /// Post process payment (used by payment gateways that require redirecting to a third-party URL) + /// + /// Payment info required for an order processing + public virtual void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) + { + // NoImpl + } + + /// + /// Gets additional handling fee + /// + /// Additional handling fee + public virtual decimal GetAdditionalHandlingFee(IList cart) + { + return 0; + } + + /// + /// Captures payment + /// + /// Capture payment request + /// Capture payment result + public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) + { + var result = new CapturePaymentResult(); + result.AddError(T("Common.Payment.NoCaptureSupport")); + return result; + } + + /// + /// Refunds a payment + /// + /// Request + /// Result + public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequest) + { + var result = new RefundPaymentResult(); + result.AddError(T("Common.Payment.NoRefundSupport")); + return result; + } + + /// + /// Voids a payment + /// + /// Request + /// Result + public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) + { + var result = new VoidPaymentResult(); + result.AddError(T("Common.Payment.NoVoidSupport")); + return result; + } + + /// + /// Process recurring payment + /// + /// Payment info required for an order processing + /// Process payment result + public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult(); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); + return result; + } + + /// + /// Cancels a recurring payment + /// + /// Request + /// Result + public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurringPaymentRequest cancelPaymentRequest) + { + var result = new CancelRecurringPaymentResult(); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); + return result; + } + + /// + /// Gets a value indicating whether customers can complete a payment after order is placed but not completed (for redirection payment methods) + /// + /// Order + /// Result + public virtual bool CanRePostProcessPayment(Order order) + { + return false; + } + + /// + /// Gets a route for provider configuration + /// + /// Action name + /// Controller name + /// Route values + public abstract void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues); + + /// + /// Gets a route for payment info + /// + /// Action name + /// Controller name + /// Route values + public abstract void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues); + + /// + /// Gets a route for the payment info handler controller action + /// + /// Order + /// Result + /// + /// The defined route is being redirected to during the checkout process > PaymentInfo page. + /// Implementors should return null if no redirection occurs. + /// + public virtual RouteInfo GetPaymentInfoHandlerRoute() + { + return null; + } + + public abstract Type GetControllerType(); + + #endregion + + #region Properties + + /// + /// Gets a value indicating whether the payment method is active and should be offered to customers + /// + public virtual bool IsActive => true; + + /// + /// Gets a value indicating whether the payment method requires user input + /// before proceeding (e.g. CreditCard, DirectDebit etc.) + /// + public virtual bool RequiresInteraction => false; + + /// + /// Gets a value indicating whether capture is supported + /// + public virtual bool SupportCapture => false; + + /// + /// Gets a value indicating whether partial refund is supported + /// + public virtual bool SupportPartiallyRefund => false; + + /// + /// Gets a value indicating whether refund is supported + /// + public virtual bool SupportRefund => false; + + /// + /// Gets a value indicating whether void is supported + /// + public virtual bool SupportVoid => false; + + /// + /// Gets a recurring payment type of payment method + /// + public virtual RecurringPaymentType RecurringPaymentType => RecurringPaymentType.NotSupported; + + /// + /// Gets a payment method type + /// + public virtual PaymentMethodType PaymentMethodType => PaymentMethodType.Unknown; + + #endregion + + } } diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentMethodType.cs b/src/Libraries/SmartStore.Services/Payments/PaymentMethodType.cs index cfa24c23b7..5caf5afd51 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentMethodType.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentMethodType.cs @@ -25,9 +25,9 @@ public enum PaymentMethodType : int /// All payment information is entered on the site and is available via button /// StandardAndButton = 25, - /// - /// Payment information is entered in checkout and customer is redirected to complete payment (e.g. 3D Secure) after order has been placed - /// - StandardAndRedirection = 30 + /// + /// Payment information is entered in checkout and customer is redirected to complete payment (e.g. 3D Secure) after order has been placed + /// + StandardAndRedirection = 30 } } diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentPluginBase.cs b/src/Libraries/SmartStore.Services/Payments/PaymentPluginBase.cs index 3ab7d5b6e3..81ee03d9ef 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentPluginBase.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentPluginBase.cs @@ -2,27 +2,27 @@ namespace SmartStore.Services.Payments { - public abstract class PaymentPluginBase : PaymentMethodBase, IPlugin - { - /// - /// Gets or sets the plugin descriptor - /// - public virtual PluginDescriptor PluginDescriptor { get; set; } + public abstract class PaymentPluginBase : PaymentMethodBase, IPlugin + { + /// + /// Gets or sets the plugin descriptor + /// + public virtual PluginDescriptor PluginDescriptor { get; set; } - /// - /// Install plugin - /// - public virtual void Install() - { - PluginManager.MarkPluginAsInstalled(this.PluginDescriptor.SystemName); - } + /// + /// Install plugin + /// + public virtual void Install() + { + PluginManager.MarkPluginAsInstalled(this.PluginDescriptor.SystemName); + } - /// - /// Uninstall plugin - /// - public virtual void Uninstall() - { - PluginManager.MarkPluginAsUninstalled(this.PluginDescriptor.SystemName); - } - } + /// + /// Uninstall plugin + /// + public virtual void Uninstall() + { + PluginManager.MarkPluginAsUninstalled(this.PluginDescriptor.SystemName); + } + } } diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs index 165c770db5..c6d32257e2 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs @@ -21,48 +21,48 @@ public partial class PaymentService : IPaymentService private const string PAYMENT_METHODS_PATTERN_KEY = "SmartStore.paymentmethod.*"; private readonly static object _lock = new object(); - private static IList _paymentMethodFilterTypes = null; + private static IList _paymentMethodFilterTypes = null; - private readonly IRepository _paymentMethodRepository; - private readonly IRepository _storeMappingRepository; - private readonly IStoreMappingService _storeMappingService; - private readonly PaymentSettings _paymentSettings; + private readonly IRepository _paymentMethodRepository; + private readonly IRepository _storeMappingRepository; + private readonly IStoreMappingService _storeMappingService; + private readonly PaymentSettings _paymentSettings; private readonly ICartRuleProvider _cartRuleProvider; private readonly IProviderManager _providerManager; - private readonly ICommonServices _services; - private readonly ITypeFinder _typeFinder; - - public PaymentService( - IRepository paymentMethodRepository, - IRepository storeMappingRepository, - IStoreMappingService storeMappingService, - PaymentSettings paymentSettings, + private readonly ICommonServices _services; + private readonly ITypeFinder _typeFinder; + + public PaymentService( + IRepository paymentMethodRepository, + IRepository storeMappingRepository, + IStoreMappingService storeMappingService, + PaymentSettings paymentSettings, ICartRuleProvider cartRuleProvider, IProviderManager providerManager, - ICommonServices services, - ITypeFinder typeFinder) + ICommonServices services, + ITypeFinder typeFinder) { - _paymentMethodRepository = paymentMethodRepository; - _storeMappingRepository = storeMappingRepository; - _storeMappingService = storeMappingService; - _paymentSettings = paymentSettings; + _paymentMethodRepository = paymentMethodRepository; + _storeMappingRepository = storeMappingRepository; + _storeMappingService = storeMappingService; + _paymentSettings = paymentSettings; _cartRuleProvider = cartRuleProvider; - _providerManager = providerManager; - _services = services; - _typeFinder = typeFinder; - } + _providerManager = providerManager; + _services = services; + _typeFinder = typeFinder; + } - public Localizer T { get; set; } = NullLocalizer.Instance; + public Localizer T { get; set; } = NullLocalizer.Instance; public ILogger Logger { get; set; } = NullLogger.Instance; public DbQuerySettings QuerySettings { get; set; } = DbQuerySettings.Default; #region Methods public virtual bool IsPaymentMethodActive(string systemName, int storeId = 0) - { - var method = LoadPaymentMethodBySystemName(systemName, true, storeId); - return method != null; - } + { + var method = LoadPaymentMethodBySystemName(systemName, true, storeId); + return method != null; + } public virtual bool IsPaymentMethodActive( string systemName, @@ -79,18 +79,18 @@ public virtual bool IsPaymentMethodActive( } public virtual IEnumerable> LoadActivePaymentMethods( - Customer customer = null, - IList cart = null, - int storeId = 0, - PaymentMethodType[] types = null, - bool provideFallbackMethod = true) + Customer customer = null, + IList cart = null, + int storeId = 0, + PaymentMethodType[] types = null, + bool provideFallbackMethod = true) { - var filterRequest = new PaymentFilterRequest - { - Cart = cart, - StoreId = storeId, - Customer = customer - }; + var filterRequest = new PaymentFilterRequest + { + Cart = cart, + StoreId = storeId, + Customer = customer + }; var allFilters = GetAllPaymentMethodFilters(); var allProviders = types != null && types.Any() @@ -100,8 +100,8 @@ public virtual IEnumerable> LoadActivePaymentMethods( var paymentMethods = GetAllPaymentMethods(storeId); var activeProviders = allProviders - .Where(p => - { + .Where(p => + { try { // Only active payment methods. @@ -129,24 +129,25 @@ public virtual IEnumerable> LoadActivePaymentMethods( } catch (Exception ex) { - Logger.Error(ex); + Logger.Error(ex); } - return true; - }); + return true; + }); - if (!activeProviders.Any() && provideFallbackMethod) - { - var fallbackMethod = allProviders.FirstOrDefault(x => x.IsPaymentMethodActive(_paymentSettings)); + if (!activeProviders.Any() && provideFallbackMethod) + { + var fallbackMethod = allProviders.FirstOrDefault(x => x.IsPaymentMethodActive(_paymentSettings)); if (fallbackMethod == null) { - fallbackMethod = allProviders.FirstOrDefault(); + fallbackMethod = allProviders.FirstOrDefault(x => x.Metadata?.PluginDescriptor?.SystemName?.IsCaseInsensitiveEqual("SmartStore.OfflinePayment") ?? false) + ?? allProviders.FirstOrDefault(); } - if (fallbackMethod != null) - { - return new Provider[] { fallbackMethod }; - } + if (fallbackMethod != null) + { + return new Provider[] { fallbackMethod }; + } if (DataSettings.DatabaseIsInstalled()) { @@ -157,47 +158,49 @@ public virtual IEnumerable> LoadActivePaymentMethods( return activeProviders; } - public virtual Provider LoadPaymentMethodBySystemName(string systemName, bool onlyWhenActive = false, int storeId = 0) + public virtual Provider LoadPaymentMethodBySystemName(string systemName, bool onlyWhenActive = false, int storeId = 0) { - var provider = _providerManager.GetProvider(systemName, storeId); - if (provider != null) - { - if (onlyWhenActive && !provider.IsPaymentMethodActive(_paymentSettings)) - { - return null; - } - - if (!QuerySettings.IgnoreMultiStore && storeId > 0) - { - // Return provider if paymentMethod is null! - var paymentMethod = _paymentMethodRepository.TableUntracked.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); - if (paymentMethod != null && !_storeMappingService.Authorize(paymentMethod, storeId)) - { - return null; - } - } - } - - return provider; + var provider = _providerManager.GetProvider(systemName, storeId); + if (provider != null) + { + if (onlyWhenActive && !provider.IsPaymentMethodActive(_paymentSettings)) + { + return null; + } + + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + // Return provider if paymentMethod is null! + var paymentMethod = _paymentMethodRepository.TableUntracked.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); + if (paymentMethod != null && !_storeMappingService.Authorize(paymentMethod, storeId)) + { + return null; + } + } + } + + return provider; } - public virtual IEnumerable> LoadAllPaymentMethods(int storeId = 0) + public virtual IEnumerable> LoadAllPaymentMethods(int storeId = 0) { - var providers = _providerManager.GetAllProviders(storeId); + var providers = _providerManager.GetAllProviders(storeId); - if (providers.Any() && !QuerySettings.IgnoreMultiStore && storeId > 0) - { - var paymentMethods = GetAllPaymentMethods(storeId); + if (providers.Any() && !QuerySettings.IgnoreMultiStore && storeId > 0) + { + var unauthorizedMethods = _paymentMethodRepository.TableUntracked + .Where(x => x.LimitedToStores) + .ToList(); - var unauthorizedMethodNames = paymentMethods.Values - .Where(x => x.LimitedToStores && !_storeMappingService.Authorize(x, storeId)) + var unauthorizedMethodNames = unauthorizedMethods + .Where(x => !_storeMappingService.Authorize(x, storeId)) .Select(x => x.PaymentMethodSystemName) .ToList(); - return providers.Where(x => !unauthorizedMethodNames.Contains(x.Metadata.SystemName)); - } + return providers.Where(x => !unauthorizedMethodNames.Contains(x.Metadata.SystemName)); + } - return providers; + return providers; } public virtual IDictionary GetAllPaymentMethods(int storeId = 0) @@ -233,53 +236,53 @@ orderby mGroup.Key return result; } - /// - /// Gets payment method extra data by system name - /// - /// Provider system name - /// Payment method entity - public virtual PaymentMethod GetPaymentMethodBySystemName(string systemName) - { - if (systemName.HasValue()) - { - return _paymentMethodRepository.Table.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); - } - - return null; - } - - /// - /// Insert payment method extra data - /// - /// Payment method - public virtual void InsertPaymentMethod(PaymentMethod paymentMethod) - { + /// + /// Gets payment method extra data by system name + /// + /// Provider system name + /// Payment method entity + public virtual PaymentMethod GetPaymentMethodBySystemName(string systemName) + { + if (systemName.HasValue()) + { + return _paymentMethodRepository.Table.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); + } + + return null; + } + + /// + /// Insert payment method extra data + /// + /// Payment method + public virtual void InsertPaymentMethod(PaymentMethod paymentMethod) + { Guard.NotNull(paymentMethod, nameof(paymentMethod)); - _paymentMethodRepository.Insert(paymentMethod); + _paymentMethodRepository.Insert(paymentMethod); _services.RequestCache.RemoveByPattern(PAYMENT_METHODS_PATTERN_KEY); } - /// - /// Updates payment method extra data - /// - /// Payment method - public virtual void UpdatePaymentMethod(PaymentMethod paymentMethod) - { + /// + /// Updates payment method extra data + /// + /// Payment method + public virtual void UpdatePaymentMethod(PaymentMethod paymentMethod) + { Guard.NotNull(paymentMethod, nameof(paymentMethod)); - _paymentMethodRepository.Update(paymentMethod); + _paymentMethodRepository.Update(paymentMethod); _services.RequestCache.RemoveByPattern(PAYMENT_METHODS_PATTERN_KEY); } - /// - /// Delete payment method extra data - /// - /// Payment method - public virtual void DeletePaymentMethod(PaymentMethod paymentMethod) - { + /// + /// Delete payment method extra data + /// + /// Payment method + public virtual void DeletePaymentMethod(PaymentMethod paymentMethod) + { if (paymentMethod != null) { _paymentMethodRepository.Delete(paymentMethod); @@ -289,27 +292,27 @@ public virtual void DeletePaymentMethod(PaymentMethod paymentMethod) } - /// - /// Pre process a payment - /// - /// Payment info required for an order processing - /// Pre process payment result - public virtual PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - if (processPaymentRequest.OrderTotal == decimal.Zero) - { - var result = new PreProcessPaymentResult(); - return result; - } - else - { - var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); - if (paymentMethod == null) - throw new SmartException(T("Payment.CouldNotLoadMethod")); - - return paymentMethod.Value.PreProcessPayment(processPaymentRequest); - } - } + /// + /// Pre process a payment + /// + /// Payment info required for an order processing + /// Pre process payment result + public virtual PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + if (processPaymentRequest.OrderTotal == decimal.Zero) + { + var result = new PreProcessPaymentResult(); + return result; + } + else + { + var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); + if (paymentMethod == null) + throw new SmartException(T("Payment.CouldNotLoadMethod")); + + return paymentMethod.Value.PreProcessPayment(processPaymentRequest); + } + } /// /// Process a payment @@ -335,9 +338,9 @@ public virtual ProcessPaymentResult ProcessPayment(ProcessPaymentRequest process processPaymentRequest.CreditCardNumber = processPaymentRequest.CreditCardNumber.Replace("-", ""); } - var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); + var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); - if (paymentMethod == null) + if (paymentMethod == null) throw new SmartException(T("Payment.CouldNotLoadMethod")); return paymentMethod.Value.ProcessPayment(processPaymentRequest); @@ -351,14 +354,14 @@ public virtual ProcessPaymentResult ProcessPayment(ProcessPaymentRequest process /// Payment info required for an order processing public virtual void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) { - if (postProcessPaymentRequest.Order.PaymentMethodSystemName.HasValue()) - { - var paymentMethod = LoadPaymentMethodBySystemName(postProcessPaymentRequest.Order.PaymentMethodSystemName); - if (paymentMethod == null) - throw new SmartException(T("Payment.CouldNotLoadMethod")); - - paymentMethod.Value.PostProcessPayment(postProcessPaymentRequest); - } + if (postProcessPaymentRequest.Order.PaymentMethodSystemName.HasValue()) + { + var paymentMethod = LoadPaymentMethodBySystemName(postProcessPaymentRequest.Order.PaymentMethodSystemName); + if (paymentMethod == null) + throw new SmartException(T("Payment.CouldNotLoadMethod")); + + paymentMethod.Value.PostProcessPayment(postProcessPaymentRequest); + } } /// @@ -378,7 +381,7 @@ public virtual bool CanRePostProcessPayment(Order order) if (paymentMethod == null) return false; //Payment method couldn't be loaded (for example, was uninstalled) - if (paymentMethod.Value.PaymentMethodType != PaymentMethodType.Redirection && paymentMethod.Value.PaymentMethodType != PaymentMethodType.StandardAndRedirection) + if (paymentMethod.Value.PaymentMethodType != PaymentMethodType.Redirection && paymentMethod.Value.PaymentMethodType != PaymentMethodType.StandardAndRedirection) return false; //this option is available only for redirection payment methods if (order.Deleted) @@ -390,7 +393,7 @@ public virtual bool CanRePostProcessPayment(Order order) if (order.PaymentStatus != PaymentStatus.Pending) return false; //payment status should be Pending - return paymentMethod.Value.CanRePostProcessPayment(order); + return paymentMethod.Value.CanRePostProcessPayment(order); } @@ -404,11 +407,11 @@ public virtual bool CanRePostProcessPayment(Order order) public virtual decimal GetAdditionalHandlingFee(IList cart, string paymentMethodSystemName) { var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); - var paymentMethodAdditionalFee = (paymentMethod != null ? paymentMethod.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); + var paymentMethodAdditionalFee = (paymentMethod != null ? paymentMethod.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); paymentMethodAdditionalFee = paymentMethodAdditionalFee.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); - return paymentMethodAdditionalFee; + return paymentMethodAdditionalFee; } @@ -423,7 +426,7 @@ public virtual bool SupportCapture(string paymentMethodSystemName) var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return false; - return paymentMethod.Value.SupportCapture; + return paymentMethod.Value.SupportCapture; } /// @@ -437,20 +440,20 @@ public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePayment if (paymentMethod == null) throw new SmartException(T("Payment.CouldNotLoadMethod")); - try - { - return paymentMethod.Value.Capture(capturePaymentRequest); - } - catch (NotSupportedException) - { - var result = new CapturePaymentResult(); - result.AddError(T("Common.Payment.NoCaptureSupport")); - return result; - } - catch - { - throw; - } + try + { + return paymentMethod.Value.Capture(capturePaymentRequest); + } + catch (NotSupportedException) + { + var result = new CapturePaymentResult(); + result.AddError(T("Common.Payment.NoCaptureSupport")); + return result; + } + catch + { + throw; + } } @@ -465,7 +468,7 @@ public virtual bool SupportPartiallyRefund(string paymentMethodSystemName) var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return false; - return paymentMethod.Value.SupportPartiallyRefund; + return paymentMethod.Value.SupportPartiallyRefund; } /// @@ -478,7 +481,7 @@ public virtual bool SupportRefund(string paymentMethodSystemName) var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return false; - return paymentMethod.Value.SupportRefund; + return paymentMethod.Value.SupportRefund; } /// @@ -492,20 +495,20 @@ public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequ if (paymentMethod == null) throw new SmartException(T("Payment.CouldNotLoadMethod")); - try - { - return paymentMethod.Value.Refund(refundPaymentRequest); - } - catch (NotSupportedException) - { - var result = new RefundPaymentResult(); - result.AddError(T("Common.Payment.NoRefundSupport")); - return result; - } - catch - { - throw; - } + try + { + return paymentMethod.Value.Refund(refundPaymentRequest); + } + catch (NotSupportedException) + { + var result = new RefundPaymentResult(); + result.AddError(T("Common.Payment.NoRefundSupport")); + return result; + } + catch + { + throw; + } } @@ -520,7 +523,7 @@ public virtual bool SupportVoid(string paymentMethodSystemName) var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return false; - return paymentMethod.Value.SupportVoid; + return paymentMethod.Value.SupportVoid; } /// @@ -534,20 +537,20 @@ public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) if (paymentMethod == null) throw new SmartException(T("Payment.CouldNotLoadMethod")); - try - { - return paymentMethod.Value.Void(voidPaymentRequest); - } - catch (NotSupportedException) - { - var result = new VoidPaymentResult(); - result.AddError(T("Common.Payment.NoVoidSupport")); - return result; - } - catch - { - throw; - } + try + { + return paymentMethod.Value.Void(voidPaymentRequest); + } + catch (NotSupportedException) + { + var result = new VoidPaymentResult(); + result.AddError(T("Common.Payment.NoVoidSupport")); + return result; + } + catch + { + throw; + } } @@ -563,7 +566,7 @@ public virtual RecurringPaymentType GetRecurringPaymentType(string paymentMethod if (paymentMethod == null) return RecurringPaymentType.NotSupported; - return paymentMethod.Value.RecurringPaymentType; + return paymentMethod.Value.RecurringPaymentType; } /// @@ -587,20 +590,20 @@ public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReques if (paymentMethod == null) throw new SmartException(T("Payment.CouldNotLoadMethod")); - try - { - return paymentMethod.Value.ProcessRecurringPayment(processPaymentRequest); - } - catch (NotSupportedException) - { - var result = new ProcessPaymentResult(); - result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); - return result; - } - catch - { - throw; - } + try + { + return paymentMethod.Value.ProcessRecurringPayment(processPaymentRequest); + } + catch (NotSupportedException) + { + var result = new ProcessPaymentResult(); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); + return result; + } + catch + { + throw; + } } } @@ -618,20 +621,20 @@ public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurri if (paymentMethod == null) throw new SmartException(T("Payment.CouldNotLoadMethod")); - try - { - return paymentMethod.Value.CancelRecurringPayment(cancelPaymentRequest); - } - catch (NotSupportedException) - { - var result = new CancelRecurringPaymentResult(); - result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); - return result; - } - catch - { - throw; - } + try + { + return paymentMethod.Value.CancelRecurringPayment(cancelPaymentRequest); + } + catch (NotSupportedException) + { + var result = new CancelRecurringPaymentResult(); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); + return result; + } + catch + { + throw; + } } @@ -647,7 +650,7 @@ public virtual PaymentMethodType GetPaymentMethodType(string paymentMethodSystem if (paymentMethod == null) return PaymentMethodType.Unknown; - return paymentMethod.Value.PaymentMethodType; + return paymentMethod.Value.PaymentMethodType; } /// @@ -672,26 +675,26 @@ public virtual string GetMaskedCreditCardNumber(string creditCardNumber) return maskedChars + last4; } - public virtual IList GetAllPaymentMethodFilters() - { - if (_paymentMethodFilterTypes == null) - { - lock (_lock) - { - if (_paymentMethodFilterTypes == null) - { - _paymentMethodFilterTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true).ToList(); - } - } - } - - var paymentMethodFilters = _paymentMethodFilterTypes - .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as IPaymentMethodFilter) - .ToList(); - - return paymentMethodFilters; - } - - #endregion - } + public virtual IList GetAllPaymentMethodFilters() + { + if (_paymentMethodFilterTypes == null) + { + lock (_lock) + { + if (_paymentMethodFilterTypes == null) + { + _paymentMethodFilterTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true).ToList(); + } + } + } + + var paymentMethodFilters = _paymentMethodFilterTypes + .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as IPaymentMethodFilter) + .ToList(); + + return paymentMethodFilters; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs b/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs index f14a6cc6aa..45fff023f4 100644 --- a/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs +++ b/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs @@ -12,14 +12,14 @@ public partial class PostProcessPaymentRequest /// public Order Order { get; set; } - /// - /// Whether the customer clicked the button to re-post the payment process - /// - public bool IsRePostProcessPayment { get; set; } + /// + /// Whether the customer clicked the button to re-post the payment process + /// + public bool IsRePostProcessPayment { get; set; } - /// - /// URL to a payment provider to fulfill the payment. The .NET core will redirect to it. - /// - public string RedirectUrl { get; set; } + /// + /// URL to a payment provider to fulfill the payment. The .NET core will redirect to it. + /// + public string RedirectUrl { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Payments/PreProcessPaymentResult.cs b/src/Libraries/SmartStore.Services/Payments/PreProcessPaymentResult.cs index d59c10eb48..3dbf7b775f 100644 --- a/src/Libraries/SmartStore.Services/Payments/PreProcessPaymentResult.cs +++ b/src/Libraries/SmartStore.Services/Payments/PreProcessPaymentResult.cs @@ -1,10 +1,10 @@  namespace SmartStore.Services.Payments { - /// - /// Represents a pre process payment result - /// - public partial class PreProcessPaymentResult : ProcessPaymentResultBase - { - } + /// + /// Represents a pre process payment result + /// + public partial class PreProcessPaymentResult : ProcessPaymentResultBase + { + } } diff --git a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs index 334a261ff0..e03d989008 100644 --- a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs +++ b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs @@ -11,17 +11,17 @@ namespace SmartStore.Services.Payments [Serializable] public partial class ProcessPaymentRequest { - public ProcessPaymentRequest() - { - CustomProperties = new Dictionary(); + public ProcessPaymentRequest() + { + CustomProperties = new Dictionary(); IsMultiOrder = false; ShoppingCartItemIds = new List(); } - /// - /// Gets or sets a store identifier - /// - public int StoreId { get; set; } + /// + /// Gets or sets a store identifier + /// + public int StoreId { get; set; } /// /// Gets or sets a customer @@ -38,10 +38,10 @@ public ProcessPaymentRequest() /// public decimal OrderTotal { get; set; } - /// - /// Gets or sets an order tax total - /// - public decimal OrderTax { get; set; } + /// + /// Gets or sets an order tax total + /// + public decimal OrderTax { get; set; } /// /// Gets or sets a payment method identifier @@ -53,10 +53,10 @@ public ProcessPaymentRequest() /// public bool IsMultiOrder { get; set; } - /// - /// Use that dictionary for any payment method or checkout flow specific data - /// - public Dictionary CustomProperties { get; set; } + /// + /// Use that dictionary for any payment method or checkout flow specific data + /// + public Dictionary CustomProperties { get; set; } public IList ShoppingCartItemIds { get; set; } @@ -112,16 +112,16 @@ public ProcessPaymentRequest() /// public string PurchaseOrderNumber { get; set; } - public int CreditCardStartYear { get; set; } - public int CreditCardStartMonth { get; set; } - public string CreditCardIssueNumber { get; set; } - public string DirectDebitAccountHolder { get; set; } - public string DirectDebitAccountNumber { get; set; } - public string DirectDebitBankCode { get; set; } - public string DirectDebitCountry { get; set; } - public string DirectDebitBankName { get; set; } - public string DirectDebitIban { get; set; } - public string DirectDebitBic { get; set; } + public int CreditCardStartYear { get; set; } + public int CreditCardStartMonth { get; set; } + public string CreditCardIssueNumber { get; set; } + public string DirectDebitAccountHolder { get; set; } + public string DirectDebitAccountNumber { get; set; } + public string DirectDebitBankCode { get; set; } + public string DirectDebitCountry { get; set; } + public string DirectDebitBankName { get; set; } + public string DirectDebitIban { get; set; } + public string DirectDebitBic { get; set; } public bool IsShippingMethodSet { get; set; } @@ -138,7 +138,7 @@ public ProcessPaymentRequest() /// Gets or sets an initial (parent) order identifier if order is recurring /// public int InitialOrderId { get; set; } - + /// /// Gets or sets the cycle length /// @@ -158,17 +158,17 @@ public ProcessPaymentRequest() } - [Serializable] - public partial class CustomPaymentRequestValue - { - /// - /// The value of the custom property - /// - public object Value { get; set; } + [Serializable] + public partial class CustomPaymentRequestValue + { + /// + /// The value of the custom property + /// + public object Value { get; set; } - /// - /// Indicates whether to automatically create a generic attribute if an order has been placed - /// - public bool AutoCreateGenericAttribute { get; set; } - } + /// + /// Indicates whether to automatically create a generic attribute if an order has been placed + /// + public bool AutoCreateGenericAttribute { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResult.cs b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResult.cs index 28977963d6..64ef7ad40e 100644 --- a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResult.cs +++ b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResult.cs @@ -44,7 +44,7 @@ public partial class ProcessPaymentResult : ProcessPaymentResultBase /// Gets or sets the subscription transaction identifier /// public string SubscriptionTransactionId { get; set; } - + /// /// Gets or sets a value indicating whether storing of credit card number, CVV2 is allowed /// @@ -60,14 +60,8 @@ public partial class ProcessPaymentResult : ProcessPaymentResultBase /// public PaymentStatus NewPaymentStatus { - get - { - return _newPaymentStatus; - } - set - { - _newPaymentStatus = value; - } + get => _newPaymentStatus; + set => _newPaymentStatus = value; } } } diff --git a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResultBase.cs b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResultBase.cs index 7657c293cc..cbce0a3ea8 100644 --- a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResultBase.cs +++ b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentResultBase.cs @@ -2,23 +2,20 @@ namespace SmartStore.Services.Payments { - public partial class ProcessPaymentResultBase - { - public ProcessPaymentResultBase() - { - this.Errors = new List(); - } + public partial class ProcessPaymentResultBase + { + public ProcessPaymentResultBase() + { + this.Errors = new List(); + } - public IList Errors { get; set; } + public IList Errors { get; set; } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); - public void AddError(string error) - { - this.Errors.Add(error); - } - } + public void AddError(string error) + { + this.Errors.Add(error); + } + } } diff --git a/src/Libraries/SmartStore.Services/Payments/RefundPaymentResult.cs b/src/Libraries/SmartStore.Services/Payments/RefundPaymentResult.cs index bda98cf05e..b880e6029c 100644 --- a/src/Libraries/SmartStore.Services/Payments/RefundPaymentResult.cs +++ b/src/Libraries/SmartStore.Services/Payments/RefundPaymentResult.cs @@ -11,17 +11,14 @@ public partial class RefundPaymentResult private PaymentStatus _newPaymentStatus = PaymentStatus.Pending; public IList Errors { get; set; } - public RefundPaymentResult() + public RefundPaymentResult() { this.Errors = new List(); } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); - public void AddError(string error) + public void AddError(string error) { this.Errors.Add(error); } @@ -33,14 +30,8 @@ public void AddError(string error) /// public PaymentStatus NewPaymentStatus { - get - { - return _newPaymentStatus; - } - set - { - _newPaymentStatus = value; - } + get => _newPaymentStatus; + set => _newPaymentStatus = value; } #endregion diff --git a/src/Libraries/SmartStore.Services/Payments/VoidPaymentResult.cs b/src/Libraries/SmartStore.Services/Payments/VoidPaymentResult.cs index 5b13365083..8ec3cba1d6 100644 --- a/src/Libraries/SmartStore.Services/Payments/VoidPaymentResult.cs +++ b/src/Libraries/SmartStore.Services/Payments/VoidPaymentResult.cs @@ -11,17 +11,14 @@ public partial class VoidPaymentResult private PaymentStatus _newPaymentStatus = PaymentStatus.Pending; public IList Errors { get; set; } - public VoidPaymentResult() + public VoidPaymentResult() { this.Errors = new List(); } - public bool Success - { - get { return (this.Errors.Count == 0); } - } + public bool Success => (this.Errors.Count == 0); - public void AddError(string error) + public void AddError(string error) { this.Errors.Add(error); } @@ -33,14 +30,8 @@ public void AddError(string error) /// public PaymentStatus NewPaymentStatus { - get - { - return _newPaymentStatus; - } - set - { - _newPaymentStatus = value; - } + get => _newPaymentStatus; + set => _newPaymentStatus = value; } #endregion diff --git a/src/Libraries/SmartStore.Services/Pdf/Content/IPdfContent.cs b/src/Libraries/SmartStore.Services/Pdf/Content/IPdfContent.cs index 9a1319426c..6f983e1afe 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Content/IPdfContent.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Content/IPdfContent.cs @@ -4,20 +4,20 @@ namespace SmartStore.Services.Pdf { - public interface IPdfContent - { - PdfContentKind Kind { get; } + public interface IPdfContent + { + PdfContentKind Kind { get; } - string Process(string flag); + string Process(string flag); - void WriteArguments(string flag, StringBuilder builder); + void WriteArguments(string flag, StringBuilder builder); - void Teardown(); - } + void Teardown(); + } - public enum PdfContentKind - { - Html, - Url - } + public enum PdfContentKind + { + Html, + Url + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/Content/PdfHtmlContent.cs b/src/Libraries/SmartStore.Services/Pdf/Content/PdfHtmlContent.cs index 226e742928..493a48b521 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Content/PdfHtmlContent.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Content/PdfHtmlContent.cs @@ -9,107 +9,104 @@ namespace SmartStore.Services.Pdf { - public class PdfHtmlContent : IPdfContent - { - private static readonly Uri _engineBaseUri; - - private string _html; - private string _originalHtml; - private bool _processed; - private string _tempFilePath; - private PdfContentKind _kind = PdfContentKind.Html; - - static PdfHtmlContent() - { - var baseUrl = CommonHelper.GetAppSetting("sm:PdfEngineBaseUrl").TrimSafe().NullEmpty(); - if (baseUrl != null) - { - Uri uri; - if (Uri.TryCreate(baseUrl, UriKind.Absolute, out uri)) - { - _engineBaseUri = uri; - } - } - } - - public PdfHtmlContent(string html) - { - Guard.NotEmpty(html, nameof(html)); - - _originalHtml = html; - _html = html; - } - - public PdfContentKind Kind - { - get { return _kind; } - } - - public string Process(string flag) - { - if (!_processed) - { - if (_engineBaseUri != null) - { - _html = WebHelper.MakeAllUrlsAbsolute(_html, _engineBaseUri.Scheme, _engineBaseUri.Authority); - } - else if (HttpContext.Current?.Request != null) - { - _html = WebHelper.MakeAllUrlsAbsolute(_html, new HttpRequestWrapper(HttpContext.Current.Request)); - } - - if (!flag.IsCaseInsensitiveEqual("page")) - { - CreateTempFile(); - } - - _processed = true; - } - - return _tempFilePath ?? _html; - } - - private void CreateTempFile() - { - // TODO: (mc) This is a very weak mechanism to determine if html is partial. Find a better way! - bool isPartial = !_html.Trim().StartsWith("\r\n\r\n{0}\r\n", html); - } - - public void WriteArguments(string flag, StringBuilder builder) - { - // noop - } - - public void Teardown() - { - if (_tempFilePath != null && File.Exists(_tempFilePath)) - { - try - { - File.Delete(_tempFilePath); - } - catch { } - } - - _kind = PdfContentKind.Html; - _html = _originalHtml; - _tempFilePath = null; - _processed = false; - } - } + public class PdfHtmlContent : IPdfContent + { + private static readonly Uri _engineBaseUri; + + private string _html; + private string _originalHtml; + private bool _processed; + private string _tempFilePath; + private PdfContentKind _kind = PdfContentKind.Html; + + static PdfHtmlContent() + { + var baseUrl = CommonHelper.GetAppSetting("sm:PdfEngineBaseUrl").TrimSafe().NullEmpty(); + if (baseUrl != null) + { + Uri uri; + if (Uri.TryCreate(baseUrl, UriKind.Absolute, out uri)) + { + _engineBaseUri = uri; + } + } + } + + public PdfHtmlContent(string html) + { + Guard.NotEmpty(html, nameof(html)); + + _originalHtml = html; + _html = html; + } + + public PdfContentKind Kind => _kind; + + public string Process(string flag) + { + if (!_processed) + { + if (_engineBaseUri != null) + { + _html = WebHelper.MakeAllUrlsAbsolute(_html, _engineBaseUri.Scheme, _engineBaseUri.Authority); + } + else if (HttpContext.Current?.Request != null) + { + _html = WebHelper.MakeAllUrlsAbsolute(_html, new HttpRequestWrapper(HttpContext.Current.Request)); + } + + if (!flag.IsCaseInsensitiveEqual("page")) + { + CreateTempFile(); + } + + _processed = true; + } + + return _tempFilePath ?? _html; + } + + private void CreateTempFile() + { + // TODO: (mc) This is a very weak mechanism to determine if html is partial. Find a better way! + bool isPartial = !_html.Trim().StartsWith("\r\n\r\n{0}\r\n", html); + } + + public void WriteArguments(string flag, StringBuilder builder) + { + // noop + } + + public void Teardown() + { + if (_tempFilePath != null && File.Exists(_tempFilePath)) + { + try + { + File.Delete(_tempFilePath); + } + catch { } + } + + _kind = PdfContentKind.Html; + _html = _originalHtml; + _tempFilePath = null; + _processed = false; + } + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/Content/PdfUrlContent.cs b/src/Libraries/SmartStore.Services/Pdf/Content/PdfUrlContent.cs index ecb19891bd..6065e91a84 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Content/PdfUrlContent.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Content/PdfUrlContent.cs @@ -10,147 +10,144 @@ namespace SmartStore.Services.Pdf { - public class PdfUrlContent : IPdfContent - { - private static readonly Uri _engineBaseUri; - private readonly string _url; - - static PdfUrlContent() - { - var baseUrl = CommonHelper.GetAppSetting("sm:PdfEngineBaseUrl").TrimSafe().NullEmpty(); - if (baseUrl != null) - { - Uri uri; - if (Uri.TryCreate(baseUrl, UriKind.Absolute, out uri)) - { - _engineBaseUri = uri; - } - } - } - - public PdfUrlContent(string url) - { - Guard.NotEmpty(url, nameof(url)); - _url = url; - - this.SendAuthCookie = false; - this.Post = new Dictionary(); - this.Cookies = new Dictionary(); - } - - #region Options - - /// - /// Send FormsAuthentication cookie to authorize - /// - public bool SendAuthCookie { get; set; } - - /// - /// HTTP Authentication username. - /// - public string UserName { get; set; } - - /// - /// HTTP Authentication password. - /// - public string Password { get; set; } - - /// - /// Sets cookies. - /// - public IDictionary Post { get; set; } - - /// - /// Sets post values. - /// - public IDictionary Cookies { get; set; } - - #endregion - - public PdfContentKind Kind - { - get { return PdfContentKind.Url; } - } - - protected virtual string GetAbsoluteUrl() - { - if (_engineBaseUri != null) - { - var url = _url; - if (url.StartsWith("~")) - { - url = VirtualPathUtility.ToAbsolute(url); - } - - return _engineBaseUri.ToString() + url.TrimStart('/'); - } - else if (HttpContext.Current?.Request != null) - { - return WebHelper.GetAbsoluteUrl(_url, new HttpRequestWrapper(HttpContext.Current.Request)); - } - - return _url; - } - - public string Process(string flag) - { - return GetAbsoluteUrl(); - } - - public void WriteArguments(string flag, StringBuilder builder) - { - if (UserName.HasValue()) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " --username {0}", UserName); - } - - if (Password.HasValue()) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " --password {0}", Password); - } - - if (Post != null && Post.Count > 0) - { - CreateRepeatableFlags("--post", Post, builder); - } - - if (Cookies != null && Cookies.Count > 0) - { - CreateRepeatableFlags("--cookie", Cookies, builder); - } - - // Send FormsAuthentication Cookie - var ctx = HttpContext.Current; - if (SendAuthCookie && ctx != null && ctx.Request != null && ctx.Request.Cookies != null) - { - var authCookieName = FormsAuthentication.FormsCookieName; - if (Cookies == null || !Cookies.ContainsKey(authCookieName)) - { - HttpCookie authCookie = null; - if (ctx.Request.Cookies.AllKeys.Contains(authCookieName)) - { - authCookie = ctx.Request.Cookies[authCookieName]; - } - if (authCookie != null) - { - var authCookieValue = authCookie.Value; - builder.AppendFormat(CultureInfo.InvariantCulture, " {0} {1} {2}", "--cookie", authCookieName, authCookieValue); - } - } - } - } - - private void CreateRepeatableFlags(string flagName, IDictionary dict, StringBuilder sb) - { - foreach (var kvp in dict) - { - sb.AppendFormat(CultureInfo.InvariantCulture, " {0} {1} {2}", flagName, kvp.Key, kvp.Value.EmptyNull()); - } - } - - public void Teardown() - { - // noop - } - } + public class PdfUrlContent : IPdfContent + { + private static readonly Uri _engineBaseUri; + private readonly string _url; + + static PdfUrlContent() + { + var baseUrl = CommonHelper.GetAppSetting("sm:PdfEngineBaseUrl").TrimSafe().NullEmpty(); + if (baseUrl != null) + { + Uri uri; + if (Uri.TryCreate(baseUrl, UriKind.Absolute, out uri)) + { + _engineBaseUri = uri; + } + } + } + + public PdfUrlContent(string url) + { + Guard.NotEmpty(url, nameof(url)); + _url = url; + + this.SendAuthCookie = false; + this.Post = new Dictionary(); + this.Cookies = new Dictionary(); + } + + #region Options + + /// + /// Send FormsAuthentication cookie to authorize + /// + public bool SendAuthCookie { get; set; } + + /// + /// HTTP Authentication username. + /// + public string UserName { get; set; } + + /// + /// HTTP Authentication password. + /// + public string Password { get; set; } + + /// + /// Sets cookies. + /// + public IDictionary Post { get; set; } + + /// + /// Sets post values. + /// + public IDictionary Cookies { get; set; } + + #endregion + + public PdfContentKind Kind => PdfContentKind.Url; + + protected virtual string GetAbsoluteUrl() + { + if (_engineBaseUri != null) + { + var url = _url; + if (url.StartsWith("~")) + { + url = VirtualPathUtility.ToAbsolute(url); + } + + return _engineBaseUri.ToString() + url.TrimStart('/'); + } + else if (HttpContext.Current?.Request != null) + { + return WebHelper.GetAbsoluteUrl(_url, new HttpRequestWrapper(HttpContext.Current.Request)); + } + + return _url; + } + + public string Process(string flag) + { + return GetAbsoluteUrl(); + } + + public void WriteArguments(string flag, StringBuilder builder) + { + if (UserName.HasValue()) + { + builder.AppendFormat(CultureInfo.InvariantCulture, " --username {0}", UserName); + } + + if (Password.HasValue()) + { + builder.AppendFormat(CultureInfo.InvariantCulture, " --password {0}", Password); + } + + if (Post != null && Post.Count > 0) + { + CreateRepeatableFlags("--post", Post, builder); + } + + if (Cookies != null && Cookies.Count > 0) + { + CreateRepeatableFlags("--cookie", Cookies, builder); + } + + // Send FormsAuthentication Cookie + var ctx = HttpContext.Current; + if (SendAuthCookie && ctx != null && ctx.Request != null && ctx.Request.Cookies != null) + { + var authCookieName = FormsAuthentication.FormsCookieName; + if (Cookies == null || !Cookies.ContainsKey(authCookieName)) + { + HttpCookie authCookie = null; + if (ctx.Request.Cookies.AllKeys.Contains(authCookieName)) + { + authCookie = ctx.Request.Cookies[authCookieName]; + } + if (authCookie != null) + { + var authCookieValue = authCookie.Value; + builder.AppendFormat(CultureInfo.InvariantCulture, " {0} {1} {2}", "--cookie", authCookieName, authCookieValue); + } + } + } + } + + private void CreateRepeatableFlags(string flagName, IDictionary dict, StringBuilder sb) + { + foreach (var kvp in dict) + { + sb.AppendFormat(CultureInfo.InvariantCulture, " {0} {1} {2}", flagName, kvp.Key, kvp.Value.EmptyNull()); + } + } + + public void Teardown() + { + // noop + } + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/IPdfConverter.cs b/src/Libraries/SmartStore.Services/Pdf/IPdfConverter.cs index 69bcad9872..0812726e74 100644 --- a/src/Libraries/SmartStore.Services/Pdf/IPdfConverter.cs +++ b/src/Libraries/SmartStore.Services/Pdf/IPdfConverter.cs @@ -5,13 +5,13 @@ namespace SmartStore.Services.Pdf { - public interface IPdfConverter - { - /// - /// Converts html content to PDF - /// - /// The settings to be used for the conversion process - /// The PDF binary data - byte[] Convert(PdfConvertSettings settings); - } + public interface IPdfConverter + { + /// + /// Converts html content to PDF + /// + /// The settings to be used for the conversion process + /// The PDF binary data + byte[] Convert(PdfConvertSettings settings); + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/Options/IPdfOptions.cs b/src/Libraries/SmartStore.Services/Pdf/Options/IPdfOptions.cs index 468a73ae6e..4f1f8e4c08 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Options/IPdfOptions.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Options/IPdfOptions.cs @@ -3,14 +3,14 @@ namespace SmartStore.Services.Pdf { - public interface IPdfOptions - { - /// - /// Processes the options by converting them to native arguments - /// - /// The section flag - /// The builder - /// Possible flags are: page | header | footer | cover | toc - void Process(string flag, StringBuilder builder); - } + public interface IPdfOptions + { + /// + /// Processes the options by converting them to native arguments + /// + /// The section flag + /// The builder + /// Possible flags are: page | header | footer | cover | toc + void Process(string flag, StringBuilder builder); + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/Options/PdfHeaderFooterOptions.cs b/src/Libraries/SmartStore.Services/Pdf/Options/PdfHeaderFooterOptions.cs index 2456fd4743..49569a396b 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Options/PdfHeaderFooterOptions.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Options/PdfHeaderFooterOptions.cs @@ -5,92 +5,89 @@ namespace SmartStore.Services.Pdf { - public class PdfHeaderFooterOptions : IPdfOptions - { + public class PdfHeaderFooterOptions : IPdfOptions + { - public PdfHeaderFooterOptions() - { - Spacing = 5; - FontSize = 10; - } + public PdfHeaderFooterOptions() + { + Spacing = 5; + FontSize = 10; + } - /// - /// Spacing between footer and content in mm (default 0) - /// - public float? Spacing { get; set; } + /// + /// Spacing between footer and content in mm (default 0) + /// + public float? Spacing { get; set; } - /// - /// Display line below the header / above the footer - /// - public bool ShowLine { get; set; } + /// + /// Display line below the header / above the footer + /// + public bool ShowLine { get; set; } - /// - /// Set font name (default Arial) - /// - public string FontName { get; set; } + /// + /// Set font name (default Arial) + /// + public string FontName { get; set; } - /// - /// Set font size (default 12) - /// - public float? FontSize { get; set; } + /// + /// Set font size (default 12) + /// + public float? FontSize { get; set; } - /// - /// Left aligned text - /// - public string TextLeft { get; set; } + /// + /// Left aligned text + /// + public string TextLeft { get; set; } - /// - /// Centered text - /// - public string TextCenter { get; set; } + /// + /// Centered text + /// + public string TextCenter { get; set; } - /// - /// Right aligned text - /// - public string TextRight { get; set; } + /// + /// Right aligned text + /// + public string TextRight { get; set; } - public bool HasText - { - get { return TextLeft.HasValue() || TextCenter.HasValue() || TextRight.HasValue(); } - } - - public void Process(string flag, StringBuilder builder) - { - if (Spacing.HasValue) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " --{0}-spacing {1}", flag, Spacing.Value); - } + public bool HasText => TextLeft.HasValue() || TextCenter.HasValue() || TextRight.HasValue(); - if (ShowLine) - { - builder.AppendFormat(" --{0}-line", flag); - } + public void Process(string flag, StringBuilder builder) + { + if (Spacing.HasValue) + { + builder.AppendFormat(CultureInfo.InvariantCulture, " --{0}-spacing {1}", flag, Spacing.Value); + } - if (HasText) - { - if (FontName.HasValue()) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " --{0}-font-name \"{1}\"", flag, FontName); - } - if (FontSize.HasValue) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " --{0}-font-size {1}", flag, FontSize.Value); - } - } + if (ShowLine) + { + builder.AppendFormat(" --{0}-line", flag); + } - if (TextLeft.HasValue()) - { - builder.AppendFormat(CultureInfo.CurrentCulture, " --{0}-left \"{1}\"", flag, TextLeft); - } - if (TextCenter.HasValue()) - { - builder.AppendFormat(CultureInfo.CurrentCulture, " --{0}-center \"{1}\"", flag, TextCenter); - } - if (TextRight.HasValue()) - { - builder.AppendFormat(CultureInfo.CurrentCulture, " --{0}-right \"{1}\"", flag, TextRight); - } - } + if (HasText) + { + if (FontName.HasValue()) + { + builder.AppendFormat(CultureInfo.InvariantCulture, " --{0}-font-name \"{1}\"", flag, FontName); + } + if (FontSize.HasValue) + { + builder.AppendFormat(CultureInfo.InvariantCulture, " --{0}-font-size {1}", flag, FontSize.Value); + } + } - } + if (TextLeft.HasValue()) + { + builder.AppendFormat(CultureInfo.CurrentCulture, " --{0}-left \"{1}\"", flag, TextLeft); + } + if (TextCenter.HasValue()) + { + builder.AppendFormat(CultureInfo.CurrentCulture, " --{0}-center \"{1}\"", flag, TextCenter); + } + if (TextRight.HasValue()) + { + builder.AppendFormat(CultureInfo.CurrentCulture, " --{0}-right \"{1}\"", flag, TextRight); + } + } + + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/Options/PdfPageOptions.cs b/src/Libraries/SmartStore.Services/Pdf/Options/PdfPageOptions.cs index 2db0049016..f9c2d82d80 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Options/PdfPageOptions.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Options/PdfPageOptions.cs @@ -4,69 +4,69 @@ namespace SmartStore.Services.Pdf { - public class PdfPageOptions : IPdfOptions - { + public class PdfPageOptions : IPdfOptions + { - public PdfPageOptions() - { - this.Zoom = 1f; - this.UsePrintMediaType = true; - } + public PdfPageOptions() + { + this.Zoom = 1f; + this.UsePrintMediaType = true; + } - /// - /// Get or set zoom factor - /// - public float Zoom { get; set; } + /// + /// Get or set zoom factor + /// + public float Zoom { get; set; } - /// - /// Indicates whether the page background should be disabled. - /// - public bool BackgroundDisabled { get; set; } + /// + /// Indicates whether the page background should be disabled. + /// + public bool BackgroundDisabled { get; set; } - /// - /// Use print media-type instead of screen - /// - public bool UsePrintMediaType { get; set; } + /// + /// Use print media-type instead of screen + /// + public bool UsePrintMediaType { get; set; } - /// - /// Specifies a user style sheet to load with every page - /// - public string UserStylesheetUrl { get; set; } + /// + /// Specifies a user style sheet to load with every page + /// + public string UserStylesheetUrl { get; set; } - /// - /// Custom page pdf tool options - /// - public string CustomFlags { get; set; } + /// + /// Custom page pdf tool options + /// + public string CustomFlags { get; set; } - public virtual void Process(string flag, StringBuilder builder) - { - if (UserStylesheetUrl.HasValue()) - { - builder.AppendFormat(CultureInfo.InvariantCulture, " --user-style-sheet \"{0}\"", UserStylesheetUrl); - } + public virtual void Process(string flag, StringBuilder builder) + { + if (UserStylesheetUrl.HasValue()) + { + builder.AppendFormat(CultureInfo.InvariantCulture, " --user-style-sheet \"{0}\"", UserStylesheetUrl); + } - if (UsePrintMediaType) - { - builder.Append(" --print-media-type"); - } + if (UsePrintMediaType) + { + builder.Append(" --print-media-type"); + } - if (BackgroundDisabled) - { - builder.Append(" --no-background"); - } + if (BackgroundDisabled) + { + builder.Append(" --no-background"); + } - if (Zoom != 1 /*&& flag.IsCaseInsensitiveEqual("page")*/) - { - builder.AppendFormat(" --zoom {0}", Zoom); - } + if (Zoom != 1 /*&& flag.IsCaseInsensitiveEqual("page")*/) + { + builder.AppendFormat(" --zoom {0}", Zoom); + } - if (CustomFlags.HasValue()) - { - builder.AppendFormat(" {0}", CustomFlags); - } - } + if (CustomFlags.HasValue()) + { + builder.AppendFormat(" {0}", CustomFlags); + } + } - } + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/Options/PdfTocOptions.cs b/src/Libraries/SmartStore.Services/Pdf/Options/PdfTocOptions.cs index 89692ca7e6..3b116de25b 100644 --- a/src/Libraries/SmartStore.Services/Pdf/Options/PdfTocOptions.cs +++ b/src/Libraries/SmartStore.Services/Pdf/Options/PdfTocOptions.cs @@ -4,75 +4,75 @@ namespace SmartStore.Services.Pdf { - public class PdfTocOptions : PdfPageOptions - { - public PdfTocOptions() - : base() - { - } - - /// - /// TOC creation enabled - /// - public bool Enabled { get; set; } - - /// - /// The header text of the toc (default Table of Contents) - /// - public string TocHeaderText { get; set; } - - /// - /// Do not use dotted lines in the toc - /// - public bool DisableDottedLines { get; set; } - - /// - /// Do not link from toc to sections - /// - public bool DisableTocLinks { get; set; } - - /// - /// For each level of headings in the toc indent by this length (default 1em) - /// - public string TocLevelIndendation { get; set; } - - /// - /// For each level of headings in the toc the font is scaled by this factor (default 0.8) - /// - public float? TocTextSizeShrink { get; set; } - - - public override void Process(string flag, StringBuilder builder) - { - builder.Append(" toc"); - - if (TocHeaderText.HasValue()) - { - builder.AppendFormat(" --toc-header-text \"{0}\"", TocHeaderText.Replace("\"", "\\\"")); - } - - if (DisableDottedLines) - { - builder.Append(" --disable-dotted-lines"); - } - - if (DisableTocLinks) - { - builder.Append(" --disable-toc-links"); - } - - if (TocLevelIndendation.HasValue()) - { - builder.AppendFormat(" --toc-level-indentation {0}", TocLevelIndendation); - } - - if (TocTextSizeShrink.HasValue) - { - builder.AppendFormat(" --toc-text-size-shrink {0}", TocTextSizeShrink.Value); - } - - base.Process(flag, builder); - } - - } + public class PdfTocOptions : PdfPageOptions + { + public PdfTocOptions() + : base() + { + } + + /// + /// TOC creation enabled + /// + public bool Enabled { get; set; } + + /// + /// The header text of the toc (default Table of Contents) + /// + public string TocHeaderText { get; set; } + + /// + /// Do not use dotted lines in the toc + /// + public bool DisableDottedLines { get; set; } + + /// + /// Do not link from toc to sections + /// + public bool DisableTocLinks { get; set; } + + /// + /// For each level of headings in the toc indent by this length (default 1em) + /// + public string TocLevelIndendation { get; set; } + + /// + /// For each level of headings in the toc the font is scaled by this factor (default 0.8) + /// + public float? TocTextSizeShrink { get; set; } + + + public override void Process(string flag, StringBuilder builder) + { + builder.Append(" toc"); + + if (TocHeaderText.HasValue()) + { + builder.AppendFormat(" --toc-header-text \"{0}\"", TocHeaderText.Replace("\"", "\\\"")); + } + + if (DisableDottedLines) + { + builder.Append(" --disable-dotted-lines"); + } + + if (DisableTocLinks) + { + builder.Append(" --disable-toc-links"); + } + + if (TocLevelIndendation.HasValue()) + { + builder.AppendFormat(" --toc-level-indentation {0}", TocLevelIndendation); + } + + if (TocTextSizeShrink.HasValue) + { + builder.AppendFormat(" --toc-text-size-shrink {0}", TocTextSizeShrink.Value); + } + + base.Process(flag, builder); + } + + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/PdfConvertSettings.cs b/src/Libraries/SmartStore.Services/Pdf/PdfConvertSettings.cs index 8aa22eb59e..c099e3ddb4 100644 --- a/src/Libraries/SmartStore.Services/Pdf/PdfConvertSettings.cs +++ b/src/Libraries/SmartStore.Services/Pdf/PdfConvertSettings.cs @@ -2,110 +2,110 @@ namespace SmartStore.Services.Pdf { - public class PdfConvertSettings - { - public PdfConvertSettings() - { - this.Margins = new PdfPageMargins(); - this.Orientation = PdfPagePrientation.Default; - this.Size = PdfPageSize.Default; - this.PageOptions = new PdfPageOptions(); - this.HeaderOptions = new PdfHeaderFooterOptions(); - this.FooterOptions = new PdfHeaderFooterOptions(); - this.CoverOptions = new PdfPageOptions(); - this.TocOptions = new PdfTocOptions(); - } - - /// - /// The title of the generated pdf file (The title of the first document is used if not specified) - /// - public string Title { get; set; } - - /// - /// Get or set option to generate grayscale PDF - /// - public bool Grayscale { get; set; } - - /// - /// Get or set option to generate low quality PDF (shrink the result document space) - /// - public bool LowQuality { get; set; } - - /// - /// Get or set PDF page margins (in mm) - /// - public PdfPageMargins Margins { get; set; } - - /// - /// Get or set PDF page orientation - /// - public PdfPagePrientation Orientation { get; set; } - - /// - /// Get or set PDF page width (in mm) - /// - public float? PageWidth { get; set; } - - /// - /// Get or set PDF page height (in mm) - /// - public float? PageHeight { get; set; } - - /// - /// Get or set PDF page orientation - /// - public PdfPageSize Size { get; set; } - - /// - /// Custom global pdf tool options - /// - public string CustomFlags { get; set; } - - - - /// - /// Cover content - /// - public IPdfContent Cover { get; set; } - - /// - /// Cover content options - /// - public PdfPageOptions CoverOptions { get; set; } - - /// - /// Toc (table of contents) options - /// - public PdfTocOptions TocOptions { get; set; } - - /// - /// Page content (required) - /// - public IPdfContent Page { get; set; } - - /// - /// Page content options - /// - public PdfPageOptions PageOptions { get; set; } - - /// - /// Footer content - /// - public IPdfContent Footer { get; set; } - - /// - /// Footer content options - /// - public PdfHeaderFooterOptions FooterOptions { get; set; } - - /// - /// Header content - /// - public IPdfContent Header { get; set; } - - /// - /// Header content options - /// - public PdfHeaderFooterOptions HeaderOptions { get; set; } - } + public class PdfConvertSettings + { + public PdfConvertSettings() + { + this.Margins = new PdfPageMargins(); + this.Orientation = PdfPagePrientation.Default; + this.Size = PdfPageSize.Default; + this.PageOptions = new PdfPageOptions(); + this.HeaderOptions = new PdfHeaderFooterOptions(); + this.FooterOptions = new PdfHeaderFooterOptions(); + this.CoverOptions = new PdfPageOptions(); + this.TocOptions = new PdfTocOptions(); + } + + /// + /// The title of the generated pdf file (The title of the first document is used if not specified) + /// + public string Title { get; set; } + + /// + /// Get or set option to generate grayscale PDF + /// + public bool Grayscale { get; set; } + + /// + /// Get or set option to generate low quality PDF (shrink the result document space) + /// + public bool LowQuality { get; set; } + + /// + /// Get or set PDF page margins (in mm) + /// + public PdfPageMargins Margins { get; set; } + + /// + /// Get or set PDF page orientation + /// + public PdfPagePrientation Orientation { get; set; } + + /// + /// Get or set PDF page width (in mm) + /// + public float? PageWidth { get; set; } + + /// + /// Get or set PDF page height (in mm) + /// + public float? PageHeight { get; set; } + + /// + /// Get or set PDF page orientation + /// + public PdfPageSize Size { get; set; } + + /// + /// Custom global pdf tool options + /// + public string CustomFlags { get; set; } + + + + /// + /// Cover content + /// + public IPdfContent Cover { get; set; } + + /// + /// Cover content options + /// + public PdfPageOptions CoverOptions { get; set; } + + /// + /// Toc (table of contents) options + /// + public PdfTocOptions TocOptions { get; set; } + + /// + /// Page content (required) + /// + public IPdfContent Page { get; set; } + + /// + /// Page content options + /// + public PdfPageOptions PageOptions { get; set; } + + /// + /// Footer content + /// + public IPdfContent Footer { get; set; } + + /// + /// Footer content options + /// + public PdfHeaderFooterOptions FooterOptions { get; set; } + + /// + /// Header content + /// + public IPdfContent Header { get; set; } + + /// + /// Header content options + /// + public PdfHeaderFooterOptions HeaderOptions { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/PdfPageMargins.cs b/src/Libraries/SmartStore.Services/Pdf/PdfPageMargins.cs index a33aec6bbe..c49c723c56 100644 --- a/src/Libraries/SmartStore.Services/Pdf/PdfPageMargins.cs +++ b/src/Libraries/SmartStore.Services/Pdf/PdfPageMargins.cs @@ -2,16 +2,16 @@ namespace SmartStore.Services.Pdf { - public class PdfPageMargins - { - public PdfPageMargins() - { - Left = 20; - } - - public float? Bottom { get; set; } - public float? Left { get; set; } - public float? Right { get; set; } - public float? Top { get; set; } - } + public class PdfPageMargins + { + public PdfPageMargins() + { + Left = 20; + } + + public float? Bottom { get; set; } + public float? Left { get; set; } + public float? Right { get; set; } + public float? Top { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/PdfPagePrientation.cs b/src/Libraries/SmartStore.Services/Pdf/PdfPagePrientation.cs index 6c46162167..3ea74faebb 100644 --- a/src/Libraries/SmartStore.Services/Pdf/PdfPagePrientation.cs +++ b/src/Libraries/SmartStore.Services/Pdf/PdfPagePrientation.cs @@ -2,10 +2,10 @@ namespace SmartStore.Services.Pdf { - public enum PdfPagePrientation - { - Default, - Landscape, - Portrait - } + public enum PdfPagePrientation + { + Default, + Landscape, + Portrait + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/PdfPageSize.cs b/src/Libraries/SmartStore.Services/Pdf/PdfPageSize.cs index c6865b5b0e..670c8b4a9d 100644 --- a/src/Libraries/SmartStore.Services/Pdf/PdfPageSize.cs +++ b/src/Libraries/SmartStore.Services/Pdf/PdfPageSize.cs @@ -2,11 +2,11 @@ namespace SmartStore.Services.Pdf { - public enum PdfPageSize - { - Default, - A4, - A3, - Letter - } + public enum PdfPageSize + { + Default, + A4, + A3, + Letter + } } diff --git a/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs b/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs index 72181616b6..3b2c9e288d 100644 --- a/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs +++ b/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs @@ -7,157 +7,158 @@ namespace SmartStore.Services.Pdf { - public class WkHtmlToPdfConverter : IPdfConverter - { - public WkHtmlToPdfConverter() - { - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - - public byte[] Convert(PdfConvertSettings settings) - { - Guard.NotNull(settings, nameof(settings)); - if (settings.Page == null) - { - throw Error.InvalidOperation("The 'Page' property of the 'settings' argument cannot be null."); - } - - try - { - var converter = CreateWkConverter(settings); - - var input = settings.Page.Process("page"); - - if (settings.Page.Kind == PdfContentKind.Url) - { - Logger.DebugFormat("Generating PDF from URL '{0}'. CustomWkHtmlPageArgs: {1}", input, converter.CustomWkHtmlPageArgs); - return converter.GeneratePdfFromFile(input, null); - } - else - { - Logger.DebugFormat("Generating PDF from HTML. CustomWkHtmlPageArgs: {0}", converter.CustomWkHtmlPageArgs); - return converter.GeneratePdf(input, null); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Html to Pdf conversion error"); - throw; - } - finally - { - TeardownContent(settings.Cover); - TeardownContent(settings.Footer); - TeardownContent(settings.Header); - TeardownContent(settings.Page); - } - } - - private void TeardownContent(IPdfContent content) - { - if (content != null) - content.Teardown(); - } - - internal HtmlToPdfConverter CreateWkConverter(PdfConvertSettings settings) - { - // Global options - var converter = new HtmlToPdfConverter - { - Grayscale = settings.Grayscale, - LowQuality = settings.LowQuality, - Orientation = (PageOrientation)(int)settings.Orientation, - PageHeight = settings.PageHeight, - PageWidth = settings.PageWidth, - Size = (PageSize)(int)settings.Size, - PdfToolPath = FileSystemHelper.TempDir("wkhtmltopdf") - }; - - if (settings.Margins != null) - { - converter.Margins.Bottom = settings.Margins.Bottom; - converter.Margins.Left = settings.Margins.Left; - converter.Margins.Right = settings.Margins.Right; - converter.Margins.Top = settings.Margins.Top; - } - - var sb = new StringBuilder(settings.CustomFlags); - - // doc title - if (settings.Title.HasValue()) - { - sb.AppendFormat(CultureInfo.CurrentCulture, " --title \"{0}\"", settings.Title); - } - - // Cover content & options - if (settings.Cover != null) - { - var path = settings.Cover.Process("cover"); - if (path.HasValue()) - { - sb.AppendFormat(" cover \"{0}\" ", path); - settings.Cover.WriteArguments("cover", sb); - if (settings.CoverOptions != null) - { - settings.CoverOptions.Process("cover", sb); - } - } - } - - // Toc options - if (settings.TocOptions != null && settings.TocOptions.Enabled) - { - settings.TocOptions.Process("toc", sb); - } - - // apply cover & toc - converter.CustomWkHtmlArgs = sb.ToString().Trim().NullEmpty(); - sb.Clear(); - - // Page options - if (settings.PageOptions != null) - { - settings.PageOptions.Process("page", sb); - } - - // Header content & options - if (settings.Header != null) - { - var path = settings.Header.Process("header"); - if (path.HasValue()) - { - sb.AppendFormat(" --header-html \"{0}\"", path); - settings.Header.WriteArguments("header", sb); - } - } - if (settings.HeaderOptions != null && (settings.Header != null || settings.HeaderOptions.HasText)) - { - settings.HeaderOptions.Process("header", sb); - } - - // Footer content & options - if (settings.Footer != null) - { - var path = settings.Footer.Process("footer"); - if (path.HasValue()) - { - sb.AppendFormat(" --footer-html \"{0}\"", path); - settings.Footer.WriteArguments("footer", sb); - } - } - if (settings.FooterOptions != null && (settings.Footer != null || settings.FooterOptions.HasText)) - { - settings.FooterOptions.Process("footer", sb); - } - - // apply settings - converter.CustomWkHtmlPageArgs = sb.ToString().Trim().NullEmpty(); - - return converter; - } - - } + public class WkHtmlToPdfConverter : IPdfConverter + { + public WkHtmlToPdfConverter() + { + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + + public byte[] Convert(PdfConvertSettings settings) + { + Guard.NotNull(settings, nameof(settings)); + if (settings.Page == null) + { + throw Error.InvalidOperation("The 'Page' property of the 'settings' argument cannot be null."); + } + + try + { + var converter = CreateWkConverter(settings); + + var input = settings.Page.Process("page"); + + if (settings.Page.Kind == PdfContentKind.Url) + { + Logger.DebugFormat("Generating PDF from URL '{0}'. CustomWkHtmlPageArgs: {1}", input, converter.CustomWkHtmlPageArgs); + return converter.GeneratePdfFromFile(input, null); + } + else + { + Logger.DebugFormat("Generating PDF from HTML. CustomWkHtmlPageArgs: {0}", converter.CustomWkHtmlPageArgs); + return converter.GeneratePdf(input, null); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Html to Pdf conversion error"); + throw; + } + finally + { + TeardownContent(settings.Cover); + TeardownContent(settings.Footer); + TeardownContent(settings.Header); + TeardownContent(settings.Page); + } + } + + private void TeardownContent(IPdfContent content) + { + if (content != null) + content.Teardown(); + } + + internal HtmlToPdfConverter CreateWkConverter(PdfConvertSettings settings) + { + // Global options + var converter = new HtmlToPdfConverter + { + Grayscale = settings.Grayscale, + LowQuality = settings.LowQuality, + Orientation = (PageOrientation)(int)settings.Orientation, + PageHeight = settings.PageHeight, + PageWidth = settings.PageWidth, + Size = (PageSize)(int)settings.Size, + PdfToolPath = FileSystemHelper.TempDir("wkhtmltopdf"), + TempFilesPath = FileSystemHelper.TempDirTenant() + }; + + if (settings.Margins != null) + { + converter.Margins.Bottom = settings.Margins.Bottom; + converter.Margins.Left = settings.Margins.Left; + converter.Margins.Right = settings.Margins.Right; + converter.Margins.Top = settings.Margins.Top; + } + + var sb = new StringBuilder(settings.CustomFlags); + + // doc title + if (settings.Title.HasValue()) + { + sb.AppendFormat(CultureInfo.CurrentCulture, " --title \"{0}\"", settings.Title); + } + + // Cover content & options + if (settings.Cover != null) + { + var path = settings.Cover.Process("cover"); + if (path.HasValue()) + { + sb.AppendFormat(" cover \"{0}\" ", path); + settings.Cover.WriteArguments("cover", sb); + if (settings.CoverOptions != null) + { + settings.CoverOptions.Process("cover", sb); + } + } + } + + // Toc options + if (settings.TocOptions != null && settings.TocOptions.Enabled) + { + settings.TocOptions.Process("toc", sb); + } + + // apply cover & toc + converter.CustomWkHtmlArgs = sb.ToString().Trim().NullEmpty(); + sb.Clear(); + + // Page options + if (settings.PageOptions != null) + { + settings.PageOptions.Process("page", sb); + } + + // Header content & options + if (settings.Header != null) + { + var path = settings.Header.Process("header"); + if (path.HasValue()) + { + sb.AppendFormat(" --header-html \"{0}\"", path); + settings.Header.WriteArguments("header", sb); + } + } + if (settings.HeaderOptions != null && (settings.Header != null || settings.HeaderOptions.HasText)) + { + settings.HeaderOptions.Process("header", sb); + } + + // Footer content & options + if (settings.Footer != null) + { + var path = settings.Footer.Process("footer"); + if (path.HasValue()) + { + sb.AppendFormat(" --footer-html \"{0}\"", path); + settings.Footer.WriteArguments("footer", sb); + } + } + if (settings.FooterOptions != null && (settings.Footer != null || settings.FooterOptions.HasText)) + { + settings.FooterOptions.Process("footer", sb); + } + + // apply settings + converter.CustomWkHtmlPageArgs = sb.ToString().Trim().NullEmpty(); + + return converter; + } + + } } diff --git a/src/Libraries/SmartStore.Services/Polls/IPollService.cs b/src/Libraries/SmartStore.Services/Polls/IPollService.cs index bdf19bab73..2aaf474e8c 100644 --- a/src/Libraries/SmartStore.Services/Polls/IPollService.cs +++ b/src/Libraries/SmartStore.Services/Polls/IPollService.cs @@ -22,7 +22,7 @@ public partial interface IPollService /// A value indicating whether to show hidden records /// Poll Poll GetPollBySystemKeyword(string systemKeyword, int languageId, bool showHidden = false); - + /// /// Gets poll collection /// @@ -51,14 +51,14 @@ public partial interface IPollService /// /// Poll void UpdatePoll(Poll poll); - + /// /// Gets a poll answer /// /// Poll answer identifier /// Poll answer PollAnswer GetPollAnswerById(int pollAnswerId); - + /// /// Deletes a poll answer /// diff --git a/src/Libraries/SmartStore.Services/Polls/PollService.cs b/src/Libraries/SmartStore.Services/Polls/PollService.cs index e12cb2f6af..3c06bebc46 100644 --- a/src/Libraries/SmartStore.Services/Polls/PollService.cs +++ b/src/Libraries/SmartStore.Services/Polls/PollService.cs @@ -15,55 +15,55 @@ public partial class PollService : IPollService private readonly IRepository _pollRepository; private readonly IRepository _pollAnswerRepository; private readonly IRepository _pollVotingRecords; - private readonly IRepository _storeMappingRepository; + private readonly IRepository _storeMappingRepository; private readonly IEventPublisher _eventPublisher; - private readonly IStoreContext _storeContext; + private readonly IStoreContext _storeContext; public PollService( - IRepository pollRepository, + IRepository pollRepository, IRepository pollAnswerRepository, IRepository pollVotingRecords, - IRepository storeMappingRepository, - IEventPublisher eventPublisher, - IStoreContext storeContext) + IRepository storeMappingRepository, + IEventPublisher eventPublisher, + IStoreContext storeContext) { this._pollRepository = pollRepository; this._pollAnswerRepository = pollAnswerRepository; this._pollVotingRecords = pollVotingRecords; - this._storeMappingRepository = storeMappingRepository; + this._storeMappingRepository = storeMappingRepository; this._eventPublisher = eventPublisher; - this._storeContext = storeContext; + this._storeContext = storeContext; - this.QuerySettings = DbQuerySettings.Default; + this.QuerySettings = DbQuerySettings.Default; } - public DbQuerySettings QuerySettings { get; set; } - - private IQueryable Filter(IQueryable query, bool showHidden) - { - if (!showHidden) - { - var utcNow = DateTime.UtcNow; - query = query.Where(p => p.Published); - query = query.Where(p => !p.StartDateUtc.HasValue || p.StartDateUtc <= utcNow); - query = query.Where(p => !p.EndDateUtc.HasValue || p.EndDateUtc >= utcNow); - - if (!QuerySettings.IgnoreMultiStore) - { - var currentStoreId = _storeContext.CurrentStore.Id; - - query = - from p in query - join sm in _storeMappingRepository.Table - on new { c1 = p.Id, c2 = "Poll" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into p_sm - from sm in p_sm.DefaultIfEmpty() - where !p.LimitedToStores || currentStoreId == sm.StoreId - select p; - } - } - - return query; - } + public DbQuerySettings QuerySettings { get; set; } + + private IQueryable Filter(IQueryable query, bool showHidden) + { + if (!showHidden) + { + var utcNow = DateTime.UtcNow; + query = query.Where(p => p.Published); + query = query.Where(p => !p.StartDateUtc.HasValue || p.StartDateUtc <= utcNow); + query = query.Where(p => !p.EndDateUtc.HasValue || p.EndDateUtc >= utcNow); + + if (!QuerySettings.IgnoreMultiStore) + { + var currentStoreId = _storeContext.CurrentStore.Id; + + query = + from p in query + join sm in _storeMappingRepository.Table + on new { c1 = p.Id, c2 = "Poll" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into p_sm + from sm in p_sm.DefaultIfEmpty() + where !p.LimitedToStores || currentStoreId == sm.StoreId + select p; + } + } + + return query; + } public virtual Poll GetPollById(int pollId) { @@ -73,38 +73,38 @@ public virtual Poll GetPollById(int pollId) return _pollRepository.GetById(pollId); } - public virtual Poll GetPollBySystemKeyword(string systemKeyword, int languageId, bool showHidden = false) + public virtual Poll GetPollBySystemKeyword(string systemKeyword, int languageId, bool showHidden = false) { if (String.IsNullOrWhiteSpace(systemKeyword)) return null; - var query = - from p in _pollRepository.Table - where p.SystemKeyword == systemKeyword && p.LanguageId == languageId - select p; + var query = + from p in _pollRepository.Table + where p.SystemKeyword == systemKeyword && p.LanguageId == languageId + select p; - query = Filter(query, showHidden); + query = Filter(query, showHidden); var poll = query.FirstOrDefault(); return poll; } - + public virtual IPagedList GetPolls(int languageId, bool loadShownOnHomePageOnly, int pageIndex, int pageSize, bool showHidden = false) { var query = _pollRepository.Table; - query = Filter(query, showHidden); - - if (loadShownOnHomePageOnly) + query = Filter(query, showHidden); + + if (loadShownOnHomePageOnly) { query = query.Where(p => p.ShowOnHomePage); } - - if (languageId > 0) + + if (languageId > 0) { query = query.Where(p => p.LanguageId == languageId); } - - query = query.OrderBy(p => p.DisplayOrder); + + query = query.OrderBy(p => p.DisplayOrder); var polls = new PagedList(query, pageIndex, pageSize); return polls; @@ -133,7 +133,7 @@ public virtual void UpdatePoll(Poll poll) _pollRepository.Update(poll); } - + public virtual PollAnswer GetPollAnswerById(int pollAnswerId) { if (pollAnswerId == 0) @@ -145,7 +145,7 @@ public virtual PollAnswer GetPollAnswerById(int pollAnswerId) var pollAnswer = query.SingleOrDefault(); return pollAnswer; } - + public virtual void DeletePollAnswer(PollAnswer pollAnswer) { if (pollAnswer == null) diff --git a/src/Libraries/SmartStore.Services/Properties/Settings.Designer.cs b/src/Libraries/SmartStore.Services/Properties/Settings.Designer.cs index ee12dcdfeb..89246c6f2e 100644 --- a/src/Libraries/SmartStore.Services/Properties/Settings.Designer.cs +++ b/src/Libraries/SmartStore.Services/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace SmartStore.Services.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.6.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -27,19 +27,9 @@ public static Settings Default { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] [global::System.Configuration.DefaultSettingValueAttribute("http://ec.europa.eu/taxation_customs/vies/services/checkVatService")] - public string SmartStore_Services_EuropaCheckVatService_checkVatService { + public string SmartStore_Services_EuropeCheckVatService_checkVatService { get { - return ((string)(this["SmartStore_Services_EuropaCheckVatService_checkVatService"])); - } - } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("http://ec.europa.eu/taxation_customs/vies/services/checkVatService")] - public string Nop_Services_EuropaCheckVatService_checkVatService { - get { - return ((string)(this["Nop_Services_EuropaCheckVatService_checkVatService"])); + return ((string)(this["SmartStore_Services_EuropeCheckVatService_checkVatService"])); } } } diff --git a/src/Libraries/SmartStore.Services/Properties/Settings.settings b/src/Libraries/SmartStore.Services/Properties/Settings.settings index ad884b468b..47fd2b1df3 100644 --- a/src/Libraries/SmartStore.Services/Properties/Settings.settings +++ b/src/Libraries/SmartStore.Services/Properties/Settings.settings @@ -2,10 +2,7 @@ - - http://ec.europa.eu/taxation_customs/vies/services/checkVatService - - + http://ec.europa.eu/taxation_customs/vies/services/checkVatService diff --git a/src/Libraries/SmartStore.Services/Rules/DefaultRuleOptionsProvider.cs b/src/Libraries/SmartStore.Services/Rules/DefaultRuleOptionsProvider.cs index a5e88730cc..dda9ef39d8 100644 --- a/src/Libraries/SmartStore.Services/Rules/DefaultRuleOptionsProvider.cs +++ b/src/Libraries/SmartStore.Services/Rules/DefaultRuleOptionsProvider.cs @@ -112,17 +112,29 @@ public virtual RuleOptionsResult GetOptions( string searchTerm) { Guard.NotNull(expression, nameof(expression)); - Guard.NotNull(expression.Descriptor, nameof(expression.Descriptor)); + + return GetOptions(reason, expression.Descriptor, expression.RawValue, pageIndex, pageSize, searchTerm); + } + + public virtual RuleOptionsResult GetOptions( + RuleOptionsRequestReason reason, + RuleDescriptor descriptor, + string value, + int pageIndex, + int pageSize, + string searchTerm) + { + Guard.NotNull(descriptor, nameof(descriptor)); var result = new RuleOptionsResult(); - if (!(expression.Descriptor.SelectList is RemoteRuleValueSelectList list)) + if (!(descriptor.SelectList is RemoteRuleValueSelectList list)) { return result; } var language = _services.WorkContext.WorkingLanguage; - var byId = expression.Descriptor.RuleType == RuleType.Int || expression.Descriptor.RuleType == RuleType.IntArray; + var byId = descriptor.RuleType == RuleType.Int || descriptor.RuleType == RuleType.IntArray; List options = null; switch (list.DataSource) @@ -130,7 +142,7 @@ public virtual RuleOptionsResult GetOptions( case "Product": if (reason == RuleOptionsRequestReason.SelectedDisplayNames) { - options = _productService.Value.GetProductsByIds(expression.RawValue.ToIntArray()) + options = _productService.Value.GetProductsByIds(value.ToIntArray()) .Select(x => new RuleValueSelectListOption { Value = x.Id.ToString(), Text = x.GetLocalized(y => y.Name, language, true, false), Hint = x.Sku }) .ToList(); } @@ -174,13 +186,13 @@ public virtual RuleOptionsResult GetOptions( case "TargetGroup": if (reason == RuleOptionsRequestReason.SelectedDisplayNames) { - options = _ruleStorage.Value.GetRuleSetsByIds(expression.RawValue.ToIntArray(), false) + options = _ruleStorage.Value.GetRuleSetsByIds(value.ToIntArray(), false) .Select(x => new RuleValueSelectListOption { Value = x.Id.ToString(), Text = x.Name }) .ToList(); } else { - var ruleSets = _ruleStorage.Value.GetAllRuleSets(false, false, expression.Descriptor.Scope, pageIndex, pageSize, false, true); + var ruleSets = _ruleStorage.Value.GetAllRuleSets(false, false, descriptor.Scope, pageIndex, pageSize, false, true); result.IsPaged = true; result.HasMoreData = ruleSets.HasNextPage; @@ -192,7 +204,7 @@ public virtual RuleOptionsResult GetOptions( case "Category": if (reason == RuleOptionsRequestReason.SelectedDisplayNames) { - options = _categoryService.Value.GetCategoriesByIds(expression.RawValue.ToIntArray()) + options = _categoryService.Value.GetCategoriesByIds(value.ToIntArray()) .Select(x => new RuleValueSelectListOption { Value = x.Id.ToString(), Text = x.GetCategoryPath(_categoryService.Value).NullEmpty() ?? x.Name }) .ToList(); } @@ -237,7 +249,7 @@ public virtual RuleOptionsResult GetOptions( case "VariantValue": if (reason == RuleOptionsRequestReason.SelectedDisplayNames) { - var ids = expression.RawValue.ToIntArray(); + var ids = value.ToIntArray(); var variantValues = _variantValueRepository.Value.TableUntracked .Where(x => ids.Contains(x.Id)) .ToList(); @@ -246,7 +258,7 @@ public virtual RuleOptionsResult GetOptions( .Select(x => new RuleValueSelectListOption { Value = x.Id.ToString(), Text = x.GetLocalized(y => y.Name, language, true, false) }) .ToList(); } - else if (expression.Descriptor.Metadata.TryGetValue("ParentId", out var objParentId)) + else if (descriptor.Metadata.TryGetValue("ParentId", out var objParentId)) { options = new List(); var pIndex = -1; @@ -264,13 +276,13 @@ public virtual RuleOptionsResult GetOptions( while (true) { var variantValues = PagedList.Create(query, ++pIndex, 500); - foreach (var value in variantValues) + foreach (var variantValue in variantValues) { - var name = value.GetLocalized(x => x.Name, language, true, false); + var name = variantValue.GetLocalized(x => x.Name, language, true, false); if (!existingValues.Contains(name)) { existingValues.Add(name); - options.Add(new RuleValueSelectListOption { Value = value.Id.ToString(), Text = name }); + options.Add(new RuleValueSelectListOption { Value = variantValue.Id.ToString(), Text = name }); } } if (!variantValues.HasNextPage) @@ -283,7 +295,7 @@ public virtual RuleOptionsResult GetOptions( case "AttributeOption": if (reason == RuleOptionsRequestReason.SelectedDisplayNames) { - var ids = expression.RawValue.ToIntArray(); + var ids = value.ToIntArray(); var attributeOptions = _attrOptionRepository.Value.TableUntracked .Where(x => ids.Contains(x.Id)) .ToList(); @@ -292,7 +304,7 @@ public virtual RuleOptionsResult GetOptions( .Select(x => new RuleValueSelectListOption { Value = x.Id.ToString(), Text = x.GetLocalized(y => y.Name, language, true, false) }) .ToList(); } - else if (expression.Descriptor.Metadata.TryGetValue("ParentId", out var objParentId)) + else if (descriptor.Metadata.TryGetValue("ParentId", out var objParentId)) { var query = _attrOptionRepository.Value.TableUntracked .Where(x => x.SpecificationAttributeId == (int)objParentId) @@ -317,9 +329,9 @@ public virtual RuleOptionsResult GetOptions( if (reason == RuleOptionsRequestReason.SelectedDisplayNames) { // Get display names of selected options. - if (expression.RawValue.HasValue()) + if (value.HasValue()) { - var selectedValues = expression.RawValue.SplitSafe(","); + var selectedValues = value.SplitSafe(","); result.Options.AddRange(options.Where(x => selectedValues.Contains(x.Value))); } } diff --git a/src/Libraries/SmartStore.Services/ScopedServiceBase.cs b/src/Libraries/SmartStore.Services/ScopedServiceBase.cs index 961fa5cd6f..c75c0849f6 100644 --- a/src/Libraries/SmartStore.Services/ScopedServiceBase.cs +++ b/src/Libraries/SmartStore.Services/ScopedServiceBase.cs @@ -3,65 +3,65 @@ namespace SmartStore.Services { - public abstract class ScopedServiceBase : IScopedService - { - /// - /// Creates a long running unit of work in which cache eviction is suppressed - /// - /// Specifies whether the cache should be evicted completely on batch disposal - /// A disposable unit of work - public IDisposable BeginScope(bool clearCache = true) - { - if (IsInScope) - { - // nested batches are not supported - return ActionDisposable.Empty; - } + public abstract class ScopedServiceBase : IScopedService + { + /// + /// Creates a long running unit of work in which cache eviction is suppressed + /// + /// Specifies whether the cache should be evicted completely on batch disposal + /// A disposable unit of work + public IDisposable BeginScope(bool clearCache = true) + { + if (IsInScope) + { + // nested batches are not supported + return ActionDisposable.Empty; + } - OnBeginScope(); - IsInScope = true; + OnBeginScope(); + IsInScope = true; - return new ActionDisposable(() => - { - IsInScope = false; - OnEndScope(); - if (clearCache && HasChanges) - { - ClearCache(); - } - }); - } + return new ActionDisposable(() => + { + IsInScope = false; + OnEndScope(); + if (clearCache && HasChanges) + { + ClearCache(); + } + }); + } - /// - /// Gets a value indicating whether data has changed during a request, making cache eviction necessary. - /// - /// - /// Cache eviction sets this member to false - /// - public bool HasChanges - { - get; - protected set; - } + /// + /// Gets a value indicating whether data has changed during a request, making cache eviction necessary. + /// + /// + /// Cache eviction sets this member to false + /// + public bool HasChanges + { + get; + protected set; + } - protected bool IsInScope - { - get; - private set; - } + protected bool IsInScope + { + get; + private set; + } - public void ClearCache() - { - if (!IsInScope) - { - OnClearCache(); - HasChanges = false; - } - } + public void ClearCache() + { + if (!IsInScope) + { + OnClearCache(); + HasChanges = false; + } + } - protected abstract void OnClearCache(); + protected abstract void OnClearCache(); - protected virtual void OnBeginScope() { } - protected virtual void OnEndScope() { } - } + protected virtual void OnBeginScope() { } + protected virtual void OnEndScope() { } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchEvents.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchEvents.cs index 44f2bab53f..04d68d3a09 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchEvents.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchEvents.cs @@ -3,31 +3,31 @@ namespace SmartStore.Services.Search { public class CatalogSearchingEvent - { + { public CatalogSearchingEvent(CatalogSearchQuery query, bool direct) { - Guard.NotNull(query, nameof(query)); + Guard.NotNull(query, nameof(query)); - Query = query; + Query = query; Direct = direct; - } + } public bool Direct { get; private set; } public CatalogSearchQuery Query { get; private set; } } - public class CatalogSearchedEvent - { - public CatalogSearchedEvent(CatalogSearchQuery query, CatalogSearchResult result) - { - Guard.NotNull(query, nameof(query)); - Guard.NotNull(result, nameof(result)); + public class CatalogSearchedEvent + { + public CatalogSearchedEvent(CatalogSearchQuery query, CatalogSearchResult result) + { + Guard.NotNull(query, nameof(query)); + Guard.NotNull(result, nameof(result)); - Query = query; - Result = result; - } + Query = query; + Result = result; + } - public CatalogSearchQuery Query { get; private set; } - public CatalogSearchResult Result { get; set; } - } + public CatalogSearchQuery Query { get; private set; } + public CatalogSearchResult Result { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs index e0f172a567..d19d862cc5 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchQuery.cs @@ -7,104 +7,104 @@ namespace SmartStore.Services.Search { public partial class CatalogSearchQuery : SearchQuery, ICloneable - { - /// - /// Initializes a new instance of the class without a search term being set - /// - public CatalogSearchQuery() - : base((string[])null, null) - { - } - - public CatalogSearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) - : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) - { - } - - public CatalogSearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) - : base(fields, term, mode, escape, isFuzzySearch) - { - } - - public CatalogSearchQuery Clone() - { - return (CatalogSearchQuery)this.MemberwiseClone(); - } - - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } - - public bool IsSubPage - { - get - { - if (PageIndex > 0) - { - return true; - } - - var hasActiveFilter = FacetDescriptors.Values.Any(x => x.Values.Any(y => y.IsSelected)); - return hasActiveFilter; - } - } - - #region Fluent builder - - public CatalogSearchQuery SortBy(ProductSortingEnum sort) - { - switch (sort) - { - case ProductSortingEnum.CreatedOnAsc: - case ProductSortingEnum.CreatedOn: - return SortBy(SearchSort.ByDateTimeField("createdon", sort == ProductSortingEnum.CreatedOn)); - - case ProductSortingEnum.NameAsc: - case ProductSortingEnum.NameDesc: - return SortBy(SearchSort.ByStringField("name", sort == ProductSortingEnum.NameDesc)); - - case ProductSortingEnum.PriceAsc: - case ProductSortingEnum.PriceDesc: - return SortBy(SearchSort.ByDoubleField("price", sort == ProductSortingEnum.PriceDesc)); - - case ProductSortingEnum.Relevance: - return SortBy(SearchSort.ByRelevance()); - - default: - return this; - } - } - - /// - /// Only products that are visible in frontend. - /// - /// Customer whose customer roles are to be checked. Can be null. - /// Catalog search query - public CatalogSearchQuery VisibleOnly(Customer customer) - { - if (customer != null) - { + { + /// + /// Initializes a new instance of the class without a search term being set + /// + public CatalogSearchQuery() + : base((string[])null, null) + { + } + + public CatalogSearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) + : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) + { + } + + public CatalogSearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) + : base(fields, term, mode, escape, isFuzzySearch) + { + } + + public CatalogSearchQuery Clone() + { + return (CatalogSearchQuery)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + + public bool IsSubPage + { + get + { + if (PageIndex > 0) + { + return true; + } + + var hasActiveFilter = FacetDescriptors.Values.Any(x => x.Values.Any(y => y.IsSelected)); + return hasActiveFilter; + } + } + + #region Fluent builder + + public CatalogSearchQuery SortBy(ProductSortingEnum sort) + { + switch (sort) + { + case ProductSortingEnum.CreatedOnAsc: + case ProductSortingEnum.CreatedOn: + return SortBy(SearchSort.ByDateTimeField("createdon", sort == ProductSortingEnum.CreatedOn)); + + case ProductSortingEnum.NameAsc: + case ProductSortingEnum.NameDesc: + return SortBy(SearchSort.ByStringField("name", sort == ProductSortingEnum.NameDesc)); + + case ProductSortingEnum.PriceAsc: + case ProductSortingEnum.PriceDesc: + return SortBy(SearchSort.ByDoubleField("price", sort == ProductSortingEnum.PriceDesc)); + + case ProductSortingEnum.Relevance: + return SortBy(SearchSort.ByRelevance()); + + default: + return this; + } + } + + /// + /// Only products that are visible in frontend. + /// + /// Customer whose customer roles are to be checked. Can be null. + /// Catalog search query + public CatalogSearchQuery VisibleOnly(Customer customer) + { + if (customer != null) + { var allowedCustomerRoleIds = customer.GetRoleIds(); - return VisibleOnly(allowedCustomerRoleIds); - } + return VisibleOnly(allowedCustomerRoleIds); + } - return VisibleOnly(new int[0]); - } + return VisibleOnly(new int[0]); + } - /// - /// Only products that are visible in frontend. - /// - /// List of allowed customer role ids. Can be null. - public CatalogSearchQuery VisibleOnly(params int[] allowedCustomerRoleIds) - { + /// + /// Only products that are visible in frontend. + /// + /// List of allowed customer role ids. Can be null. + public CatalogSearchQuery VisibleOnly(params int[] allowedCustomerRoleIds) + { PublishedOnly(true); AvailableByDate(true); AllowedCustomerRoles(allowedCustomerRoleIds); - return this; - } + return this; + } public CatalogSearchQuery AllowedCustomerRoles(params int[] customerRoleIds) { @@ -122,9 +122,9 @@ public CatalogSearchQuery AllowedCustomerRoles(params int[] customerRoleIds) } public CatalogSearchQuery PublishedOnly(bool value) - { - return WithFilter(SearchFilter.ByField("published", value).Mandatory().ExactMatch().NotAnalyzed()); - } + { + return WithFilter(SearchFilter.ByField("published", value).Mandatory().ExactMatch().NotAnalyzed()); + } /// /// Filters products based on their stock level. @@ -160,109 +160,109 @@ public CatalogSearchQuery WithVisibility(ProductVisibility value) return WithFilter(SearchFilter.ByField("visibility", (int)value).Mandatory().ExactMatch().NotAnalyzed()); } - public CatalogSearchQuery HasParentGroupedProduct(params int[] parentProductIds) - { + public CatalogSearchQuery HasParentGroupedProduct(params int[] parentProductIds) + { return CreateFilter("parentid", parentProductIds); - } + } - public override CatalogSearchQuery HasStoreId(int id) - { - base.HasStoreId(id); + public override CatalogSearchQuery HasStoreId(int id) + { + base.HasStoreId(id); - if (id == 0) - { + if (id == 0) + { // 0 is ignored in queries, i.e. no filtering takes place. // This should be kept here so that search engines do not provide different results. //WithFilter(SearchFilter.ByField("storeid", 0).ExactMatch().NotAnalyzed()); } - else - { - WithFilter(SearchFilter.Combined( - SearchFilter.ByField("storeid", 0).ExactMatch().NotAnalyzed(), - SearchFilter.ByField("storeid", id).ExactMatch().NotAnalyzed()) - ); - } - - return this; - } - - public CatalogSearchQuery IsProductType(ProductType type) - { - return WithFilter(SearchFilter.ByField("typeid", (int)type).Mandatory().ExactMatch().NotAnalyzed()); - } - - public CatalogSearchQuery WithProductIds(params int[] ids) - { + else + { + WithFilter(SearchFilter.Combined( + SearchFilter.ByField("storeid", 0).ExactMatch().NotAnalyzed(), + SearchFilter.ByField("storeid", id).ExactMatch().NotAnalyzed()) + ); + } + + return this; + } + + public CatalogSearchQuery IsProductType(ProductType type) + { + return WithFilter(SearchFilter.ByField("typeid", (int)type).Mandatory().ExactMatch().NotAnalyzed()); + } + + public CatalogSearchQuery WithProductIds(params int[] ids) + { return CreateFilter("id", ids); - } - - public CatalogSearchQuery WithProductId(int? fromId, int? toId) - { - if (fromId == null && toId == null) - { - return this; - } - - return WithFilter(SearchFilter.ByRange("id", fromId, toId, fromId.HasValue, toId.HasValue).Mandatory().ExactMatch().NotAnalyzed()); - } - - /// - /// Filter by category identifiers. - /// - /// - /// A value indicating whether loaded products are marked as featured (relates only to categories and manufacturers). - /// false to load featured products only, true to load unfeatured products only, null to load all products. - /// - /// The category identifiers. - /// Search query. - public CatalogSearchQuery WithCategoryIds(bool? featuredOnly, params int[] ids) - { - var fieldName = featuredOnly.HasValue + } + + public CatalogSearchQuery WithProductId(int? fromId, int? toId) + { + if (fromId == null && toId == null) + { + return this; + } + + return WithFilter(SearchFilter.ByRange("id", fromId, toId, fromId.HasValue, toId.HasValue).Mandatory().ExactMatch().NotAnalyzed()); + } + + /// + /// Filter by category identifiers. + /// + /// + /// A value indicating whether loaded products are marked as featured (relates only to categories and manufacturers). + /// false to load featured products only, true to load unfeatured products only, null to load all products. + /// + /// The category identifiers. + /// Search query. + public CatalogSearchQuery WithCategoryIds(bool? featuredOnly, params int[] ids) + { + var fieldName = featuredOnly.HasValue ? featuredOnly.Value ? "featuredcategoryid" : "notfeaturedcategoryid" : "categoryid"; return CreateFilter(fieldName, ids); - } - - /// Includes only published categories. - public CatalogSearchQuery HasAnyCategory(bool value) - { - if (value) - { - return WithFilter(SearchFilter.ByRange("categoryid", 1, int.MaxValue, true, true).Mandatory().NotAnalyzed()); - } - else - { - return WithFilter(SearchFilter.ByField("categoryid", 0).Mandatory().ExactMatch().NotAnalyzed()); - } - } - - public CatalogSearchQuery WithManufacturerIds(bool? featuredOnly, params int[] ids) - { - var fieldName = featuredOnly.HasValue + } + + /// Includes only published categories. + public CatalogSearchQuery HasAnyCategory(bool value) + { + if (value) + { + return WithFilter(SearchFilter.ByRange("categoryid", 1, int.MaxValue, true, true).Mandatory().NotAnalyzed()); + } + else + { + return WithFilter(SearchFilter.ByField("categoryid", 0).Mandatory().ExactMatch().NotAnalyzed()); + } + } + + public CatalogSearchQuery WithManufacturerIds(bool? featuredOnly, params int[] ids) + { + var fieldName = featuredOnly.HasValue ? featuredOnly.Value ? "featuredmanufacturerid" : "notfeaturedmanufacturerid" : "manufacturerid"; return CreateFilter(fieldName, ids); - } - - /// Includes only published manufacturers. - public CatalogSearchQuery HasAnyManufacturer(bool value) - { - if (value) - { - return WithFilter(SearchFilter.ByRange("manufacturerid", 1, int.MaxValue, true, true).Mandatory().NotAnalyzed()); - } - else - { - return WithFilter(SearchFilter.ByField("manufacturerid", 0).Mandatory().ExactMatch().NotAnalyzed()); - } - } - - public CatalogSearchQuery WithProductTagIds(params int[] ids) - { + } + + /// Includes only published manufacturers. + public CatalogSearchQuery HasAnyManufacturer(bool value) + { + if (value) + { + return WithFilter(SearchFilter.ByRange("manufacturerid", 1, int.MaxValue, true, true).Mandatory().NotAnalyzed()); + } + else + { + return WithFilter(SearchFilter.ByField("manufacturerid", 0).Mandatory().ExactMatch().NotAnalyzed()); + } + } + + public CatalogSearchQuery WithProductTagIds(params int[] ids) + { return CreateFilter("tagid", ids); - } + } public CatalogSearchQuery WithDeliveryTimeIds(params int[] ids) { @@ -330,16 +330,16 @@ public CatalogSearchQuery WithStockQuantity( int? toQuantity, bool? includeFrom = null, bool? includeTo = null) - { - if (fromQuantity == null && toQuantity == null) - { - return this; - } + { + if (fromQuantity == null && toQuantity == null) + { + return this; + } if (fromQuantity.HasValue && toQuantity.HasValue && fromQuantity == toQuantity) { var forbidden = includeFrom.HasValue && includeTo.HasValue && !includeFrom.Value && !includeTo.Value; - + return WithFilter(SearchFilter.ByField("stockquantity", fromQuantity.Value).Mandatory(!forbidden).ExactMatch().NotAnalyzed()); } else @@ -353,22 +353,22 @@ public CatalogSearchQuery WithStockQuantity( return WithFilter(filter.Mandatory().ExactMatch().NotAnalyzed()); } - } + } public CatalogSearchQuery PriceBetween( decimal? fromPrice, decimal? toPrice, bool? includeFrom = null, bool? includeTo = null) - { - if (fromPrice == null && toPrice == null) - { - return this; - } + { + if (fromPrice == null && toPrice == null) + { + return this; + } - Guard.NotEmpty(CurrencyCode, nameof(CurrencyCode)); + Guard.NotEmpty(CurrencyCode, nameof(CurrencyCode)); - var fieldName = "price_c-" + CurrencyCode.EmptyNull().ToLower(); + var fieldName = "price_c-" + CurrencyCode.EmptyNull().ToLower(); if (fromPrice.HasValue && toPrice.HasValue && fromPrice == toPrice) { @@ -386,18 +386,18 @@ public CatalogSearchQuery PriceBetween( return WithFilter(filter.Mandatory().ExactMatch().NotAnalyzed()); } - } + } - public CatalogSearchQuery CreatedBetween( + public CatalogSearchQuery CreatedBetween( DateTime? fromUtc, DateTime? toUtc, bool? includeFrom = null, bool? includeTo = null) - { - if (fromUtc == null && toUtc == null) - { - return this; - } + { + if (fromUtc == null && toUtc == null) + { + return this; + } if (fromUtc.HasValue && toUtc.HasValue && fromUtc == toUtc) { @@ -416,26 +416,26 @@ public CatalogSearchQuery CreatedBetween( return WithFilter(filter.Mandatory().ExactMatch().NotAnalyzed()); } - } + } - public CatalogSearchQuery WithRating( + public CatalogSearchQuery WithRating( double? fromRate, double? toRate, bool? includeFrom = null, bool? includeTo = null) - { - if (fromRate == null && toRate == null) - { - return this; - } - if (fromRate.HasValue) - { - Guard.InRange(fromRate.Value, 0.0, 5.0, nameof(fromRate.Value)); - } - if (toRate.HasValue) - { - Guard.InRange(toRate.Value, 0.0, 5.0, nameof(toRate.Value)); - } + { + if (fromRate == null && toRate == null) + { + return this; + } + if (fromRate.HasValue) + { + Guard.InRange(fromRate.Value, 0.0, 5.0, nameof(fromRate.Value)); + } + if (toRate.HasValue) + { + Guard.InRange(toRate.Value, 0.0, 5.0, nameof(toRate.Value)); + } if (fromRate.HasValue && toRate.HasValue && fromRate == toRate) { @@ -454,7 +454,7 @@ public CatalogSearchQuery WithRating( return WithFilter(filter.Mandatory().ExactMatch().NotAnalyzed()); } - } + } #endregion } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs index 48e0ab8632..671840c654 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchResult.cs @@ -8,64 +8,64 @@ namespace SmartStore.Services.Search { public partial class CatalogSearchResult - { + { private readonly Func> _hitsFactory; - private IPagedList _hits; + private IPagedList _hits; - public CatalogSearchResult( - ISearchEngine engine, - CatalogSearchQuery query, - int totalHitsCount, + public CatalogSearchResult( + ISearchEngine engine, + CatalogSearchQuery query, + int totalHitsCount, int[] hitsEntityIds, Func> hitsFactory, - string[] spellCheckerSuggestions, - IDictionary facets) - { - Guard.NotNull(query, nameof(query)); + string[] spellCheckerSuggestions, + IDictionary facets) + { + Guard.NotNull(query, nameof(query)); - Engine = engine; - Query = query; - SpellCheckerSuggestions = spellCheckerSuggestions ?? new string[0]; - Facets = facets ?? new Dictionary(); + Engine = engine; + Query = query; + SpellCheckerSuggestions = spellCheckerSuggestions ?? new string[0]; + Facets = facets ?? new Dictionary(); - _hitsFactory = hitsFactory ?? (() => new List()); + _hitsFactory = hitsFactory ?? (() => new List()); HitsEntityIds = hitsEntityIds ?? new int[0]; - TotalHitsCount = totalHitsCount; - } + TotalHitsCount = totalHitsCount; + } - /// - /// Constructor for an instance without any search hits - /// - /// Catalog search query - public CatalogSearchResult(CatalogSearchQuery query) - : this(null, query, 0, null, () => new List(), null, null) - { - } + /// + /// Constructor for an instance without any search hits + /// + /// Catalog search query + public CatalogSearchResult(CatalogSearchQuery query) + : this(null, query, 0, null, () => new List(), null, null) + { + } /// /// Entity identifiers of found products. /// public int[] HitsEntityIds { get; private set; } - /// - /// Products found - /// - public IPagedList Hits - { - get - { - if (_hits == null) - { - var products = TotalHitsCount == 0 - ? new List() - : _hitsFactory.Invoke(); + /// + /// Products found + /// + public IPagedList Hits + { + get + { + if (_hits == null) + { + var products = TotalHitsCount == 0 + ? new List() + : _hitsFactory.Invoke(); - _hits = new PagedList(products, Query.PageIndex, Query.Take, TotalHitsCount); - } + _hits = new PagedList(products, Query.PageIndex, Query.Take, TotalHitsCount); + } - return _hits; - } - } + return _hits; + } + } public int TotalHitsCount { get; } @@ -73,30 +73,30 @@ public IPagedList Hits /// The original catalog search query /// public CatalogSearchQuery Query - { - get; - private set; - } + { + get; + private set; + } - /// - /// Gets spell checking suggestions/corrections - /// - public string[] SpellCheckerSuggestions - { - get; - set; - } + /// + /// Gets spell checking suggestions/corrections + /// + public string[] SpellCheckerSuggestions + { + get; + set; + } - public IDictionary Facets - { - get; - private set; - } + public IDictionary Facets + { + get; + private set; + } - public ISearchEngine Engine - { - get; - private set; - } - } + public ISearchEngine Engine + { + get; + private set; + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs index 39f0ed2d80..480c5190d0 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/CatalogSearchService.cs @@ -17,50 +17,50 @@ namespace SmartStore.Services.Search { public partial class CatalogSearchService : SearchServiceBase, ICatalogSearchService, IXmlSitemapPublisher - { - private readonly ICommonServices _services; - private readonly IIndexManager _indexManager; - private readonly Lazy _productService; - private readonly ILogger _logger; - private readonly IPriceFormatter _priceFormatter; - private readonly UrlHelper _urlHelper; - - public CatalogSearchService( - ICommonServices services, - IIndexManager indexManager, - Lazy productService, - ILogger logger, - IPriceFormatter priceFormatter, - UrlHelper urlHelper) - { - _services = services; - _indexManager = indexManager; - _productService = productService; - _logger = logger; - _priceFormatter = priceFormatter; - _urlHelper = urlHelper; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - /// - /// Bypasses the index provider and directly searches in the database - /// - /// Search query - /// LOad flags - /// Catalog search result - protected virtual CatalogSearchResult SearchDirect(CatalogSearchQuery searchQuery, ProductLoadFlags loadFlags = ProductLoadFlags.None) - { - // Fallback to linq search. - var linqCatalogSearchService = _services.Container.ResolveNamed("linq"); - - var result = linqCatalogSearchService.Search(searchQuery, loadFlags, true); - ApplyFacetLabels(result.Facets); - - return result; - } + { + private readonly ICommonServices _services; + private readonly IIndexManager _indexManager; + private readonly Lazy _productService; + private readonly ILogger _logger; + private readonly IPriceFormatter _priceFormatter; + private readonly UrlHelper _urlHelper; + + public CatalogSearchService( + ICommonServices services, + IIndexManager indexManager, + Lazy productService, + ILogger logger, + IPriceFormatter priceFormatter, + UrlHelper urlHelper) + { + _services = services; + _indexManager = indexManager; + _productService = productService; + _logger = logger; + _priceFormatter = priceFormatter; + _urlHelper = urlHelper; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + /// + /// Bypasses the index provider and directly searches in the database + /// + /// Search query + /// LOad flags + /// Catalog search result + protected virtual CatalogSearchResult SearchDirect(CatalogSearchQuery searchQuery, ProductLoadFlags loadFlags = ProductLoadFlags.None) + { + // Fallback to linq search. + var linqCatalogSearchService = _services.Container.ResolveNamed("linq"); + + var result = linqCatalogSearchService.Search(searchQuery, loadFlags, true); + ApplyFacetLabels(result.Facets); + + return result; + } protected virtual void ApplyFacetLabels(IDictionary facets) { @@ -142,168 +142,168 @@ protected virtual string FormatPrice(decimal price) } public CatalogSearchResult Search( - CatalogSearchQuery searchQuery, - ProductLoadFlags loadFlags = ProductLoadFlags.None, - bool direct = false) - { - Guard.NotNull(searchQuery, nameof(searchQuery)); - Guard.NotNegative(searchQuery.Take, nameof(searchQuery.Take)); - - var provider = _indexManager.GetIndexProvider("Catalog"); - - if (!direct && provider != null) - { - var indexStore = provider.GetIndexStore("Catalog"); - if (indexStore.Exists) - { - var searchEngine = provider.GetSearchEngine(indexStore, searchQuery); - var stepPrefix = searchEngine.GetType().Name + " - "; - - int totalCount = 0; + CatalogSearchQuery searchQuery, + ProductLoadFlags loadFlags = ProductLoadFlags.None, + bool direct = false) + { + Guard.NotNull(searchQuery, nameof(searchQuery)); + Guard.NotNegative(searchQuery.Take, nameof(searchQuery.Take)); + + var provider = _indexManager.GetIndexProvider("Catalog"); + + if (!direct && provider != null) + { + var indexStore = provider.GetIndexStore("Catalog"); + if (indexStore.Exists) + { + var searchEngine = provider.GetSearchEngine(indexStore, searchQuery); + var stepPrefix = searchEngine.GetType().Name + " - "; + + int totalCount = 0; int[] hitsEntityIds = null; string[] spellCheckerSuggestions = null; - IEnumerable searchHits; - Func> hitsFactory = null; - IDictionary facets = null; - - _services.EventPublisher.Publish(new CatalogSearchingEvent(searchQuery, false)); - - if (searchQuery.Take > 0) - { - using (_services.Chronometer.Step(stepPrefix + "Search")) - { - totalCount = searchEngine.Count(); - // Fix paging boundaries - if (searchQuery.Skip > 0 && searchQuery.Skip >= totalCount) - { - searchQuery.Slice((totalCount / searchQuery.Take) * searchQuery.Take, searchQuery.Take); - } - } - - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) - { - using (_services.Chronometer.Step(stepPrefix + "Hits")) - { - searchHits = searchEngine.Search(); - } - - using (_services.Chronometer.Step(stepPrefix + "Collect")) - { + IEnumerable searchHits; + Func> hitsFactory = null; + IDictionary facets = null; + + _services.EventPublisher.Publish(new CatalogSearchingEvent(searchQuery, false)); + + if (searchQuery.Take > 0) + { + using (_services.Chronometer.Step(stepPrefix + "Search")) + { + totalCount = searchEngine.Count(); + // Fix paging boundaries + if (searchQuery.Skip > 0 && searchQuery.Skip >= totalCount) + { + searchQuery.Slice((totalCount / searchQuery.Take) * searchQuery.Take, searchQuery.Take); + } + } + + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) + { + using (_services.Chronometer.Step(stepPrefix + "Hits")) + { + searchHits = searchEngine.Search(); + } + + using (_services.Chronometer.Step(stepPrefix + "Collect")) + { hitsEntityIds = searchHits.Select(x => x.EntityId).ToArray(); - hitsFactory = () => _productService.Value.GetProductsByIds(hitsEntityIds, loadFlags); - } - } - - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithFacets)) - { - try - { - using (_services.Chronometer.Step(stepPrefix + "Facets")) - { - facets = searchEngine.GetFacetMap(); - ApplyFacetLabels(facets); - } - } - catch (Exception ex) - { - _logger.Error(ex); - } - } - } - - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithSuggestions)) - { - try - { - using (_services.Chronometer.Step(stepPrefix + "Spellcheck")) - { - spellCheckerSuggestions = searchEngine.CheckSpelling(); - } - } - catch (Exception ex) - { - // Spell checking should not break the search. - _logger.Error(ex); - } - } - - var result = new CatalogSearchResult( - searchEngine, - searchQuery, - totalCount, + hitsFactory = () => _productService.Value.GetProductsByIds(hitsEntityIds, loadFlags); + } + } + + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithFacets)) + { + try + { + using (_services.Chronometer.Step(stepPrefix + "Facets")) + { + facets = searchEngine.GetFacetMap(); + ApplyFacetLabels(facets); + } + } + catch (Exception ex) + { + _logger.Error(ex); + } + } + } + + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithSuggestions)) + { + try + { + using (_services.Chronometer.Step(stepPrefix + "Spellcheck")) + { + spellCheckerSuggestions = searchEngine.CheckSpelling(); + } + } + catch (Exception ex) + { + // Spell checking should not break the search. + _logger.Error(ex); + } + } + + var result = new CatalogSearchResult( + searchEngine, + searchQuery, + totalCount, hitsEntityIds, hitsFactory, - spellCheckerSuggestions, - facets); + spellCheckerSuggestions, + facets); var searchedEvent = new CatalogSearchedEvent(searchQuery, result); _services.EventPublisher.Publish(searchedEvent); - return searchedEvent.Result; - } - else if (searchQuery.Origin.IsCaseInsensitiveEqual("Search/Search")) - { - IndexingRequiredNotification(_services, _urlHelper); - } - } - - return SearchDirect(searchQuery); - } - - public IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery = null) - { - var linqCatalogSearchService = _services.Container.ResolveNamed("linq"); - return linqCatalogSearchService.PrepareQuery(searchQuery, baseQuery); - } - - #region XML Sitemap - - public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) - { - if (!context.LoadSetting().XmlSitemapIncludesProducts) - return null; - - var searchQuery = new CatalogSearchQuery() - .VisibleOnly(_services.WorkContext.CurrentCustomer) - .WithVisibility(ProductVisibility.Full) - .HasStoreId(context.RequestStoreId); - - var query = PrepareQuery(searchQuery); - - return new ProductXmlSitemapResult { Query = query, Context = context }; - } - - class ProductXmlSitemapResult : XmlSitemapProvider - { - public IQueryable Query { get; set; } - public XmlSitemapBuildContext Context { get; set; } - - public override int GetTotalCount() - { - return Query.Count(); - } - - public override IEnumerable Enlist() - { - var pager = new FastPager(Query.AsNoTracking(), Context.MaximumNodeCount); - - while (pager.ReadNextPage(x => new { x.Id, x.UpdatedOnUtc }, x => x.Id, out var products)) - { - if (Context.CancellationToken.IsCancellationRequested) - { - break; - } - - foreach (var x in products) - { - yield return new NamedEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }; - } - } - } - - public override int Order => int.MaxValue; - } - - #endregion - } + return searchedEvent.Result; + } + else if (searchQuery.Origin.IsCaseInsensitiveEqual("Search/Search")) + { + IndexingRequiredNotification(_services, _urlHelper); + } + } + + return SearchDirect(searchQuery); + } + + public IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery = null) + { + var linqCatalogSearchService = _services.Container.ResolveNamed("linq"); + return linqCatalogSearchService.PrepareQuery(searchQuery, baseQuery); + } + + #region XML Sitemap + + public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) + { + if (!context.LoadSetting().XmlSitemapIncludesProducts) + return null; + + var searchQuery = new CatalogSearchQuery() + .VisibleOnly(_services.WorkContext.CurrentCustomer) + .WithVisibility(ProductVisibility.Full) + .HasStoreId(context.RequestStoreId); + + var query = PrepareQuery(searchQuery); + + return new ProductXmlSitemapResult { Query = query, Context = context }; + } + + class ProductXmlSitemapResult : XmlSitemapProvider + { + public IQueryable Query { get; set; } + public XmlSitemapBuildContext Context { get; set; } + + public override int GetTotalCount() + { + return Query.Count(); + } + + public override IEnumerable Enlist() + { + var pager = new FastPager(Query.AsNoTracking(), Context.MaximumNodeCount); + + while (pager.ReadNextPage(x => new { x.Id, x.UpdatedOnUtc }, x => x.Id, out var products)) + { + if (Context.CancellationToken.IsCancellationRequested) + { + break; + } + + foreach (var x in products) + { + yield return new NamedEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } + } + } + + public override int Order => int.MaxValue; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/ICatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/ICatalogSearchService.cs index 21ffebbbe1..8327b3bade 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/ICatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/ICatalogSearchService.cs @@ -4,23 +4,23 @@ namespace SmartStore.Services.Search { - public partial interface ICatalogSearchService - { - /// - /// Searches for products - /// - /// Search term, filters and other parameters used for searching - /// Which product navigation properties to eager load - /// Bypasses the index provider (if available) and directly searches in the database - /// Catalog search result - CatalogSearchResult Search(CatalogSearchQuery searchQuery, ProductLoadFlags loadFlags = ProductLoadFlags.None, bool direct = false); + public partial interface ICatalogSearchService + { + /// + /// Searches for products + /// + /// Search term, filters and other parameters used for searching + /// Which product navigation properties to eager load + /// Bypasses the index provider (if available) and directly searches in the database + /// Catalog search result + CatalogSearchResult Search(CatalogSearchQuery searchQuery, ProductLoadFlags loadFlags = ProductLoadFlags.None, bool direct = false); - /// - /// Builds a product query using linq search - /// - /// Search term, filters and other parameters used for searching - /// Optional query used to build the product query. - /// Product queryable - IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery = null); - } + /// + /// Builds a product query using linq search + /// + /// Search term, filters and other parameters used for searching + /// Optional query used to build the product query. + /// Product queryable + IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery = null); + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs index 0f5a1f1e05..6e709dca0d 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/LinqCatalogSearchService.cs @@ -17,48 +17,48 @@ namespace SmartStore.Services.Search { public partial class LinqCatalogSearchService : SearchServiceBase, ICatalogSearchService { - private static int[] _priceThresholds = new int[] { 10, 25, 50, 100, 250, 500, 1000 }; + private static int[] _priceThresholds = new int[] { 10, 25, 50, 100, 250, 500, 1000 }; private readonly ICommonServices _services; private readonly IProductService _productService; - private readonly IRepository _productRepository; - private readonly IRepository _localizedPropertyRepository; - private readonly IRepository _storeMappingRepository; - private readonly IRepository _aclRepository; - private readonly IDeliveryTimeService _deliveryTimeService; - private readonly IManufacturerService _manufacturerService; - private readonly ICategoryService _categoryService; - - public LinqCatalogSearchService( + private readonly IRepository _productRepository; + private readonly IRepository _localizedPropertyRepository; + private readonly IRepository _storeMappingRepository; + private readonly IRepository _aclRepository; + private readonly IDeliveryTimeService _deliveryTimeService; + private readonly IManufacturerService _manufacturerService; + private readonly ICategoryService _categoryService; + + public LinqCatalogSearchService( ICommonServices services, IProductService productService, - IRepository productRepository, - IRepository localizedPropertyRepository, - IRepository storeMappingRepository, - IRepository aclRepository, - IDeliveryTimeService deliveryTimeService, - IManufacturerService manufacturerService, - ICategoryService categoryService) - { + IRepository productRepository, + IRepository localizedPropertyRepository, + IRepository storeMappingRepository, + IRepository aclRepository, + IDeliveryTimeService deliveryTimeService, + IManufacturerService manufacturerService, + ICategoryService categoryService) + { _services = services; _productService = productService; - _productRepository = productRepository; - _localizedPropertyRepository = localizedPropertyRepository; - _storeMappingRepository = storeMappingRepository; - _aclRepository = aclRepository; - _deliveryTimeService = deliveryTimeService; - _manufacturerService = manufacturerService; - _categoryService = categoryService; - - QuerySettings = DbQuerySettings.Default; - } + _productRepository = productRepository; + _localizedPropertyRepository = localizedPropertyRepository; + _storeMappingRepository = storeMappingRepository; + _aclRepository = aclRepository; + _deliveryTimeService = deliveryTimeService; + _manufacturerService = manufacturerService; + _categoryService = categoryService; + + QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } #region Utilities - private IQueryable QueryCategories(IQueryable query, List ids, bool? featuredOnly) - { + private IQueryable QueryCategories(IQueryable query, List ids, bool? featuredOnly) + { return from p in query from pc in p.ProductCategories.Where(pc => ids.Contains(pc.CategoryId)) @@ -67,7 +67,7 @@ from pc in p.ProductCategories.Where(pc => ids.Contains(pc.CategoryId)) } private IQueryable QueryManufacturers(IQueryable query, List ids, bool? featuredOnly) - { + { return from p in query from pm in p.ProductManufacturers.Where(pm => ids.Contains(pm.ManufacturerId)) @@ -76,88 +76,88 @@ from pm in p.ProductManufacturers.Where(pm => ids.Contains(pm.ManufacturerId)) } protected virtual IQueryable ApplySearchTerm(IQueryable query, CatalogSearchQuery searchQuery, out bool isGroupingRequired) - { + { isGroupingRequired = false; - var term = searchQuery.Term; - var fields = searchQuery.Fields; - var languageId = searchQuery.LanguageId ?? 0; + var term = searchQuery.Term; + var fields = searchQuery.Fields; + var languageId = searchQuery.LanguageId ?? 0; - if (term.HasValue() && fields != null && fields.Length != 0 && fields.Any(x => x.HasValue())) - { + if (term.HasValue() && fields != null && fields.Length != 0 && fields.Any(x => x.HasValue())) + { isGroupingRequired = true; - // SearchMode.ExactMatch doesn't make sense here - if (searchQuery.Mode == SearchMode.StartsWith) - { - query = - from p in query - join lp in _localizedPropertyRepository.Table on p.Id equals lp.EntityId into plp - from lp in plp.DefaultIfEmpty() - where - (fields.Contains("name") && p.Name.StartsWith(term)) || - (fields.Contains("sku") && p.Sku.StartsWith(term)) || - (fields.Contains("shortdescription") && p.ShortDescription.StartsWith(term)) || - (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "Name" && lp.LocaleValue.StartsWith(term)) || - (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "ShortDescription" && lp.LocaleValue.StartsWith(term)) - select p; - } - else - { - query = - from p in query - join lp in _localizedPropertyRepository.Table on p.Id equals lp.EntityId into plp - from lp in plp.DefaultIfEmpty() - where - (fields.Contains("name") && p.Name.Contains(term)) || - (fields.Contains("sku") && p.Sku.Contains(term)) || - (fields.Contains("shortdescription") && p.ShortDescription.Contains(term)) || - (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "Name" && lp.LocaleValue.Contains(term)) || - (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "ShortDescription" && lp.LocaleValue.Contains(term)) - select p; - } - } - - return query; - } - - protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery) - { - var ordered = false; - var utcNow = DateTime.UtcNow; + // SearchMode.ExactMatch doesn't make sense here + if (searchQuery.Mode == SearchMode.StartsWith) + { + query = + from p in query + join lp in _localizedPropertyRepository.Table on p.Id equals lp.EntityId into plp + from lp in plp.DefaultIfEmpty() + where + (fields.Contains("name") && p.Name.StartsWith(term)) || + (fields.Contains("sku") && p.Sku.StartsWith(term)) || + (fields.Contains("shortdescription") && p.ShortDescription.StartsWith(term)) || + (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "Name" && lp.LocaleValue.StartsWith(term)) || + (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "ShortDescription" && lp.LocaleValue.StartsWith(term)) + select p; + } + else + { + query = + from p in query + join lp in _localizedPropertyRepository.Table on p.Id equals lp.EntityId into plp + from lp in plp.DefaultIfEmpty() + where + (fields.Contains("name") && p.Name.Contains(term)) || + (fields.Contains("sku") && p.Sku.Contains(term)) || + (fields.Contains("shortdescription") && p.ShortDescription.Contains(term)) || + (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "Name" && lp.LocaleValue.Contains(term)) || + (languageId != 0 && lp.LanguageId == languageId && lp.LocaleKeyGroup == "Product" && lp.LocaleKey == "ShortDescription" && lp.LocaleValue.Contains(term)) + select p; + } + } + + return query; + } + + protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery) + { + var ordered = false; + var utcNow = DateTime.UtcNow; var categoryId = 0; var manufacturerId = 0; - var query = baseQuery ?? _productRepository.Table; + var query = baseQuery ?? _productRepository.Table; - query = query.Where(x => !x.Deleted && !x.IsSystemProduct); - query = ApplySearchTerm(query, searchQuery, out var isGroupingRequired); + query = query.Where(x => !x.Deleted && !x.IsSystemProduct); + query = ApplySearchTerm(query, searchQuery, out var isGroupingRequired); - #region Filters + #region Filters - var filters = new List(); - FlattenFilters(searchQuery.Filters, filters); + var filters = new List(); + FlattenFilters(searchQuery.Filters, filters); - var productIds = GetIdList(filters, "id"); - if (productIds.Any()) - { + var productIds = GetIdList(filters, "id"); + if (productIds.Any()) + { query = query.Where(x => productIds.Contains(x.Id)); - } + } - var categoryIds = GetIdList(filters, "categoryid"); - if (categoryIds.Any()) - { + var categoryIds = GetIdList(filters, "categoryid"); + if (categoryIds.Any()) + { isGroupingRequired = true; categoryId = categoryIds.First(); - if (categoryIds.Count == 1 && categoryId == 0) - { - // Has no category. - query = query.Where(x => x.ProductCategories.Count == 0); - } - else - { + if (categoryIds.Count == 1 && categoryId == 0) + { + // Has no category. + query = query.Where(x => x.ProductCategories.Count == 0); + } + else + { query = QueryCategories(query, categoryIds, null); - } - } + } + } var featuredCategoryIds = GetIdList(filters, "featuredcategoryid"); var notFeaturedCategoryIds = GetIdList(filters, "notfeaturedcategoryid"); @@ -174,21 +174,21 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ query = QueryCategories(query, notFeaturedCategoryIds, false); } - var manufacturerIds = GetIdList(filters, "manufacturerid"); - if (manufacturerIds.Any()) - { + var manufacturerIds = GetIdList(filters, "manufacturerid"); + if (manufacturerIds.Any()) + { isGroupingRequired = true; manufacturerId = manufacturerIds.First(); - if (manufacturerIds.Count == 1 && manufacturerId == 0) - { - // Has no manufacturer. - query = query.Where(x => x.ProductManufacturers.Count == 0); - } - else - { + if (manufacturerIds.Count == 1 && manufacturerId == 0) + { + // Has no manufacturer. + query = query.Where(x => x.ProductManufacturers.Count == 0); + } + else + { query = QueryManufacturers(query, manufacturerIds, null); - } - } + } + } var featuredManuIds = GetIdList(filters, "featuredmanufacturerid"); var notFeaturedManuIds = GetIdList(filters, "notfeaturedmanufacturerid"); @@ -205,42 +205,42 @@ protected virtual IQueryable GetProductQuery(CatalogSearchQuery searchQ query = QueryManufacturers(query, notFeaturedManuIds, false); } - var tagIds = GetIdList(filters, "tagid"); - if (tagIds.Any()) - { + var tagIds = GetIdList(filters, "tagid"); + if (tagIds.Any()) + { isGroupingRequired = true; query = - from p in query - from pt in p.ProductTags.Where(pt => tagIds.Contains(pt.Id)) - select p; - } - - if (!QuerySettings.IgnoreAcl) - { - var roleIds = GetIdList(filters, "roleid"); - if (roleIds.Any()) - { + from p in query + from pt in p.ProductTags.Where(pt => tagIds.Contains(pt.Id)) + select p; + } + + if (!QuerySettings.IgnoreAcl) + { + var roleIds = GetIdList(filters, "roleid"); + if (roleIds.Any()) + { isGroupingRequired = true; query = - from p in query - join acl in _aclRepository.Table on new { pid = p.Id, pname = "Product" } equals new { pid = acl.EntityId, pname = acl.EntityName } into pacl - from acl in pacl.DefaultIfEmpty() - where !p.SubjectToAcl || roleIds.Contains(acl.CustomerRoleId) - select p; - } - } - - var deliverTimeIds = GetIdList(filters, "deliveryid"); - if (deliverTimeIds.Any()) - { + from p in query + join acl in _aclRepository.Table on new { pid = p.Id, pname = "Product" } equals new { pid = acl.EntityId, pname = acl.EntityName } into pacl + from acl in pacl.DefaultIfEmpty() + where !p.SubjectToAcl || roleIds.Contains(acl.CustomerRoleId) + select p; + } + } + + var deliverTimeIds = GetIdList(filters, "deliveryid"); + if (deliverTimeIds.Any()) + { query = query.Where(x => x.DeliveryTimeId != null && deliverTimeIds.Contains(x.DeliveryTimeId.Value)); - } + } - var parentProductIds = GetIdList(filters, "parentid"); - if (parentProductIds.Any()) - { + var parentProductIds = GetIdList(filters, "parentid"); + if (parentProductIds.Any()) + { query = query.Where(x => parentProductIds.Contains(x.ParentGroupedProductId)); - } + } var conditions = GetIdList(filters, "condition"); if (conditions.Any()) @@ -248,9 +248,9 @@ from acl in pacl.DefaultIfEmpty() query = query.Where(x => conditions.Contains((int)x.Condition)); } - foreach (IAttributeSearchFilter filter in filters) - { - var rf = filter as IRangeSearchFilter; + foreach (IAttributeSearchFilter filter in filters) + { + var rf = filter as IRangeSearchFilter; if (filter.FieldName == "id") { @@ -459,12 +459,10 @@ from acl in pacl.DefaultIfEmpty() } else if (filter.FieldName == "available") { - // We cannot include ManageInventoryMethod.ManageStockByAttributes because it's only functional with MergeWithCombination. - var manageStock = (int)ManageInventoryMethod.ManageStock; - query = query.Where(x => - x.ManageInventoryMethodId != manageStock || - (x.ManageInventoryMethodId == manageStock && (x.StockQuantity > 0 || x.BackorderModeId != (int)BackorderMode.NoBackorders)) + x.ManageInventoryMethodId == (int)ManageInventoryMethod.DontManageStock || + (x.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock && (x.StockQuantity > 0 || x.BackorderModeId != (int)BackorderMode.NoBackorders)) || + (x.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes && x.ProductVariantAttributeCombinations.Any(pvac => pvac.StockQuantity > 0 || pvac.AllowOutOfStockOrders)) ); } else if (filter.FieldName.StartsWith("price")) @@ -590,7 +588,7 @@ from sm in psm.DefaultIfEmpty() } } } - } + } #endregion @@ -607,314 +605,302 @@ orderby grp.Key #region Sorting foreach (var sort in searchQuery.Sorting) - { - if (sort.FieldName.IsEmpty()) - { - // Sort by relevance. - if (categoryId != 0) - { - query = OrderBy(ref ordered, query, x => x.ProductCategories.Where(pc => pc.CategoryId == categoryId).FirstOrDefault().DisplayOrder); - } - else if (manufacturerId != 0) - { - query = OrderBy(ref ordered, query, x => x.ProductManufacturers.Where(pm => pm.ManufacturerId == manufacturerId).FirstOrDefault().DisplayOrder); - } - else if (FindFilter(searchQuery.Filters, "parentid") != null) - { - query = OrderBy(ref ordered, query, x => x.DisplayOrder); - } - else - { - query = OrderBy(ref ordered, query, x => x.Name); - } - } - else if (sort.FieldName == "createdon") - { - query = OrderBy(ref ordered, query, x => x.CreatedOnUtc, sort.Descending); - } - else if (sort.FieldName == "name") - { - query = OrderBy(ref ordered, query, x => x.Name, sort.Descending); - } - else if (sort.FieldName == "price") - { - query = OrderBy(ref ordered, query, x => x.Price, sort.Descending); - } - else - { - query = OrderBy(ref ordered, query, x => x.Name); - } - } - - if (!ordered) - { - if (FindFilter(searchQuery.Filters, "parentid") != null) - { - query = query.OrderBy(x => x.DisplayOrder); - } - else - { - query = query.OrderBy(x => x.Id); - } - } + { + if (sort.FieldName.IsEmpty()) + { + // Sort by relevance. + if (categoryId != 0) + { + query = OrderBy(ref ordered, query, x => x.ProductCategories.Where(pc => pc.CategoryId == categoryId).FirstOrDefault().DisplayOrder); + } + else if (manufacturerId != 0) + { + query = OrderBy(ref ordered, query, x => x.ProductManufacturers.Where(pm => pm.ManufacturerId == manufacturerId).FirstOrDefault().DisplayOrder); + } + } + else if (sort.FieldName == "createdon") + { + query = OrderBy(ref ordered, query, x => x.CreatedOnUtc, sort.Descending); + } + else if (sort.FieldName == "name") + { + query = OrderBy(ref ordered, query, x => x.Name, sort.Descending); + } + else if (sort.FieldName == "price") + { + query = OrderBy(ref ordered, query, x => x.Price, sort.Descending); + } + } + + if (!ordered) + { + if (FindFilter(searchQuery.Filters, "parentid") != null) + { + query = query.OrderBy(x => x.DisplayOrder); + } + else + { + query = query.OrderBy(x => x.Id); + } + } #endregion - return query; - } - - protected virtual IDictionary GetFacets(CatalogSearchQuery searchQuery, int totalHits) - { - var result = new Dictionary(); - var storeId = searchQuery.StoreId ?? _services.StoreContext.CurrentStore.Id; - var languageId = searchQuery.LanguageId ?? _services.WorkContext.WorkingLanguage.Id; - - foreach (var key in searchQuery.FacetDescriptors.Keys) - { - var descriptor = searchQuery.FacetDescriptors[key]; - var facets = new List(); - var kind = FacetGroup.GetKindByKey("Catalog", key); - - switch (kind) - { - case FacetGroupKind.Category: - case FacetGroupKind.Brand: - case FacetGroupKind.DeliveryTime: - case FacetGroupKind.Rating: - case FacetGroupKind.Price: - if (totalHits == 0 && !descriptor.Values.Any(x => x.IsSelected)) - { - continue; - } - break; - } - - if (kind == FacetGroupKind.Category) - { - var categoryTree = _categoryService.GetCategoryTree(0, false, storeId); - var categories = categoryTree.Flatten(false); - - if (descriptor.MaxChoicesCount > 0) - { - categories = categories.Take(descriptor.MaxChoicesCount); - } - - var nameQuery = _localizedPropertyRepository.TableUntracked - .Where(x => x.LocaleKeyGroup == "Category" && x.LocaleKey == "Name" && x.LanguageId == languageId); - var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); - - foreach (var category in categories) - { - names.TryGetValue(category.Id, out var label); - - facets.Add(new Facet(new FacetValue(category.Id, IndexTypeCode.Int32) - { - IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(category.Id)), - Label = label.HasValue() ? label : category.Name, - DisplayOrder = category.DisplayOrder - })); - } - } - else if (kind == FacetGroupKind.Brand) - { - var manufacturers = _manufacturerService.GetAllManufacturers(null, storeId); - if (descriptor.MaxChoicesCount > 0) - { - manufacturers = manufacturers.Take(descriptor.MaxChoicesCount).ToList(); - } - - var nameQuery = _localizedPropertyRepository.TableUntracked - .Where(x => x.LocaleKeyGroup == "Manufacturer" && x.LocaleKey == "Name" && x.LanguageId == languageId); - var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); - - foreach (var manu in manufacturers) - { - names.TryGetValue(manu.Id, out var label); - - facets.Add(new Facet(new FacetValue(manu.Id, IndexTypeCode.Int32) - { - IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(manu.Id)), - Label = label.HasValue() ? label : manu.Name, - DisplayOrder = manu.DisplayOrder - })); - } - } - else if (kind == FacetGroupKind.DeliveryTime) - { - var deliveryTimes = _deliveryTimeService.GetAllDeliveryTimes(); - var nameQuery = _localizedPropertyRepository.TableUntracked - .Where(x => x.LocaleKeyGroup == "DeliveryTime" && x.LocaleKey == "Name" && x.LanguageId == languageId); - var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); - - foreach (var deliveryTime in deliveryTimes) - { - if (descriptor.MaxChoicesCount > 0 && facets.Count >= descriptor.MaxChoicesCount) - break; - - names.TryGetValue(deliveryTime.Id, out var label); - - facets.Add(new Facet(new FacetValue(deliveryTime.Id, IndexTypeCode.Int32) - { - IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(deliveryTime.Id)), - Label = label.HasValue() ? label : deliveryTime.Name, - DisplayOrder = deliveryTime.DisplayOrder - })); - } - } - else if (kind == FacetGroupKind.Price) - { - var count = 0; - var hasActivePredefinedFacet = false; - var minPrice = _productRepository.Table.Where(x => x.Published && !x.Deleted && !x.IsSystemProduct).Min(x => (double)x.Price); - var maxPrice = _productRepository.Table.Where(x => x.Published && !x.Deleted && !x.IsSystemProduct).Max(x => (double)x.Price); - minPrice = FacetUtility.MakePriceEven(minPrice); - maxPrice = FacetUtility.MakePriceEven(maxPrice); - - for (var i = 0; i < _priceThresholds.Length; ++i) - { - if (descriptor.MaxChoicesCount > 0 && facets.Count >= descriptor.MaxChoicesCount) - break; - - var price = _priceThresholds[i]; - if (price < minPrice) - continue; - - if (price >= maxPrice) - i = int.MaxValue - 1; - - var selected = descriptor.Values.Any(x => x.IsSelected && x.Value == null && x.UpperValue != null && (double)x.UpperValue == price); - if (selected) - hasActivePredefinedFacet = true; - - facets.Add(new Facet(new FacetValue(null, price, IndexTypeCode.Double, false, true) - { - DisplayOrder = ++count, - IsSelected = selected - })); - } - - // Add facet for custom price range. - var priceDescriptorValue = descriptor.Values.FirstOrDefault(); - - var customPriceFacetValue = new FacetValue( - priceDescriptorValue != null && !hasActivePredefinedFacet ? priceDescriptorValue.Value : null, - priceDescriptorValue != null && !hasActivePredefinedFacet ? priceDescriptorValue.UpperValue : null, - IndexTypeCode.Double, - true, - true); - - customPriceFacetValue.IsSelected = customPriceFacetValue.Value != null || customPriceFacetValue.UpperValue != null; - - if (!(totalHits == 0 && !customPriceFacetValue.IsSelected)) - { - facets.Insert(0, new Facet("custom", customPriceFacetValue)); - } - } - else if (kind == FacetGroupKind.Rating) - { - foreach (var rating in FacetUtility.GetRatings()) - { - var newFacet = new Facet(rating); - newFacet.Value.IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(rating.Value)); - facets.Add(newFacet); - } - } - else if (kind == FacetGroupKind.Availability || kind == FacetGroupKind.NewArrivals) - { - var value = descriptor.Values.FirstOrDefault(); - if (value != null) - { - if (kind == FacetGroupKind.NewArrivals && totalHits == 0 && !value.IsSelected) - { - continue; - } - - var newValue = value.Clone(); - newValue.Value = true; - newValue.TypeCode = IndexTypeCode.Boolean; - newValue.IsRange = false; - newValue.IsSelected = value.IsSelected; - - facets.Add(new Facet(newValue)); - } - } - - if (facets.Any(x => x.Published)) - { - //facets.Each(x => $"{key} {x.Value.ToString()}".Dump()); - - result.Add(key, new FacetGroup( + return query; + } + + protected virtual IDictionary GetFacets(CatalogSearchQuery searchQuery, int totalHits) + { + var result = new Dictionary(); + var storeId = searchQuery.StoreId ?? _services.StoreContext.CurrentStore.Id; + var languageId = searchQuery.LanguageId ?? _services.WorkContext.WorkingLanguage.Id; + + foreach (var key in searchQuery.FacetDescriptors.Keys) + { + var descriptor = searchQuery.FacetDescriptors[key]; + var facets = new List(); + var kind = FacetGroup.GetKindByKey("Catalog", key); + + switch (kind) + { + case FacetGroupKind.Category: + case FacetGroupKind.Brand: + case FacetGroupKind.DeliveryTime: + case FacetGroupKind.Rating: + case FacetGroupKind.Price: + if (totalHits == 0 && !descriptor.Values.Any(x => x.IsSelected)) + { + continue; + } + break; + } + + if (kind == FacetGroupKind.Category) + { + var categoryTree = _categoryService.GetCategoryTree(0, false, storeId); + var categories = categoryTree.Flatten(false); + + if (descriptor.MaxChoicesCount > 0) + { + categories = categories.Take(descriptor.MaxChoicesCount); + } + + var nameQuery = _localizedPropertyRepository.TableUntracked + .Where(x => x.LocaleKeyGroup == "Category" && x.LocaleKey == "Name" && x.LanguageId == languageId); + var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); + + foreach (var category in categories) + { + names.TryGetValue(category.Id, out var label); + + facets.Add(new Facet(new FacetValue(category.Id, IndexTypeCode.Int32) + { + IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(category.Id)), + Label = label.HasValue() ? label : category.Name, + DisplayOrder = category.DisplayOrder + })); + } + } + else if (kind == FacetGroupKind.Brand) + { + var manufacturers = _manufacturerService.GetAllManufacturers(null, storeId); + if (descriptor.MaxChoicesCount > 0) + { + manufacturers = manufacturers.Take(descriptor.MaxChoicesCount).ToList(); + } + + var nameQuery = _localizedPropertyRepository.TableUntracked + .Where(x => x.LocaleKeyGroup == "Manufacturer" && x.LocaleKey == "Name" && x.LanguageId == languageId); + var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); + + foreach (var manu in manufacturers) + { + names.TryGetValue(manu.Id, out var label); + + facets.Add(new Facet(new FacetValue(manu.Id, IndexTypeCode.Int32) + { + IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(manu.Id)), + Label = label.HasValue() ? label : manu.Name, + DisplayOrder = manu.DisplayOrder + })); + } + } + else if (kind == FacetGroupKind.DeliveryTime) + { + var deliveryTimes = _deliveryTimeService.GetAllDeliveryTimes(); + var nameQuery = _localizedPropertyRepository.TableUntracked + .Where(x => x.LocaleKeyGroup == "DeliveryTime" && x.LocaleKey == "Name" && x.LanguageId == languageId); + var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); + + foreach (var deliveryTime in deliveryTimes) + { + if (descriptor.MaxChoicesCount > 0 && facets.Count >= descriptor.MaxChoicesCount) + break; + + names.TryGetValue(deliveryTime.Id, out var label); + + facets.Add(new Facet(new FacetValue(deliveryTime.Id, IndexTypeCode.Int32) + { + IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(deliveryTime.Id)), + Label = label.HasValue() ? label : deliveryTime.Name, + DisplayOrder = deliveryTime.DisplayOrder + })); + } + } + else if (kind == FacetGroupKind.Price) + { + var count = 0; + var hasActivePredefinedFacet = false; + var minPrice = _productRepository.Table.Where(x => x.Published && !x.Deleted && !x.IsSystemProduct).Min(x => (double)x.Price); + var maxPrice = _productRepository.Table.Where(x => x.Published && !x.Deleted && !x.IsSystemProduct).Max(x => (double)x.Price); + minPrice = FacetUtility.MakePriceEven(minPrice); + maxPrice = FacetUtility.MakePriceEven(maxPrice); + + for (var i = 0; i < _priceThresholds.Length; ++i) + { + if (descriptor.MaxChoicesCount > 0 && facets.Count >= descriptor.MaxChoicesCount) + break; + + var price = _priceThresholds[i]; + if (price < minPrice) + continue; + + if (price >= maxPrice) + i = int.MaxValue - 1; + + var selected = descriptor.Values.Any(x => x.IsSelected && x.Value == null && x.UpperValue != null && (double)x.UpperValue == price); + if (selected) + hasActivePredefinedFacet = true; + + facets.Add(new Facet(new FacetValue(null, price, IndexTypeCode.Double, false, true) + { + DisplayOrder = ++count, + IsSelected = selected + })); + } + + // Add facet for custom price range. + var priceDescriptorValue = descriptor.Values.FirstOrDefault(); + + var customPriceFacetValue = new FacetValue( + priceDescriptorValue != null && !hasActivePredefinedFacet ? priceDescriptorValue.Value : null, + priceDescriptorValue != null && !hasActivePredefinedFacet ? priceDescriptorValue.UpperValue : null, + IndexTypeCode.Double, + true, + true); + + customPriceFacetValue.IsSelected = customPriceFacetValue.Value != null || customPriceFacetValue.UpperValue != null; + + if (!(totalHits == 0 && !customPriceFacetValue.IsSelected)) + { + facets.Insert(0, new Facet("custom", customPriceFacetValue)); + } + } + else if (kind == FacetGroupKind.Rating) + { + foreach (var rating in FacetUtility.GetRatings()) + { + var newFacet = new Facet(rating); + newFacet.Value.IsSelected = descriptor.Values.Any(x => x.IsSelected && x.Value.Equals(rating.Value)); + facets.Add(newFacet); + } + } + else if (kind == FacetGroupKind.Availability || kind == FacetGroupKind.NewArrivals) + { + var value = descriptor.Values.FirstOrDefault(); + if (value != null) + { + if (kind == FacetGroupKind.NewArrivals && totalHits == 0 && !value.IsSelected) + { + continue; + } + + var newValue = value.Clone(); + newValue.Value = true; + newValue.TypeCode = IndexTypeCode.Boolean; + newValue.IsRange = false; + newValue.IsSelected = value.IsSelected; + + facets.Add(new Facet(newValue)); + } + } + + if (facets.Any(x => x.Published)) + { + //facets.Each(x => $"{key} {x.Value.ToString()}".Dump()); + + result.Add(key, new FacetGroup( "Catalog", key, - descriptor.Label, - descriptor.IsMultiSelect, - false, - descriptor.DisplayOrder, - facets.OrderBy(descriptor))); - } - } - - return result; - } + descriptor.Label, + descriptor.IsMultiSelect, + false, + descriptor.DisplayOrder, + facets.OrderBy(descriptor))); + } + } + + return result; + } #endregion - public CatalogSearchResult Search(CatalogSearchQuery searchQuery, ProductLoadFlags loadFlags = ProductLoadFlags.None, bool direct = false) - { + public CatalogSearchResult Search(CatalogSearchQuery searchQuery, ProductLoadFlags loadFlags = ProductLoadFlags.None, bool direct = false) + { _services.EventPublisher.Publish(new CatalogSearchingEvent(searchQuery, true)); - var totalHits = 0; + var totalHits = 0; int[] hitsEntityIds = null; Func> hitsFactory = null; - IDictionary facets = null; + IDictionary facets = null; - if (searchQuery.Take > 0) - { - var query = GetProductQuery(searchQuery, null); + if (searchQuery.Take > 0) + { + var query = GetProductQuery(searchQuery, null); - totalHits = query.Count(); + totalHits = query.Count(); - // Fix paging boundaries - if (searchQuery.Skip > 0 && searchQuery.Skip >= totalHits) - { - searchQuery.Slice((totalHits / searchQuery.Take) * searchQuery.Take, searchQuery.Take); - } + // Fix paging boundaries + if (searchQuery.Skip > 0 && searchQuery.Skip >= totalHits) + { + searchQuery.Slice((totalHits / searchQuery.Take) * searchQuery.Take, searchQuery.Take); + } - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) - { + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) + { var skip = searchQuery.PageIndex * searchQuery.Take; query = query - .Skip(() => skip) - .Take(() => searchQuery.Take); + .Skip(() => skip) + .Take(() => searchQuery.Take); hitsEntityIds = query.Select(x => x.Id).ToArray(); - hitsFactory = () => _productService.GetProductsByIds(hitsEntityIds, loadFlags); - } - - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithFacets) && searchQuery.FacetDescriptors.Any()) - { - facets = GetFacets(searchQuery, totalHits); - } - } - - var result = new CatalogSearchResult( - null, - searchQuery, - totalHits, + hitsFactory = () => _productService.GetProductsByIds(hitsEntityIds, loadFlags); + } + + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithFacets) && searchQuery.FacetDescriptors.Any()) + { + facets = GetFacets(searchQuery, totalHits); + } + } + + var result = new CatalogSearchResult( + null, + searchQuery, + totalHits, hitsEntityIds, hitsFactory, - null, - facets); + null, + facets); - _services.EventPublisher.Publish(new CatalogSearchedEvent(searchQuery, result)); + _services.EventPublisher.Publish(new CatalogSearchedEvent(searchQuery, result)); - return result; - } + return result; + } - public IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery = null) - { - return GetProductQuery(searchQuery, baseQuery); - } - } + public IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryable baseQuery = null) + { + return GetProductQuery(searchQuery, baseQuery); + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs index 6cfae38fd0..18e94da4a2 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs @@ -15,60 +15,60 @@ namespace SmartStore.Services.Search.Modelling { public class CatalogSearchQueryAliasMapper : ICatalogSearchQueryAliasMapper - { - private const string ALL_ATTRIBUTE_ID_BY_ALIAS_KEY = "search:attribute.id.alias.mappings.all"; - private const string ALL_ATTRIBUTE_ALIAS_BY_ID_KEY = "search:attribute.alias.id.mappings.all"; - private const string ALL_COMMONFACET_ALIAS_BY_KIND_KEY = "search:commonfacet.alias.kind.mappings.all"; - - private const string ALL_VARIANT_ID_BY_ALIAS_KEY = "search:variant.id.alias.mappings.all"; - private const string ALL_VARIANT_ALIAS_BY_ID_KEY = "search:variant.alias.id.mappings.all"; - - private readonly ICacheManager _cacheManager; - private readonly IRepository _localizedPropertyRepository; - private readonly IRepository _productAttributeRepository; - private readonly IRepository _productVariantAttributeValueRepository; - private readonly ISpecificationAttributeService _specificationAttributeService; - private readonly ISettingService _settingService; - private readonly ILanguageService _languageService; - - public CatalogSearchQueryAliasMapper( - ICacheManager cacheManager, - IRepository localizedPropertyRepository, - IRepository productAttributeRepository, - IRepository productVariantAttributeValueRepository, - ISpecificationAttributeService specificationAttributeService, - ISettingService settingService, - ILanguageService languageService) - { - _cacheManager = cacheManager; - _localizedPropertyRepository = localizedPropertyRepository; - _productAttributeRepository = productAttributeRepository; - _productVariantAttributeValueRepository = productVariantAttributeValueRepository; - _specificationAttributeService = specificationAttributeService; - _settingService = settingService; - _languageService = languageService; - } - - protected string CreateKey(string prefix, int languageId, string alias) - { - return $"{prefix}.{languageId}.{alias}"; - } - protected string CreateKey(string prefix, int languageId, int attributeId) - { - return $"{prefix}.{languageId}.{attributeId}"; - } - - protected string CreateOptionKey(string prefix, int languageId, int attributeId, string optionAlias) - { - return $"{prefix}.{languageId}.{attributeId}.{optionAlias}"; - } - protected string CreateOptionKey(string prefix, int languageId, int optionId) - { - return $"{prefix}.{languageId}.{optionId}"; - } - - protected void CachedLocalizedAlias(string localeKeyGroup, Action caching) - { + { + private const string ALL_ATTRIBUTE_ID_BY_ALIAS_KEY = "search:attribute.id.alias.mappings.all"; + private const string ALL_ATTRIBUTE_ALIAS_BY_ID_KEY = "search:attribute.alias.id.mappings.all"; + private const string ALL_COMMONFACET_ALIAS_BY_KIND_KEY = "search:commonfacet.alias.kind.mappings.all"; + + private const string ALL_VARIANT_ID_BY_ALIAS_KEY = "search:variant.id.alias.mappings.all"; + private const string ALL_VARIANT_ALIAS_BY_ID_KEY = "search:variant.alias.id.mappings.all"; + + private readonly ICacheManager _cacheManager; + private readonly IRepository _localizedPropertyRepository; + private readonly IRepository _productAttributeRepository; + private readonly IRepository _productVariantAttributeValueRepository; + private readonly ISpecificationAttributeService _specificationAttributeService; + private readonly ISettingService _settingService; + private readonly ILanguageService _languageService; + + public CatalogSearchQueryAliasMapper( + ICacheManager cacheManager, + IRepository localizedPropertyRepository, + IRepository productAttributeRepository, + IRepository productVariantAttributeValueRepository, + ISpecificationAttributeService specificationAttributeService, + ISettingService settingService, + ILanguageService languageService) + { + _cacheManager = cacheManager; + _localizedPropertyRepository = localizedPropertyRepository; + _productAttributeRepository = productAttributeRepository; + _productVariantAttributeValueRepository = productVariantAttributeValueRepository; + _specificationAttributeService = specificationAttributeService; + _settingService = settingService; + _languageService = languageService; + } + + protected string CreateKey(string prefix, int languageId, string alias) + { + return $"{prefix}.{languageId}.{alias}"; + } + protected string CreateKey(string prefix, int languageId, int attributeId) + { + return $"{prefix}.{languageId}.{attributeId}"; + } + + protected string CreateOptionKey(string prefix, int languageId, int attributeId, string optionAlias) + { + return $"{prefix}.{languageId}.{attributeId}.{optionAlias}"; + } + protected string CreateOptionKey(string prefix, int languageId, int optionId) + { + return $"{prefix}.{languageId}.{optionId}"; + } + + protected void CachedLocalizedAlias(string localeKeyGroup, Action caching) + { var properties = _localizedPropertyRepository.TableUntracked .Where(x => x.LocaleKeyGroup == localeKeyGroup && x.LocaleKey == "Alias") .ToList(); @@ -78,418 +78,418 @@ protected void CachedLocalizedAlias(string localeKeyGroup, Action !string.IsNullOrWhiteSpace(x.LocaleValue)) .ToList() .ForEach(caching); - } - - #region Specification Attributes - - protected virtual IDictionary GetAttributeIdByAliasMappings() - { - return _cacheManager.Get(ALL_ATTRIBUTE_ID_BY_ALIAS_KEY, () => - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - var optionIdMappings = new Dictionary(); - IPagedList attributes = null; - var attributeId = 0; - var pageIndex = 0; - - var query = _specificationAttributeService.GetSpecificationAttributes() - .Expand(x => x.SpecificationAttributeOptions) - .OrderBy(x => x.Id); - - do - { - attributes = new PagedList(query, pageIndex++, 500); - - foreach (var attribute in attributes) - { - if (attribute.Alias.HasValue()) - { - result[CreateKey("attr", 0, attribute.Alias)] = attribute.Id; - } - - foreach (var option in attribute.SpecificationAttributeOptions) - { - optionIdMappings[option.Id] = option.SpecificationAttributeId; - - if (option.Alias.HasValue()) - { - result[CreateOptionKey("attr.option", 0, attribute.Id, option.Alias)] = option.Id; - } - } - } - } - while (attributes.HasNextPage); - - attributes.Clear(); - - CachedLocalizedAlias("SpecificationAttribute", x => result[CreateKey("attr", x.LanguageId, x.LocaleValue)] = x.EntityId); - CachedLocalizedAlias("SpecificationAttributeOption", x => - { - if (optionIdMappings.TryGetValue(x.EntityId, out attributeId)) - result[CreateOptionKey("attr.option", x.LanguageId, attributeId, x.LocaleValue)] = x.EntityId; - }); - - return result; - }); - } - - protected virtual IDictionary GetAttributeAliasByIdMappings() - { - return _cacheManager.Get(ALL_ATTRIBUTE_ALIAS_BY_ID_KEY, () => - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - IPagedList attributes = null; - var pageIndex = 0; - - var query = _specificationAttributeService.GetSpecificationAttributes() - .Expand(x => x.SpecificationAttributeOptions) - .OrderBy(x => x.Id); - - do - { - attributes = new PagedList(query, pageIndex++, 500); - - foreach (var attribute in attributes) - { - if (attribute.Alias.HasValue()) - { - result[CreateKey("attr", 0, attribute.Id)] = attribute.Alias; - } - - foreach (var option in attribute.SpecificationAttributeOptions.Where(x => x.Alias.HasValue())) - { - result[CreateOptionKey("attr.option", 0, option.Id)] = option.Alias; - } - } - } - while (attributes.HasNextPage); - - attributes.Clear(); - - CachedLocalizedAlias("SpecificationAttribute", x => result[CreateKey("attr", x.LanguageId, x.EntityId)] = x.LocaleValue); - CachedLocalizedAlias("SpecificationAttributeOption", x => result[CreateOptionKey("attr.option", x.LanguageId, x.EntityId)] = x.LocaleValue); - - return result; - }); - } - - public void ClearAttributeCache() - { - _cacheManager.Remove(ALL_ATTRIBUTE_ID_BY_ALIAS_KEY); - _cacheManager.Remove(ALL_ATTRIBUTE_ALIAS_BY_ID_KEY); - } - - public int GetAttributeIdByAlias(string attributeAlias, int languageId = 0) - { - var result = 0; - - if (attributeAlias.HasValue()) - { - var mappings = GetAttributeIdByAliasMappings(); - - if (!mappings.TryGetValue(CreateKey("attr", languageId, attributeAlias), out result) && languageId != 0) - { - mappings.TryGetValue(CreateKey("attr", 0, attributeAlias), out result); - } - } - - return result; - } - - public int GetAttributeOptionIdByAlias(string optionAlias, int attributeId, int languageId = 0) - { - var result = 0; - - if (optionAlias.HasValue() && attributeId != 0) - { - var mappings = GetAttributeIdByAliasMappings(); - - if (!mappings.TryGetValue(CreateOptionKey("attr.option", languageId, attributeId, optionAlias), out result) && languageId != 0) - { - mappings.TryGetValue(CreateOptionKey("attr.option", 0, attributeId, optionAlias), out result); - } - } - - return result; - } - - public string GetAttributeAliasById(int attributeId, int languageId = 0) - { - string result = null; - - if (attributeId != 0) - { - var mappings = GetAttributeAliasByIdMappings(); - - if (!mappings.TryGetValue(CreateKey("attr", languageId, attributeId), out result) && languageId != 0) - { - mappings.TryGetValue(CreateKey("attr", 0, attributeId), out result); - } - } - - return result; - } - - public string GetAttributeOptionAliasById(int optionId, int languageId = 0) - { - string result = null; - - if (optionId != 0) - { - var mappings = GetAttributeAliasByIdMappings(); - - if (!mappings.TryGetValue(CreateOptionKey("attr.option", languageId, optionId), out result) && languageId != 0) - { - mappings.TryGetValue(CreateOptionKey("attr.option", 0, optionId), out result); - } - } - - return result; - } - - #endregion - - #region Product Variants - - protected virtual IDictionary GetVariantIdByAliasMappings() - { - return _cacheManager.Get(ALL_VARIANT_ID_BY_ALIAS_KEY, () => - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - IPagedList variants = null; - IPagedList options = null; - var variantId = 0; - var pageIndex = 0; - - var variantQuery = _productAttributeRepository.TableUntracked - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(x => x.Id); - - var optionQuery = _productVariantAttributeValueRepository.TableUntracked - .Expand(x => x.ProductVariantAttribute) - .Expand("ProductVariantAttribute.ProductAttribute") - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(x => x.Id); - - do - { - variants = new PagedList(variantQuery, pageIndex++, 500); - - foreach (var variant in variants) - { - result[CreateKey("vari", 0, variant.Alias)] = variant.Id; - } - } - while (variants.HasNextPage); - pageIndex = 0; - variants.Clear(); - - do - { - options = new PagedList(optionQuery, pageIndex++, 500); - - foreach (var option in options) - { - var variant = option.ProductVariantAttribute.ProductAttribute; - result[CreateOptionKey("vari.option", 0, variant.Id, option.Alias)] = option.Id; - } - } - while (options.HasNextPage); - options.Clear(); - - var optionIdMappings = _productVariantAttributeValueRepository.TableUntracked - .Expand(x => x.ProductVariantAttribute.ProductAttribute) - .Select(x => new - { - OptionId = x.Id, - VariantId = x.ProductVariantAttribute.ProductAttribute.Id - }) - .ToDictionary(x => x.OptionId, x => x.VariantId); - - CachedLocalizedAlias("ProductAttribute", x => result[CreateKey("vari", x.LanguageId, x.LocaleValue)] = x.EntityId); - CachedLocalizedAlias("ProductVariantAttributeValue", x => - { - if (optionIdMappings.TryGetValue(x.EntityId, out variantId)) - result[CreateOptionKey("vari.option", x.LanguageId, variantId, x.LocaleValue)] = x.EntityId; - }); - - return result; - }); - } - - protected virtual IDictionary GetVariantAliasByIdMappings() - { - return _cacheManager.Get(ALL_VARIANT_ALIAS_BY_ID_KEY, () => - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - IPagedList variants = null; - IPagedList options = null; - var pageIndex = 0; - - var variantQuery = _productAttributeRepository.TableUntracked - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(x => x.Id); - - var optionQuery = _productVariantAttributeValueRepository.TableUntracked - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(x => x.Id); - - do - { - variants = new PagedList(variantQuery, pageIndex++, 500); - - foreach (var variant in variants) - { - result[CreateKey("vari", 0, variant.Id)] = variant.Alias; - } - } - while (variants.HasNextPage); - pageIndex = 0; - variants.Clear(); - - do - { - options = new PagedList(optionQuery, pageIndex++, 500); - - foreach (var option in options) - { - result[CreateOptionKey("attr.option", 0, option.Id)] = option.Alias; - } - } - while (options.HasNextPage); - options.Clear(); - - CachedLocalizedAlias("ProductAttribute", x => result[CreateKey("vari", x.LanguageId, x.EntityId)] = x.LocaleValue); - CachedLocalizedAlias("ProductVariantAttributeValue", x => result[CreateOptionKey("vari.option", x.LanguageId, x.EntityId)] = x.LocaleValue); - - return result; - }); - } - - public void ClearVariantCache() - { - _cacheManager.Remove(ALL_VARIANT_ID_BY_ALIAS_KEY); - _cacheManager.Remove(ALL_VARIANT_ALIAS_BY_ID_KEY); - } - - public int GetVariantIdByAlias(string variantAlias, int languageId = 0) - { - var result = 0; - - if (variantAlias.HasValue()) - { - var mappings = GetVariantIdByAliasMappings(); - - if (!mappings.TryGetValue(CreateKey("vari", languageId, variantAlias), out result) && languageId != 0) - { - mappings.TryGetValue(CreateKey("vari", 0, variantAlias), out result); - } - } - - return result; - } - - public int GetVariantOptionIdByAlias(string optionAlias, int variantId, int languageId = 0) - { - var result = 0; - - if (optionAlias.HasValue() && variantId != 0) - { - var mappings = GetVariantIdByAliasMappings(); - - if (!mappings.TryGetValue(CreateOptionKey("vari.option", languageId, variantId, optionAlias), out result) && languageId != 0) - { - mappings.TryGetValue(CreateOptionKey("vari.option", 0, variantId, optionAlias), out result); - } - } - - return result; - } - - public string GetVariantAliasById(int variantId, int languageId = 0) - { - string result = null; - - if (variantId != 0) - { - var mappings = GetVariantAliasByIdMappings(); - - if (!mappings.TryGetValue(CreateKey("vari", languageId, variantId), out result) && languageId != 0) - { - mappings.TryGetValue(CreateKey("vari", 0, variantId), out result); - } - } - - return result; - } - - public string GetVariantOptionAliasById(int optionId, int languageId = 0) - { - string result = null; - - if (optionId != 0) - { - var mappings = GetVariantAliasByIdMappings(); - - if (!mappings.TryGetValue(CreateOptionKey("vari.option", languageId, optionId), out result) && languageId != 0) - { - mappings.TryGetValue(CreateOptionKey("vari.option", 0, optionId), out result); - } - } - - return result; - } - - #endregion - - #region Common Facets - - protected virtual IDictionary GetCommonFacetAliasByGroupKindMappings() - { - return _cacheManager.Get(ALL_COMMONFACET_ALIAS_BY_KIND_KEY, () => - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var groupKinds = new FacetGroupKind[] - { - FacetGroupKind.Category, - FacetGroupKind.Brand, - FacetGroupKind.Price, - FacetGroupKind.Rating, - FacetGroupKind.DeliveryTime, - FacetGroupKind.Availability, - FacetGroupKind.NewArrivals - }; - - foreach (var language in _languageService.GetAllLanguages()) - { - foreach (var groupKind in groupKinds) - { - var key = FacetUtility.GetFacetAliasSettingKey(groupKind, language.Id); - var value = _settingService.GetSettingByKey(key); - if (value.HasValue()) - { - result.Add(key, value); - } - } - } - - return result; - }); - } - - public void ClearCommonFacetCache() - { - _cacheManager.Remove(ALL_COMMONFACET_ALIAS_BY_KIND_KEY); - } - - public string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId) - { - var mappings = GetCommonFacetAliasByGroupKindMappings(); - - return mappings.Get(FacetUtility.GetFacetAliasSettingKey(kind, languageId)); - } - - #endregion - } + } + + #region Specification Attributes + + protected virtual IDictionary GetAttributeIdByAliasMappings() + { + return _cacheManager.Get(ALL_ATTRIBUTE_ID_BY_ALIAS_KEY, () => + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var optionIdMappings = new Dictionary(); + IPagedList attributes = null; + var attributeId = 0; + var pageIndex = 0; + + var query = _specificationAttributeService.GetSpecificationAttributes() + .Expand(x => x.SpecificationAttributeOptions) + .OrderBy(x => x.Id); + + do + { + attributes = new PagedList(query, pageIndex++, 500); + + foreach (var attribute in attributes) + { + if (attribute.Alias.HasValue()) + { + result[CreateKey("attr", 0, attribute.Alias)] = attribute.Id; + } + + foreach (var option in attribute.SpecificationAttributeOptions) + { + optionIdMappings[option.Id] = option.SpecificationAttributeId; + + if (option.Alias.HasValue()) + { + result[CreateOptionKey("attr.option", 0, attribute.Id, option.Alias)] = option.Id; + } + } + } + } + while (attributes.HasNextPage); + + attributes.Clear(); + + CachedLocalizedAlias("SpecificationAttribute", x => result[CreateKey("attr", x.LanguageId, x.LocaleValue)] = x.EntityId); + CachedLocalizedAlias("SpecificationAttributeOption", x => + { + if (optionIdMappings.TryGetValue(x.EntityId, out attributeId)) + result[CreateOptionKey("attr.option", x.LanguageId, attributeId, x.LocaleValue)] = x.EntityId; + }); + + return result; + }); + } + + protected virtual IDictionary GetAttributeAliasByIdMappings() + { + return _cacheManager.Get(ALL_ATTRIBUTE_ALIAS_BY_ID_KEY, () => + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + IPagedList attributes = null; + var pageIndex = 0; + + var query = _specificationAttributeService.GetSpecificationAttributes() + .Expand(x => x.SpecificationAttributeOptions) + .OrderBy(x => x.Id); + + do + { + attributes = new PagedList(query, pageIndex++, 500); + + foreach (var attribute in attributes) + { + if (attribute.Alias.HasValue()) + { + result[CreateKey("attr", 0, attribute.Id)] = attribute.Alias; + } + + foreach (var option in attribute.SpecificationAttributeOptions.Where(x => x.Alias.HasValue())) + { + result[CreateOptionKey("attr.option", 0, option.Id)] = option.Alias; + } + } + } + while (attributes.HasNextPage); + + attributes.Clear(); + + CachedLocalizedAlias("SpecificationAttribute", x => result[CreateKey("attr", x.LanguageId, x.EntityId)] = x.LocaleValue); + CachedLocalizedAlias("SpecificationAttributeOption", x => result[CreateOptionKey("attr.option", x.LanguageId, x.EntityId)] = x.LocaleValue); + + return result; + }); + } + + public void ClearAttributeCache() + { + _cacheManager.Remove(ALL_ATTRIBUTE_ID_BY_ALIAS_KEY); + _cacheManager.Remove(ALL_ATTRIBUTE_ALIAS_BY_ID_KEY); + } + + public int GetAttributeIdByAlias(string attributeAlias, int languageId = 0) + { + var result = 0; + + if (attributeAlias.HasValue()) + { + var mappings = GetAttributeIdByAliasMappings(); + + if (!mappings.TryGetValue(CreateKey("attr", languageId, attributeAlias), out result) && languageId != 0) + { + mappings.TryGetValue(CreateKey("attr", 0, attributeAlias), out result); + } + } + + return result; + } + + public int GetAttributeOptionIdByAlias(string optionAlias, int attributeId, int languageId = 0) + { + var result = 0; + + if (optionAlias.HasValue() && attributeId != 0) + { + var mappings = GetAttributeIdByAliasMappings(); + + if (!mappings.TryGetValue(CreateOptionKey("attr.option", languageId, attributeId, optionAlias), out result) && languageId != 0) + { + mappings.TryGetValue(CreateOptionKey("attr.option", 0, attributeId, optionAlias), out result); + } + } + + return result; + } + + public string GetAttributeAliasById(int attributeId, int languageId = 0) + { + string result = null; + + if (attributeId != 0) + { + var mappings = GetAttributeAliasByIdMappings(); + + if (!mappings.TryGetValue(CreateKey("attr", languageId, attributeId), out result) && languageId != 0) + { + mappings.TryGetValue(CreateKey("attr", 0, attributeId), out result); + } + } + + return result; + } + + public string GetAttributeOptionAliasById(int optionId, int languageId = 0) + { + string result = null; + + if (optionId != 0) + { + var mappings = GetAttributeAliasByIdMappings(); + + if (!mappings.TryGetValue(CreateOptionKey("attr.option", languageId, optionId), out result) && languageId != 0) + { + mappings.TryGetValue(CreateOptionKey("attr.option", 0, optionId), out result); + } + } + + return result; + } + + #endregion + + #region Product Variants + + protected virtual IDictionary GetVariantIdByAliasMappings() + { + return _cacheManager.Get(ALL_VARIANT_ID_BY_ALIAS_KEY, () => + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + IPagedList variants = null; + IPagedList options = null; + var variantId = 0; + var pageIndex = 0; + + var variantQuery = _productAttributeRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(x => x.Id); + + var optionQuery = _productVariantAttributeValueRepository.TableUntracked + .Expand(x => x.ProductVariantAttribute) + .Expand("ProductVariantAttribute.ProductAttribute") + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(x => x.Id); + + do + { + variants = new PagedList(variantQuery, pageIndex++, 500); + + foreach (var variant in variants) + { + result[CreateKey("vari", 0, variant.Alias)] = variant.Id; + } + } + while (variants.HasNextPage); + pageIndex = 0; + variants.Clear(); + + do + { + options = new PagedList(optionQuery, pageIndex++, 500); + + foreach (var option in options) + { + var variant = option.ProductVariantAttribute.ProductAttribute; + result[CreateOptionKey("vari.option", 0, variant.Id, option.Alias)] = option.Id; + } + } + while (options.HasNextPage); + options.Clear(); + + var optionIdMappings = _productVariantAttributeValueRepository.TableUntracked + .Expand(x => x.ProductVariantAttribute.ProductAttribute) + .Select(x => new + { + OptionId = x.Id, + VariantId = x.ProductVariantAttribute.ProductAttribute.Id + }) + .ToDictionary(x => x.OptionId, x => x.VariantId); + + CachedLocalizedAlias("ProductAttribute", x => result[CreateKey("vari", x.LanguageId, x.LocaleValue)] = x.EntityId); + CachedLocalizedAlias("ProductVariantAttributeValue", x => + { + if (optionIdMappings.TryGetValue(x.EntityId, out variantId)) + result[CreateOptionKey("vari.option", x.LanguageId, variantId, x.LocaleValue)] = x.EntityId; + }); + + return result; + }); + } + + protected virtual IDictionary GetVariantAliasByIdMappings() + { + return _cacheManager.Get(ALL_VARIANT_ALIAS_BY_ID_KEY, () => + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + IPagedList variants = null; + IPagedList options = null; + var pageIndex = 0; + + var variantQuery = _productAttributeRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(x => x.Id); + + var optionQuery = _productVariantAttributeValueRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(x => x.Id); + + do + { + variants = new PagedList(variantQuery, pageIndex++, 500); + + foreach (var variant in variants) + { + result[CreateKey("vari", 0, variant.Id)] = variant.Alias; + } + } + while (variants.HasNextPage); + pageIndex = 0; + variants.Clear(); + + do + { + options = new PagedList(optionQuery, pageIndex++, 500); + + foreach (var option in options) + { + result[CreateOptionKey("attr.option", 0, option.Id)] = option.Alias; + } + } + while (options.HasNextPage); + options.Clear(); + + CachedLocalizedAlias("ProductAttribute", x => result[CreateKey("vari", x.LanguageId, x.EntityId)] = x.LocaleValue); + CachedLocalizedAlias("ProductVariantAttributeValue", x => result[CreateOptionKey("vari.option", x.LanguageId, x.EntityId)] = x.LocaleValue); + + return result; + }); + } + + public void ClearVariantCache() + { + _cacheManager.Remove(ALL_VARIANT_ID_BY_ALIAS_KEY); + _cacheManager.Remove(ALL_VARIANT_ALIAS_BY_ID_KEY); + } + + public int GetVariantIdByAlias(string variantAlias, int languageId = 0) + { + var result = 0; + + if (variantAlias.HasValue()) + { + var mappings = GetVariantIdByAliasMappings(); + + if (!mappings.TryGetValue(CreateKey("vari", languageId, variantAlias), out result) && languageId != 0) + { + mappings.TryGetValue(CreateKey("vari", 0, variantAlias), out result); + } + } + + return result; + } + + public int GetVariantOptionIdByAlias(string optionAlias, int variantId, int languageId = 0) + { + var result = 0; + + if (optionAlias.HasValue() && variantId != 0) + { + var mappings = GetVariantIdByAliasMappings(); + + if (!mappings.TryGetValue(CreateOptionKey("vari.option", languageId, variantId, optionAlias), out result) && languageId != 0) + { + mappings.TryGetValue(CreateOptionKey("vari.option", 0, variantId, optionAlias), out result); + } + } + + return result; + } + + public string GetVariantAliasById(int variantId, int languageId = 0) + { + string result = null; + + if (variantId != 0) + { + var mappings = GetVariantAliasByIdMappings(); + + if (!mappings.TryGetValue(CreateKey("vari", languageId, variantId), out result) && languageId != 0) + { + mappings.TryGetValue(CreateKey("vari", 0, variantId), out result); + } + } + + return result; + } + + public string GetVariantOptionAliasById(int optionId, int languageId = 0) + { + string result = null; + + if (optionId != 0) + { + var mappings = GetVariantAliasByIdMappings(); + + if (!mappings.TryGetValue(CreateOptionKey("vari.option", languageId, optionId), out result) && languageId != 0) + { + mappings.TryGetValue(CreateOptionKey("vari.option", 0, optionId), out result); + } + } + + return result; + } + + #endregion + + #region Common Facets + + protected virtual IDictionary GetCommonFacetAliasByGroupKindMappings() + { + return _cacheManager.Get(ALL_COMMONFACET_ALIAS_BY_KIND_KEY, () => + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var groupKinds = new FacetGroupKind[] + { + FacetGroupKind.Category, + FacetGroupKind.Brand, + FacetGroupKind.Price, + FacetGroupKind.Rating, + FacetGroupKind.DeliveryTime, + FacetGroupKind.Availability, + FacetGroupKind.NewArrivals + }; + + foreach (var language in _languageService.GetAllLanguages()) + { + foreach (var groupKind in groupKinds) + { + var key = FacetUtility.GetFacetAliasSettingKey(groupKind, language.Id); + var value = _settingService.GetSettingByKey(key); + if (value.HasValue()) + { + result.Add(key, value); + } + } + } + + return result; + }); + } + + public void ClearCommonFacetCache() + { + _cacheManager.Remove(ALL_COMMONFACET_ALIAS_BY_KIND_KEY); + } + + public string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId) + { + var mappings = GetCommonFacetAliasByGroupKindMappings(); + + return mappings.Get(FacetUtility.GetFacetAliasSettingKey(kind, languageId)); + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs index a9cde30a30..f03d97bd69 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs @@ -37,53 +37,53 @@ public class CatalogSearchQueryFactory : SearchQueryFactoryBase, ICatalogSearchQ { protected static readonly string[] _instantSearchFields = new string[] { "manufacturer", "sku", "gtin", "mpn", "attrname", "variantname" }; - protected readonly CatalogSettings _catalogSettings; - protected readonly SearchSettings _searchSettings; - protected readonly ICommonServices _services; - protected readonly ICatalogSearchQueryAliasMapper _catalogSearchQueryAliasMapper; - - public CatalogSearchQueryFactory( - HttpContextBase httpContext, - CatalogSettings catalogSettings, - SearchSettings searchSettings, - ICommonServices services, - ICatalogSearchQueryAliasMapper catalogSearchQueryAliasMapper) + protected readonly CatalogSettings _catalogSettings; + protected readonly SearchSettings _searchSettings; + protected readonly ICommonServices _services; + protected readonly ICatalogSearchQueryAliasMapper _catalogSearchQueryAliasMapper; + + public CatalogSearchQueryFactory( + HttpContextBase httpContext, + CatalogSettings catalogSettings, + SearchSettings searchSettings, + ICommonServices services, + ICatalogSearchQueryAliasMapper catalogSearchQueryAliasMapper) : base(httpContext) - { - _catalogSettings = catalogSettings; - _searchSettings = searchSettings; - _services = services; - _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; + { + _catalogSettings = catalogSettings; + _searchSettings = searchSettings; + _services = services; + _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; - QuerySettings = DbQuerySettings.Default; - } + QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } - public CatalogSearchQuery Current { get; private set; } + public CatalogSearchQuery Current { get; private set; } protected override string[] Tokens => new string[] { "q", "i", "s", "o", "p", "c", "m", "r", "a", "n", "d", "v" }; public CatalogSearchQuery CreateFromQuery() - { - var ctx = _httpContext; - - if (ctx.Request == null) - return null; - - var routeData = ctx.Request.RequestContext.RouteData; - var area = routeData.GetAreaName(); - var controller = routeData.GetRequiredString("controller"); - var action = routeData.GetRequiredString("action"); - var origin = "{0}{1}/{2}".FormatInvariant(area == null ? "" : area + "/", controller, action); - var fields = new List { "name" }; - var term = GetValueFor("q"); - var isInstantSearch = origin.IsCaseInsensitiveEqual("Search/InstantSearch"); - - if (isInstantSearch) - { - fields.Add("shortdescription"); - fields.Add("tagname"); + { + var ctx = _httpContext; + + if (ctx.Request == null) + return null; + + var routeData = ctx.Request.RequestContext.RouteData; + var area = routeData.GetAreaName(); + var controller = routeData.GetRequiredString("controller"); + var action = routeData.GetRequiredString("action"); + var origin = "{0}{1}/{2}".FormatInvariant(area == null ? "" : area + "/", controller, action); + var fields = new List { "name" }; + var term = GetValueFor("q"); + var isInstantSearch = origin.IsCaseInsensitiveEqual("Search/InstantSearch"); + + if (isInstantSearch) + { + fields.Add("shortdescription"); + fields.Add("tagname"); foreach (var fieldName in _instantSearchFields) { @@ -93,19 +93,19 @@ public CatalogSearchQuery CreateFromQuery() } } } - else - { - fields.AddRange(_searchSettings.SearchFields); - } + else + { + fields.AddRange(_searchSettings.SearchFields); + } - var query = new CatalogSearchQuery(fields.ToArray(), term, _searchSettings.SearchMode) - .OriginatesFrom(origin) - .WithLanguage(_services.WorkContext.WorkingLanguage) - .WithCurrency(_services.WorkContext.WorkingCurrency) - .BuildFacetMap(!isInstantSearch); + var query = new CatalogSearchQuery(fields.ToArray(), term, _searchSettings.SearchMode) + .OriginatesFrom(origin) + .WithLanguage(_services.WorkContext.WorkingLanguage) + .WithCurrency(_services.WorkContext.WorkingCurrency) + .BuildFacetMap(!isInstantSearch); - // Visibility. - query.VisibleOnly(!QuerySettings.IgnoreAcl ? _services.WorkContext.CurrentCustomer : null); + // Visibility. + query.VisibleOnly(!QuerySettings.IgnoreAcl ? _services.WorkContext.CurrentCustomer : null); if (isInstantSearch || origin.IsCaseInsensitiveEqual("Search/Search")) { @@ -116,495 +116,495 @@ public CatalogSearchQuery CreateFromQuery() query.WithVisibility(ProductVisibility.Full); } - // Store. - if (!QuerySettings.IgnoreMultiStore) - { - query.HasStoreId(_services.StoreContext.CurrentStore.Id); - } - - // Availability. - ConvertAvailability(query, routeData, origin); - - // Instant-Search never uses these filter parameters. - if (!isInstantSearch) - { - ConvertPagingSorting(query, routeData, origin); - ConvertPrice(query, routeData, origin); - ConvertCategory(query, routeData, origin); - ConvertManufacturer(query, routeData, origin); - ConvertRating(query, routeData, origin); - ConvertNewArrivals(query, routeData, origin); - ConvertDeliveryTime(query, routeData, origin); - } - - OnConverted(query, routeData, origin); - - Current = query; - return query; - } - - protected virtual void ConvertPagingSorting(CatalogSearchQuery query, RouteData routeData, string origin) - { - var index = Math.Max(1, GetValueFor("i") ?? 1); - var size = GetPageSize(query, routeData, origin); - - query.Slice((index - 1) * size, size); - - var orderBy = GetValueFor("o"); - if (orderBy == null || orderBy == ProductSortingEnum.Initial) - { + // Store. + if (!QuerySettings.IgnoreMultiStore) + { + query.HasStoreId(_services.StoreContext.CurrentStore.Id); + } + + // Availability. + ConvertAvailability(query, routeData, origin); + + // Instant-Search never uses these filter parameters. + if (!isInstantSearch) + { + ConvertPagingSorting(query, routeData, origin); + ConvertPrice(query, routeData, origin); + ConvertCategory(query, routeData, origin); + ConvertManufacturer(query, routeData, origin); + ConvertRating(query, routeData, origin); + ConvertNewArrivals(query, routeData, origin); + ConvertDeliveryTime(query, routeData, origin); + } + + OnConverted(query, routeData, origin); + + Current = query; + return query; + } + + protected virtual void ConvertPagingSorting(CatalogSearchQuery query, RouteData routeData, string origin) + { + var index = Math.Max(1, GetValueFor("i") ?? 1); + var size = GetPageSize(query, routeData, origin); + + query.Slice((index - 1) * size, size); + + var orderBy = GetValueFor("o"); + if (orderBy == null || orderBy == ProductSortingEnum.Initial) + { orderBy = origin.IsCaseInsensitiveEqual("Search/Search") ? _searchSettings.DefaultSortOrder : _catalogSettings.DefaultSortOrder; - } - - query.CustomData["CurrentSortOrder"] = orderBy.Value; - - query.SortBy(orderBy.Value); - } - - private int GetPageSize(CatalogSearchQuery query, RouteData routeData, string origin) - { - string entityViewMode = null; - - // Determine entity id if possible - IPagingOptions entity = null; - int? entityId; - if (origin.IsCaseInsensitiveEqual("Catalog/Category")) - { - entityId = (int?)routeData.Values["categoryId"]; - if (entityId.HasValue) - { - entity = _services.Resolve().GetCategoryById(entityId.Value) as IPagingOptions; - entityViewMode = ((Category)entity)?.DefaultViewMode; - } - } - else if (origin.IsCaseInsensitiveEqual("Catalog/Manufacturer")) - { - entityId = (int?)routeData.Values["manufacturerId"]; - if (entityId.HasValue) - { - entity = _services.Resolve().GetManufacturerById(entityId.Value) as IPagingOptions; - } - } - - var entitySize = entity?.PageSize; - - var sessionKey = origin; - if (entitySize.HasValue) - { - sessionKey += "/" + entitySize.Value; - } - - DetectViewMode(query, sessionKey, entityViewMode); - - var allowChange = entity?.AllowCustomersToSelectPageSize ?? _catalogSettings.AllowCustomersToSelectPageSize; - if (!allowChange) - { - return entitySize ?? _catalogSettings.DefaultProductListPageSize; - } - - sessionKey = "PageSize:" + sessionKey; - - // Get from form or query - var selectedSize = GetValueFor("s"); - - if (selectedSize.HasValue) - { - // Save the selection in session. We'll fetch this session value - // on subsequent requests for this route. - if (_httpContext.Session != null) - { - _httpContext.Session[sessionKey] = selectedSize.Value; - } - return selectedSize.Value; - } - - // Return user size from session - if (_httpContext.Session != null) - { - var sessionSize = _httpContext.Session[sessionKey].Convert(); - if (sessionSize.HasValue) - { - return sessionSize.Value; - } - } - - // Return default size for entity (IPagingOptions) - if (entitySize.HasValue) - { - return entitySize.Value; - } - - // Return default page size - return _catalogSettings.DefaultProductListPageSize; - } - - private void DetectViewMode(CatalogSearchQuery query, string sessionKey, string entityViewMode = null) - { - if (!_catalogSettings.AllowProductViewModeChanging) - { - query.CustomData["ViewMode"] = entityViewMode.NullEmpty() ?? _catalogSettings.DefaultViewMode; - return; - } - - var selectedViewMode = GetValueFor("v"); - - sessionKey = "ViewMode:" + sessionKey; - - if (selectedViewMode != null) - { - // Save the view mode selection in session. We'll fetch this session value - // on subsequent requests for this route. - if (_httpContext.Session != null) - { - _httpContext.Session[sessionKey] = selectedViewMode; - } - query.CustomData["ViewMode"] = selectedViewMode; - return; - } - - // Set view mode from session - if (_httpContext.Session != null) - { - var sessionViewMode = _httpContext.Session[sessionKey].Convert(); - if (sessionViewMode != null) - { - query.CustomData["ViewMode"] = sessionViewMode; - return; - } - } - - // Set default view mode for entity - if (entityViewMode != null) - { - query.CustomData["ViewMode"] = entityViewMode; - return; - } - - // Set default view mode - query.CustomData["ViewMode"] = _catalogSettings.DefaultViewMode; - } - - private void AddFacet( - CatalogSearchQuery query, - FacetGroupKind kind, - bool isMultiSelect, - FacetSorting sorting, - Action addValues) - { - string fieldName; - var displayOrder = 0; - - switch (kind) - { - case FacetGroupKind.Category: - fieldName = _catalogSettings.IncludeFeaturedProductsInNormalLists ? "categoryid" : "notfeaturedcategoryid"; - break; - case FacetGroupKind.Brand: - if (_searchSettings.BrandDisabled) - return; - fieldName = "manufacturerid"; - displayOrder = _searchSettings.BrandDisplayOrder; - break; - case FacetGroupKind.Price: - if (_searchSettings.PriceDisabled || !_services.Permissions.Authorize(Permissions.Catalog.DisplayPrice)) - return; - fieldName = "price"; - displayOrder = _searchSettings.PriceDisplayOrder; - break; - case FacetGroupKind.Rating: - if (_searchSettings.RatingDisabled) - return; - fieldName = "rating"; - displayOrder = _searchSettings.RatingDisplayOrder; - break; - case FacetGroupKind.DeliveryTime: - if (_searchSettings.DeliveryTimeDisabled) - return; - fieldName = "deliveryid"; - displayOrder = _searchSettings.DeliveryTimeDisplayOrder; - break; - case FacetGroupKind.Availability: - if (_searchSettings.AvailabilityDisabled) - return; - fieldName = "available"; - displayOrder = _searchSettings.AvailabilityDisplayOrder; - break; - case FacetGroupKind.NewArrivals: - if (_searchSettings.NewArrivalsDisabled) - return; - fieldName = "createdon"; - displayOrder = _searchSettings.NewArrivalsDisplayOrder; - break; - default: - throw new SmartException($"Unknown field name for facet group '{kind.ToString()}'"); - } - - var descriptor = new FacetDescriptor(fieldName) - { - Label = _services.Localization.GetResource(FacetUtility.GetLabelResourceKey(kind) ?? kind.ToString()), - IsMultiSelect = isMultiSelect, - DisplayOrder = displayOrder, - OrderBy = sorting, - MinHitCount = _searchSettings.FilterMinHitCount, - MaxChoicesCount = _searchSettings.FilterMaxChoicesCount - }; - - addValues(descriptor); - query.WithFacet(descriptor); - } - - protected virtual void ConvertCategory(CatalogSearchQuery query, RouteData routeData, string origin) - { - if (origin == "Catalog/Category") - { - // we don't need category facetting in category pages - return; - } - - List ids; - - if (GetValueFor(query, "c", FacetGroupKind.Category, out ids) && ids != null && ids.Any()) - { - // TODO; (mc) Get deep ids (???) Make a low-level version of CatalogHelper.GetChildCategoryIds() - query.WithCategoryIds(_catalogSettings.IncludeFeaturedProductsInNormalLists ? null : (bool?)false, ids.ToArray()); - } - - AddFacet(query, FacetGroupKind.Category, true, FacetSorting.HitsDesc, descriptor => - { - if (ids != null) - { - foreach (var id in ids) - { - descriptor.AddValue(new FacetValue(id, IndexTypeCode.Int32) - { - IsSelected = true - }); - } - } - }); - } - - protected virtual void ConvertManufacturer(CatalogSearchQuery query, RouteData routeData, string origin) - { - //List ids = null; - //int? minHitCount = null; - - //GetValueFor(query, "m", FacetGroupKind.Brand, out ids); - - //// Preselect manufacturer on manufacturer page.... and then? - //if (origin.IsCaseInsensitiveEqual("Catalog/Manufacturer")) - //{ - // minHitCount = 0; - - // var manufacturerId = routeData.Values["manufacturerid"].ToString().ToInt(); - // if (manufacturerId != 0) - // { - // if (ids == null) - // ids = new List { manufacturerId }; - // else if (!ids.Contains(manufacturerId)) - // ids.Add(manufacturerId); - // } - //} - - if (origin.IsCaseInsensitiveEqual("Catalog/Manufacturer")) - { - // we don't need brand facetting in brand pages - return; - } - - List ids; - - if (GetValueFor(query, "m", FacetGroupKind.Brand, out ids) && ids != null && ids.Any()) - { - query.WithManufacturerIds(null, ids.ToArray()); - } - - AddFacet(query, FacetGroupKind.Brand, true, FacetSorting.LabelAsc, descriptor => - { - if (ids != null) - { - foreach (var id in ids) - { - descriptor.AddValue(new FacetValue(id, IndexTypeCode.Int32) - { - IsSelected = true - }); - } - } - }); - } - - protected virtual void ConvertPrice(CatalogSearchQuery query, RouteData routeData, string origin) - { - string price; - double? minPrice = null; - double? maxPrice = null; - - if (GetValueFor(query, "p", FacetGroupKind.Price, out price) && TryParseRange(price, out minPrice, out maxPrice)) - { - var currency = _services.WorkContext.WorkingCurrency; - - if (minPrice.HasValue) - { - // TODO: (mc) Why the heck did I convert this??!! - //minPrice = _currencyService.ConvertToPrimaryStoreCurrency(minPrice.Value, currency); - } - - if (maxPrice.HasValue) - { - //maxPrice = _currencyService.ConvertToPrimaryStoreCurrency(maxPrice.Value, currency); - } - - if (minPrice.HasValue && maxPrice.HasValue && minPrice > maxPrice) - { - var tmp = minPrice; - minPrice = maxPrice; - maxPrice = tmp; - } - - if (minPrice.HasValue || maxPrice.HasValue) - { - query.PriceBetween((decimal?)minPrice, (decimal?)maxPrice); - } - } - - AddFacet(query, FacetGroupKind.Price, false, FacetSorting.DisplayOrder, descriptor => - { - if (minPrice.HasValue || maxPrice.HasValue) - { - descriptor.AddValue(new FacetValue( - minPrice, - maxPrice, - IndexTypeCode.Double, - minPrice.HasValue, - maxPrice.HasValue) - { - IsSelected = true - }); - } - }); - } - - protected virtual void ConvertRating(CatalogSearchQuery query, RouteData routeData, string origin) - { - double? fromRate; - - if (GetValueFor(query, "r", FacetGroupKind.Rating, out fromRate) && fromRate.HasValue) - { - query.WithRating(fromRate, null); - } - - AddFacet(query, FacetGroupKind.Rating, false, FacetSorting.DisplayOrder, descriptor => - { - descriptor.MinHitCount = 0; - descriptor.MaxChoicesCount = 5; - - if (fromRate.HasValue) - { - descriptor.AddValue(new FacetValue(fromRate.Value, IndexTypeCode.Double) - { - IsSelected = true - }); - } - }); - } - - protected virtual void ConvertAvailability(CatalogSearchQuery query, RouteData routeData, string origin) - { - bool availability; - GetValueFor(query, "a", FacetGroupKind.Availability, out availability); - - // Setting specifies the logical direction of the filter. That's smarter than just to specify a default value. - if (_searchSettings.IncludeNotAvailable) - { - // False = show, True = hide unavailable products. - if (availability) - { - query.AvailableOnly(true); - } - } - else - { - // False = hide, True = show unavailable products. - if (!availability) - { - query.AvailableOnly(true); - } - } - - AddFacet(query, FacetGroupKind.Availability, true, FacetSorting.LabelAsc, descriptor => - { - descriptor.MinHitCount = 0; - - var newValue = availability - ? new FacetValue(true, IndexTypeCode.Boolean) - : new FacetValue(null, IndexTypeCode.Empty); - - newValue.IsSelected = availability; - newValue.Label = _services.Localization.GetResource(_searchSettings.IncludeNotAvailable ? "Search.Facet.ExcludeOutOfStock" : "Search.Facet.IncludeOutOfStock"); - - descriptor.AddValue(newValue); - }); - } - - protected virtual void ConvertNewArrivals(CatalogSearchQuery query, RouteData routeData, string origin) - { - var newForMaxDays = _catalogSettings.LabelAsNewForMaxDays ?? 0; - if (newForMaxDays <= 0) - { - // we cannot filter without it - return; - } - - bool newArrivalsOnly; - var fromUtc = DateTime.UtcNow.Subtract(TimeSpan.FromDays(newForMaxDays)); - - if (GetValueFor(query, "n", FacetGroupKind.NewArrivals, out newArrivalsOnly) && newArrivalsOnly) - { - query.CreatedBetween(fromUtc, null); - } - - AddFacet(query, FacetGroupKind.NewArrivals, true, FacetSorting.LabelAsc, descriptor => - { - descriptor.AddValue(new FacetValue(fromUtc, null, IndexTypeCode.DateTime, true, false) - { - IsSelected = newArrivalsOnly, - Label = _services.Localization.GetResource("Search.Facet.LastDays").FormatInvariant(newForMaxDays) - }); - }); - } - - protected virtual void ConvertDeliveryTime(CatalogSearchQuery query, RouteData routeData, string origin) - { - List ids; - - if (GetValueFor(query, "d", FacetGroupKind.DeliveryTime, out ids) && ids != null && ids.Any()) - { - query.WithDeliveryTimeIds(ids.ToArray()); - } - - AddFacet(query, FacetGroupKind.DeliveryTime, true, FacetSorting.DisplayOrder, descriptor => - { - if (ids != null) - { - foreach (var id in ids) - { - descriptor.AddValue(new FacetValue(id, IndexTypeCode.Int32) - { - IsSelected = true - }); - } - } - }); - } - - protected virtual void OnConverted(CatalogSearchQuery query, RouteData routeData, string origin) - { - } - - protected virtual bool GetValueFor(CatalogSearchQuery query, string key, FacetGroupKind kind, out T value) - { - return TryGetValueFor(_catalogSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); - } - } + } + + query.CustomData["CurrentSortOrder"] = orderBy.Value; + + query.SortBy(orderBy.Value); + } + + private int GetPageSize(CatalogSearchQuery query, RouteData routeData, string origin) + { + string entityViewMode = null; + + // Determine entity id if possible + IPagingOptions entity = null; + int? entityId; + if (origin.IsCaseInsensitiveEqual("Catalog/Category")) + { + entityId = (int?)routeData.Values["categoryId"]; + if (entityId.HasValue) + { + entity = _services.Resolve().GetCategoryById(entityId.Value) as IPagingOptions; + entityViewMode = ((Category)entity)?.DefaultViewMode; + } + } + else if (origin.IsCaseInsensitiveEqual("Catalog/Manufacturer")) + { + entityId = (int?)routeData.Values["manufacturerId"]; + if (entityId.HasValue) + { + entity = _services.Resolve().GetManufacturerById(entityId.Value) as IPagingOptions; + } + } + + var entitySize = entity?.PageSize; + + var sessionKey = origin; + if (entitySize.HasValue) + { + sessionKey += "/" + entitySize.Value; + } + + DetectViewMode(query, sessionKey, entityViewMode); + + var allowChange = entity?.AllowCustomersToSelectPageSize ?? _catalogSettings.AllowCustomersToSelectPageSize; + if (!allowChange) + { + return entitySize ?? _catalogSettings.DefaultProductListPageSize; + } + + sessionKey = "PageSize:" + sessionKey; + + // Get from form or query + var selectedSize = GetValueFor("s"); + + if (selectedSize.HasValue) + { + // Save the selection in session. We'll fetch this session value + // on subsequent requests for this route. + if (_httpContext.Session != null) + { + _httpContext.Session[sessionKey] = selectedSize.Value; + } + return selectedSize.Value; + } + + // Return user size from session + if (_httpContext.Session != null) + { + var sessionSize = _httpContext.Session[sessionKey].Convert(); + if (sessionSize.HasValue) + { + return sessionSize.Value; + } + } + + // Return default size for entity (IPagingOptions) + if (entitySize.HasValue) + { + return entitySize.Value; + } + + // Return default page size + return _catalogSettings.DefaultProductListPageSize; + } + + private void DetectViewMode(CatalogSearchQuery query, string sessionKey, string entityViewMode = null) + { + if (!_catalogSettings.AllowProductViewModeChanging) + { + query.CustomData["ViewMode"] = entityViewMode.NullEmpty() ?? _catalogSettings.DefaultViewMode; + return; + } + + var selectedViewMode = GetValueFor("v"); + + sessionKey = "ViewMode:" + sessionKey; + + if (selectedViewMode != null) + { + // Save the view mode selection in session. We'll fetch this session value + // on subsequent requests for this route. + if (_httpContext.Session != null) + { + _httpContext.Session[sessionKey] = selectedViewMode; + } + query.CustomData["ViewMode"] = selectedViewMode; + return; + } + + // Set view mode from session + if (_httpContext.Session != null) + { + var sessionViewMode = _httpContext.Session[sessionKey].Convert(); + if (sessionViewMode != null) + { + query.CustomData["ViewMode"] = sessionViewMode; + return; + } + } + + // Set default view mode for entity + if (entityViewMode != null) + { + query.CustomData["ViewMode"] = entityViewMode; + return; + } + + // Set default view mode + query.CustomData["ViewMode"] = _catalogSettings.DefaultViewMode; + } + + private void AddFacet( + CatalogSearchQuery query, + FacetGroupKind kind, + bool isMultiSelect, + FacetSorting sorting, + Action addValues) + { + string fieldName; + var displayOrder = 0; + + switch (kind) + { + case FacetGroupKind.Category: + fieldName = _catalogSettings.IncludeFeaturedProductsInNormalLists ? "categoryid" : "notfeaturedcategoryid"; + break; + case FacetGroupKind.Brand: + if (_searchSettings.BrandDisabled) + return; + fieldName = "manufacturerid"; + displayOrder = _searchSettings.BrandDisplayOrder; + break; + case FacetGroupKind.Price: + if (_searchSettings.PriceDisabled || !_services.Permissions.Authorize(Permissions.Catalog.DisplayPrice)) + return; + fieldName = "price"; + displayOrder = _searchSettings.PriceDisplayOrder; + break; + case FacetGroupKind.Rating: + if (_searchSettings.RatingDisabled) + return; + fieldName = "rating"; + displayOrder = _searchSettings.RatingDisplayOrder; + break; + case FacetGroupKind.DeliveryTime: + if (_searchSettings.DeliveryTimeDisabled) + return; + fieldName = "deliveryid"; + displayOrder = _searchSettings.DeliveryTimeDisplayOrder; + break; + case FacetGroupKind.Availability: + if (_searchSettings.AvailabilityDisabled) + return; + fieldName = "available"; + displayOrder = _searchSettings.AvailabilityDisplayOrder; + break; + case FacetGroupKind.NewArrivals: + if (_searchSettings.NewArrivalsDisabled) + return; + fieldName = "createdon"; + displayOrder = _searchSettings.NewArrivalsDisplayOrder; + break; + default: + throw new SmartException($"Unknown field name for facet group '{kind.ToString()}'"); + } + + var descriptor = new FacetDescriptor(fieldName) + { + Label = _services.Localization.GetResource(FacetUtility.GetLabelResourceKey(kind) ?? kind.ToString()), + IsMultiSelect = isMultiSelect, + DisplayOrder = displayOrder, + OrderBy = sorting, + MinHitCount = _searchSettings.FilterMinHitCount, + MaxChoicesCount = _searchSettings.FilterMaxChoicesCount + }; + + addValues(descriptor); + query.WithFacet(descriptor); + } + + protected virtual void ConvertCategory(CatalogSearchQuery query, RouteData routeData, string origin) + { + if (origin == "Catalog/Category") + { + // we don't need category facetting in category pages + return; + } + + List ids; + + if (GetValueFor(query, "c", FacetGroupKind.Category, out ids) && ids != null && ids.Any()) + { + // TODO; (mc) Get deep ids (???) Make a low-level version of CatalogHelper.GetChildCategoryIds() + query.WithCategoryIds(_catalogSettings.IncludeFeaturedProductsInNormalLists ? null : (bool?)false, ids.ToArray()); + } + + AddFacet(query, FacetGroupKind.Category, true, FacetSorting.HitsDesc, descriptor => + { + if (ids != null) + { + foreach (var id in ids) + { + descriptor.AddValue(new FacetValue(id, IndexTypeCode.Int32) + { + IsSelected = true + }); + } + } + }); + } + + protected virtual void ConvertManufacturer(CatalogSearchQuery query, RouteData routeData, string origin) + { + //List ids = null; + //int? minHitCount = null; + + //GetValueFor(query, "m", FacetGroupKind.Brand, out ids); + + //// Preselect manufacturer on manufacturer page.... and then? + //if (origin.IsCaseInsensitiveEqual("Catalog/Manufacturer")) + //{ + // minHitCount = 0; + + // var manufacturerId = routeData.Values["manufacturerid"].ToString().ToInt(); + // if (manufacturerId != 0) + // { + // if (ids == null) + // ids = new List { manufacturerId }; + // else if (!ids.Contains(manufacturerId)) + // ids.Add(manufacturerId); + // } + //} + + if (origin.IsCaseInsensitiveEqual("Catalog/Manufacturer")) + { + // we don't need brand facetting in brand pages + return; + } + + List ids; + + if (GetValueFor(query, "m", FacetGroupKind.Brand, out ids) && ids != null && ids.Any()) + { + query.WithManufacturerIds(null, ids.ToArray()); + } + + AddFacet(query, FacetGroupKind.Brand, true, FacetSorting.LabelAsc, descriptor => + { + if (ids != null) + { + foreach (var id in ids) + { + descriptor.AddValue(new FacetValue(id, IndexTypeCode.Int32) + { + IsSelected = true + }); + } + } + }); + } + + protected virtual void ConvertPrice(CatalogSearchQuery query, RouteData routeData, string origin) + { + string price; + double? minPrice = null; + double? maxPrice = null; + + if (GetValueFor(query, "p", FacetGroupKind.Price, out price) && TryParseRange(price, out minPrice, out maxPrice)) + { + var currency = _services.WorkContext.WorkingCurrency; + + if (minPrice.HasValue) + { + // TODO: (mc) Why the heck did I convert this??!! + //minPrice = _currencyService.ConvertToPrimaryStoreCurrency(minPrice.Value, currency); + } + + if (maxPrice.HasValue) + { + //maxPrice = _currencyService.ConvertToPrimaryStoreCurrency(maxPrice.Value, currency); + } + + if (minPrice.HasValue && maxPrice.HasValue && minPrice > maxPrice) + { + var tmp = minPrice; + minPrice = maxPrice; + maxPrice = tmp; + } + + if (minPrice.HasValue || maxPrice.HasValue) + { + query.PriceBetween((decimal?)minPrice, (decimal?)maxPrice); + } + } + + AddFacet(query, FacetGroupKind.Price, false, FacetSorting.DisplayOrder, descriptor => + { + if (minPrice.HasValue || maxPrice.HasValue) + { + descriptor.AddValue(new FacetValue( + minPrice, + maxPrice, + IndexTypeCode.Double, + minPrice.HasValue, + maxPrice.HasValue) + { + IsSelected = true + }); + } + }); + } + + protected virtual void ConvertRating(CatalogSearchQuery query, RouteData routeData, string origin) + { + double? fromRate; + + if (GetValueFor(query, "r", FacetGroupKind.Rating, out fromRate) && fromRate.HasValue) + { + query.WithRating(fromRate, null); + } + + AddFacet(query, FacetGroupKind.Rating, false, FacetSorting.DisplayOrder, descriptor => + { + descriptor.MinHitCount = 0; + descriptor.MaxChoicesCount = 5; + + if (fromRate.HasValue) + { + descriptor.AddValue(new FacetValue(fromRate.Value, IndexTypeCode.Double) + { + IsSelected = true + }); + } + }); + } + + protected virtual void ConvertAvailability(CatalogSearchQuery query, RouteData routeData, string origin) + { + bool availability; + GetValueFor(query, "a", FacetGroupKind.Availability, out availability); + + // Setting specifies the logical direction of the filter. That's smarter than just to specify a default value. + if (_searchSettings.IncludeNotAvailable) + { + // False = show, True = hide unavailable products. + if (availability) + { + query.AvailableOnly(true); + } + } + else + { + // False = hide, True = show unavailable products. + if (!availability) + { + query.AvailableOnly(true); + } + } + + AddFacet(query, FacetGroupKind.Availability, true, FacetSorting.LabelAsc, descriptor => + { + descriptor.MinHitCount = 0; + + var newValue = availability + ? new FacetValue(true, IndexTypeCode.Boolean) + : new FacetValue(null, IndexTypeCode.Empty); + + newValue.IsSelected = availability; + newValue.Label = _services.Localization.GetResource(_searchSettings.IncludeNotAvailable ? "Search.Facet.ExcludeOutOfStock" : "Search.Facet.IncludeOutOfStock"); + + descriptor.AddValue(newValue); + }); + } + + protected virtual void ConvertNewArrivals(CatalogSearchQuery query, RouteData routeData, string origin) + { + var newForMaxDays = _catalogSettings.LabelAsNewForMaxDays ?? 0; + if (newForMaxDays <= 0) + { + // we cannot filter without it + return; + } + + bool newArrivalsOnly; + var fromUtc = DateTime.UtcNow.Subtract(TimeSpan.FromDays(newForMaxDays)); + + if (GetValueFor(query, "n", FacetGroupKind.NewArrivals, out newArrivalsOnly) && newArrivalsOnly) + { + query.CreatedBetween(fromUtc, null); + } + + AddFacet(query, FacetGroupKind.NewArrivals, true, FacetSorting.LabelAsc, descriptor => + { + descriptor.AddValue(new FacetValue(fromUtc, null, IndexTypeCode.DateTime, true, false) + { + IsSelected = newArrivalsOnly, + Label = _services.Localization.GetResource("Search.Facet.LastDays").FormatInvariant(newForMaxDays) + }); + }); + } + + protected virtual void ConvertDeliveryTime(CatalogSearchQuery query, RouteData routeData, string origin) + { + List ids; + + if (GetValueFor(query, "d", FacetGroupKind.DeliveryTime, out ids) && ids != null && ids.Any()) + { + query.WithDeliveryTimeIds(ids.ToArray()); + } + + AddFacet(query, FacetGroupKind.DeliveryTime, true, FacetSorting.DisplayOrder, descriptor => + { + if (ids != null) + { + foreach (var id in ids) + { + descriptor.AddValue(new FacetValue(id, IndexTypeCode.Int32) + { + IsSelected = true + }); + } + } + }); + } + + protected virtual void OnConverted(CatalogSearchQuery query, RouteData routeData, string origin) + { + } + + protected virtual bool GetValueFor(CatalogSearchQuery query, string key, FacetGroupKind kind, out T value) + { + return TryGetValueFor(_catalogSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryModelBinder.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryModelBinder.cs index dce6a49f57..817f082d51 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryModelBinder.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryModelBinder.cs @@ -4,39 +4,39 @@ namespace SmartStore.Services.Search.Modelling { [ModelBinderType(typeof(CatalogSearchQuery))] - public class CatalogSearchQueryModelBinder : IModelBinder - { - private readonly ICatalogSearchQueryFactory _factory; + public class CatalogSearchQueryModelBinder : IModelBinder + { + private readonly ICatalogSearchQueryFactory _factory; - public CatalogSearchQueryModelBinder(ICatalogSearchQueryFactory factory) - { - _factory = factory; - } + public CatalogSearchQueryModelBinder(ICatalogSearchQueryFactory factory) + { + _factory = factory; + } - public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) - { - if (_factory.Current != null) - { - // Don't bind again for current request. - return _factory.Current; - } + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (_factory.Current != null) + { + // Don't bind again for current request. + return _factory.Current; + } - if (controllerContext.IsChildAction) - { - // Never attempt to bind in child actions. We require the binding to happen - // in a parent action. If the child action is part of a request with an already bound - // 'CatalogSearchQuery', good for you :-) You'll get an instance, but null otherwise. - return _factory.Current; - } + if (controllerContext.IsChildAction) + { + // Never attempt to bind in child actions. We require the binding to happen + // in a parent action. If the child action is part of a request with an already bound + // 'CatalogSearchQuery', good for you :-) You'll get an instance, but null otherwise. + return _factory.Current; + } - var modelType = bindingContext.ModelType; - if (modelType != typeof(CatalogSearchQuery)) - { - return new CatalogSearchQuery(); - } + var modelType = bindingContext.ModelType; + if (modelType != typeof(CatalogSearchQuery)) + { + return new CatalogSearchQuery(); + } - var query = _factory.CreateFromQuery(); - return query; - } - } + var query = _factory.CreateFromQuery(); + return query; + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryAliasMapper.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryAliasMapper.cs index c036029c6d..16d08f7524 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryAliasMapper.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryAliasMapper.cs @@ -2,95 +2,95 @@ namespace SmartStore.Services.Search.Modelling { - public interface ICatalogSearchQueryAliasMapper - { - /// - /// Clears all cached attribute mappings - /// - void ClearAttributeCache(); + public interface ICatalogSearchQueryAliasMapper + { + /// + /// Clears all cached attribute mappings + /// + void ClearAttributeCache(); - /// - /// Gets the attribute id by attribute alias - /// - /// Attribute alias - /// Language identifier - /// Attribute identifier - int GetAttributeIdByAlias(string attributeAlias, int languageId = 0); + /// + /// Gets the attribute id by attribute alias + /// + /// Attribute alias + /// Language identifier + /// Attribute identifier + int GetAttributeIdByAlias(string attributeAlias, int languageId = 0); - /// - /// Gets the attribute option id by option alias - /// - /// Attribute option alias - /// Attribute identifier - /// Language identifier - /// Attribute option identifier - int GetAttributeOptionIdByAlias(string optionAlias, int attributeId, int languageId = 0); + /// + /// Gets the attribute option id by option alias + /// + /// Attribute option alias + /// Attribute identifier + /// Language identifier + /// Attribute option identifier + int GetAttributeOptionIdByAlias(string optionAlias, int attributeId, int languageId = 0); - /// - /// Gets the attribute alias by attribute id - /// - /// Attribute identifier - /// Language identifier - /// Attribute alias - string GetAttributeAliasById(int attributeId, int languageId = 0); + /// + /// Gets the attribute alias by attribute id + /// + /// Attribute identifier + /// Language identifier + /// Attribute alias + string GetAttributeAliasById(int attributeId, int languageId = 0); - /// - /// Get the attribute option alias by option id - /// - /// Option identifier - /// Language identifier - /// Attribute option alias - string GetAttributeOptionAliasById(int optionId, int languageId = 0); + /// + /// Get the attribute option alias by option id + /// + /// Option identifier + /// Language identifier + /// Attribute option alias + string GetAttributeOptionAliasById(int optionId, int languageId = 0); - /// - /// Clears all cached variant mappings - /// - void ClearVariantCache(); + /// + /// Clears all cached variant mappings + /// + void ClearVariantCache(); - /// - /// Gets the variant id by variant alias - /// - /// Variant alias - /// Language identifier - /// Variant identifier - int GetVariantIdByAlias(string variantAlias, int languageId = 0); + /// + /// Gets the variant id by variant alias + /// + /// Variant alias + /// Language identifier + /// Variant identifier + int GetVariantIdByAlias(string variantAlias, int languageId = 0); - /// - /// Gets the variant option id by option alias - /// - /// Variant option alias - /// Variant identifier - /// Language identifier - /// Variant option identifier - int GetVariantOptionIdByAlias(string optionAlias, int variantId, int languageId = 0); + /// + /// Gets the variant option id by option alias + /// + /// Variant option alias + /// Variant identifier + /// Language identifier + /// Variant option identifier + int GetVariantOptionIdByAlias(string optionAlias, int variantId, int languageId = 0); - /// - /// Gets the variant alias by variant id - /// - /// Variant identifier - /// Language identifier - /// Variant alias - string GetVariantAliasById(int variantId, int languageId = 0); + /// + /// Gets the variant alias by variant id + /// + /// Variant identifier + /// Language identifier + /// Variant alias + string GetVariantAliasById(int variantId, int languageId = 0); - /// - /// Get the variant option alias by value id - /// - /// Option identifier - /// Language identifier - /// Variant option alias - string GetVariantOptionAliasById(int optionId, int languageId = 0); + /// + /// Get the variant option alias by value id + /// + /// Option identifier + /// Language identifier + /// Variant option alias + string GetVariantOptionAliasById(int optionId, int languageId = 0); - /// - /// Clears all cached common facet mappings - /// - void ClearCommonFacetCache(); + /// + /// Clears all cached common facet mappings + /// + void ClearCommonFacetCache(); - /// - /// Get the common facet alias by facet group kind - /// - /// Facet group kind - /// Language identifier - /// Common facet alias - string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId); - } + /// + /// Get the common facet alias by facet group kind + /// + /// Facet group kind + /// Language identifier + /// Common facet alias + string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId); + } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryFactory.cs index 7b0afdba68..03026bf532 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/ICatalogSearchQueryFactory.cs @@ -1,17 +1,17 @@ namespace SmartStore.Services.Search.Modelling { - public interface ICatalogSearchQueryFactory - { - /// - /// Creates a instance from the current - /// by looking up corresponding keys in posted form and/or query string - /// - /// The query object - CatalogSearchQuery CreateFromQuery(); + public interface ICatalogSearchQueryFactory + { + /// + /// Creates a instance from the current + /// by looking up corresponding keys in posted form and/or query string + /// + /// The query object + CatalogSearchQuery CreateFromQuery(); - /// - /// The last created query instance. The MVC model binder uses this property to avoid repeated binding. - /// - CatalogSearchQuery Current { get; } - } + /// + /// The last created query instance. The MVC model binder uses this property to avoid repeated binding. + /// + CatalogSearchQuery Current { get; } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Extensions/FacetUrlHelper.cs b/src/Libraries/SmartStore.Services/Search/Extensions/FacetUrlHelper.cs index d39648af11..407dc43f9c 100644 --- a/src/Libraries/SmartStore.Services/Search/Extensions/FacetUrlHelper.cs +++ b/src/Libraries/SmartStore.Services/Search/Extensions/FacetUrlHelper.cs @@ -13,26 +13,26 @@ namespace SmartStore.Services.Search.Extensions { public class FacetUrlHelper - { - private readonly ICatalogSearchQueryAliasMapper _catalogAliasMapper; + { + private readonly ICatalogSearchQueryAliasMapper _catalogAliasMapper; private readonly IForumSearchQueryAliasMapper _forumAliasMapper; private readonly IWorkContext _workContext; - private readonly HttpRequestBase _httpRequest; + private readonly HttpRequestBase _httpRequest; - private readonly int _languageId; - private readonly string _url; - private readonly QueryString _initialQuery; + private readonly int _languageId; + private readonly string _url; + private readonly QueryString _initialQuery; - private readonly static IDictionary _queryNames = new Dictionary - { + private readonly static IDictionary _queryNames = new Dictionary + { // Catalog. { FacetGroupKind.Brand, "m" }, - { FacetGroupKind.Category, "c" }, - { FacetGroupKind.Price, "p" }, - { FacetGroupKind.Rating, "r" }, - { FacetGroupKind.DeliveryTime, "d" }, - { FacetGroupKind.Availability, "a" }, - { FacetGroupKind.NewArrivals, "n" }, + { FacetGroupKind.Category, "c" }, + { FacetGroupKind.Price, "p" }, + { FacetGroupKind.Rating, "r" }, + { FacetGroupKind.DeliveryTime, "d" }, + { FacetGroupKind.Availability, "a" }, + { FacetGroupKind.NewArrivals, "n" }, // Forum. { FacetGroupKind.Forum, "f" }, @@ -40,173 +40,173 @@ public class FacetUrlHelper { FacetGroupKind.Date, "d" } }; - public FacetUrlHelper( - ICatalogSearchQueryAliasMapper catalogAliasMapper, + public FacetUrlHelper( + ICatalogSearchQueryAliasMapper catalogAliasMapper, IForumSearchQueryAliasMapper forumAliasMapper, IWorkContext workContext, - HttpRequestBase httpRequest) - { - _catalogAliasMapper = catalogAliasMapper; + HttpRequestBase httpRequest) + { + _catalogAliasMapper = catalogAliasMapper; _forumAliasMapper = forumAliasMapper; - _workContext = workContext; - _httpRequest = httpRequest; - - _languageId = _workContext.WorkingLanguage.Id; - _url = _httpRequest.CurrentExecutionFilePath; - //_initialQuery = new QueryString().FillFromString(_httpRequest.QueryString.ToString(), false); - _initialQuery = QueryString.CurrentUnvalidated; - - // Remove page index (i) from query string - _initialQuery.Remove("i"); - } - - public string Toggle(Facet facet) - { - if (facet.Value.IsSelected) - { - return Remove(facet); - } - else - { - return Add(facet); - } - } - - public string Add(params Facet[] facets) - { - var qs = new QueryString(_initialQuery); - - foreach (var facet in facets) - { - var parts = GetQueryParts(facet); - foreach (var name in parts.AllKeys) - { - qs.Add(name, parts[name], !facet.FacetGroup.IsMultiSelect); - } - } - - return _url + qs.ToString(false); - } - - public string Remove(params Facet[] facets) - { - var qs = new QueryString(_initialQuery); - - foreach (var facet in facets) - { - var parts = GetQueryParts(facet); - foreach (var name in parts.AllKeys) - { - var qsName = name; - - if (!qs.AllKeys.Contains(name)) - { - // Query string does not contain that name. Try the unmapped name. - switch (facet.FacetGroup.Kind) - { - case FacetGroupKind.Attribute: - qsName = "attr" + facet.Value.ParentId; - break; - case FacetGroupKind.Variant: - qsName = "vari" + facet.Value.ParentId; - break; - case FacetGroupKind.Category: - case FacetGroupKind.Brand: - case FacetGroupKind.Price: - case FacetGroupKind.Rating: - case FacetGroupKind.DeliveryTime: - case FacetGroupKind.Availability: - case FacetGroupKind.NewArrivals: + _workContext = workContext; + _httpRequest = httpRequest; + + _languageId = _workContext.WorkingLanguage.Id; + _url = _httpRequest.CurrentExecutionFilePath; + //_initialQuery = new QueryString().FillFromString(_httpRequest.QueryString.ToString(), false); + _initialQuery = QueryString.CurrentUnvalidated; + + // Remove page index (i) from query string + _initialQuery.Remove("i"); + } + + public string Toggle(Facet facet) + { + if (facet.Value.IsSelected) + { + return Remove(facet); + } + else + { + return Add(facet); + } + } + + public string Add(params Facet[] facets) + { + var qs = new QueryString(_initialQuery); + + foreach (var facet in facets) + { + var parts = GetQueryParts(facet); + foreach (var name in parts.AllKeys) + { + qs.Add(name, parts[name], !facet.FacetGroup.IsMultiSelect); + } + } + + return _url + qs.ToString(false); + } + + public string Remove(params Facet[] facets) + { + var qs = new QueryString(_initialQuery); + + foreach (var facet in facets) + { + var parts = GetQueryParts(facet); + foreach (var name in parts.AllKeys) + { + var qsName = name; + + if (!qs.AllKeys.Contains(name)) + { + // Query string does not contain that name. Try the unmapped name. + switch (facet.FacetGroup.Kind) + { + case FacetGroupKind.Attribute: + qsName = "attr" + facet.Value.ParentId; + break; + case FacetGroupKind.Variant: + qsName = "vari" + facet.Value.ParentId; + break; + case FacetGroupKind.Category: + case FacetGroupKind.Brand: + case FacetGroupKind.Price: + case FacetGroupKind.Rating: + case FacetGroupKind.DeliveryTime: + case FacetGroupKind.Availability: + case FacetGroupKind.NewArrivals: case FacetGroupKind.Forum: case FacetGroupKind.Customer: case FacetGroupKind.Date: - qsName = _queryNames[facet.FacetGroup.Kind]; - break; - } - } - - string[] currentValues = null; - - // The query string value is not necessarily equal to the facet value. - // We must skip subsequent lines here to not add the removed value again and again. - if (facet.FacetGroup.Kind != FacetGroupKind.Price && - facet.FacetGroup.Kind != FacetGroupKind.Availability && - facet.FacetGroup.Kind != FacetGroupKind.NewArrivals) - { - currentValues = qs.Get(qsName)?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); - } - - qs.Remove(qsName); - - if (currentValues != null) - { - var removeValues = parts.GetValues(name); - var newValues = currentValues.Except(removeValues).ToArray(); - if (newValues.Length > 0) - { - newValues.Each(x => qs.Add(name, x, false)); - } - } - } - } - - return _url + qs.ToString(false); - } - - public string GetQueryName(Facet facet) - { - var parts = GetQueryParts(facet); - return parts.GetKey(0); - } - - protected virtual NameValueCollection GetQueryParts(Facet facet) - { - string name = null; - string value = null; - int entityId; - - var group = facet.FacetGroup; - var val = facet.Value; - - var result = new NameValueCollection(2); - - switch (group.Kind) - { - case FacetGroupKind.Attribute: - if (facet.Value.TypeCode == IndexTypeCode.Double) - { - value = "{0}~{1}".FormatInvariant( - val.Value != null ? ((double)val.Value).ToString(CultureInfo.InvariantCulture) : "", - val.UpperValue != null ? ((double)val.UpperValue).ToString(CultureInfo.InvariantCulture) : ""); - } - else - { - entityId = val.Value.Convert(); - value = _catalogAliasMapper.GetAttributeOptionAliasById(entityId, _languageId) ?? "opt" + entityId; - } - name = _catalogAliasMapper.GetAttributeAliasById(val.ParentId, _languageId) ?? "attr" + val.ParentId; - result.Add(name, value); - break; - case FacetGroupKind.Variant: - entityId = val.Value.Convert(); - name = _catalogAliasMapper.GetVariantAliasById(val.ParentId, _languageId) ?? "vari" + val.ParentId; - value = _catalogAliasMapper.GetVariantOptionAliasById(entityId, _languageId) ?? "opt" + entityId; - result.Add(name, value); - break; - case FacetGroupKind.Category: - case FacetGroupKind.Brand: - case FacetGroupKind.Price: - case FacetGroupKind.Rating: - case FacetGroupKind.DeliveryTime: - case FacetGroupKind.Availability: - case FacetGroupKind.NewArrivals: - value = val.ToString(); - if (value.HasValue()) - { - name = _catalogAliasMapper.GetCommonFacetAliasByGroupKind(group.Kind, _languageId) ?? _queryNames[group.Kind]; - result.Add(name, value); - } - break; + qsName = _queryNames[facet.FacetGroup.Kind]; + break; + } + } + + string[] currentValues = null; + + // The query string value is not necessarily equal to the facet value. + // We must skip subsequent lines here to not add the removed value again and again. + if (facet.FacetGroup.Kind != FacetGroupKind.Price && + facet.FacetGroup.Kind != FacetGroupKind.Availability && + facet.FacetGroup.Kind != FacetGroupKind.NewArrivals) + { + currentValues = qs.Get(qsName)?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); + } + + qs.Remove(qsName); + + if (currentValues != null) + { + var removeValues = parts.GetValues(name); + var newValues = currentValues.Except(removeValues).ToArray(); + if (newValues.Length > 0) + { + newValues.Each(x => qs.Add(name, x, false)); + } + } + } + } + + return _url + qs.ToString(false); + } + + public string GetQueryName(Facet facet) + { + var parts = GetQueryParts(facet); + return parts.GetKey(0); + } + + protected virtual NameValueCollection GetQueryParts(Facet facet) + { + string name = null; + string value = null; + int entityId; + + var group = facet.FacetGroup; + var val = facet.Value; + + var result = new NameValueCollection(2); + + switch (group.Kind) + { + case FacetGroupKind.Attribute: + if (facet.Value.TypeCode == IndexTypeCode.Double) + { + value = "{0}~{1}".FormatInvariant( + val.Value != null ? ((double)val.Value).ToString(CultureInfo.InvariantCulture) : "", + val.UpperValue != null ? ((double)val.UpperValue).ToString(CultureInfo.InvariantCulture) : ""); + } + else + { + entityId = val.Value.Convert(); + value = _catalogAliasMapper.GetAttributeOptionAliasById(entityId, _languageId) ?? "opt" + entityId; + } + name = _catalogAliasMapper.GetAttributeAliasById(val.ParentId, _languageId) ?? "attr" + val.ParentId; + result.Add(name, value); + break; + case FacetGroupKind.Variant: + entityId = val.Value.Convert(); + name = _catalogAliasMapper.GetVariantAliasById(val.ParentId, _languageId) ?? "vari" + val.ParentId; + value = _catalogAliasMapper.GetVariantOptionAliasById(entityId, _languageId) ?? "opt" + entityId; + result.Add(name, value); + break; + case FacetGroupKind.Category: + case FacetGroupKind.Brand: + case FacetGroupKind.Price: + case FacetGroupKind.Rating: + case FacetGroupKind.DeliveryTime: + case FacetGroupKind.Availability: + case FacetGroupKind.NewArrivals: + value = val.ToString(); + if (value.HasValue()) + { + name = _catalogAliasMapper.GetCommonFacetAliasByGroupKind(group.Kind, _languageId) ?? _queryNames[group.Kind]; + result.Add(name, value); + } + break; case FacetGroupKind.Forum: case FacetGroupKind.Customer: case FacetGroupKind.Date: @@ -217,9 +217,9 @@ protected virtual NameValueCollection GetQueryParts(Facet facet) result.Add(name, value); } break; - } + } - return result; - } - } + return result; + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs b/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs index 5503a09110..6f29143638 100644 --- a/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs +++ b/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs @@ -14,119 +14,119 @@ namespace SmartStore.Services.Search.Extensions /// Contains methods that are specifically required for facet processing. /// public static class FacetUtility - { - private const double MAX_PRICE = 1000000000; - - private static int[,] _priceThresholds = new int[,] - { - { 10, 5 }, - { 25, 15 }, - { 200, 25 }, - { 500, 50 }, - { 1000, 100 }, - { 2000, 250 }, - { 5000, 500 }, - { 10000, 1000 }, - { 20000, 2500 }, - { 50000, 5000 }, - { 100000, 10000 }, - { 200000, 25000 }, - { 500000, 50000 }, - { 1000000, 100000 }, - { 2000000, 250000 }, - { 5000000, 500000 }, - { 10000000, 1000000 }, - { 20000000, 2500000 }, - { 50000000, 5000000 } - }; - - public static double GetNextPrice(double price) - { - for (var i = 0; i <= _priceThresholds.GetUpperBound(0); ++i) - { - if (price < _priceThresholds[i, 0]) - return price + _priceThresholds[i, 1]; - } - - return MAX_PRICE; - } - - public static double MakePriceEven(double price) - { - if (price == 0.0) - return GetNextPrice(0.0); - - // Get previous threshold for price. - var result = 0.0; - for (var i = 1; i <= _priceThresholds.GetUpperBound(0) && result == 0.0; ++i) - { - if (price < _priceThresholds[i, 0]) - result = _priceThresholds[i - 1, 0]; - } - - while (result < price && result < MAX_PRICE) - { - result = GetNextPrice(result); - } - - return result; - } - - public static List GetLessPriceFacets(List facets, int maxNumber) - { - const double expFactor = 2.0; - const double flatten = 2.0; - - if (facets.Count <= 3) - return facets; - - // Remove too granular facets. - if (facets.Any(x => x.Value.UpperValue != null && (double)x.Value.UpperValue == 25.0)) - { - facets.RemoveFacet(5.0, true); - facets.RemoveFacet(10.0, true); - } - - var result = new List(); - var expIndexes = new HashSet(); - var lastIndex = facets.Count - 1; - - // Get exponential distributed indexes. - for (var i = 0.0; i < lastIndex; ++i) - { - var x = (int)Math.Floor(Math.Pow(expFactor, i / flatten)); - expIndexes.Add(x); - } - - for (var index = 0; index <= lastIndex; ++index) - { - var facet = facets[index]; - - // Always return first, last and selected facets. - if (index == 0 || index == lastIndex || facet.Value.IsSelected) - { - result.Add(facet); - } - else if (expIndexes.Contains(index) && result.Count < maxNumber && index < (lastIndex - 1)) - { - result.Add(facet); - } - } - - return result; - } - - public static IEnumerable GetRatings() - { - var count = 0; - for (double rate = 1.0; rate <= 5.0; ++rate) - { - yield return new FacetValue(rate, IndexTypeCode.Double) - { - DisplayOrder = ++count - }; - } - } + { + private const double MAX_PRICE = 1000000000; + + private static int[,] _priceThresholds = new int[,] + { + { 10, 5 }, + { 25, 15 }, + { 200, 25 }, + { 500, 50 }, + { 1000, 100 }, + { 2000, 250 }, + { 5000, 500 }, + { 10000, 1000 }, + { 20000, 2500 }, + { 50000, 5000 }, + { 100000, 10000 }, + { 200000, 25000 }, + { 500000, 50000 }, + { 1000000, 100000 }, + { 2000000, 250000 }, + { 5000000, 500000 }, + { 10000000, 1000000 }, + { 20000000, 2500000 }, + { 50000000, 5000000 } + }; + + public static double GetNextPrice(double price) + { + for (var i = 0; i <= _priceThresholds.GetUpperBound(0); ++i) + { + if (price < _priceThresholds[i, 0]) + return price + _priceThresholds[i, 1]; + } + + return MAX_PRICE; + } + + public static double MakePriceEven(double price) + { + if (price == 0.0) + return GetNextPrice(0.0); + + // Get previous threshold for price. + var result = 0.0; + for (var i = 1; i <= _priceThresholds.GetUpperBound(0) && result == 0.0; ++i) + { + if (price < _priceThresholds[i, 0]) + result = _priceThresholds[i - 1, 0]; + } + + while (result < price && result < MAX_PRICE) + { + result = GetNextPrice(result); + } + + return result; + } + + public static List GetLessPriceFacets(List facets, int maxNumber) + { + const double expFactor = 2.0; + const double flatten = 2.0; + + if (facets.Count <= 3) + return facets; + + // Remove too granular facets. + if (facets.Any(x => x.Value.UpperValue != null && (double)x.Value.UpperValue == 25.0)) + { + facets.RemoveFacet(5.0, true); + facets.RemoveFacet(10.0, true); + } + + var result = new List(); + var expIndexes = new HashSet(); + var lastIndex = facets.Count - 1; + + // Get exponential distributed indexes. + for (var i = 0.0; i < lastIndex; ++i) + { + var x = (int)Math.Floor(Math.Pow(expFactor, i / flatten)); + expIndexes.Add(x); + } + + for (var index = 0; index <= lastIndex; ++index) + { + var facet = facets[index]; + + // Always return first, last and selected facets. + if (index == 0 || index == lastIndex || facet.Value.IsSelected) + { + result.Add(facet); + } + else if (expIndexes.Contains(index) && result.Count < maxNumber && index < (lastIndex - 1)) + { + result.Add(facet); + } + } + + return result; + } + + public static IEnumerable GetRatings() + { + var count = 0; + for (double rate = 1.0; rate <= 5.0; ++rate) + { + yield return new FacetValue(rate, IndexTypeCode.Double) + { + DisplayOrder = ++count + }; + } + } public static IQueryable GetCustomersByNumberOfPosts( IRepository forumPostRepository, diff --git a/src/Libraries/SmartStore.Services/Search/Extensions/UrlHelperExtensions.cs b/src/Libraries/SmartStore.Services/Search/Extensions/UrlHelperExtensions.cs index 1f07ae6323..32e074e7ba 100644 --- a/src/Libraries/SmartStore.Services/Search/Extensions/UrlHelperExtensions.cs +++ b/src/Libraries/SmartStore.Services/Search/Extensions/UrlHelperExtensions.cs @@ -4,30 +4,30 @@ namespace SmartStore.Services.Search.Extensions { - public static class UrlHelperExtensions - { - public static string FacetToggle(this UrlHelper urlHelper, Facet facet) - { - var facetUrlHelper = EngineContext.Current.Resolve(); - return facetUrlHelper.Toggle(facet); - } + public static class UrlHelperExtensions + { + public static string FacetToggle(this UrlHelper urlHelper, Facet facet) + { + var facetUrlHelper = EngineContext.Current.Resolve(); + return facetUrlHelper.Toggle(facet); + } - public static string FacetAdd(this UrlHelper urlHelper, params Facet[] facets) - { - var facetUrlHelper = EngineContext.Current.Resolve(); - return facetUrlHelper.Add(facets); - } + public static string FacetAdd(this UrlHelper urlHelper, params Facet[] facets) + { + var facetUrlHelper = EngineContext.Current.Resolve(); + return facetUrlHelper.Add(facets); + } - public static string FacetRemove(this UrlHelper urlHelper, params Facet[] facets) - { - var facetUrlHelper = EngineContext.Current.Resolve(); - return facetUrlHelper.Remove(facets); - } + public static string FacetRemove(this UrlHelper urlHelper, params Facet[] facets) + { + var facetUrlHelper = EngineContext.Current.Resolve(); + return facetUrlHelper.Remove(facets); + } - public static string GetFacetQueryName(this UrlHelper urlHelper, Facet facet) - { - var facetUrlHelper = EngineContext.Current.Resolve(); - return facetUrlHelper.GetQueryName(facet); - } - } + public static string GetFacetQueryName(this UrlHelper urlHelper, Facet facet) + { + var facetUrlHelper = EngineContext.Current.Resolve(); + return facetUrlHelper.GetQueryName(facet); + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchEvents.cs b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchEvents.cs index 7c1c6f986c..91b55db08a 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchEvents.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchEvents.cs @@ -1,29 +1,29 @@ namespace SmartStore.Services.Search { public class ForumSearchingEvent - { + { public ForumSearchingEvent(ForumSearchQuery query) { - Guard.NotNull(query, nameof(query)); + Guard.NotNull(query, nameof(query)); - Query = query; - } + Query = query; + } public ForumSearchQuery Query { get; private set; } } - public class ForumSearchedEvent + public class ForumSearchedEvent { - public ForumSearchedEvent(ForumSearchQuery query, ForumSearchResult result) - { - Guard.NotNull(query, nameof(query)); - Guard.NotNull(result, nameof(result)); + public ForumSearchedEvent(ForumSearchQuery query, ForumSearchResult result) + { + Guard.NotNull(query, nameof(query)); + Guard.NotNull(result, nameof(result)); - Query = query; - Result = result; - } + Query = query; + Result = result; + } - public ForumSearchQuery Query { get; private set; } - public ForumSearchResult Result { get; private set; } - } + public ForumSearchQuery Query { get; private set; } + public ForumSearchResult Result { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchQuery.cs b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchQuery.cs index edd039de93..53313b67cc 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchQuery.cs @@ -8,34 +8,34 @@ namespace SmartStore.Services.Search { public partial class ForumSearchQuery : SearchQuery, ICloneable - { + { /// /// Initializes a new instance of the class without a search term being set. /// public ForumSearchQuery() - : base((string[])null, null) - { - } - - public ForumSearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) - : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) - { - } - - public ForumSearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) - : base(fields, term, mode, escape, isFuzzySearch) - { - } - - public ForumSearchQuery Clone() - { - return (ForumSearchQuery)this.MemberwiseClone(); - } - - object ICloneable.Clone() - { - return this.MemberwiseClone(); - } + : base((string[])null, null) + { + } + + public ForumSearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) + : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) + { + } + + public ForumSearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) + : base(fields, term, mode, escape, isFuzzySearch) + { + } + + public ForumSearchQuery Clone() + { + return (ForumSearchQuery)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } #region Fluent builder diff --git a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchResult.cs b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchResult.cs index 031c94b177..540a93de22 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchResult.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchResult.cs @@ -10,37 +10,37 @@ namespace SmartStore.Services.Search public partial class ForumSearchResult { private readonly Func> _hitsFactory; - private IPagedList _hits; + private IPagedList _hits; - public ForumSearchResult( - ISearchEngine engine, + public ForumSearchResult( + ISearchEngine engine, ForumSearchQuery query, - int totalHitsCount, + int totalHitsCount, int[] hitsEntityIds, Func> hitsFactory, - string[] spellCheckerSuggestions, + string[] spellCheckerSuggestions, IDictionary facets) - { - Guard.NotNull(query, nameof(query)); + { + Guard.NotNull(query, nameof(query)); - Engine = engine; - Query = query; - SpellCheckerSuggestions = spellCheckerSuggestions ?? new string[0]; + Engine = engine; + Query = query; + SpellCheckerSuggestions = spellCheckerSuggestions ?? new string[0]; Facets = facets ?? new Dictionary(); _hitsFactory = hitsFactory ?? (() => new List()); HitsEntityIds = hitsEntityIds ?? new int[0]; TotalHitsCount = totalHitsCount; - } + } /// /// Constructor for an instance without any search hits /// /// Forum search query public ForumSearchResult(ForumSearchQuery query) - : this(null, query, 0, null, () => new List(), null, null) - { - } + : this(null, query, 0, null, () => new List(), null, null) + { + } /// /// Entity identifiers of found forum posts. @@ -51,21 +51,21 @@ public ForumSearchResult(ForumSearchQuery query) /// Forum posts found. /// public IPagedList Hits - { - get - { - if (_hits == null) - { - var entities = TotalHitsCount == 0 - ? new List() - : _hitsFactory.Invoke(); + { + get + { + if (_hits == null) + { + var entities = TotalHitsCount == 0 + ? new List() + : _hitsFactory.Invoke(); - _hits = new PagedList(entities, Query.PageIndex, Query.Take, TotalHitsCount); - } + _hits = new PagedList(entities, Query.PageIndex, Query.Take, TotalHitsCount); + } - return _hits; - } - } + return _hits; + } + } public int TotalHitsCount { get; } @@ -74,13 +74,13 @@ public IPagedList Hits /// public ForumSearchQuery Query { get; private set; } - /// - /// Gets spell checking suggestions/corrections. - /// - public string[] SpellCheckerSuggestions { get; set; } + /// + /// Gets spell checking suggestions/corrections. + /// + public string[] SpellCheckerSuggestions { get; set; } public IDictionary Facets { get; private set; } - public ISearchEngine Engine { get; private set; } + public ISearchEngine Engine { get; private set; } } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchService.cs b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchService.cs index 0066621ac1..0cff069cf5 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/ForumSearchService.cs @@ -12,25 +12,25 @@ namespace SmartStore.Services.Search { public partial class ForumSearchService : SearchServiceBase, IForumSearchService - { - private readonly ICommonServices _services; - private readonly IIndexManager _indexManager; + { + private readonly ICommonServices _services; + private readonly IIndexManager _indexManager; private readonly Lazy _forumService; private readonly ILogger _logger; - private readonly UrlHelper _urlHelper; + private readonly UrlHelper _urlHelper; - public ForumSearchService( - ICommonServices services, - IIndexManager indexManager, + public ForumSearchService( + ICommonServices services, + IIndexManager indexManager, Lazy forumService, - ILogger logger, - UrlHelper urlHelper) - { - _services = services; - _indexManager = indexManager; + ILogger logger, + UrlHelper urlHelper) + { + _services = services; + _indexManager = indexManager; _forumService = forumService; - _logger = logger; - _urlHelper = urlHelper; + _logger = logger; + _urlHelper = urlHelper; } /// @@ -39,58 +39,58 @@ public ForumSearchService( /// Search query. /// Forum search result. protected virtual ForumSearchResult SearchDirect(ForumSearchQuery searchQuery) - { - // Fallback to linq search. - var linqForumSearchService = _services.Container.ResolveNamed("linq"); - var result = linqForumSearchService.Search(searchQuery, true); + { + // Fallback to linq search. + var linqForumSearchService = _services.Container.ResolveNamed("linq"); + var result = linqForumSearchService.Search(searchQuery, true); return result; - } + } public ForumSearchResult Search(ForumSearchQuery searchQuery, bool direct = false) { - Guard.NotNull(searchQuery, nameof(searchQuery)); - Guard.NotNegative(searchQuery.Take, nameof(searchQuery.Take)); - - var provider = _indexManager.GetIndexProvider("Forum"); - - if (!direct && provider != null) - { - var indexStore = provider.GetIndexStore("Forum"); - if (indexStore.Exists) - { - var searchEngine = provider.GetSearchEngine(indexStore, searchQuery); - var stepPrefix = searchEngine.GetType().Name + " - "; - var totalCount = 0; + Guard.NotNull(searchQuery, nameof(searchQuery)); + Guard.NotNegative(searchQuery.Take, nameof(searchQuery.Take)); + + var provider = _indexManager.GetIndexProvider("Forum"); + + if (!direct && provider != null) + { + var indexStore = provider.GetIndexStore("Forum"); + if (indexStore.Exists) + { + var searchEngine = provider.GetSearchEngine(indexStore, searchQuery); + var stepPrefix = searchEngine.GetType().Name + " - "; + var totalCount = 0; int[] hitsEntityIds = null; string[] spellCheckerSuggestions = null; - IEnumerable searchHits; - Func> hitsFactory = null; + IEnumerable searchHits; + Func> hitsFactory = null; IDictionary facets = null; _services.EventPublisher.Publish(new ForumSearchingEvent(searchQuery)); - if (searchQuery.Take > 0) - { - using (_services.Chronometer.Step(stepPrefix + "Count")) - { - totalCount = searchEngine.Count(); - // Fix paging boundaries. - if (searchQuery.Skip > 0 && searchQuery.Skip >= totalCount) - { - searchQuery.Slice((totalCount / searchQuery.Take) * searchQuery.Take, searchQuery.Take); - } - } - - using (_services.Chronometer.Step(stepPrefix + "Hits")) - { - searchHits = searchEngine.Search(); - } - - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) - { - using (_services.Chronometer.Step(stepPrefix + "Collect")) - { + if (searchQuery.Take > 0) + { + using (_services.Chronometer.Step(stepPrefix + "Count")) + { + totalCount = searchEngine.Count(); + // Fix paging boundaries. + if (searchQuery.Skip > 0 && searchQuery.Skip >= totalCount) + { + searchQuery.Slice((totalCount / searchQuery.Take) * searchQuery.Take, searchQuery.Take); + } + } + + using (_services.Chronometer.Step(stepPrefix + "Hits")) + { + searchHits = searchEngine.Search(); + } + + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithHits)) + { + using (_services.Chronometer.Step(stepPrefix + "Collect")) + { hitsEntityIds = searchHits.Select(x => x.EntityId).ToArray(); hitsFactory = () => _forumService.Value.GetPostsByIds(hitsEntityIds); } @@ -112,48 +112,48 @@ public ForumSearchResult Search(ForumSearchQuery searchQuery, bool direct = fals } } - if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithSuggestions)) - { - try - { - using (_services.Chronometer.Step(stepPrefix + "Spellcheck")) - { - spellCheckerSuggestions = searchEngine.CheckSpelling(); - } - } - catch (Exception ex) - { - // Spell checking should not break the search. - _logger.Error(ex); - } - } - - var result = new ForumSearchResult( - searchEngine, - searchQuery, - totalCount, + if (searchQuery.ResultFlags.HasFlag(SearchResultFlags.WithSuggestions)) + { + try + { + using (_services.Chronometer.Step(stepPrefix + "Spellcheck")) + { + spellCheckerSuggestions = searchEngine.CheckSpelling(); + } + } + catch (Exception ex) + { + // Spell checking should not break the search. + _logger.Error(ex); + } + } + + var result = new ForumSearchResult( + searchEngine, + searchQuery, + totalCount, hitsEntityIds, hitsFactory, - spellCheckerSuggestions, + spellCheckerSuggestions, facets); - _services.EventPublisher.Publish(new ForumSearchedEvent(searchQuery, result)); + _services.EventPublisher.Publish(new ForumSearchedEvent(searchQuery, result)); - return result; - } - else if (searchQuery.Origin.IsCaseInsensitiveEqual("Boards/Search")) - { - IndexingRequiredNotification(_services, _urlHelper); - } - } + return result; + } + else if (searchQuery.Origin.IsCaseInsensitiveEqual("Boards/Search")) + { + IndexingRequiredNotification(_services, _urlHelper); + } + } - return SearchDirect(searchQuery); - } + return SearchDirect(searchQuery); + } - public IQueryable PrepareQuery(ForumSearchQuery searchQuery, IQueryable baseQuery = null) + public IQueryable PrepareQuery(ForumSearchQuery searchQuery, IQueryable baseQuery = null) { - var linqForumSearchService = _services.Container.ResolveNamed("linq"); - return linqForumSearchService.PrepareQuery(searchQuery, baseQuery); - } - } + var linqForumSearchService = _services.Container.ResolveNamed("linq"); + return linqForumSearchService.PrepareQuery(searchQuery, baseQuery); + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/IForumSearchService.cs b/src/Libraries/SmartStore.Services/Search/Forum/IForumSearchService.cs index e6a7f135af..a14dce90dc 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/IForumSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/IForumSearchService.cs @@ -4,7 +4,7 @@ namespace SmartStore.Services.Search { public partial interface IForumSearchService - { + { /// /// Searches for forum posts. /// @@ -20,5 +20,5 @@ public partial interface IForumSearchService /// Optional query used to build the forum post query. /// Forum post queryable. IQueryable PrepareQuery(ForumSearchQuery searchQuery, IQueryable baseQuery = null); - } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs b/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs index 347806f060..0d9257c4f1 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/LinqForumSearchService.cs @@ -39,7 +39,7 @@ public LinqForumSearchService( IForumService forumService, ICommonServices services, CustomerSettings customerSettings) - { + { _forumPostRepository = forumPostRepository; _forumTopicRepository = forumTopicRepository; _forumRepository = forumRepository; @@ -47,7 +47,7 @@ public LinqForumSearchService( _storeMappingRepository = storeMappingRepository; _aclRepository = aclRepository; _forumService = forumService; - _services = services; + _services = services; _customerSettings = customerSettings; T = NullLocalizer.Instance; @@ -381,16 +381,16 @@ protected virtual IDictionary GetFacets(ForumSearchQuery sea } public ForumSearchResult Search(ForumSearchQuery searchQuery, bool direct = false) - { + { _services.EventPublisher.Publish(new ForumSearchingEvent(searchQuery)); - var totalHits = 0; + var totalHits = 0; int[] hitsEntityIds = null; Func> hitsFactory = null; IDictionary facets = null; if (searchQuery.Take > 0) - { + { var query = GetPostQuery(searchQuery, null); totalHits = query.Count(); @@ -418,22 +418,22 @@ public ForumSearchResult Search(ForumSearchQuery searchQuery, bool direct = fals } var result = new ForumSearchResult( - null, - searchQuery, - totalHits, + null, + searchQuery, + totalHits, hitsEntityIds, hitsFactory, - null, + null, facets); _services.EventPublisher.Publish(new ForumSearchedEvent(searchQuery, result)); - return result; - } + return result; + } - public IQueryable PrepareQuery(ForumSearchQuery searchQuery, IQueryable baseQuery = null) - { + public IQueryable PrepareQuery(ForumSearchQuery searchQuery, IQueryable baseQuery = null) + { return GetPostQuery(searchQuery, baseQuery); } - } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryAliasMapper.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryAliasMapper.cs index ccded4cd3f..de2c8be999 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryAliasMapper.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryAliasMapper.cs @@ -9,63 +9,63 @@ namespace SmartStore.Services.Search.Modelling { public class ForumSearchQueryAliasMapper : IForumSearchQueryAliasMapper - { - private const string ALL_FORUM_COMMONFACET_ALIAS_BY_KIND_KEY = "search.forum.commonfacet.alias.kind.mappings.all"; + { + private const string ALL_FORUM_COMMONFACET_ALIAS_BY_KIND_KEY = "search.forum.commonfacet.alias.kind.mappings.all"; - private readonly ICacheManager _cacheManager; - private readonly ISettingService _settingService; - private readonly ILanguageService _languageService; + private readonly ICacheManager _cacheManager; + private readonly ISettingService _settingService; + private readonly ILanguageService _languageService; - public ForumSearchQueryAliasMapper( - ICacheManager cacheManager, - ISettingService settingService, - ILanguageService languageService) - { - _cacheManager = cacheManager; - _settingService = settingService; - _languageService = languageService; - } + public ForumSearchQueryAliasMapper( + ICacheManager cacheManager, + ISettingService settingService, + ILanguageService languageService) + { + _cacheManager = cacheManager; + _settingService = settingService; + _languageService = languageService; + } - protected virtual IDictionary GetCommonFacetAliasByGroupKindMappings() - { - return _cacheManager.Get(ALL_FORUM_COMMONFACET_ALIAS_BY_KIND_KEY, () => - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + protected virtual IDictionary GetCommonFacetAliasByGroupKindMappings() + { + return _cacheManager.Get(ALL_FORUM_COMMONFACET_ALIAS_BY_KIND_KEY, () => + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - var groupKinds = new FacetGroupKind[] - { + var groupKinds = new FacetGroupKind[] + { FacetGroupKind.Forum, FacetGroupKind.Customer, FacetGroupKind.Date - }; + }; - foreach (var language in _languageService.GetAllLanguages()) - { - foreach (var groupKind in groupKinds) - { - var key = FacetUtility.GetFacetAliasSettingKey(groupKind, language.Id, "Forum"); - var value = _settingService.GetSettingByKey(key); - if (value.HasValue()) - { - result.Add(key, value); - } - } - } + foreach (var language in _languageService.GetAllLanguages()) + { + foreach (var groupKind in groupKinds) + { + var key = FacetUtility.GetFacetAliasSettingKey(groupKind, language.Id, "Forum"); + var value = _settingService.GetSettingByKey(key); + if (value.HasValue()) + { + result.Add(key, value); + } + } + } - return result; - }); - } + return result; + }); + } - public void ClearCommonFacetCache() - { - _cacheManager.Remove(ALL_FORUM_COMMONFACET_ALIAS_BY_KIND_KEY); - } + public void ClearCommonFacetCache() + { + _cacheManager.Remove(ALL_FORUM_COMMONFACET_ALIAS_BY_KIND_KEY); + } - public string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId) - { - var mappings = GetCommonFacetAliasByGroupKindMappings(); + public string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId) + { + var mappings = GetCommonFacetAliasByGroupKindMappings(); - return mappings.Get(FacetUtility.GetFacetAliasSettingKey(kind, languageId, "Forum")); - } - } + return mappings.Get(FacetUtility.GetFacetAliasSettingKey(kind, languageId, "Forum")); + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs index 6d14641314..6310fbbfa4 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs @@ -144,14 +144,20 @@ private void AddFacet( switch (kind) { case FacetGroupKind.Forum: + if (_searchSettings.ForumDisabled) + return; fieldName = "forumid"; displayOrder = _searchSettings.ForumDisplayOrder; break; case FacetGroupKind.Customer: + if (_searchSettings.CustomerDisabled) + return; fieldName = "customerid"; displayOrder = _searchSettings.CustomerDisplayOrder; break; case FacetGroupKind.Date: + if (_searchSettings.DateDisabled) + return; fieldName = "createdon"; displayOrder = _searchSettings.DateDisplayOrder; break; diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryModelBinder.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryModelBinder.cs index ec47720bb5..cc3e935fe7 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryModelBinder.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryModelBinder.cs @@ -4,39 +4,39 @@ namespace SmartStore.Services.Search.Modelling { [ModelBinderType(typeof(ForumSearchQuery))] - public class ForumSearchQueryModelBinder : IModelBinder - { - private readonly IForumSearchQueryFactory _factory; + public class ForumSearchQueryModelBinder : IModelBinder + { + private readonly IForumSearchQueryFactory _factory; - public ForumSearchQueryModelBinder(IForumSearchQueryFactory factory) - { - _factory = factory; - } + public ForumSearchQueryModelBinder(IForumSearchQueryFactory factory) + { + _factory = factory; + } - public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) - { - if (_factory.Current != null) - { - // Don't bind again for current request. - return _factory.Current; - } + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (_factory.Current != null) + { + // Don't bind again for current request. + return _factory.Current; + } - if (controllerContext.IsChildAction) - { + if (controllerContext.IsChildAction) + { // Never attempt to bind in child actions. We require the binding to happen // in a parent action. If the child action is part of a request with an already bound // 'ForumSearchQuery', good for you :-) You'll get an instance, but null otherwise. return _factory.Current; - } + } - var modelType = bindingContext.ModelType; - if (modelType != typeof(ForumSearchQuery)) - { - return new ForumSearchQuery(); - } + var modelType = bindingContext.ModelType; + if (modelType != typeof(ForumSearchQuery)) + { + return new ForumSearchQuery(); + } - var query = _factory.CreateFromQuery(); - return query; - } - } + var query = _factory.CreateFromQuery(); + return query; + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryAliasMapper.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryAliasMapper.cs index 70b8785535..042dd41947 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryAliasMapper.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryAliasMapper.cs @@ -2,19 +2,19 @@ namespace SmartStore.Services.Search.Modelling { - public interface IForumSearchQueryAliasMapper - { - /// - /// Clears all cached common facet mappings - /// - void ClearCommonFacetCache(); + public interface IForumSearchQueryAliasMapper + { + /// + /// Clears all cached common facet mappings + /// + void ClearCommonFacetCache(); - /// - /// Get the common facet alias by facet group kind - /// - /// Facet group kind - /// Language identifier - /// Common facet alias - string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId); - } + /// + /// Get the common facet alias by facet group kind + /// + /// Facet group kind + /// Language identifier + /// Common facet alias + string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId); + } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryFactory.cs index 05ca8c4866..fc936ce5a6 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/IForumSearchQueryFactory.cs @@ -1,6 +1,6 @@ namespace SmartStore.Services.Search.Modelling { - public interface IForumSearchQueryFactory + public interface IForumSearchQueryFactory { /// /// Creates a instance from the current @@ -13,5 +13,5 @@ public interface IForumSearchQueryFactory /// The last created query instance. The MVC model binder uses this property to avoid repeated binding. /// ForumSearchQuery Current { get; } - } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Rendering/FacetTemplateProvider.cs b/src/Libraries/SmartStore.Services/Search/Rendering/FacetTemplateProvider.cs index c8e4860470..f8c30e06f6 100644 --- a/src/Libraries/SmartStore.Services/Search/Rendering/FacetTemplateProvider.cs +++ b/src/Libraries/SmartStore.Services/Search/Rendering/FacetTemplateProvider.cs @@ -5,23 +5,23 @@ namespace SmartStore.Services.Search.Rendering { - public class FacetTemplateProvider : IFacetTemplateProvider - { - private readonly IEnumerable _selectors; + public class FacetTemplateProvider : IFacetTemplateProvider + { + private readonly IEnumerable _selectors; - public FacetTemplateProvider(IEnumerable selectors) - { - _selectors = selectors; - } + public FacetTemplateProvider(IEnumerable selectors) + { + _selectors = selectors; + } - public RouteInfo GetTemplateRoute(FacetGroup facetGroup) - { - var route = _selectors - .OrderByDescending(x => x.Ordinal) - .Select(x => x.GetTemplateRoute(facetGroup)) - .FirstOrDefault(x => x != null); + public RouteInfo GetTemplateRoute(FacetGroup facetGroup) + { + var route = _selectors + .OrderByDescending(x => x.Ordinal) + .Select(x => x.GetTemplateRoute(facetGroup)) + .FirstOrDefault(x => x != null); - return route; - } - } + return route; + } + } } diff --git a/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateProvider.cs b/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateProvider.cs index 45893675d6..9f8eb7757a 100644 --- a/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateProvider.cs +++ b/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateProvider.cs @@ -3,8 +3,8 @@ namespace SmartStore.Services.Search.Rendering { - public interface IFacetTemplateProvider - { - RouteInfo GetTemplateRoute(FacetGroup facetGroup); - } + public interface IFacetTemplateProvider + { + RouteInfo GetTemplateRoute(FacetGroup facetGroup); + } } diff --git a/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateSelector.cs b/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateSelector.cs index d7efdbc334..3d6f9fbea8 100644 --- a/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateSelector.cs +++ b/src/Libraries/SmartStore.Services/Search/Rendering/IFacetTemplateSelector.cs @@ -3,8 +3,8 @@ namespace SmartStore.Services.Search.Rendering { - public interface IFacetTemplateSelector : IOrdered - { - RouteInfo GetTemplateRoute(FacetGroup facetGroup); - } + public interface IFacetTemplateSelector : IOrdered + { + RouteInfo GetTemplateRoute(FacetGroup facetGroup); + } } diff --git a/src/Libraries/SmartStore.Services/Security/AclService.cs b/src/Libraries/SmartStore.Services/Security/AclService.cs index 9d7bbe0193..c92db21540 100644 --- a/src/Libraries/SmartStore.Services/Security/AclService.cs +++ b/src/Libraries/SmartStore.Services/Security/AclService.cs @@ -13,51 +13,51 @@ namespace SmartStore.Services.Security { public partial class AclService : IAclService { - /// - /// 0 = segment (EntityName.IdRange) - /// - const string ACL_SEGMENT_KEY = "acl:range-{0}"; - internal const string ACL_SEGMENT_PATTERN = "acl:range-*"; + /// + /// 0 = segment (EntityName.IdRange) + /// + const string ACL_SEGMENT_KEY = "acl:range-{0}"; + internal const string ACL_SEGMENT_PATTERN = "acl:range-*"; private readonly IRepository _aclRecordRepository; private readonly Work _workContext; private readonly ICacheManager _cacheManager; - private readonly ICustomerService _customerService; + private readonly Work _customerService; - private bool? _hasActiveAcl; + private bool? _hasActiveAcl; public AclService( - ICacheManager cacheManager, - Work workContext, - IRepository aclRecordRepository, - ICustomerService customerService) + ICacheManager cacheManager, + Work workContext, + IRepository aclRecordRepository, + Work customerService) { _cacheManager = cacheManager; _workContext = workContext; _aclRecordRepository = aclRecordRepository; - _customerService = customerService; - } - - public bool HasActiveAcl - { - get - { - if (!_hasActiveAcl.HasValue) - { - _hasActiveAcl = _aclRecordRepository.TableUntracked.Any(x => !x.IsIdle); - } - return _hasActiveAcl.Value; - } - } + _customerService = customerService; + } + + public bool HasActiveAcl + { + get + { + if (!_hasActiveAcl.HasValue) + { + _hasActiveAcl = _aclRecordRepository.TableUntracked.Any(x => !x.IsIdle); + } + return _hasActiveAcl.Value; + } + } public virtual void DeleteAclRecord(AclRecord aclRecord) { - Guard.NotNull(aclRecord,nameof(aclRecord)); + Guard.NotNull(aclRecord, nameof(aclRecord)); _aclRecordRepository.Delete(aclRecord); - ClearCacheSegment(aclRecord.EntityName, aclRecord.EntityId); - } + ClearCacheSegment(aclRecord.EntityName, aclRecord.EntityId); + } public virtual AclRecord GetAclRecordById(int aclRecordId) { @@ -70,73 +70,73 @@ public virtual AclRecord GetAclRecordById(int aclRecordId) public IList GetAclRecords(T entity) where T : BaseEntity, IAclSupported { - Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(entity, nameof(entity)); - int entityId = entity.Id; + int entityId = entity.Id; string entityName = entity.GetEntityName(); - return GetAclRecordsFor(entityName, entityId); + return GetAclRecordsFor(entityName, entityId); } - public virtual IList GetAclRecordsFor(string entityName, int entityId) - { - Guard.IsPositive(entityId, nameof(entityId)); - Guard.NotEmpty(entityName, nameof(entityName)); - - var query = from ur in _aclRecordRepository.Table - where ur.EntityId == entityId && - ur.EntityName == entityName - select ur; - var aclRecords = query.ToList(); - return aclRecords; - } - - public virtual void SaveAclMappings(T entity, params int[] selectedCustomerRoleIds) where T : BaseEntity, IAclSupported - { - var existingAclRecords = GetAclRecords(entity); - var allCustomerRoles = _customerService.GetAllCustomerRoles(true); + public virtual IList GetAclRecordsFor(string entityName, int entityId) + { + Guard.IsPositive(entityId, nameof(entityId)); + Guard.NotEmpty(entityName, nameof(entityName)); + + var query = from ur in _aclRecordRepository.Table + where ur.EntityId == entityId && + ur.EntityName == entityName + select ur; + var aclRecords = query.ToList(); + return aclRecords; + } + + public virtual void SaveAclMappings(T entity, params int[] selectedCustomerRoleIds) where T : BaseEntity, IAclSupported + { + var existingAclRecords = GetAclRecords(entity); + var allCustomerRoles = _customerService.Value.GetAllCustomerRoles(true); entity.SubjectToAcl = selectedCustomerRoleIds.Length == 1 && selectedCustomerRoleIds[0] == 0 ? false : selectedCustomerRoleIds.Any(); foreach (var customerRole in allCustomerRoles) - { - if (selectedCustomerRoleIds != null && selectedCustomerRoleIds.Contains(customerRole.Id)) - { - // New role - if (!existingAclRecords.Any(x => x.CustomerRoleId == customerRole.Id)) - InsertAclRecord(entity, customerRole.Id); - } - else - { - // Removed role - var aclRecordToDelete = existingAclRecords.FirstOrDefault(x => x.CustomerRoleId == customerRole.Id); - if (aclRecordToDelete != null) - DeleteAclRecord(aclRecordToDelete); - } - } + { + if (selectedCustomerRoleIds != null && selectedCustomerRoleIds.Contains(customerRole.Id)) + { + // New role + if (!existingAclRecords.Any(x => x.CustomerRoleId == customerRole.Id)) + InsertAclRecord(entity, customerRole.Id); + } + else + { + // Removed role + var aclRecordToDelete = existingAclRecords.FirstOrDefault(x => x.CustomerRoleId == customerRole.Id); + if (aclRecordToDelete != null) + DeleteAclRecord(aclRecordToDelete); + } + } // TODO: Find a way to detect the context of the entity. Until then we don't check for modified props //if (_aclRecordRepository.Context.TryGetModifiedProperty(entity, nameof(entity.SubjectToAcl), out _)) //{ - _aclRecordRepository.Context.SaveChanges(); + _aclRecordRepository.Context.SaveChanges(); //} - } + } - public virtual void InsertAclRecord(AclRecord aclRecord) + public virtual void InsertAclRecord(AclRecord aclRecord) { - Guard.NotNull(aclRecord, nameof(aclRecord)); + Guard.NotNull(aclRecord, nameof(aclRecord)); - _aclRecordRepository.Insert(aclRecord); + _aclRecordRepository.Insert(aclRecord); - ClearCacheSegment(aclRecord.EntityName, aclRecord.EntityId); - } + ClearCacheSegment(aclRecord.EntityName, aclRecord.EntityId); + } public virtual void InsertAclRecord(T entity, int customerRoleId) where T : BaseEntity, IAclSupported { - Guard.NotNull(entity, nameof(entity)); - - if (customerRoleId == 0) + Guard.NotNull(entity, nameof(entity)); + + if (customerRoleId == 0) throw new ArgumentOutOfRangeException(nameof(customerRoleId)); int entityId = entity.Id; @@ -154,127 +154,127 @@ public virtual void InsertAclRecord(T entity, int customerRoleId) where T : B public virtual void UpdateAclRecord(AclRecord aclRecord) { - Guard.NotNull(aclRecord, nameof(aclRecord)); + Guard.NotNull(aclRecord, nameof(aclRecord)); - _aclRecordRepository.Update(aclRecord); + _aclRecordRepository.Update(aclRecord); - ClearCacheSegment(aclRecord.EntityName, aclRecord.EntityId); - } + ClearCacheSegment(aclRecord.EntityName, aclRecord.EntityId); + } - public virtual int[] GetCustomerRoleIdsWithAccessTo(string entityName, int entityId) - { - Guard.NotEmpty(entityName, nameof(entityName)); + public virtual int[] GetCustomerRoleIdsWithAccessTo(string entityName, int entityId) + { + Guard.NotEmpty(entityName, nameof(entityName)); - if (entityId <= 0) - return new int[0]; + if (entityId <= 0) + return new int[0]; - var cacheSegment = GetCacheSegment(entityName, entityId); + var cacheSegment = GetCacheSegment(entityName, entityId); - if (!cacheSegment.TryGetValue(entityId, out var roleIds)) - { - return Array.Empty(); - } + if (!cacheSegment.TryGetValue(entityId, out var roleIds)) + { + return Array.Empty(); + } - return roleIds; - } + return roleIds; + } - public bool Authorize(string entityName, int entityId) - { - return Authorize(entityName, entityId, _workContext.Value.CurrentCustomer?.CustomerRoleMappings?.Select(x => x.CustomerRole)); - } + public bool Authorize(string entityName, int entityId) + { + return Authorize(entityName, entityId, _workContext.Value.CurrentCustomer?.CustomerRoleMappings?.Select(x => x.CustomerRole)); + } - public virtual bool Authorize(string entityName, int entityId, IEnumerable roles) - { - Guard.NotEmpty(entityName, nameof(entityName)); + public virtual bool Authorize(string entityName, int entityId, IEnumerable roles) + { + Guard.NotEmpty(entityName, nameof(entityName)); - if (entityId <= 0) - return false; + if (entityId <= 0) + return false; if (!HasActiveAcl) - return true; - - if (roles == null) - return false; - - foreach (var role in roles) - { - if (!role.Active) - continue; - - foreach (var role2Id in GetCustomerRoleIdsWithAccessTo(entityName, entityId)) - { - if (role.Id == role2Id) - { - // Yes, we have such permission - return true; - } - } - } - - // No permission granted - return false; - } - - #region Cache segmenting - - protected virtual IDictionary GetCacheSegment(string entityName, int entityId) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - var segmentKey = GetSegmentKeyPart(entityName, entityId, out var minEntityId, out var maxEntityId); - var cacheKey = BuildCacheSegmentKey(segmentKey); - - return _cacheManager.Get(cacheKey, () => - { - var query = from sm in _aclRecordRepository.TableUntracked - where - sm.EntityId >= minEntityId && - sm.EntityId <= maxEntityId && - sm.EntityName == entityName - select sm; - - var mappings = query.ToLookup(x => x.EntityId, x => x.CustomerRoleId); - - var dict = new Dictionary(mappings.Count); - - foreach (var sm in mappings) - { - dict[sm.Key] = sm.ToArray(); - } - - return dict; - }); - } - - /// - /// Clears the cached segment from the cache - /// - protected virtual void ClearCacheSegment(string entityName, int entityId) - { - try - { - var segmentKey = GetSegmentKeyPart(entityName, entityId); - _cacheManager.Remove(BuildCacheSegmentKey(segmentKey)); - } - catch { } - } - - private string BuildCacheSegmentKey(string segment) - { - return String.Format(ACL_SEGMENT_KEY, segment); - } - - private string GetSegmentKeyPart(string entityName, int entityId) - { - return GetSegmentKeyPart(entityName, entityId, out _, out _); - } - - private string GetSegmentKeyPart(string entityName, int entityId, out int minId, out int maxId) - { - maxId = entityId.GetRange(1000, out minId); - return (entityName + "." + maxId.ToString()).ToLowerInvariant(); - } - - #endregion - } + return true; + + if (roles == null) + return false; + + foreach (var role in roles) + { + if (!role.Active) + continue; + + foreach (var role2Id in GetCustomerRoleIdsWithAccessTo(entityName, entityId)) + { + if (role.Id == role2Id) + { + // Yes, we have such permission + return true; + } + } + } + + // No permission granted + return false; + } + + #region Cache segmenting + + protected virtual IDictionary GetCacheSegment(string entityName, int entityId) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + var segmentKey = GetSegmentKeyPart(entityName, entityId, out var minEntityId, out var maxEntityId); + var cacheKey = BuildCacheSegmentKey(segmentKey); + + return _cacheManager.Get(cacheKey, () => + { + var query = from sm in _aclRecordRepository.TableUntracked + where + sm.EntityId >= minEntityId && + sm.EntityId <= maxEntityId && + sm.EntityName == entityName + select sm; + + var mappings = query.ToLookup(x => x.EntityId, x => x.CustomerRoleId); + + var dict = new Dictionary(mappings.Count); + + foreach (var sm in mappings) + { + dict[sm.Key] = sm.ToArray(); + } + + return dict; + }); + } + + /// + /// Clears the cached segment from the cache + /// + protected virtual void ClearCacheSegment(string entityName, int entityId) + { + try + { + var segmentKey = GetSegmentKeyPart(entityName, entityId); + _cacheManager.Remove(BuildCacheSegmentKey(segmentKey)); + } + catch { } + } + + private string BuildCacheSegmentKey(string segment) + { + return String.Format(ACL_SEGMENT_KEY, segment); + } + + private string GetSegmentKeyPart(string entityName, int entityId) + { + return GetSegmentKeyPart(entityName, entityId, out _, out _); + } + + private string GetSegmentKeyPart(string entityName, int entityId, out int minId, out int maxId) + { + (minId, maxId) = entityId.GetRange(1000); + return (entityName + "." + minId.ToString()).ToLowerInvariant(); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Security/IAclService.cs b/src/Libraries/SmartStore.Services/Security/IAclService.cs index 7fbc3bfd2c..c42785ee94 100644 --- a/src/Libraries/SmartStore.Services/Security/IAclService.cs +++ b/src/Libraries/SmartStore.Services/Security/IAclService.cs @@ -12,12 +12,12 @@ namespace SmartStore.Services.Security /// public partial interface IAclService { - /// - /// Gets a value indicating whether at least one ACL record is in active state system-wide - /// - bool HasActiveAcl { get; } - - /// + /// + /// Gets a value indicating whether at least one ACL record is in active state system-wide + /// + bool HasActiveAcl { get; } + + /// /// Deletes an ACL record /// /// ACL record @@ -29,7 +29,7 @@ public partial interface IAclService /// ACL record identifier /// ACL record AclRecord GetAclRecordById(int aclRecordId); - + /// /// Gets ACL records /// @@ -38,34 +38,34 @@ public partial interface IAclService /// ACL records IList GetAclRecords(T entity) where T : BaseEntity, IAclSupported; - /// - /// Gets ACL records - /// - /// Name of entity - /// Id of entity - /// ACL records - IList GetAclRecordsFor(string entityName, int entityId); - - /// - /// Save the ACL mappings for an entity - /// - /// Entity type - /// The entity - /// Array of selected customer role ids with access to the passed entity - void SaveAclMappings(T entity, params int[] selectedCustomerRoleIds) where T : BaseEntity, IAclSupported; - - /// - /// Inserts an ACL record - /// - /// ACL record - void InsertAclRecord(AclRecord aclRecord); - + /// + /// Gets ACL records + /// + /// Name of entity + /// Id of entity + /// ACL records + IList GetAclRecordsFor(string entityName, int entityId); + + /// + /// Save the ACL mappings for an entity + /// + /// Entity type + /// The entity + /// Array of selected customer role ids with access to the passed entity + void SaveAclMappings(T entity, params int[] selectedCustomerRoleIds) where T : BaseEntity, IAclSupported; + + /// + /// Inserts an ACL record + /// + /// ACL record + void InsertAclRecord(AclRecord aclRecord); + /// /// Inserts an ACL record /// /// Type /// Entity - /// Customer role id + /// Customer role id void InsertAclRecord(T entity, int customerRoleId) where T : BaseEntity, IAclSupported; /// @@ -74,115 +74,115 @@ public partial interface IAclService /// ACL record void UpdateAclRecord(AclRecord aclRecord); - /// - /// Find customer role identifiers with granted access - /// - /// Entity name to check permission for - /// Entity id to check permission for - /// Customer role identifiers - int[] GetCustomerRoleIdsWithAccessTo(string entityName, int entityId); - - /// - /// Authorize ACL permission - /// - /// Type - /// Entity name to check permission for - /// Entity id to check permission for - /// true - authorized; otherwise, false - bool Authorize(string entityName, int entityId); - - ///// - ///// Authorize ACL permission - ///// - ///// Type - ///// Entity name to check permission for - ///// Entity id to check permission for - ///// Customer - ///// true - authorized; otherwise, false - //bool Authorize(string entityName, int entityId, Customer customer); - - /// - /// Authorize ACL permission - /// - /// Entity name to check permission for - /// Entity id to check permission for - /// Roles to check access permission for. Inactive roles will be skipped. - /// true - authorized; otherwise, false - bool Authorize(string entityName, int entityId, IEnumerable roles); - } - - public static class IAclServiceExtensions - { - /// - /// Find customer role identifiers with granted access - /// - /// Type - /// Entity - /// Customer role identifiers - public static int[] GetCustomerRoleIdsWithAccessTo(this IAclService aclService, T entity) where T : BaseEntity, IAclSupported - { - if (entity == null) - return new int[0]; - - return aclService.GetCustomerRoleIdsWithAccessTo(entity.GetEntityName(), entity.Id); - } - - public static bool Authorize(this IAclService aclService, string entityName, int entityId, Customer customer) - { - return aclService.Authorize(entityName, entityId, customer?.CustomerRoleMappings?.Select(x => x.CustomerRole)); - } - - /// - /// Authorize ACL permission - /// - /// Type - /// Entity - /// true - authorized; otherwise, false - public static bool Authorize(this IAclService aclService, T entity) where T : BaseEntity, IAclSupported - { - if (entity == null) - return false; - - if (!entity.SubjectToAcl) - return true; - - return aclService.Authorize(entity.GetEntityName(), entity.Id); - } - - /// - /// Authorize ACL permission - /// - /// Type - /// Entity - /// Customer - /// true - authorized; otherwise, false - public static bool Authorize(this IAclService aclService, T entity, Customer customer) where T : BaseEntity, IAclSupported - { - if (entity == null) - return false; - - if (!entity.SubjectToAcl) - return true; - - return aclService.Authorize(entity.GetEntityName(), entity.Id, customer?.CustomerRoleMappings?.Select(x => x.CustomerRole)); - } - - /// - /// Authorize ACL permission - /// - /// Type - /// Entity - /// Roles to check access permission for. Inactive roles will be skipped. - /// true - authorized; otherwise, false - public static bool Authorize(this IAclService aclService, T entity, IEnumerable roles) where T : BaseEntity, IAclSupported - { - if (entity == null) - return false; - - if (!entity.SubjectToAcl) - return true; - - return aclService.Authorize(entity.GetEntityName(), entity.Id, roles); - } - } + /// + /// Find customer role identifiers with granted access + /// + /// Entity name to check permission for + /// Entity id to check permission for + /// Customer role identifiers + int[] GetCustomerRoleIdsWithAccessTo(string entityName, int entityId); + + /// + /// Authorize ACL permission + /// + /// Type + /// Entity name to check permission for + /// Entity id to check permission for + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId); + + ///// + ///// Authorize ACL permission + ///// + ///// Type + ///// Entity name to check permission for + ///// Entity id to check permission for + ///// Customer + ///// true - authorized; otherwise, false + //bool Authorize(string entityName, int entityId, Customer customer); + + /// + /// Authorize ACL permission + /// + /// Entity name to check permission for + /// Entity id to check permission for + /// Roles to check access permission for. Inactive roles will be skipped. + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId, IEnumerable roles); + } + + public static class IAclServiceExtensions + { + /// + /// Find customer role identifiers with granted access + /// + /// Type + /// Entity + /// Customer role identifiers + public static int[] GetCustomerRoleIdsWithAccessTo(this IAclService aclService, T entity) where T : BaseEntity, IAclSupported + { + if (entity == null) + return new int[0]; + + return aclService.GetCustomerRoleIdsWithAccessTo(entity.GetEntityName(), entity.Id); + } + + public static bool Authorize(this IAclService aclService, string entityName, int entityId, Customer customer) + { + return aclService.Authorize(entityName, entityId, customer?.CustomerRoleMappings?.Select(x => x.CustomerRole)); + } + + /// + /// Authorize ACL permission + /// + /// Type + /// Entity + /// true - authorized; otherwise, false + public static bool Authorize(this IAclService aclService, T entity) where T : BaseEntity, IAclSupported + { + if (entity == null) + return false; + + if (!entity.SubjectToAcl) + return true; + + return aclService.Authorize(entity.GetEntityName(), entity.Id); + } + + /// + /// Authorize ACL permission + /// + /// Type + /// Entity + /// Customer + /// true - authorized; otherwise, false + public static bool Authorize(this IAclService aclService, T entity, Customer customer) where T : BaseEntity, IAclSupported + { + if (entity == null) + return false; + + if (!entity.SubjectToAcl) + return true; + + return aclService.Authorize(entity.GetEntityName(), entity.Id, customer?.CustomerRoleMappings?.Select(x => x.CustomerRole)); + } + + /// + /// Authorize ACL permission + /// + /// Type + /// Entity + /// Roles to check access permission for. Inactive roles will be skipped. + /// true - authorized; otherwise, false + public static bool Authorize(this IAclService aclService, T entity, IEnumerable roles) where T : BaseEntity, IAclSupported + { + if (entity == null) + return false; + + if (!entity.SubjectToAcl) + return true; + + return aclService.Authorize(entity.GetEntityName(), entity.Id, roles); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Security/InstallPermissionsStarter.cs b/src/Libraries/SmartStore.Services/Security/InstallPermissionsStarter.cs index 1d07e7767c..3f627046c2 100644 --- a/src/Libraries/SmartStore.Services/Security/InstallPermissionsStarter.cs +++ b/src/Libraries/SmartStore.Services/Security/InstallPermissionsStarter.cs @@ -8,6 +8,7 @@ using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Core.Security; +using SmartStore.Data.Setup; namespace SmartStore.Services.Security { @@ -43,8 +44,12 @@ public void Start(HttpContextBase httpContext) var removeUnusedPermissions = true; var providers = new List(); - if (PluginManager.PluginChangeDetected || !_permissionRepository.TableUntracked.Any()) + if (PluginManager.PluginChangeDetected || DbMigrationContext.Current.GetAppliedMigrations().Any() || !_permissionRepository.TableUntracked.Any()) { + // INFO: even if no plugin has changed: directly after a DB migration this code block MUST run. It seems awkward + // that pending migrations exist when binaries has not changed. But after a manual DB reset for a migration rerun + // nobody touches the binaries usually. + // Standard permission provider and all plugin providers. var types = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true).ToList(); foreach (var type in types) diff --git a/src/Libraries/SmartStore.Services/Security/PermissionService.cs b/src/Libraries/SmartStore.Services/Security/PermissionService.cs index a578fbe997..55f515ea91 100644 --- a/src/Libraries/SmartStore.Services/Security/PermissionService.cs +++ b/src/Libraries/SmartStore.Services/Security/PermissionService.cs @@ -12,6 +12,7 @@ using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Security; +using SmartStore.Data.Utilities; using SmartStore.Services.Customers; using SmartStore.Services.Localization; @@ -78,7 +79,7 @@ public partial class PermissionService : IPermissionService { "acl", "Admin.Configuration.ACL" }, { "emailaccount", "Admin.Configuration.EmailAccounts" }, { "store", "Admin.Common.Stores" }, - { "shipping", "Admin.Configuration.DeliveryTimes" }, + { "shipping", "Admin.Configuration.Shipping.Methods" }, { "tax", "Admin.Configuration.Tax.Providers" }, { "plugin", "Admin.Configuration.Plugins" }, { "upload", "Common.Upload" }, @@ -109,6 +110,7 @@ public partial class PermissionService : IPermissionService private readonly ILocalizationService _localizationService; private readonly IWorkContext _workContext; private readonly ICacheManager _cacheManager; + private readonly IRequestCache _requestCache; public PermissionService( IRepository permissionRepository, @@ -116,7 +118,8 @@ public PermissionService( Lazy customerService, ILocalizationService localizationService, IWorkContext workContext, - ICacheManager cacheManager) + ICacheManager cacheManager, + IRequestCache requestCache) { _permissionRepository = permissionRepository; _permissionMappingRepository = permissionMappingRepository; @@ -124,6 +127,7 @@ public PermissionService( _localizationService = localizationService; _workContext = workContext; _cacheManager = cacheManager; + _requestCache = requestCache; T = NullLocalizer.Instance; Logger = NullLogger.Instance; @@ -308,11 +312,17 @@ public virtual void InstallPermissions(IPermissionProvider[] permissionProviders { if (existingRoles == null) { - var allRoles = _customerService.Value.GetAllCustomerRoles(true); + existingRoles = new Dictionary(); - existingRoles = allRoles - .Where(x => !string.IsNullOrEmpty(x.SystemName)) - .ToDictionarySafe(x => x.SystemName, x => x); + var rolesQuery = _customerService.Value.GetAllCustomerRoles(true).SourceQuery; + rolesQuery = rolesQuery.Where(x => !string.IsNullOrEmpty(x.SystemName)); + + var rolesPager = new FastPager(rolesQuery, 500); + + while (rolesPager.ReadNextPage(out var roles)) + { + roles.Each(x => existingRoles[x.SystemName] = x); + } } if (!existingRoles.TryGetValue(roleName, out var role)) @@ -360,14 +370,17 @@ public virtual void InstallPermissions(IPermissionProvider[] permissionProviders // Remove permissions no longer supported by providers. if (removeUnusedPermissions) { - var toDelete = existing.Except(providerPermissions); + var toDelete = existing.Except(providerPermissions).ToList(); if (toDelete.Any()) { clearCache = true; - var entities = _permissionRepository.Table.Where(x => toDelete.Contains(x.SystemName)).ToList(); - entities.Each(x => _permissionRepository.Delete(x)); - scope.Commit(); + foreach (var chunk in toDelete.Slice(500)) + { + var entities = _permissionRepository.Table.Where(x => chunk.Contains(x.SystemName)).ToList(); + entities.Each(x => _permissionRepository.Delete(x)); + scope.Commit(); + } if (log) { @@ -389,17 +402,13 @@ public virtual void InstallPermissions(IPermissionProvider[] permissionProviders [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual bool Authorize(PermissionRecord permission) { - return Authorize(permission, _workContext.CurrentCustomer); + return Authorize(permission?.SystemName, _workContext.CurrentCustomer); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual bool Authorize(PermissionRecord permission, Customer customer) { - if (permission == null || customer == null) - { - return false; - } - - return Authorize(permission.SystemName, customer); + return Authorize(permission?.SystemName, customer); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -415,26 +424,38 @@ public virtual bool Authorize(string permissionSystemName, Customer customer) return false; } - foreach (var role in customer.CustomerRoleMappings.Select(x => x.CustomerRole).Where(x => x.Active)) + var cacheKey = "Permission." + customer.Id.ToString() + "." + permissionSystemName; + + var authorized = _requestCache.Get(cacheKey, () => { - var tree = GetPermissionTree(role); - var node = tree.SelectNodeById(permissionSystemName); - if (node == null) - { - continue; - } + var roles = customer.CustomerRoleMappings + .Where(x => x.CustomerRole?.Active ?? false) + .Select(x => x.CustomerRole) + .ToArray(); - while (node != null && !node.Value.Allow.HasValue) - { - node = node.Parent; - } - if (node?.Value?.Allow ?? false) + foreach (var role in roles) { - return true; + var tree = GetPermissionTree(role); + var node = tree.SelectNodeById(permissionSystemName); + if (node == null) + { + continue; + } + + while (node != null && !node.Value.Allow.HasValue) + { + node = node.Parent; + } + if (node?.Value?.Allow ?? false) + { + return true; + } } - } - return false; + return false; + }); + + return authorized; } public virtual bool AuthorizeByAlias(string permissionSystemName) diff --git a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs index 5973923741..d356ce3094 100644 --- a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs +++ b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs @@ -11,7 +11,7 @@ public partial class StandardPermissionProvider : IPermissionProvider public virtual IEnumerable GetPermissions() { var permissionSystemNames = PermissionHelper.GetPermissions(typeof(Permissions)); - var permissions = permissionSystemNames.Select(x => new PermissionRecord { SystemName = x }); + var permissions = permissionSystemNames.Select(x => new PermissionRecord { SystemName = x }); return permissions; } diff --git a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs index 4ec0aa0df3..10676d15d7 100644 --- a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs @@ -9,7 +9,7 @@ namespace SmartStore.Services.Seo /// /// Provides information about URL records /// - public partial interface IUrlRecordService : IScopedService + public partial interface IUrlRecordService : IScopedService { /// /// Deletes an URL record @@ -24,12 +24,12 @@ public partial interface IUrlRecordService : IScopedService /// URL record UrlRecord GetUrlRecordById(int urlRecordId); - /// - /// Gets URL records by identifiers - /// - /// - /// List of URL records - IList GetUrlRecordsByIds(int[] urlRecordIds); + /// + /// Gets URL records by identifiers + /// + /// + /// List of URL records + IList GetUrlRecordsByIds(int[] urlRecordIds); /// /// Inserts an URL record @@ -63,47 +63,47 @@ public partial interface IUrlRecordService : IScopedService /// Customer collection IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive); - /// - /// Prefetches a collection of url records properties for a range of entities in one go - /// and caches them for the duration of the current request. - /// - /// Entity name - /// - /// The entity ids to prefetch url records for. Can be null, - /// in which case all records for the requested entity name are loaded. - /// - /// Whether represents a range of ids (perf). - /// Whether is already sorted (perf). - /// Url record collection - /// - /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). - /// - void PrefetchUrlRecords(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false); + /// + /// Prefetches a collection of url records properties for a range of entities in one go + /// and caches them for the duration of the current request. + /// + /// Entity name + /// + /// The entity ids to prefetch url records for. Can be null, + /// in which case all records for the requested entity name are loaded. + /// + /// Whether represents a range of ids (perf). + /// Whether is already sorted (perf). + /// Url record collection + /// + /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). + /// + void PrefetchUrlRecords(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false); - /// - /// Prefetches a collection of url records properties for a range of entities in one go. - /// - /// Entity name - /// - /// The entity ids to prefetch url records for. Can be null, - /// in which case all records for the requested entity name are loaded. - /// - /// Whether represents a range of ids (perf). - /// Whether is already sorted (perf). - /// Url record collection - /// - /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). - /// - UrlRecordCollection GetUrlRecordCollection(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false); + /// + /// Prefetches a collection of url records properties for a range of entities in one go. + /// + /// Entity name + /// + /// The entity ids to prefetch url records for. Can be null, + /// in which case all records for the requested entity name are loaded. + /// + /// Whether represents a range of ids (perf). + /// Whether is already sorted (perf). + /// Url record collection + /// + /// Be careful not to load large amounts of data at once (e.g. for "Product" scope with large range). + /// + UrlRecordCollection GetUrlRecordCollection(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false); - /// - /// Gets all URL records for the specified entity - /// - /// Type - /// Entity - /// Specifies whether only active URL records should be returned - /// List of URL records - IList GetUrlRecordsFor(string entityName, int entityId, bool activeOnly = false); + /// + /// Gets all URL records for the specified entity + /// + /// Type + /// Entity + /// Specifies whether only active URL records should be returned + /// List of URL records + IList GetUrlRecordsFor(string entityName, int entityId, bool activeOnly = false); /// /// Find slug @@ -126,28 +126,28 @@ public partial interface IUrlRecordService : IScopedService /// UrlRecord SaveSlug(T entity, string slug, int languageId) where T : BaseEntity, ISlugSupported; - /// - /// Save slug - /// - /// Type - /// Entity - /// Name of a property - /// Url record - UrlRecord SaveSlug(T entity, Func nameProperty) where T : BaseEntity, ISlugSupported; + /// + /// Save slug + /// + /// Type + /// Entity + /// Name of a property + /// Url record + UrlRecord SaveSlug(T entity, Func nameProperty) where T : BaseEntity, ISlugSupported; - /// - /// Get number of slugs per entity - /// - /// URL record identifier - /// Dictionary of slugs per entity count - Dictionary CountSlugsPerEntity(int[] urlRecordIds); + /// + /// Get number of slugs per entity + /// + /// URL record identifier + /// Dictionary of slugs per entity count + Dictionary CountSlugsPerEntity(int[] urlRecordIds); - /// - /// Get number of slugs per entity - /// - /// Entity name - /// Entity identifier - /// Number of slugs per entity - int CountSlugsPerEntity(string entityName, int entityId); + /// + /// Get number of slugs per entity + /// + /// Entity name + /// Entity identifier + /// Number of slugs per entity + int CountSlugsPerEntity(string entityName, int entityId); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs index 5e833a086e..0987f2b928 100644 --- a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs @@ -10,42 +10,42 @@ namespace SmartStore.Services.Seo /// public partial interface IXmlSitemapGenerator { - /// - /// Gets the sitemap partition - /// - /// - /// The page index. 0 retrieves the first document, which can be a sitemap INDEX document - /// (when the sitemap size exceeded the limits). An index greater 0 retrieves the - /// sitemap XML document at this index, but only when the sitemap is actually indexed (otherwise null is returned) - /// - /// Sitemap partition - Task GetSitemapPartAsync(int index = 0); + /// + /// Gets the sitemap partition + /// + /// + /// The page index. 0 retrieves the first document, which can be a sitemap INDEX document + /// (when the sitemap size exceeded the limits). An index greater 0 retrieves the + /// sitemap XML document at this index, but only when the sitemap is actually indexed (otherwise null is returned) + /// + /// Sitemap partition + Task GetSitemapPartAsync(int index = 0); - /// - /// Rebuilds the collection of XML sitemap documents for a store/language combination. If there are less than 1.000 sitemap - /// nodes, only one sitemap document will exist in the collection, otherwise a sitemap index document will be - /// the first entry in the collection and all other entries will be sitemap XML documents. - /// - /// The build context - /// - /// During rebuilding, requests are being served from the existing cache. - /// Once rebuild is completed, the cache is updated. - /// - Task RebuildAsync(XmlSitemapBuildContext ctx); + /// + /// Rebuilds the collection of XML sitemap documents for a store/language combination. If there are less than 1.000 sitemap + /// nodes, only one sitemap document will exist in the collection, otherwise a sitemap index document will be + /// the first entry in the collection and all other entries will be sitemap XML documents. + /// + /// The build context + /// + /// During rebuilding, requests are being served from the existing cache. + /// Once rebuild is completed, the cache is updated. + /// + Task RebuildAsync(XmlSitemapBuildContext ctx); - /// - /// Determines whether a rebuild is already running. - /// - bool IsRebuilding(int storeId, int languageId); + /// + /// Determines whether a rebuild is already running. + /// + bool IsRebuilding(int storeId, int languageId); - /// - /// Indicates whether the sitemap has been generated and cached. - /// - bool IsGenerated(int storeId, int languageId); + /// + /// Indicates whether the sitemap has been generated and cached. + /// + bool IsGenerated(int storeId, int languageId); - /// - /// Removes the sitemap from the cache for a rebuild. - /// - void Invalidate(int storeId, int languageId); - } + /// + /// Removes the sitemap from the cache for a rebuild. + /// + void Invalidate(int storeId, int languageId); + } } diff --git a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapPublisher.cs b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapPublisher.cs index eb830a5291..7e75a9f4fa 100644 --- a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapPublisher.cs +++ b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapPublisher.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using SmartStore.Core.Domain.Localization; @@ -32,7 +31,7 @@ public virtual IEnumerable EnlistNodes(Language language) public virtual XmlSitemapNode CreateNode(UrlHelper urlHelper, string baseUrl, NamedEntity entity, UrlRecordCollection slugs, Language language) { var slug = slugs.GetSlug(language.Id, entity.Id, true); - var path = urlHelper.RouteUrl(entity.EntityName, new { SeName = slug }).TrimStart('/'); + var path = urlHelper.RouteUrl(entity.EntityName, new { SeName = slug }).EmptyNull().TrimStart('/'); var loc = baseUrl + path; return new XmlSitemapNode diff --git a/src/Libraries/SmartStore.Services/Seo/NamedEntity.cs b/src/Libraries/SmartStore.Services/Seo/NamedEntity.cs index f8bb6fd0e0..79f1e0d83c 100644 --- a/src/Libraries/SmartStore.Services/Seo/NamedEntity.cs +++ b/src/Libraries/SmartStore.Services/Seo/NamedEntity.cs @@ -4,17 +4,17 @@ namespace SmartStore.Services.Seo { - public class NamedEntity : BaseEntity, ISlugSupported - { - public string EntityName { get; set; } - public string DisplayName { get; set; } - public string Slug { get; set; } - public DateTime LastMod { get; set; } - public int? LanguageId { get; set; } + public class NamedEntity : BaseEntity, ISlugSupported + { + public string EntityName { get; set; } + public string DisplayName { get; set; } + public string Slug { get; set; } + public DateTime LastMod { get; set; } + public int? LanguageId { get; set; } - public override string GetEntityName() - { - return EntityName; - } - } + public override string GetEntityName() + { + return EntityName; + } + } } diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index fd989a0f4f..e47abff4c5 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; using SmartStore.Core; using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Catalog; @@ -7,10 +8,9 @@ using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Seo; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; using SmartStore.Services.Localization; using SmartStore.Utilities; -using SmartStore.Core.Localization; -using System.Runtime.CompilerServices; namespace SmartStore.Services.Seo { @@ -37,21 +37,22 @@ public static string GetSeName(this ProductTag productTag) /// Product tag SE (search engine) name public static string GetSeName(this ProductTag productTag, int languageId) { - Guard.NotNull(productTag, nameof(productTag)); + Guard.NotNull(productTag, nameof(productTag)); - return GetSeName((string)productTag.GetLocalized(x => x.Name, languageId)); + return GetSeName((string)productTag.GetLocalized(x => x.Name, languageId)); } /// /// Gets blog post SE (search engine) name /// /// Blog post + /// Language identifier /// Blog post SE (search engine) name - public static string GetSeName(this BlogPost blogPost) + public static string GetSeName(this BlogPost blogPost, int languageId) { - Guard.NotNull(blogPost, nameof(blogPost)); + Guard.NotNull(blogPost, nameof(blogPost)); - return GetSeName(blogPost.Title); + return GetSeName(blogPost.GetLocalized(x => x.Title, languageId)); } /// @@ -61,33 +62,34 @@ public static string GetSeName(this BlogPost blogPost) /// Blog post SE (search engine) name public static string GetSeName(this BlogPostTag blogPostTag) { - Guard.NotNull(blogPostTag, nameof(blogPostTag)); + Guard.NotNull(blogPostTag, nameof(blogPostTag)); - return GetSeName(blogPostTag.Name); + return GetSeName(blogPostTag.Name); } /// /// Gets news item SE (search engine) name /// /// News item + /// Language identifier /// News item SE (search engine) name - public static string GetSeName(this NewsItem newsItem) + public static string GetSeName(this NewsItem newsItem, int languageId) { - Guard.NotNull(newsItem, nameof(newsItem)); + Guard.NotNull(newsItem, nameof(newsItem)); - return GetSeName(newsItem.Title); + return GetSeName(newsItem.GetLocalized(x => x.Title, languageId)); } - /// - /// Gets ForumTopic SE (search engine) name - /// - /// ForumTopic - /// ForumTopic SE (search engine) name - public static string GetSeName(this ForumTopic forumTopic) + /// + /// Gets ForumTopic SE (search engine) name + /// + /// ForumTopic + /// ForumTopic SE (search engine) name + public static string GetSeName(this ForumTopic forumTopic) { - Guard.NotNull(forumTopic, nameof(forumTopic)); + Guard.NotNull(forumTopic, nameof(forumTopic)); - string seName = GetSeName(forumTopic.Subject); + string seName = GetSeName(forumTopic.Subject); // Trim SE name to avoid URLs that are too long var maxLength = 100; @@ -106,11 +108,11 @@ public static string GetSeName(this ForumTopic forumTopic) /// SEO slug [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetSeName(this ICategoryNode node) - { - Guard.NotNull(node, nameof(node)); + { + Guard.NotNull(node, nameof(node)); return EngineContext.Current.Resolve().GetSeName("Category", node.Id, null); - } + } #endregion @@ -126,79 +128,79 @@ public static string GetSeName(this ICategoryNode node) /// A value indicating whether to ensure that we have at least two published languages; otherwise, load only default value /// SEO slug [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetSeName(this T entity, - int? languageId = null, - bool returnDefaultValue = true, - bool ensureTwoPublishedLanguages = true) + public static string GetSeName(this T entity, + int? languageId = null, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true) where T : BaseEntity, ISlugSupported { - Guard.NotNull(entity, nameof(entity)); - - return EngineContext.Current.Resolve().GetSeName( - entity.GetEntityName(), - entity.Id, - languageId, - returnDefaultValue, - ensureTwoPublishedLanguages); + Guard.NotNull(entity, nameof(entity)); + + return EngineContext.Current.Resolve().GetSeName( + entity.GetEntityName(), + entity.Id, + languageId, + returnDefaultValue, + ensureTwoPublishedLanguages); } - /// - /// Validate search engine name - /// - /// Entity - /// Search engine name to validate - /// User-friendly name used to generate sename - /// Ensreu that sename is not empty - /// Valid sename - public static string ValidateSeName(this T entity, string seName, string name, bool ensureNotEmpty, int? languageId = null) + /// + /// Validate search engine name + /// + /// Entity + /// Search engine name to validate + /// User-friendly name used to generate sename + /// Ensreu that sename is not empty + /// Valid sename + public static string ValidateSeName(this T entity, string seName, string name, bool ensureNotEmpty, int? languageId = null) where T : BaseEntity, ISlugSupported { - return entity.ValidateSeName( - seName, - name, - ensureNotEmpty, - EngineContext.Current.Resolve(), - EngineContext.Current.Resolve(), - languageId); + return entity.ValidateSeName( + seName, + name, + ensureNotEmpty, + EngineContext.Current.Resolve(), + EngineContext.Current.Resolve(), + languageId); } - public static string ValidateSeName(this T entity, - string seName, - string name, - bool ensureNotEmpty, - IUrlRecordService urlRecordService, - SeoSettings seoSettings, - int? languageId = null, - Func extraSlugLookup = null) - where T : BaseEntity, ISlugSupported - { - Guard.NotNull(urlRecordService, nameof(urlRecordService)); - Guard.NotNull(seoSettings, nameof(seoSettings)); - Guard.NotNull(entity, nameof(entity)); - - // use name if sename is not specified - if (String.IsNullOrWhiteSpace(seName) && !String.IsNullOrWhiteSpace(name)) - seName = name; - - // validation - seName = GetSeName(seName, seoSettings); - - // max length - seName = seName.Truncate(400); - - if (String.IsNullOrWhiteSpace(seName)) - { - if (ensureNotEmpty) - { - // use entity identifier as sename if empty - seName = entity.Id.ToString(); - } - else - { - // return. no need for further processing - return seName; - } - } + public static string ValidateSeName(this T entity, + string seName, + string name, + bool ensureNotEmpty, + IUrlRecordService urlRecordService, + SeoSettings seoSettings, + int? languageId = null, + Func extraSlugLookup = null) + where T : BaseEntity, ISlugSupported + { + Guard.NotNull(urlRecordService, nameof(urlRecordService)); + Guard.NotNull(seoSettings, nameof(seoSettings)); + Guard.NotNull(entity, nameof(entity)); + + // use name if sename is not specified + if (String.IsNullOrWhiteSpace(seName) && !String.IsNullOrWhiteSpace(name)) + seName = name; + + // validation + seName = GetSeName(seName, seoSettings); + + // max length + seName = seName.Truncate(400); + + if (String.IsNullOrWhiteSpace(seName)) + { + if (ensureNotEmpty) + { + // use entity identifier as sename if empty + seName = entity.Id.ToString(); + } + else + { + // return. no need for further processing + return seName; + } + } // validate and alter SeName if it could be interpreted as SEO code if (LocalizationHelper.IsValidCultureCode(seName)) @@ -208,33 +210,33 @@ public static string ValidateSeName(this T entity, seName += "-0"; } } - - // ensure this sename is not reserved yet - string entityName = entity.GetEntityName(); - int i = 2; - var tempSeName = seName; - while (true) - { - // check whether such slug already exists (and that it's not the current entity) - var urlRecord = urlRecordService.GetBySlug(tempSeName) ?? extraSlugLookup?.Invoke(tempSeName); - var reserved1 = urlRecord != null && !(urlRecord.EntityId == entity.Id && urlRecord.EntityName.Equals(entityName, StringComparison.InvariantCultureIgnoreCase)); + // ensure this sename is not reserved yet + string entityName = entity.GetEntityName(); + int i = 2; + var tempSeName = seName; + + while (true) + { + // check whether such slug already exists (and that it's not the current entity) + var urlRecord = urlRecordService.GetBySlug(tempSeName) ?? extraSlugLookup?.Invoke(tempSeName); + var reserved1 = urlRecord != null && !(urlRecord.EntityId == entity.Id && urlRecord.EntityName.Equals(entityName, StringComparison.InvariantCultureIgnoreCase)); - if (!reserved1 && urlRecord != null && languageId.HasValue) - reserved1 = (urlRecord.LanguageId != languageId.Value); + if (!reserved1 && urlRecord != null && languageId.HasValue) + reserved1 = (urlRecord.LanguageId != languageId.Value); - // and it's not in the list of reserved slugs - var reserved2 = seoSettings.ReservedUrlRecordSlugs.Contains(tempSeName, StringComparer.InvariantCultureIgnoreCase); - if (!reserved1 && !reserved2) - break; + // and it's not in the list of reserved slugs + var reserved2 = seoSettings.ReservedUrlRecordSlugs.Contains(tempSeName, StringComparer.InvariantCultureIgnoreCase); + if (!reserved1 && !reserved2) + break; - tempSeName = string.Format("{0}-{1}", seName, i); - i++; - } - seName = tempSeName; + tempSeName = string.Format("{0}-{1}", seName, i); + i++; + } + seName = tempSeName; - return seName; - } + return seName; + } /// @@ -245,24 +247,24 @@ public static string ValidateSeName(this T entity, public static string GetSeName(string name) { var seoSettings = EngineContext.Current.Resolve(); - return GetSeName(name, seoSettings); + return GetSeName(name, seoSettings); } - /// - /// Get SEO friendly name - /// - /// Name - /// SEO settings - /// Result - public static string GetSeName(string name, SeoSettings seoSettings) - { - return SeoHelper.GetSeName( - name, - seoSettings == null ? false : seoSettings.ConvertNonWesternChars, - seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls, + /// + /// Get SEO friendly name + /// + /// Name + /// SEO settings + /// Result + public static string GetSeName(string name, SeoSettings seoSettings) + { + return SeoHelper.GetSeName( + name, + seoSettings == null ? false : seoSettings.ConvertNonWesternChars, + seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls, true, - seoSettings?.SeoNameCharConversion); - } + seoSettings?.SeoNameCharConversion); + } #endregion } diff --git a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs index 034f4b3414..e2e99c680f 100644 --- a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs +++ b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs @@ -11,45 +11,45 @@ namespace SmartStore.Services.Seo { - public class RebuildXmlSitemapTask : AsyncTask - { - private readonly IStoreService _storeService; - private readonly ILanguageService _languageService; - private readonly IXmlSitemapGenerator _generator; - private readonly ISettingService _settingService; + public class RebuildXmlSitemapTask : AsyncTask + { + private readonly IStoreService _storeService; + private readonly ILanguageService _languageService; + private readonly IXmlSitemapGenerator _generator; + private readonly ISettingService _settingService; - public RebuildXmlSitemapTask( - IStoreService storeService, - ILanguageService languageService, - IXmlSitemapGenerator generator, - ISettingService settingService) - { - _storeService = storeService; - _languageService = languageService; - _generator = generator; - _settingService = settingService; - } + public RebuildXmlSitemapTask( + IStoreService storeService, + ILanguageService languageService, + IXmlSitemapGenerator generator, + ISettingService settingService) + { + _storeService = storeService; + _languageService = languageService; + _generator = generator; + _settingService = settingService; + } - public override async Task ExecuteAsync(TaskExecutionContext ctx) - { - var stores = _storeService.GetAllStores(); - - foreach (var store in stores) - { - var languages = _languageService.GetAllLanguages(false, store.Id); - var buildContext = new XmlSitemapBuildContext(store, languages.ToArray(), _settingService, _storeService.IsSingleStoreMode()) - { - CancellationToken = ctx.CancellationToken, - ProgressCallback = OnProgress - }; + public override async Task ExecuteAsync(TaskExecutionContext ctx) + { + var stores = _storeService.GetAllStores(); - await _generator.RebuildAsync(buildContext); - } + foreach (var store in stores) + { + var languages = _languageService.GetAllLanguages(false, store.Id); + var buildContext = new XmlSitemapBuildContext(store, languages.ToArray(), _settingService, _storeService.IsSingleStoreMode()) + { + CancellationToken = ctx.CancellationToken, + ProgressCallback = OnProgress + }; - void OnProgress(int value, int max, string msg) - { - ctx.SetProgress(value, max, msg, true); - } - } - } + await _generator.RebuildAsync(buildContext); + } + + void OnProgress(int value, int max, string msg) + { + ctx.SetProgress(value, max, msg, true); + } + } + } } diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index adf9767858..2a723529ee 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -13,81 +13,81 @@ namespace SmartStore.Services.Seo { public partial class UrlRecordService : ScopedServiceBase, IUrlRecordService { - /// - /// 0 = segment (EntityName.IdRange), 1 = language id - /// - const string URLRECORD_SEGMENT_KEY = "urlrecord:segment:{0}-lang-{1}"; - const string URLRECORD_SEGMENT_PATTERN = "urlrecord:segment:{0}*"; - const string URLRECORD_ALL_PATTERN = "urlrecord:*"; - const string URLRECORD_ALL_ACTIVESLUGS_KEY = "urlrecord:all-active-slugs"; - - private readonly IRepository _urlRecordRepository; + /// + /// 0 = segment (EntityName.IdRange), 1 = language id + /// + const string URLRECORD_SEGMENT_KEY = "urlrecord:segment:{0}-lang-{1}"; + const string URLRECORD_SEGMENT_PATTERN = "urlrecord:segment:{0}*"; + const string URLRECORD_ALL_PATTERN = "urlrecord:*"; + const string URLRECORD_ALL_ACTIVESLUGS_KEY = "urlrecord:all-active-slugs"; + + private readonly IRepository _urlRecordRepository; private readonly ICacheManager _cacheManager; - private readonly SeoSettings _seoSettings; - private readonly PerformanceSettings _performanceSettings; + private readonly SeoSettings _seoSettings; + private readonly PerformanceSettings _performanceSettings; - private readonly IDictionary _prefetchedCollections; - private static int _lastCacheSegmentSize = -1; + private readonly IDictionary _prefetchedCollections; + private static int _lastCacheSegmentSize = -1; - public UrlRecordService( - ICacheManager cacheManager, - IRepository urlRecordRepository, - SeoSettings seoSettings, - PerformanceSettings performanceSettings) + public UrlRecordService( + ICacheManager cacheManager, + IRepository urlRecordRepository, + SeoSettings seoSettings, + PerformanceSettings performanceSettings) { _cacheManager = cacheManager; _urlRecordRepository = urlRecordRepository; - _seoSettings = seoSettings; - _performanceSettings = performanceSettings; + _seoSettings = seoSettings; + _performanceSettings = performanceSettings; - _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); + _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); - ValidateCacheState(); + ValidateCacheState(); } - private void ValidateCacheState() - { - // Ensure that after a segment size change the cache segments are invalidated. - var size = _performanceSettings.CacheSegmentSize; - var changed = _lastCacheSegmentSize == -1; - - if (size <= 0) - { - _performanceSettings.CacheSegmentSize = size = 1; - } - - if (_lastCacheSegmentSize > 0 && _lastCacheSegmentSize != size) - { - _cacheManager.RemoveByPattern(URLRECORD_SEGMENT_PATTERN); - changed = true; - } - - if (changed) - { - Interlocked.Exchange(ref _lastCacheSegmentSize, size); - } - } - - protected override void OnClearCache() - { - _cacheManager.Remove(URLRECORD_ALL_PATTERN); - } - - public virtual void DeleteUrlRecord(UrlRecord urlRecord) + private void ValidateCacheState() { - Guard.NotNull(urlRecord, nameof(urlRecord)); + // Ensure that after a segment size change the cache segments are invalidated. + var size = _performanceSettings.CacheSegmentSize; + var changed = _lastCacheSegmentSize == -1; - try - { - // cache - ClearCacheSegment(urlRecord.EntityName, urlRecord.EntityId, urlRecord.LanguageId); - HasChanges = true; + if (size <= 0) + { + _performanceSettings.CacheSegmentSize = size = 1; + } + + if (_lastCacheSegmentSize > 0 && _lastCacheSegmentSize != size) + { + _cacheManager.RemoveByPattern(URLRECORD_SEGMENT_PATTERN); + changed = true; + } - // db - _urlRecordRepository.Delete(urlRecord); - } - catch { } - } + if (changed) + { + Interlocked.Exchange(ref _lastCacheSegmentSize, size); + } + } + + protected override void OnClearCache() + { + _cacheManager.RemoveByPattern(URLRECORD_ALL_PATTERN); + } + + public virtual void DeleteUrlRecord(UrlRecord urlRecord) + { + Guard.NotNull(urlRecord, nameof(urlRecord)); + + try + { + // cache + ClearCacheSegment(urlRecord.EntityName, urlRecord.EntityId, urlRecord.LanguageId); + HasChanges = true; + + // db + _urlRecordRepository.Delete(urlRecord); + } + catch { } + } public virtual UrlRecord GetUrlRecordById(int urlRecordId) { @@ -98,267 +98,267 @@ public virtual UrlRecord GetUrlRecordById(int urlRecordId) return urlRecord; } - public virtual IList GetUrlRecordsByIds(int[] urlRecordIds) - { - if (urlRecordIds == null || urlRecordIds.Length == 0) - return new List(); + public virtual IList GetUrlRecordsByIds(int[] urlRecordIds) + { + if (urlRecordIds == null || urlRecordIds.Length == 0) + return new List(); - var urlRecords = _urlRecordRepository.Table - .Where(x => urlRecordIds.Contains(x.Id)) - .ToList(); + var urlRecords = _urlRecordRepository.Table + .Where(x => urlRecordIds.Contains(x.Id)) + .ToList(); - return urlRecords; - } + return urlRecords; + } public virtual void InsertUrlRecord(UrlRecord urlRecord) { - Guard.NotNull(urlRecord, nameof(urlRecord)); - - try - { - // db - _urlRecordRepository.Insert(urlRecord); - HasChanges = true; - - // cache - ClearCacheSegment(urlRecord.EntityName, urlRecord.EntityId, urlRecord.LanguageId); - } - catch { } + Guard.NotNull(urlRecord, nameof(urlRecord)); + + try + { + // db + _urlRecordRepository.Insert(urlRecord); + HasChanges = true; + + // cache + ClearCacheSegment(urlRecord.EntityName, urlRecord.EntityId, urlRecord.LanguageId); + } + catch { } } public virtual void UpdateUrlRecord(UrlRecord urlRecord) { - Guard.NotNull(urlRecord, nameof(urlRecord)); + Guard.NotNull(urlRecord, nameof(urlRecord)); - try - { - // db - _urlRecordRepository.Update(urlRecord); - HasChanges = true; + try + { + // db + _urlRecordRepository.Update(urlRecord); + HasChanges = true; - // cache - ClearCacheSegment(urlRecord.EntityName, urlRecord.EntityId, urlRecord.LanguageId); - } - catch { } - } + // cache + ClearCacheSegment(urlRecord.EntityName, urlRecord.EntityId, urlRecord.LanguageId); + } + catch { } + } public virtual IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive) { var query = _urlRecordRepository.Table; - if (slug.HasValue()) - query = query.Where(x => x.Slug.Contains(slug)); + if (slug.HasValue()) + query = query.Where(x => x.Slug.Contains(slug)); + + if (entityName.HasValue()) + query = query.Where(x => x.EntityName == entityName); + + if (entityId.HasValue) + query = query.Where(x => x.EntityId == entityId.Value); + + if (isActive.HasValue) + query = query.Where(x => x.IsActive == isActive.Value); + + if (languageId.HasValue) + query = query.Where(x => x.LanguageId == languageId); - if (entityName.HasValue()) - query = query.Where(x => x.EntityName == entityName); + query = query.OrderBy(x => x.Slug); - if (entityId.HasValue) - query = query.Where(x => x.EntityId == entityId.Value); + var urlRecords = new PagedList(query, pageIndex, pageSize); + return urlRecords; + } + + public virtual IList GetUrlRecordsFor(string entityName, int entityId, bool activeOnly = false) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + var query = from ur in _urlRecordRepository.Table + where ur.EntityId == entityId && + ur.EntityName == entityName + select ur; + + if (activeOnly) + { + query = query.Where(ur => ur.IsActive); + } - if (isActive.HasValue) - query = query.Where(x => x.IsActive == isActive.Value); + return query.ToList(); + } - if (languageId.HasValue) - query = query.Where(x => x.LanguageId == languageId); + public virtual void PrefetchUrlRecords(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) + { + var collection = GetUrlRecordCollectionInternal(entityName, languageIds, entityIds, isRange, isSorted); - query = query.OrderBy(x => x.Slug); + if (_prefetchedCollections.TryGetValue(entityName, out var existing)) + { + collection.MergeWith(existing); + } + else + { + _prefetchedCollections[entityName] = collection; + } + } - var urlRecords = new PagedList(query, pageIndex, pageSize); - return urlRecords; + public virtual UrlRecordCollection GetUrlRecordCollection(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) + { + return GetUrlRecordCollectionInternal(entityName, languageIds, entityIds, isRange, isSorted); } - public virtual IList GetUrlRecordsFor(string entityName, int entityId, bool activeOnly = false) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - var query = from ur in _urlRecordRepository.Table - where ur.EntityId == entityId && - ur.EntityName == entityName - select ur; - - if (activeOnly) - { - query = query.Where(ur => ur.IsActive); - } - - return query.ToList(); - } - - public virtual void PrefetchUrlRecords(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) - { - var collection = GetUrlRecordCollectionInternal(entityName, languageIds, entityIds, isRange, isSorted); - - if (_prefetchedCollections.TryGetValue(entityName, out var existing)) - { - collection.MergeWith(existing); - } - else - { - _prefetchedCollections[entityName] = collection; - } - } - - public virtual UrlRecordCollection GetUrlRecordCollection(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) - { - return GetUrlRecordCollectionInternal(entityName, languageIds, entityIds, isRange, isSorted); - } - - public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - using (new DbContextScope(proxyCreation: false, lazyLoading: false)) - { - var query = from x in _urlRecordRepository.TableUntracked - where x.EntityName == entityName && x.IsActive - select x; - - var requestedSet = entityIds; - - if (entityIds != null && entityIds.Length > 0) - { - if (isRange) - { - if (!isSorted) - { - Array.Sort(entityIds); - } - - var min = entityIds[0]; - var max = entityIds[entityIds.Length - 1]; - - if (entityIds.Length == 2 && max > min + 1) - { - // Only min & max were passed, create the range sequence. - requestedSet = Enumerable.Range(min, max - min + 1).ToArray(); - } - - query = query.Where(x => x.EntityId >= min && x.EntityId <= max); - } - else - { - requestedSet = entityIds; - query = query.Where(x => entityIds.Contains(x.EntityId)); - } - } - - if (languageIds != null && languageIds.Length > 0) - { - if (languageIds.Length == 1) - { + public virtual UrlRecordCollection GetUrlRecordCollectionInternal(string entityName, int[] languageIds, int[] entityIds, bool isRange = false, bool isSorted = false) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) + { + var query = from x in _urlRecordRepository.TableUntracked + where x.EntityName == entityName && x.IsActive + select x; + + var requestedSet = entityIds; + + if (entityIds != null && entityIds.Length > 0) + { + if (isRange) + { + if (!isSorted) + { + Array.Sort(entityIds); + } + + var min = entityIds[0]; + var max = entityIds[entityIds.Length - 1]; + + if (entityIds.Length == 2 && max > min + 1) + { + // Only min & max were passed, create the range sequence. + requestedSet = Enumerable.Range(min, max - min + 1).ToArray(); + } + + query = query.Where(x => x.EntityId >= min && x.EntityId <= max); + } + else + { + requestedSet = entityIds; + query = query.Where(x => entityIds.Contains(x.EntityId)); + } + } + + if (languageIds != null && languageIds.Length > 0) + { + if (languageIds.Length == 1) + { // Avoid "The LINQ expression node type 'ArrayIndex' is not supported in LINQ to Entities". var languageId = languageIds[0]; query = query.Where(x => x.LanguageId == languageId); - } - else - { - query = query.Where(x => languageIds.Contains(x.LanguageId)); - } - } - - // Don't sort DESC, because latter items overwrite exisiting ones (it's the same as sorting DESC and taking the first) - return new UrlRecordCollection(entityName, requestedSet, query.OrderBy(x => x.Id).ToList()); - } - } - - public virtual UrlRecord GetBySlug(string slug) - { - // INFO: (mc) Caching unnecessary here. This is not a 'bottleneck' function. - if (String.IsNullOrEmpty(slug)) - return null; - - var query = from ur in _urlRecordRepository.Table - where ur.Slug == slug - select ur; - - var urlRecord = query.FirstOrDefault(); - return urlRecord; - } + } + else + { + query = query.Where(x => languageIds.Contains(x.LanguageId)); + } + } + + // Don't sort DESC, because latter items overwrite exisiting ones (it's the same as sorting DESC and taking the first) + return new UrlRecordCollection(entityName, requestedSet, query.OrderBy(x => x.Id).ToList()); + } + } + + public virtual UrlRecord GetBySlug(string slug) + { + // INFO: (mc) Caching unnecessary here. This is not a 'bottleneck' function. + if (String.IsNullOrEmpty(slug)) + return null; + + var query = from ur in _urlRecordRepository.Table + where ur.Slug == slug + select ur; + + var urlRecord = query.FirstOrDefault(); + return urlRecord; + } public virtual string GetActiveSlug(int entityId, string entityName, int languageId) { - if (_prefetchedCollections.TryGetValue(entityName, out var collection)) - { - var cachedItem = collection.Find(languageId, entityId); - if (cachedItem != null) - { - return cachedItem.Slug.EmptyNull(); - } - } - - if (IsInScope) - { - return GetActiveSlugUncached(entityId, entityName, languageId); - } - - string slug = null; - - if (_seoSettings.LoadAllUrlAliasesOnStartup) - { - var allActiveSlugs = _cacheManager.Get(URLRECORD_ALL_ACTIVESLUGS_KEY, () => - { - using (new DbContextScope(proxyCreation: false, lazyLoading: false)) - { - var query = from x in _urlRecordRepository.TableUntracked - where x.IsActive - orderby x.Id descending - select x; - - var result = query.ToDictionarySafe( - x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), - x => x.Slug, - StringComparer.OrdinalIgnoreCase); - - return result; - } - }, independent: true); - - var key = GenerateKey(entityId, entityName, languageId); - if (!allActiveSlugs.TryGetValue(key, out slug)) - { - return string.Empty; - } - } - else - { - var slugs = GetCacheSegment(entityName, entityId, languageId); - - if (!slugs.TryGetValue(entityId, out slug)) - { - return string.Empty; - } - } - - return slug; + if (_prefetchedCollections.TryGetValue(entityName, out var collection)) + { + var cachedItem = collection.Find(languageId, entityId); + if (cachedItem != null) + { + return cachedItem.Slug.EmptyNull(); + } + } + + if (IsInScope) + { + return GetActiveSlugUncached(entityId, entityName, languageId); + } + + string slug = null; + + if (_seoSettings.LoadAllUrlAliasesOnStartup) + { + var allActiveSlugs = _cacheManager.Get(URLRECORD_ALL_ACTIVESLUGS_KEY, () => + { + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) + { + var query = from x in _urlRecordRepository.TableUntracked + where x.IsActive + orderby x.Id descending + select x; + + var result = query.ToDictionarySafe( + x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), + x => x.Slug, + StringComparer.OrdinalIgnoreCase); + + return result; + } + }, independent: true); + + var key = GenerateKey(entityId, entityName, languageId); + if (!allActiveSlugs.TryGetValue(key, out slug)) + { + return string.Empty; + } + } + else + { + var slugs = GetCacheSegment(entityName, entityId, languageId); + + if (!slugs.TryGetValue(entityId, out slug)) + { + return string.Empty; + } + } + + return slug; } - protected string GetActiveSlugUncached(int entityId, string entityName, int languageId) - { - var query = from x in _urlRecordRepository.TableUntracked - where - x.EntityId == entityId && - x.EntityName == entityName && - x.LanguageId == languageId && - x.IsActive - orderby x.Id descending - select x.Slug; - - return query.FirstOrDefault().EmptyNull(); - } - - public virtual UrlRecord SaveSlug(T entity, string slug, int languageId) where T : BaseEntity, ISlugSupported + protected string GetActiveSlugUncached(int entityId, string entityName, int languageId) { - Guard.NotNull(entity, nameof(entity)); + var query = from x in _urlRecordRepository.TableUntracked + where + x.EntityId == entityId && + x.EntityName == entityName && + x.LanguageId == languageId && + x.IsActive + orderby x.Id descending + select x.Slug; + + return query.FirstOrDefault().EmptyNull(); + } + + public virtual UrlRecord SaveSlug(T entity, string slug, int languageId) where T : BaseEntity, ISlugSupported + { + Guard.NotNull(entity, nameof(entity)); int entityId = entity.Id; string entityName = entity.GetEntityName(); - UrlRecord result = null; + UrlRecord result = null; var query = from ur in _urlRecordRepository.Table where ur.EntityId == entityId && ur.EntityName == entityName && ur.LanguageId == languageId - orderby ur.Id descending + orderby ur.Id descending select ur; var allUrlRecords = query.ToList(); @@ -385,7 +385,7 @@ orderby ur.Id descending IsActive = true, }; InsertUrlRecord(urlRecord); - result = urlRecord; + result = urlRecord; } } @@ -421,164 +421,164 @@ orderby ur.Id descending } else { - // MC: Absolutely ensure, that we have no duplicate active record for this entity. - // In such case a record other than "activeUrlRecord" could have same seName - // and the DB would report an Index error. - var alreadyActiveDuplicate = allUrlRecords.FirstOrDefault(x => x.Slug.IsCaseInsensitiveEqual(slug) && x.IsActive); - if (alreadyActiveDuplicate != null) - { - // deactivate all - allUrlRecords.Each(x => x.IsActive = false); - // set the existing one to active again - alreadyActiveDuplicate.IsActive = true; - // update all records - allUrlRecords.Each(x => UpdateUrlRecord(x)); - } - else - { - // insert new record - // we do not update the existing record because we should track all previously entered slugs - // to ensure that URLs will work fine - var urlRecord = new UrlRecord - { - EntityId = entity.Id, - EntityName = entityName, - Slug = slug, - LanguageId = languageId, - IsActive = true, - }; - InsertUrlRecord(urlRecord); - result = urlRecord; - - // disable the previous active URL record - activeUrlRecord.IsActive = false; - UpdateUrlRecord(activeUrlRecord); - } + // MC: Absolutely ensure, that we have no duplicate active record for this entity. + // In such case a record other than "activeUrlRecord" could have same seName + // and the DB would report an Index error. + var alreadyActiveDuplicate = allUrlRecords.FirstOrDefault(x => x.Slug.IsCaseInsensitiveEqual(slug) && x.IsActive); + if (alreadyActiveDuplicate != null) + { + // deactivate all + allUrlRecords.Each(x => x.IsActive = false); + // set the existing one to active again + alreadyActiveDuplicate.IsActive = true; + // update all records + allUrlRecords.Each(x => UpdateUrlRecord(x)); + } + else + { + // insert new record + // we do not update the existing record because we should track all previously entered slugs + // to ensure that URLs will work fine + var urlRecord = new UrlRecord + { + EntityId = entity.Id, + EntityName = entityName, + Slug = slug, + LanguageId = languageId, + IsActive = true, + }; + InsertUrlRecord(urlRecord); + result = urlRecord; + + // disable the previous active URL record + activeUrlRecord.IsActive = false; + UpdateUrlRecord(activeUrlRecord); + } } } } - return result; + return result; + } + + public virtual UrlRecord SaveSlug(T entity, Func nameProperty) where T : BaseEntity, ISlugSupported + { + string name = nameProperty.Invoke(entity); + + string existingSeName = entity.GetSeName(0, true, false); + existingSeName = entity.ValidateSeName(existingSeName, name, true); + + return SaveSlug(entity, existingSeName, 0); + } + + private string GenerateKey(int entityId, string entityName, int languageId) + { + return "{0}.{1}.{2}".FormatInvariant(entityId, entityName, languageId); } - public virtual UrlRecord SaveSlug(T entity, Func nameProperty) where T : BaseEntity, ISlugSupported - { - string name = nameProperty.Invoke(entity); - - string existingSeName = entity.GetSeName(0, true, false); - existingSeName = entity.ValidateSeName(existingSeName, name, true); - - return SaveSlug(entity, existingSeName, 0); - } - - private string GenerateKey(int entityId, string entityName, int languageId) - { - return "{0}.{1}.{2}".FormatInvariant(entityId, entityName, languageId); - } - - public virtual Dictionary CountSlugsPerEntity(int[] urlRecordIds) - { - if (urlRecordIds == null || urlRecordIds.Length == 0) - return new Dictionary(); - - var query = - from x in _urlRecordRepository.TableUntracked - where urlRecordIds.Contains(x.Id) - select new - { - Id = x.Id, - Count = _urlRecordRepository.TableUntracked.Where(y => y.EntityName == x.EntityName && y.EntityId == x.EntityId).Count() - }; - - var result = query - .ToList() - .ToDictionary(x => x.Id, x => x.Count); - - return result; - } - - public virtual int CountSlugsPerEntity(string entityName, int entityId) - { - var count = _urlRecordRepository.Table - .Where(x => x.EntityName == entityName && x.EntityId == entityId) - .Count(); - - return count; - } - - protected virtual IDictionary GetCacheSegment(string entityName, int entityId, int languageId) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - var segmentKey = GetSegmentKeyPart(entityName, entityId, out var minEntityId, out var maxEntityId); - var cacheKey = BuildCacheSegmentKey(segmentKey, languageId); - - return _cacheManager.Get(cacheKey, () => - { - using (new DbContextScope(proxyCreation: false, lazyLoading: false)) - { - var query = from ur in _urlRecordRepository.TableUntracked - where - ur.EntityId >= minEntityId && - ur.EntityId <= maxEntityId && - ur.EntityName == entityName && - ur.LanguageId == languageId && - ur.IsActive - orderby ur.Id descending - select ur; - - var urlRecords = query.ToList(); - - var dict = new Dictionary(urlRecords.Count); - - foreach (var ur in urlRecords) - { - dict[ur.EntityId] = ur.Slug.EmptyNull(); - } - - return dict; - } - }, independent: true); - } - - /// - /// Clears the cached segment from the cache - /// - protected virtual void ClearCacheSegment(string entityName, int entityId, int? languageId = null) - { - if (IsInScope) - return; - - var segmentKey = GetSegmentKeyPart(entityName, entityId); - - if (languageId.HasValue && languageId.Value > 0) - { - _cacheManager.Remove(BuildCacheSegmentKey(segmentKey, languageId.Value)); - } - else - { - _cacheManager.RemoveByPattern(URLRECORD_SEGMENT_PATTERN.FormatInvariant(segmentKey)); - } - - // always delete this (in case when LoadAllOnStartup is true) - _cacheManager.Remove(URLRECORD_ALL_ACTIVESLUGS_KEY); - } - - private string BuildCacheSegmentKey(string segment, int languageId) - { - return String.Format(URLRECORD_SEGMENT_KEY, segment, languageId); - } - - private string GetSegmentKeyPart(string entityName, int entityId) - { - return GetSegmentKeyPart(entityName, entityId, out _, out _); - } - - private string GetSegmentKeyPart(string entityName, int entityId, out int minId, out int maxId) - { - maxId = entityId.GetRange(_performanceSettings.CacheSegmentSize, out minId); - return (entityName + "." + maxId.ToString()).ToLowerInvariant(); - } - } + public virtual Dictionary CountSlugsPerEntity(int[] urlRecordIds) + { + if (urlRecordIds == null || urlRecordIds.Length == 0) + return new Dictionary(); + + var query = + from x in _urlRecordRepository.TableUntracked + where urlRecordIds.Contains(x.Id) + select new + { + Id = x.Id, + Count = _urlRecordRepository.TableUntracked.Where(y => y.EntityName == x.EntityName && y.EntityId == x.EntityId).Count() + }; + + var result = query + .ToList() + .ToDictionary(x => x.Id, x => x.Count); + + return result; + } + + public virtual int CountSlugsPerEntity(string entityName, int entityId) + { + var count = _urlRecordRepository.Table + .Where(x => x.EntityName == entityName && x.EntityId == entityId) + .Count(); + + return count; + } + + protected virtual IDictionary GetCacheSegment(string entityName, int entityId, int languageId) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + var segmentKey = GetSegmentKeyPart(entityName, entityId, out var minEntityId, out var maxEntityId); + var cacheKey = BuildCacheSegmentKey(segmentKey, languageId); + + return _cacheManager.Get(cacheKey, () => + { + using (new DbContextScope(proxyCreation: false, lazyLoading: false)) + { + var query = from ur in _urlRecordRepository.TableUntracked + where + ur.EntityId >= minEntityId && + ur.EntityId <= maxEntityId && + ur.EntityName == entityName && + ur.LanguageId == languageId && + ur.IsActive + orderby ur.Id descending + select ur; + + var urlRecords = query.ToList(); + + var dict = new Dictionary(urlRecords.Count); + + foreach (var ur in urlRecords) + { + dict[ur.EntityId] = ur.Slug.EmptyNull(); + } + + return dict; + } + }, independent: true); + } + + /// + /// Clears the cached segment from the cache + /// + protected virtual void ClearCacheSegment(string entityName, int entityId, int? languageId = null) + { + if (IsInScope) + return; + + var segmentKey = GetSegmentKeyPart(entityName, entityId); + + if (languageId.HasValue && languageId.Value > 0) + { + _cacheManager.Remove(BuildCacheSegmentKey(segmentKey, languageId.Value)); + } + else + { + _cacheManager.RemoveByPattern(URLRECORD_SEGMENT_PATTERN.FormatInvariant(segmentKey)); + } + + // always delete this (in case when LoadAllOnStartup is true) + _cacheManager.Remove(URLRECORD_ALL_ACTIVESLUGS_KEY); + } + + private string BuildCacheSegmentKey(string segment, int languageId) + { + return string.Format(URLRECORD_SEGMENT_KEY, segment, languageId); + } + + private string GetSegmentKeyPart(string entityName, int entityId) + { + return GetSegmentKeyPart(entityName, entityId, out _, out _); + } + + private string GetSegmentKeyPart(string entityName, int entityId, out int minId, out int maxId) + { + (minId, maxId) = entityId.GetRange(_performanceSettings.CacheSegmentSize); + return (entityName + "." + minId.ToString()).ToLowerInvariant(); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs index 02ab997f92..82f7514c67 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapBuildContext.cs @@ -9,38 +9,38 @@ namespace SmartStore.Services.Seo { - public class XmlSitemapBuildContext - { - private readonly ISettingService _settingService; - private readonly bool _isSingleStoreMode; - - public XmlSitemapBuildContext(Store store, Language[] languages, ISettingService settingService, bool isSingleStoreMode) - { - Guard.NotNull(store, nameof(store)); - Guard.NotEmpty(languages, nameof(languages)); - - Store = store; - Languages = languages; - Protocol = Store.ForceSslForAllPages ? "https" : "http"; - - _settingService = settingService; - _isSingleStoreMode = isSingleStoreMode; - - RequestStoreId = _isSingleStoreMode ? 0 : store.Id; - } - - public CancellationToken CancellationToken { get; set; } - public ProgressCallback ProgressCallback { get; set; } - public Store Store { get; set; } - public int RequestStoreId { get; private set; } - public Language[] Languages { get; set; } - public int MaximumNodeCount { get; set; } = XmlSitemapGenerator.MaximumSiteMapNodeCount; - - public string Protocol { get; private set; } - - public T LoadSetting() where T : ISettings, new() - { - return _settingService.LoadSetting(RequestStoreId); - } - } + public class XmlSitemapBuildContext + { + private readonly ISettingService _settingService; + private readonly bool _isSingleStoreMode; + + public XmlSitemapBuildContext(Store store, Language[] languages, ISettingService settingService, bool isSingleStoreMode) + { + Guard.NotNull(store, nameof(store)); + Guard.NotEmpty(languages, nameof(languages)); + + Store = store; + Languages = languages; + Protocol = Store.ForceSslForAllPages ? "https" : "http"; + + _settingService = settingService; + _isSingleStoreMode = isSingleStoreMode; + + RequestStoreId = _isSingleStoreMode ? 0 : store.Id; + } + + public CancellationToken CancellationToken { get; set; } + public ProgressCallback ProgressCallback { get; set; } + public Store Store { get; set; } + public int RequestStoreId { get; private set; } + public Language[] Languages { get; set; } + public int MaximumNodeCount { get; set; } = XmlSitemapGenerator.MaximumSiteMapNodeCount; + + public string Protocol { get; private set; } + + public T LoadSetting() where T : ISettings, new() + { + return _settingService.LoadSetting(RequestStoreId); + } + } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index 0e7c4086c7..088ed43b75 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -22,677 +22,678 @@ namespace SmartStore.Services.Seo { - public partial class XmlSitemapGenerator : IXmlSitemapGenerator + public partial class XmlSitemapGenerator : IXmlSitemapGenerator { - private const string SiteMapsNamespace = "http://www.sitemaps.org/schemas/sitemap/0.9"; - private const string XhtmlNamespace = "http://www.w3.org/1999/xhtml"; - private const string SiteMapFileNamePattern = "sitemap-{0}.xml"; - private const string LockFileNamePattern = "sitemap-{0}-{1}.lock"; - - /// - /// The maximum number of sitemaps a sitemap index file can contain. - /// - private const int MaximumSiteMapCount = 50000; - - /// - /// The maximum number of sitemap nodes allowed in a sitemap file. The absolute maximum allowed is 50,000 - /// according to the specification. See http://www.sitemaps.org/protocol.html but the file size must also be - /// less than 10MB. After some experimentation, a maximum of 2.000 nodes keeps the file size below 10MB. - /// - internal const int MaximumSiteMapNodeCount = 2000; - - /// - /// The maximum size of a sitemap file in bytes (10MB). - /// - private const int MaximumSiteMapSizeInBytes = 10485760; - - private readonly IEnumerable> _publishers; - private readonly ILanguageService _languageService; - private readonly ICustomerService _customerService; - private readonly IUrlRecordService _urlRecordService; - private readonly ICommonServices _services; - private readonly ILockFileManager _lockFileManager; - private readonly UrlHelper _urlHelper; - - private readonly IVirtualFolder _tenantFolder; - private readonly string _baseDir; - - public XmlSitemapGenerator( - IEnumerable> publishers, - ILanguageService languageService, - ICustomerService customerService, - IUrlRecordService urlRecordService, - ICommonServices services, - ILockFileManager lockFileManager, - UrlHelper urlHelper) + private const string SiteMapsNamespace = "http://www.sitemaps.org/schemas/sitemap/0.9"; + private const string XhtmlNamespace = "http://www.w3.org/1999/xhtml"; + private const string SiteMapFileNamePattern = "sitemap-{0}.xml"; + private const string LockFileNamePattern = "sitemap-{0}-{1}.lock"; + + /// + /// The maximum number of sitemaps a sitemap index file can contain. + /// + private const int MaximumSiteMapCount = 50000; + + /// + /// The maximum number of sitemap nodes allowed in a sitemap file. The absolute maximum allowed is 50,000 + /// according to the specification. See http://www.sitemaps.org/protocol.html but the file size must also be + /// less than 10MB. After some experimentation, a maximum of 2.000 nodes keeps the file size below 10MB. + /// + internal const int MaximumSiteMapNodeCount = 2000; + + /// + /// The maximum size of a sitemap file in bytes (10MB). + /// + private const int MaximumSiteMapSizeInBytes = 10485760; + + private readonly IEnumerable> _publishers; + private readonly ILanguageService _languageService; + private readonly ICustomerService _customerService; + private readonly IUrlRecordService _urlRecordService; + private readonly ICommonServices _services; + private readonly ILockFileManager _lockFileManager; + private readonly UrlHelper _urlHelper; + + private readonly IVirtualFolder _tenantFolder; + private readonly string _baseDir; + + public XmlSitemapGenerator( + IEnumerable> publishers, + ILanguageService languageService, + ICustomerService customerService, + IUrlRecordService urlRecordService, + ICommonServices services, + ILockFileManager lockFileManager, + UrlHelper urlHelper) { - _publishers = publishers; - _languageService = languageService; - _customerService = customerService; - _urlRecordService = urlRecordService; - _services = services; - _lockFileManager = lockFileManager; - _urlHelper = urlHelper; - - _tenantFolder = _services.ApplicationEnvironment.TenantFolder; - _baseDir = _tenantFolder.Combine("Sitemaps"); - - Logger = NullLogger.Instance; - - } - - public ILogger Logger { get; set; } - - public virtual async Task GetSitemapPartAsync(int index = 0) - { - return await GetSitemapPartAsync(index, false); - } - - private async Task GetSitemapPartAsync(int index, bool isRetry) - { - Guard.NotNegative(index, nameof(index)); - - var store = _services.StoreContext.CurrentStore; - var language = _services.WorkContext.WorkingLanguage; - - var exists = SitemapFileExists(store.Id, language.Id, index, out var path, out var name); - - if (exists) - { - return new XmlSitemapPartition - { - Index = index, - Name = name, - LanguageId = language.Id, - StoreId = store.Id, - ModifiedOnUtc = _tenantFolder.GetFileLastWriteTimeUtc(path), - Stream = _tenantFolder.OpenFile(path) - }; - } - - if (isRetry) - { - var msg = "Could not generate XML sitemap. Index: {0}, Date: {1}".FormatInvariant(index, DateTime.UtcNow); - Logger.Error(msg); - throw new SmartException(msg); - } - - if (index > 0) - { - // File with index greater 0 has been requested, but it does not exist. - // Now we have to determine whether just the passed index is out of range - // or the files has never been created before. - // If the main file (index 0) exists, the action should return NotFoundResult, - // otherwise the rebuild process should be started or waited for. - - if (SitemapFileExists(store.Id, language.Id, 0, out path, out name)) - { - throw new IndexOutOfRangeException("The sitemap file '{0}' does not exist.".FormatInvariant(name)); - } - } - - // The main sitemap document with index 0 does not exist, meaning: the whole sitemap - // needs to be created and cached by partitions. - - var wasRebuilding = false; - var lockFilePath = GetLockFilePath(store.Id, language.Id); - - while (IsRebuilding(lockFilePath)) - { - // The rebuild process is already running, either started - // by the task scheduler or another HTTP request. - // We should wait for completion. - - wasRebuilding = true; - Thread.Sleep(1000); - } - - if (!wasRebuilding) - { - // No lock. Rebuild now. - var buildContext = new XmlSitemapBuildContext(store, new[] { language }, _services.Settings, _services.StoreService.IsSingleStoreMode()) - { - CancellationToken = CancellationToken.None - }; - - await RebuildAsync(buildContext); - } - - // DRY: call self to get sitemap partition object - return await GetSitemapPartAsync(index, true); - } - - private bool SitemapFileExists(int storeId, int languageId, int index, out string path, out string name) - { - path = BuildSitemapFilePath(storeId, languageId, index, out name); - - // Does not work reliably with symlinks due to framework caching - //var exists = _tenantFolder.FileExists(path); - - var exists = File.Exists(_tenantFolder.MapPath(path)); - - if (!exists) - { - path = null; - name = null; - } - - return exists; - } - - private string BuildSitemapFilePath(int storeId, int languageId, int index, out string fileName) - { - fileName = SiteMapFileNamePattern.FormatInvariant(index); - return _tenantFolder.Combine(BuildSitemapDirPath(storeId, languageId), fileName); - } - - private string BuildSitemapDirPath(int storeId, int languageId) - { - return _tenantFolder.Combine(_baseDir, storeId + "/" + languageId); - } - - private string GetLockFilePath(int storeId, int languageId) - { - var fileName = LockFileNamePattern.FormatInvariant(storeId, languageId); - return _tenantFolder.Combine(_baseDir, fileName); - } - - public virtual async Task RebuildAsync(XmlSitemapBuildContext ctx) - { - Guard.NotNull(ctx, nameof(ctx)); - - // Double seo code otherwise - _urlHelper.RequestContext.RouteData.DataTokens["SeoCodeReplacement"] = string.Empty; - - var languageData = new Dictionary(); - - foreach (var language in ctx.Languages) - { - var lockFilePath = GetLockFilePath(ctx.Store.Id, language.Id); - - if (_lockFileManager.TryAcquireLock(lockFilePath, out var lockFile)) - { - // Process only languages that are unlocked right now - // It is possible that an HTTP request triggered the generation - // of a language specific sitemap. - - var sitemapDir = BuildSitemapDirPath(ctx.Store.Id, language.Id); - var data = new LanguageData - { - Store = ctx.Store, - Language = language, - LockFile = lockFile, - LockFilePath = lockFilePath, - TempDir = sitemapDir + "~", - FinalDir = sitemapDir, - BaseUrl = BuildBaseUrl(ctx.Store, language) - }; - - _tenantFolder.TryDeleteDirectory(data.TempDir); - _tenantFolder.CreateDirectory(data.TempDir); - - languageData[language.Id] = data; - } - } - - if (languageData.Count == 0) - { - Logger.Warn("XML sitemap rebuild already in process."); - return; - } - - var languages = languageData.Values.Select(x => x.Language); - var languageIds = languages.Select(x => x.Id).Concat(new[] { 0 }).ToArray(); - - // All sitemaps grouped by language - var sitemaps = new Multimap(); - - var compositeFileLock = new ActionDisposable(() => - { - foreach (var data in languageData.Values) - { - data.LockFile.Release(); - } - }); - - using (compositeFileLock) - { - // Impersonate - var prevCustomer = _services.WorkContext.CurrentCustomer; - // no need to vary xml sitemap by customer roles: it's relevant to crawlers only. - _services.WorkContext.CurrentCustomer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); - - try - { - var nodes = new List(); - - var providers = CreateProviders(ctx); - var total = providers.Sum(x => x.GetTotalCount()); - - var totalSegments = (int)Math.Ceiling(total / (double)MaximumSiteMapNodeCount); - var hasIndex = totalSegments > 1; - var indexNodes = new Multimap(); - var segment = 0; - var numProcessed = 0; - - CheckSitemapCount(totalSegments); - - using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) - { - var entities = EnlistEntities(providers); - - foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) - { - if (ctx.CancellationToken.IsCancellationRequested) - { - break; - } - - segment++; - numProcessed = segment * MaximumSiteMapNodeCount; - ctx.ProgressCallback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); - - var slugs = GetUrlRecordCollectionsForBatch(batch.Select(x => x.Entry), languageIds); - - foreach (var data in languageData.Values) - { - var language = data.Language; - var baseUrl = data.BaseUrl; - - // Create all node entries for this segment - var entries = batch - .Where(x => x.Entry.LanguageId.GetValueOrDefault() == 0 || x.Entry.LanguageId.Value == language.Id) - .Select(x => x.Provider.CreateNode(_urlHelper, baseUrl, x.Entry, slugs[x.Entry.EntityName], language)); - sitemaps[language.Id].AddRange(entries.Where(x => x != null)); - - // Create index node for this segment/language combination - if (hasIndex) - { - indexNodes[language.Id].Add(new XmlSitemapNode - { - LastMod = sitemaps[language.Id].Select(x => x.LastMod).Where(x => x.HasValue).DefaultIfEmpty().Max(), - Loc = GetSitemapIndexUrl(segment, baseUrl), - }); - } - - if (segment % 5 == 0 || segment == totalSegments) - { - // Commit every 5th segment (10.000 nodes) temporarily to disk to minimize RAM usage - var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id]); - await SaveTempAsync(documents, data, segment - documents.Count + (hasIndex ? 1 : 0)); - - documents.Clear(); - sitemaps.RemoveAll(language.Id); - } - } - - slugs.Clear(); - - //GC.Collect(); - //GC.WaitForPendingFinalizers(); - } - - // Process custom nodes - if (!ctx.CancellationToken.IsCancellationRequested) - { - ctx.ProgressCallback?.Invoke(numProcessed, total, "Processing custom nodes".FormatCurrent(numProcessed, total)); - ProcessCustomNodes(ctx, sitemaps); - - foreach (var data in languageData.Values) - { - if (sitemaps.ContainsKey(data.Language.Id) && sitemaps[data.Language.Id].Count > 0) - { - var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[data.Language.Id]); - await SaveTempAsync(documents, data, (segment + 1) - documents.Count + (hasIndex ? 1 : 0)); - } - else if (segment == 0) - { - // Ensure that at least one entry exists. Otherwise, - // the system will try to rebuild again. - var homeNode = new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = data.BaseUrl }; - var documents = GetSiteMapDocuments(new List { homeNode }); - await SaveTempAsync(documents, data, 0); - } - - } - } - } - - ctx.CancellationToken.ThrowIfCancellationRequested(); - - ctx.ProgressCallback?.Invoke(totalSegments, totalSegments, "Finalizing...'"); - - foreach (var data in languageData.Values) - { - // Create index documents (if any) - if (hasIndex && indexNodes.Any()) - { - var indexDocument = CreateSitemapIndexDocument(indexNodes[data.Language.Id]); - await SaveTempAsync(new List { indexDocument }, data, 0); - } - - // Save finally (actually renames temp folder) - SaveFinal(data); - } - } - finally - { - // Undo impersonation - _services.WorkContext.CurrentCustomer = prevCustomer; - sitemaps.Clear(); - - foreach (var data in languageData.Values) - { - if (_tenantFolder.DirectoryExists(data.TempDir)) - { - _tenantFolder.TryDeleteDirectory(data.TempDir); - } - } - - //GC.Collect(); - //GC.WaitForPendingFinalizers(); - } - } - } - - private string BuildBaseUrl(Store store, Language language) - { - var host = _services.StoreService.GetHost(store).EnsureEndsWith("/"); - - var locSettings = _services.Settings.LoadSetting(store.Id); - if (locSettings.SeoFriendlyUrlsForLanguagesEnabled) - { - var defaultLangId = _languageService.GetDefaultLanguageId(store.Id); - if (language.Id != defaultLangId || locSettings.DefaultLanguageRedirectBehaviour < DefaultLanguageRedirectBehaviour.StripSeoCode) - { - host += language.GetTwoLetterISOLanguageName() + "/"; - } - } - - return host; - } - - private async Task SaveTempAsync(List documents, LanguageData data, int start) - { - for (int i = 0; i < documents.Count; i++) - { - // Save segment to disk - var fileName = SiteMapFileNamePattern.FormatInvariant(i + start); - var filePath = _tenantFolder.Combine(data.TempDir, fileName); - - await _tenantFolder.CreateTextFileAsync(filePath, documents[i]); - } - } - - private void SaveFinal(LanguageData data) - { - // Delete current sitemap dir - _tenantFolder.TryDeleteDirectory(data.FinalDir); - - var source = _tenantFolder.MapPath(data.TempDir); - var dest = _tenantFolder.MapPath(data.FinalDir); - - // Move/Rename new (temp) dir to current - System.IO.Directory.Move(source, dest); - - int retries = 0; - while (!SitemapFileExists(data.Store.Id, data.Language.Id, 0, out _, out _)) - { - if (retries > 20) - { - break; - } - - // IO breathe: directly after a folder rename a file check fails. Wait a sec... - Task.Delay(500).Wait(); - retries++; - } - } - - private IEnumerable EnlistEntities(XmlSitemapProvider[] providers) - { - var result = Enumerable.Empty(); - foreach (var provider in providers) - { - result = result.Concat(provider.Enlist().Select(x => new NodeEntry { Entry = x, Provider = provider })); - } - - return result; - } - - private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int[] languageIds) - { - var result = new Dictionary(); - - if (batch.First().EntityName == "Product") - { - // nothing comes after product - int min = batch.Last().Id; - int max = batch.First().Id; - - result["Product"] = _urlRecordService.GetUrlRecordCollection("Product", languageIds, new[] { min, max }, true, true); - } - - var entityGroups = batch.ToMultimap(x => x.EntityName, x => x.Id); - foreach (var group in entityGroups) - { - var isRange = group.Key == "Product"; - var entityIds = isRange ? new[] { group.Value.Last(), group.Value.First() } : group.Value.ToArray(); - - result[group.Key] = _urlRecordService.GetUrlRecordCollection(group.Key, languageIds, entityIds, isRange, isRange); - } - - return result; - } - - protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes) - { - int siteMapCount = (int)Math.Ceiling(nodes.Count / (double)MaximumSiteMapNodeCount); - CheckSitemapCount(siteMapCount); - - var siteMaps = Enumerable - .Range(0, siteMapCount) - .Select(x => - { - return new KeyValuePair>( - x + 1, - nodes.Skip(x * MaximumSiteMapNodeCount).Take(MaximumSiteMapNodeCount)); - }); - - var siteMapDocuments = new List(siteMapCount); - - foreach (var kvp in siteMaps) - { - siteMapDocuments.Add(this.GetSitemapDocument(kvp.Value)); - } - - return siteMapDocuments; - } - - /// - /// Gets the sitemap XML document for the specified set of nodes. - /// - /// The sitemap nodes. - /// The sitemap XML document for the specified set of nodes. - private string GetSitemapDocument(IEnumerable nodes) - { - //var languages = _languageService.GetAllLanguages(); - - XNamespace ns = SiteMapsNamespace; - XNamespace xhtml = XhtmlNamespace; - - XElement root = new XElement( - ns + "urlset", - new XAttribute(XNamespace.Xmlns + "xhtml", xhtml)); - - foreach (var node in nodes) - { - // url - var xel = new XElement - ( - ns + "url", - // url/loc - new XElement(ns + "loc", node.Loc), - // url/lastmod - node.LastMod == null ? null : new XElement( - ns + "lastmod", - node.LastMod.Value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz")), - // url/changefreq - node.ChangeFreq == null ? null : new XElement( - ns + "changefreq", - node.ChangeFreq.Value.ToString().ToLowerInvariant()), - // url/priority - node.Priority == null ? null : new XElement( - ns + "priority", - node.Priority.Value.ToString("F1", CultureInfo.InvariantCulture)) - ); - - if (node.Links != null) - { - foreach (var entry in node.Links) - { - // url/xhtml:link[culture] - xel.Add(new XElement - ( - xhtml + "link", - new XAttribute("rel", "alternate"), - new XAttribute("hreflang", entry.Lang), - new XAttribute("href", entry.Href) - )); - } - } - - root.Add(xel); - } - - XDocument document = new XDocument(root); - var xml = document.ToString(SaveOptions.DisableFormatting); - CheckDocumentSize(xml); - - return xml; - } - - /// - /// Gets the sitemap index XML document, containing links to all the sitemap XML documents. - /// - /// The collection of sitemaps containing their index and nodes. - /// The sitemap index XML document, containing links to all the sitemap XML documents. - private string CreateSitemapIndexDocument(IEnumerable nodes) - { - XNamespace ns = SiteMapsNamespace; - - XElement root = new XElement(ns + "sitemapindex"); - - foreach (var node in nodes) - { - var xel = new XElement( - ns + "sitemap", - new XElement(ns + "loc", node.Loc), - node.LastMod.HasValue ? - new XElement( - ns + "lastmod", - node.LastMod.Value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz")) : - null); - - root.Add(xel); - } - - var document = new XDocument(root); - var xml = document.ToString(SaveOptions.DisableFormatting); - CheckDocumentSize(xml); - - return xml; - } - - private string GetSitemapIndexUrl(int index, string baseUrl) - { - var url = _urlHelper.RouteUrl("XmlSitemap", new { index }).TrimStart('/'); - return baseUrl + url; - } - - private XmlSitemapProvider[] CreateProviders(XmlSitemapBuildContext context) - { - return _publishers - .Select(x => x.Value.PublishXmlSitemap(context)) - .Where(x => x != null) - .OrderBy(x => x.Order) - .ToArray(); - } - - protected void ProcessCustomNodes(XmlSitemapBuildContext ctx, Multimap sitemaps) - { - // For inheritors - } - - /// - /// Checks the size of the XML sitemap document. If it is over 10MB, logs an error. - /// - /// The sitemap XML document. - private void CheckDocumentSize(string siteMapXml) - { - if (siteMapXml.Length >= MaximumSiteMapSizeInBytes) - { - Logger.Error(new InvalidOperationException($"Sitemap exceeds the maximum size of 10MB. This is because you have unusually long URL's. Consider reducing the MaximumSitemapNodeCount. Size:<{siteMapXml.Length}>")); - } - } - - /// - /// Checks the count of the number of sitemaps. If it is over 50,000, logs an error. - /// - /// The sitemap count. - private void CheckSitemapCount(int sitemapCount) - { - if (sitemapCount > MaximumSiteMapCount) - { - var ex = new InvalidOperationException($"Sitemap index file exceeds the maximum number of allowed sitemaps of 50,000. Count:<{sitemapCount}>"); - Logger.Warn(ex, ex.Message); - } - } - - public bool IsRebuilding(int storeId, int languageId) - { - return IsRebuilding(GetLockFilePath(storeId, languageId)); - } - - private bool IsRebuilding(string lockFilePath) - { - return _lockFileManager.IsLocked(lockFilePath); - } - - public virtual bool IsGenerated(int storeId, int languageId) - { - return SitemapFileExists(storeId, languageId, 0, out _, out _); - } - - public virtual void Invalidate(int storeId, int languageId) - { - var dir = BuildSitemapDirPath(storeId, languageId); - - if (_tenantFolder.DirectoryExists(dir)) - { - _tenantFolder.DeleteDirectory(dir); - } - } - - #region Nested classes - - struct NodeEntry - { - public NamedEntity Entry { get; set; } - public XmlSitemapProvider Provider { get; set; } - } - - class LanguageData - { - public Store Store { get; set; } - public Language Language { get; set; } - public ILockFile LockFile { get; set; } - public string LockFilePath { get; set; } - public string TempDir { get; set; } - public string FinalDir { get; set; } - public string BaseUrl { get; set; } - } - - #endregion - } + _publishers = publishers; + _languageService = languageService; + _customerService = customerService; + _urlRecordService = urlRecordService; + _services = services; + _lockFileManager = lockFileManager; + _urlHelper = urlHelper; + + _tenantFolder = _services.ApplicationEnvironment.TenantFolder; + _baseDir = _tenantFolder.Combine("Sitemaps"); + + Logger = NullLogger.Instance; + + } + + public ILogger Logger { get; set; } + + public virtual async Task GetSitemapPartAsync(int index = 0) + { + return await GetSitemapPartAsync(index, false); + } + + private async Task GetSitemapPartAsync(int index, bool isRetry) + { + Guard.NotNegative(index, nameof(index)); + + var store = _services.StoreContext.CurrentStore; + var language = _services.WorkContext.WorkingLanguage; + + var exists = SitemapFileExists(store.Id, language.Id, index, out var path, out var name); + + if (exists) + { + return new XmlSitemapPartition + { + Index = index, + Name = name, + LanguageId = language.Id, + StoreId = store.Id, + ModifiedOnUtc = _tenantFolder.GetFileLastWriteTimeUtc(path), + Stream = _tenantFolder.OpenFile(path) + }; + } + + if (isRetry) + { + var msg = "Could not generate XML sitemap. Index: {0}, Date: {1}".FormatInvariant(index, DateTime.UtcNow); + Logger.Error(msg); + throw new SmartException(msg); + } + + if (index > 0) + { + // File with index greater 0 has been requested, but it does not exist. + // Now we have to determine whether just the passed index is out of range + // or the files has never been created before. + // If the main file (index 0) exists, the action should return NotFoundResult, + // otherwise the rebuild process should be started or waited for. + + if (SitemapFileExists(store.Id, language.Id, 0, out path, out name)) + { + throw new IndexOutOfRangeException("The sitemap file '{0}' does not exist.".FormatInvariant(name)); + } + } + + // The main sitemap document with index 0 does not exist, meaning: the whole sitemap + // needs to be created and cached by partitions. + + var wasRebuilding = false; + var lockFilePath = GetLockFilePath(store.Id, language.Id); + + while (IsRebuilding(lockFilePath)) + { + // The rebuild process is already running, either started + // by the task scheduler or another HTTP request. + // We should wait for completion. + + wasRebuilding = true; + Thread.Sleep(1000); + } + + if (!wasRebuilding) + { + // No lock. Rebuild now. + var buildContext = new XmlSitemapBuildContext(store, new[] { language }, _services.Settings, _services.StoreService.IsSingleStoreMode()) + { + CancellationToken = CancellationToken.None + }; + + await RebuildAsync(buildContext); + } + + // DRY: call self to get sitemap partition object + return await GetSitemapPartAsync(index, true); + } + + private bool SitemapFileExists(int storeId, int languageId, int index, out string path, out string name) + { + path = BuildSitemapFilePath(storeId, languageId, index, out name); + + // Does not work reliably with symlinks due to framework caching + //var exists = _tenantFolder.FileExists(path); + + var exists = File.Exists(_tenantFolder.MapPath(path)); + + if (!exists) + { + path = null; + name = null; + } + + return exists; + } + + private string BuildSitemapFilePath(int storeId, int languageId, int index, out string fileName) + { + fileName = SiteMapFileNamePattern.FormatInvariant(index); + return _tenantFolder.Combine(BuildSitemapDirPath(storeId, languageId), fileName); + } + + private string BuildSitemapDirPath(int storeId, int languageId) + { + return _tenantFolder.Combine(_baseDir, storeId + "/" + languageId); + } + + private string GetLockFilePath(int storeId, int languageId) + { + var fileName = LockFileNamePattern.FormatInvariant(storeId, languageId); + return _tenantFolder.Combine(_baseDir, fileName); + } + + public virtual async Task RebuildAsync(XmlSitemapBuildContext ctx) + { + Guard.NotNull(ctx, nameof(ctx)); + + // Double seo code otherwise + _urlHelper.RequestContext.RouteData.DataTokens["SeoCodeReplacement"] = string.Empty; + + var languageData = new Dictionary(); + + foreach (var language in ctx.Languages) + { + var lockFilePath = GetLockFilePath(ctx.Store.Id, language.Id); + + if (_lockFileManager.TryAcquireLock(lockFilePath, out var lockFile)) + { + // Process only languages that are unlocked right now + // It is possible that an HTTP request triggered the generation + // of a language specific sitemap. + + var sitemapDir = BuildSitemapDirPath(ctx.Store.Id, language.Id); + var data = new LanguageData + { + Store = ctx.Store, + Language = language, + LockFile = lockFile, + LockFilePath = lockFilePath, + TempDir = sitemapDir + "~", + FinalDir = sitemapDir, + BaseUrl = BuildBaseUrl(ctx.Store, language) + }; + + _tenantFolder.TryDeleteDirectory(data.TempDir); + _tenantFolder.CreateDirectory(data.TempDir); + + languageData[language.Id] = data; + } + } + + if (languageData.Count == 0) + { + Logger.Warn("XML sitemap rebuild already in process."); + return; + } + + var languages = languageData.Values.Select(x => x.Language); + var languageIds = languages.Select(x => x.Id).Concat(new[] { 0 }).ToArray(); + + // All sitemaps grouped by language + var sitemaps = new Multimap(); + + var compositeFileLock = new ActionDisposable(() => + { + foreach (var data in languageData.Values) + { + data.LockFile.Release(); + } + }); + + using (compositeFileLock) + { + // Impersonate + var prevCustomer = _services.WorkContext.CurrentCustomer; + // no need to vary xml sitemap by customer roles: it's relevant to crawlers only. + _services.WorkContext.CurrentCustomer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); + + try + { + var nodes = new List(); + + var providers = CreateProviders(ctx); + var total = providers.Sum(x => x.GetTotalCount()); + + var totalSegments = (int)Math.Ceiling(total / (double)MaximumSiteMapNodeCount); + var hasIndex = totalSegments > 1; + var indexNodes = new Multimap(); + var segment = 0; + var numProcessed = 0; + + CheckSitemapCount(totalSegments); + + using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) + { + var entities = EnlistEntities(providers); + + foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) + { + if (ctx.CancellationToken.IsCancellationRequested) + { + break; + } + + segment++; + numProcessed = segment * MaximumSiteMapNodeCount; + ctx.ProgressCallback?.Invoke(numProcessed, total, "{0} / {1}".FormatCurrent(numProcessed, total)); + + var slugs = GetUrlRecordCollectionsForBatch(batch.Select(x => x.Entry), languageIds); + + foreach (var data in languageData.Values) + { + var language = data.Language; + var baseUrl = data.BaseUrl; + + // Create all node entries for this segment + var entries = batch + .Where(x => x.Entry.LanguageId.GetValueOrDefault() == 0 || x.Entry.LanguageId.Value == language.Id) + .Select(x => x.Provider.CreateNode(_urlHelper, baseUrl, x.Entry, slugs[x.Entry.EntityName], language)); + sitemaps[language.Id].AddRange(entries.Where(x => x != null)); + + // Create index node for this segment/language combination + if (hasIndex) + { + indexNodes[language.Id].Add(new XmlSitemapNode + { + LastMod = sitemaps[language.Id].Select(x => x.LastMod).Where(x => x.HasValue).DefaultIfEmpty().Max(), + Loc = GetSitemapIndexUrl(segment, baseUrl), + }); + } + + if (segment % 5 == 0 || segment == totalSegments) + { + // Commit every 5th segment (10.000 nodes) temporarily to disk to minimize RAM usage + var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id]); + await SaveTempAsync(documents, data, segment - documents.Count + (hasIndex ? 1 : 0)); + + documents.Clear(); + sitemaps.RemoveAll(language.Id); + } + } + + slugs.Clear(); + + //GC.Collect(); + //GC.WaitForPendingFinalizers(); + } + + // Process custom nodes + if (!ctx.CancellationToken.IsCancellationRequested) + { + ctx.ProgressCallback?.Invoke(numProcessed, total, "Processing custom nodes".FormatCurrent(numProcessed, total)); + ProcessCustomNodes(ctx, sitemaps); + + foreach (var data in languageData.Values) + { + if (sitemaps.ContainsKey(data.Language.Id) && sitemaps[data.Language.Id].Count > 0) + { + var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[data.Language.Id]); + await SaveTempAsync(documents, data, (segment + 1) - documents.Count + (hasIndex ? 1 : 0)); + } + else if (segment == 0) + { + // Ensure that at least one entry exists. Otherwise, + // the system will try to rebuild again. + var homeNode = new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = data.BaseUrl }; + var documents = GetSiteMapDocuments(new List { homeNode }); + await SaveTempAsync(documents, data, 0); + } + + } + } + } + + ctx.CancellationToken.ThrowIfCancellationRequested(); + + ctx.ProgressCallback?.Invoke(totalSegments, totalSegments, "Finalizing...'"); + + foreach (var data in languageData.Values) + { + // Create index documents (if any) + if (hasIndex && indexNodes.Any()) + { + var indexDocument = CreateSitemapIndexDocument(indexNodes[data.Language.Id]); + await SaveTempAsync(new List { indexDocument }, data, 0); + } + + // Save finally (actually renames temp folder) + SaveFinal(data); + } + } + finally + { + // Undo impersonation + _services.WorkContext.CurrentCustomer = prevCustomer; + sitemaps.Clear(); + + foreach (var data in languageData.Values) + { + if (_tenantFolder.DirectoryExists(data.TempDir)) + { + _tenantFolder.TryDeleteDirectory(data.TempDir); + } + } + + //GC.Collect(); + //GC.WaitForPendingFinalizers(); + } + } + } + + private string BuildBaseUrl(Store store, Language language) + { + var host = _services.StoreService.GetHost(store).EnsureEndsWith("/"); + + var locSettings = _services.Settings.LoadSetting(store.Id); + if (locSettings.SeoFriendlyUrlsForLanguagesEnabled) + { + var defaultLangId = _languageService.GetDefaultLanguageId(store.Id); + if (language.Id != defaultLangId || locSettings.DefaultLanguageRedirectBehaviour < DefaultLanguageRedirectBehaviour.StripSeoCode) + { + host += language.GetTwoLetterISOLanguageName() + "/"; + } + } + + return host; + } + + private async Task SaveTempAsync(List documents, LanguageData data, int start) + { + for (int i = 0; i < documents.Count; i++) + { + // Save segment to disk + var fileName = SiteMapFileNamePattern.FormatInvariant(i + start); + var filePath = _tenantFolder.Combine(data.TempDir, fileName); + + await _tenantFolder.CreateTextFileAsync(filePath, documents[i]); + } + } + + private void SaveFinal(LanguageData data) + { + // Delete current sitemap dir + _tenantFolder.TryDeleteDirectory(data.FinalDir); + + var source = _tenantFolder.MapPath(data.TempDir); + var dest = _tenantFolder.MapPath(data.FinalDir); + + // Move/Rename new (temp) dir to current + System.IO.Directory.Move(source, dest); + + int retries = 0; + while (!SitemapFileExists(data.Store.Id, data.Language.Id, 0, out _, out _)) + { + if (retries > 20) + { + break; + } + + // IO breathe: directly after a folder rename a file check fails. Wait a sec... + Task.Delay(500).Wait(); + retries++; + } + } + + private IEnumerable EnlistEntities(XmlSitemapProvider[] providers) + { + var result = Enumerable.Empty(); + foreach (var provider in providers) + { + result = result.Concat(provider.Enlist().Select(x => new NodeEntry { Entry = x, Provider = provider })); + } + + return result; + } + + private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int[] languageIds) + { + var result = new Dictionary(); + + if (batch.First().EntityName == "Product") + { + // nothing comes after product + int min = batch.Last().Id; + int max = batch.First().Id; + + result["Product"] = _urlRecordService.GetUrlRecordCollection("Product", languageIds, new[] { min, max }, true, true); + } + + var entityGroups = batch.ToMultimap(x => x.EntityName, x => x.Id); + foreach (var group in entityGroups) + { + var isRange = group.Key == "Product"; + var entityIds = isRange ? new[] { group.Value.Last(), group.Value.First() } : group.Value.ToArray(); + + result[group.Key] = _urlRecordService.GetUrlRecordCollection(group.Key, languageIds, entityIds, isRange, isRange); + } + + return result; + } + + protected virtual List GetSiteMapDocuments(IReadOnlyCollection nodes) + { + int siteMapCount = (int)Math.Ceiling(nodes.Count / (double)MaximumSiteMapNodeCount); + CheckSitemapCount(siteMapCount); + + var siteMaps = Enumerable + .Range(0, siteMapCount) + .Select(x => + { + return new KeyValuePair>( + x + 1, + nodes.Skip(x * MaximumSiteMapNodeCount).Take(MaximumSiteMapNodeCount)); + }); + + var siteMapDocuments = new List(siteMapCount); + + foreach (var kvp in siteMaps) + { + siteMapDocuments.Add(this.GetSitemapDocument(kvp.Value)); + } + + return siteMapDocuments; + } + + /// + /// Gets the sitemap XML document for the specified set of nodes. + /// + /// The sitemap nodes. + /// The sitemap XML document for the specified set of nodes. + private string GetSitemapDocument(IEnumerable nodes) + { + //var languages = _languageService.GetAllLanguages(); + + XNamespace ns = SiteMapsNamespace; + XNamespace xhtml = XhtmlNamespace; + + XElement root = new XElement( + ns + "urlset", + new XAttribute(XNamespace.Xmlns + "xhtml", xhtml)); + + foreach (var node in nodes) + { + // url + var xel = new XElement + ( + ns + "url", + // url/loc + new XElement(ns + "loc", node.Loc), + // url/lastmod + node.LastMod == null ? null : new XElement( + ns + "lastmod", + node.LastMod.Value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz")), + // url/changefreq + node.ChangeFreq == null ? null : new XElement( + ns + "changefreq", + node.ChangeFreq.Value.ToString().ToLowerInvariant()), + // url/priority + node.Priority == null ? null : new XElement( + ns + "priority", + node.Priority.Value.ToString("F1", CultureInfo.InvariantCulture)) + ); + + if (node.Links != null) + { + foreach (var entry in node.Links) + { + // url/xhtml:link[culture] + xel.Add(new XElement + ( + xhtml + "link", + new XAttribute("rel", "alternate"), + new XAttribute("hreflang", entry.Lang), + new XAttribute("href", entry.Href) + )); + } + } + + root.Add(xel); + } + + XDeclaration declaration = new XDeclaration("1.0", "UTF-8", "yes"); + XDocument document = new XDocument(root); + var xml = declaration.ToString() + document.ToString(SaveOptions.DisableFormatting); + CheckDocumentSize(xml); + + return xml; + } + + /// + /// Gets the sitemap index XML document, containing links to all the sitemap XML documents. + /// + /// The collection of sitemaps containing their index and nodes. + /// The sitemap index XML document, containing links to all the sitemap XML documents. + private string CreateSitemapIndexDocument(IEnumerable nodes) + { + XNamespace ns = SiteMapsNamespace; + + XElement root = new XElement(ns + "sitemapindex"); + + foreach (var node in nodes) + { + var xel = new XElement( + ns + "sitemap", + new XElement(ns + "loc", node.Loc), + node.LastMod.HasValue ? + new XElement( + ns + "lastmod", + node.LastMod.Value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz")) : + null); + + root.Add(xel); + } + + var document = new XDocument(root); + var xml = document.ToString(SaveOptions.DisableFormatting); + CheckDocumentSize(xml); + + return xml; + } + + private string GetSitemapIndexUrl(int index, string baseUrl) + { + var url = _urlHelper.RouteUrl("XmlSitemap", new { index }).TrimStart('/'); + return baseUrl + url; + } + + private XmlSitemapProvider[] CreateProviders(XmlSitemapBuildContext context) + { + return _publishers + .Select(x => x.Value.PublishXmlSitemap(context)) + .Where(x => x != null) + .OrderBy(x => x.Order) + .ToArray(); + } + + protected void ProcessCustomNodes(XmlSitemapBuildContext ctx, Multimap sitemaps) + { + // For inheritors + } + + /// + /// Checks the size of the XML sitemap document. If it is over 10MB, logs an error. + /// + /// The sitemap XML document. + private void CheckDocumentSize(string siteMapXml) + { + if (siteMapXml.Length >= MaximumSiteMapSizeInBytes) + { + Logger.Error(new InvalidOperationException($"Sitemap exceeds the maximum size of 10MB. This is because you have unusually long URL's. Consider reducing the MaximumSitemapNodeCount. Size:<{siteMapXml.Length}>")); + } + } + + /// + /// Checks the count of the number of sitemaps. If it is over 50,000, logs an error. + /// + /// The sitemap count. + private void CheckSitemapCount(int sitemapCount) + { + if (sitemapCount > MaximumSiteMapCount) + { + var ex = new InvalidOperationException($"Sitemap index file exceeds the maximum number of allowed sitemaps of 50,000. Count:<{sitemapCount}>"); + Logger.Warn(ex, ex.Message); + } + } + + public bool IsRebuilding(int storeId, int languageId) + { + return IsRebuilding(GetLockFilePath(storeId, languageId)); + } + + private bool IsRebuilding(string lockFilePath) + { + return _lockFileManager.IsLocked(lockFilePath); + } + + public virtual bool IsGenerated(int storeId, int languageId) + { + return SitemapFileExists(storeId, languageId, 0, out _, out _); + } + + public virtual void Invalidate(int storeId, int languageId) + { + var dir = BuildSitemapDirPath(storeId, languageId); + + if (_tenantFolder.DirectoryExists(dir)) + { + _tenantFolder.DeleteDirectory(dir); + } + } + + #region Nested classes + + struct NodeEntry + { + public NamedEntity Entry { get; set; } + public XmlSitemapProvider Provider { get; set; } + } + + class LanguageData + { + public Store Store { get; set; } + public Language Language { get; set; } + public ILockFile LockFile { get; set; } + public string LockFilePath { get; set; } + public string TempDir { get; set; } + public string FinalDir { get; set; } + public string BaseUrl { get; set; } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapNode.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapNode.cs index a3d2f89f6b..30b35a9d27 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapNode.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapNode.cs @@ -6,53 +6,53 @@ namespace SmartStore.Services.Seo { - public class XmlSitemapNode - { - public string Loc { get; set; } - public DateTime? LastMod { get; set; } - public ChangeFrequency? ChangeFreq { get; set; } - public float? Priority { get; set; } - public IEnumerable Links { get; set; } + public class XmlSitemapNode + { + public string Loc { get; set; } + public DateTime? LastMod { get; set; } + public ChangeFrequency? ChangeFreq { get; set; } + public float? Priority { get; set; } + public IEnumerable Links { get; set; } - public class LinkEntry - { - public string Lang { get; set; } - public string Href { get; set; } - } - } + public class LinkEntry + { + public string Lang { get; set; } + public string Href { get; set; } + } + } - /// - /// Represents a sitemap update frequency - /// - public enum ChangeFrequency - { - /// - /// Always - /// - Always, - /// - /// Hourly - /// - Hourly, - /// - /// Daily - /// - Daily, - /// - /// Weekly - /// - Weekly, - /// - /// Monthly - /// - Monthly, - /// - /// Yearly - /// - Yearly, - /// - /// Never - /// - Never - } + /// + /// Represents a sitemap update frequency + /// + public enum ChangeFrequency + { + /// + /// Always + /// + Always, + /// + /// Hourly + /// + Hourly, + /// + /// Daily + /// + Daily, + /// + /// Weekly + /// + Weekly, + /// + /// Monthly + /// + Monthly, + /// + /// Yearly + /// + Yearly, + /// + /// Never + /// + Never + } } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs index ec5e519913..152e16b5c1 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapPartition.cs @@ -3,13 +3,13 @@ namespace SmartStore.Services.Seo { - public class XmlSitemapPartition - { - public string Name { get; set; } - public int Index { get; set; } - public int StoreId { get; set; } - public int LanguageId { get; set; } - public DateTime ModifiedOnUtc { get; set; } - public Stream Stream { get; set; } - } + public class XmlSitemapPartition + { + public string Name { get; set; } + public int Index { get; set; } + public int StoreId { get; set; } + public int LanguageId { get; set; } + public DateTime ModifiedOnUtc { get; set; } + public Stream Stream { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs b/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs index feaca72088..16dc5affa7 100644 --- a/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs +++ b/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs @@ -7,39 +7,39 @@ namespace SmartStore.Services { - public partial class ServiceCacheBuster : DbSaveHook - { - public const string STORE_LANGUAGE_MAP_KEY = "svc:storelangmap*"; + public partial class ServiceCacheBuster : DbSaveHook + { + public const string STORE_LANGUAGE_MAP_KEY = "svc:storelangmap*"; - private readonly ICacheManager _cacheManager; + private readonly ICacheManager _cacheManager; public ServiceCacheBuster(ICacheManager cacheManager) { - _cacheManager = cacheManager; + _cacheManager = cacheManager; } - protected override void OnInserted(BaseEntity entity, IHookedEntity entry) - { - if (entry.EntityType != typeof(Store) && entry.EntityType != typeof(Language)) - throw new NotImplementedException(); + protected override void OnInserted(BaseEntity entity, IHookedEntity entry) + { + if (entry.EntityType != typeof(Store) && entry.EntityType != typeof(Language)) + throw new NotSupportedException(); - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } + _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); + } - protected override void OnDeleted(BaseEntity entity, IHookedEntity entry) - { - if (entry.EntityType != typeof(Store) && entry.EntityType != typeof(Language)) - throw new NotImplementedException(); + protected override void OnDeleted(BaseEntity entity, IHookedEntity entry) + { + if (entry.EntityType != typeof(Store) && entry.EntityType != typeof(Language)) + throw new NotImplementedException(); - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } + _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); + } - protected override void OnUpdated(BaseEntity entity, IHookedEntity entry) - { - if (entry.EntityType != typeof(Language)) - throw new NotImplementedException(); + protected override void OnUpdated(BaseEntity entity, IHookedEntity entry) + { + if (entry.EntityType != typeof(Language)) + throw new NotImplementedException(); - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } - } + _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); + } + } } diff --git a/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs b/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs index 3439b6fd4f..80af4d3a4c 100644 --- a/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs +++ b/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs @@ -16,10 +16,10 @@ public GetShippingOptionRequest() this.Items = new List(); } - /// - /// The context store identifier - /// - public int StoreId { get; set; } + /// + /// The context store identifier + /// + public int StoreId { get; set; } /// /// Gets or sets a customer diff --git a/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionResponse.cs b/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionResponse.cs index cbb2586fa5..eea7127b98 100644 --- a/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionResponse.cs +++ b/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionResponse.cs @@ -24,13 +24,7 @@ public GetShippingOptionResponse() /// public IList Errors { get; set; } - public bool Success - { - get - { - return this.Errors.Count == 0; - } - } + public bool Success => this.Errors.Count == 0; public void AddError(string error) { diff --git a/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs b/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs index 0a544651ad..db53312558 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs @@ -26,9 +26,9 @@ public partial interface IShipmentService /// Page index /// Page size /// Customer collection - IPagedList GetAllShipments(string trackingNumber, DateTime? createdFrom, DateTime? createdTo, + IPagedList GetAllShipments(string trackingNumber, DateTime? createdFrom, DateTime? createdTo, int pageIndex, int pageSize); - + /// /// Get shipments by identifiers /// @@ -36,12 +36,12 @@ IPagedList GetAllShipments(string trackingNumber, DateTime? createdFro /// Shipments IList GetShipmentsByIds(int[] shipmentIds); - /// - /// Get shipments by order identifiers - /// - /// Order identifiers - /// Shipments - Multimap GetShipmentsByOrderIds(int[] orderIds); + /// + /// Get shipments by order identifiers + /// + /// Order identifiers + /// Shipments + Multimap GetShipmentsByOrderIds(int[] orderIds); /// /// Gets a shipment diff --git a/src/Libraries/SmartStore.Services/Shipping/IShippingRateComputationMethod.cs b/src/Libraries/SmartStore.Services/Shipping/IShippingRateComputationMethod.cs index fd13e71b4c..52f1a590fb 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShippingRateComputationMethod.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShippingRateComputationMethod.cs @@ -33,9 +33,9 @@ public partial interface IShippingRateComputationMethod : IProvider, IUserEditab /// IShipmentTracker ShipmentTracker { get; } - /// - /// Gets a value indicating whether the shipping rate computation method is active and should be offered to customers - /// - bool IsActive { get; } + /// + /// Gets a value indicating whether the shipping rate computation method is active and should be offered to customers + /// + bool IsActive { get; } } } diff --git a/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs index f1694fe50a..22a56abbda 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs @@ -47,13 +47,13 @@ public partial interface IShippingService ShippingMethod GetShippingMethodById(int shippingMethodId); - /// - /// Gets all shipping methods - /// - /// Shipping option request to filter out shipping methods. null to load all shipping methods. - /// Whether to filter methods by store identifier. - /// Shipping method collection - IList GetAllShippingMethods(GetShippingOptionRequest request = null, int storeId = 0); + /// + /// Gets all shipping methods + /// + /// Shipping option request to filter out shipping methods. null to load all shipping methods. + /// Whether to filter methods by store identifier. + /// Shipping method collection + IList GetAllShippingMethods(GetShippingOptionRequest request = null, int storeId = 0); /// /// Inserts a shipping method @@ -90,15 +90,15 @@ public partial interface IShippingService /// Shopping cart weight decimal GetShoppingCartTotalWeight(IList cart, bool includeFreeShippingProducts = true); - + /// /// Create shipment package from shopping cart /// /// Shopping cart /// Shipping address - /// Store identifier + /// Store identifier /// Shipment package - GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress, int storeId); + GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress, int storeId); /// /// Gets available shipping options @@ -109,6 +109,6 @@ public partial interface IShippingService /// Load records allows only in specified store; pass 0 to load all records /// Shipping options GetShippingOptionResponse GetShippingOptions(IList cart, Address shippingAddress, - string allowedShippingRateComputationMethodSystemName = "", int storeId = 0); + string allowedShippingRateComputationMethodSystemName = "", int storeId = 0); } } diff --git a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs index 8cd9b39030..18fff61b17 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs @@ -22,7 +22,7 @@ public partial class ShipmentService : IShipmentService private readonly IRepository _siRepository; private readonly IRepository _orderRepository; private readonly IEventPublisher _eventPublisher; - + #endregion #region Ctor @@ -58,32 +58,32 @@ public virtual void DeleteShipment(Shipment shipment) if (shipment == null) throw new ArgumentNullException("shipment"); - int orderId = shipment.OrderId; + int orderId = shipment.OrderId; _shipmentRepository.Delete(shipment); - if (orderId != 0) - { - var order = _orderRepository.GetById(orderId); - _eventPublisher.PublishOrderUpdated(order); - } + if (orderId != 0) + { + var order = _orderRepository.GetById(orderId); + _eventPublisher.PublishOrderUpdated(order); + } } - + /// /// Search shipments /// - /// Search by tracking number + /// Search by tracking number /// Created date from; null to load all records /// Created date to; null to load all records /// Page index /// Page size /// Customer collection - public virtual IPagedList GetAllShipments(string trackingNumber, DateTime? createdFrom, DateTime? createdTo, + public virtual IPagedList GetAllShipments(string trackingNumber, DateTime? createdFrom, DateTime? createdTo, int pageIndex, int pageSize) { var query = _shipmentRepository.Table.Expand(x => x.Order); - if (!String.IsNullOrEmpty(trackingNumber)) - query = query.Where(s => s.TrackingNumber.Contains(trackingNumber)); + if (!String.IsNullOrEmpty(trackingNumber)) + query = query.Where(s => s.TrackingNumber.Contains(trackingNumber)); if (createdFrom.HasValue) query = query.Where(s => createdFrom.Value <= s.CreatedOnUtc); if (createdTo.HasValue) @@ -105,33 +105,33 @@ public virtual IList GetShipmentsByIds(int[] shipmentIds) if (shipmentIds == null || shipmentIds.Length == 0) return new List(); - var query = from o in _shipmentRepository.Table.Expand(x => x.Order) + var query = from o in _shipmentRepository.Table.Expand(x => x.Order) where shipmentIds.Contains(o.Id) select o; var shipments = query.ToList(); - // sort by passed identifier sequence - return shipments.OrderBySequence(shipmentIds).ToList(); - } + // sort by passed identifier sequence + return shipments.OrderBySequence(shipmentIds).ToList(); + } - public virtual Multimap GetShipmentsByOrderIds(int[] orderIds) - { - Guard.NotNull(orderIds, nameof(orderIds)); + public virtual Multimap GetShipmentsByOrderIds(int[] orderIds) + { + Guard.NotNull(orderIds, nameof(orderIds)); - var query = - from x in _shipmentRepository.TableUntracked.Expand(x => x.ShipmentItems) - where orderIds.Contains(x.OrderId) - select x; + var query = + from x in _shipmentRepository.TableUntracked.Expand(x => x.ShipmentItems) + where orderIds.Contains(x.OrderId) + select x; - var map = query - .OrderBy(x => x.OrderId) - .ThenBy(x => x.CreatedOnUtc) - .ToList() - .ToMultimap(x => x.OrderId, x => x); + var map = query + .OrderBy(x => x.OrderId) + .ThenBy(x => x.CreatedOnUtc) + .ToList() + .ToMultimap(x => x.OrderId, x => x); - return map; - } + return map; + } /// /// Gets a shipment @@ -159,7 +159,7 @@ public virtual void InsertShipment(Shipment shipment) _shipmentRepository.Insert(shipment); //event notification - _eventPublisher.PublishOrderUpdated(shipment.Order); + _eventPublisher.PublishOrderUpdated(shipment.Order); } /// @@ -174,11 +174,11 @@ public virtual void UpdateShipment(Shipment shipment) _shipmentRepository.Update(shipment); //event notification - _eventPublisher.PublishOrderUpdated(shipment.Order); + _eventPublisher.PublishOrderUpdated(shipment.Order); } - + /// /// Deletes a shipment item /// @@ -186,17 +186,17 @@ public virtual void UpdateShipment(Shipment shipment) public virtual void DeleteShipmentItem(ShipmentItem shipmentItem) { if (shipmentItem == null) - throw new ArgumentNullException("shipmentItem"); + throw new ArgumentNullException("shipmentItem"); - int orderId = shipmentItem.Shipment.OrderId; + int orderId = shipmentItem.Shipment.OrderId; _siRepository.Delete(shipmentItem); - if (orderId != 0) - { - var order = _orderRepository.GetById(orderId); - _eventPublisher.PublishOrderUpdated(order); - } + if (orderId != 0) + { + var order = _orderRepository.GetById(orderId); + _eventPublisher.PublishOrderUpdated(order); + } } /// @@ -212,7 +212,7 @@ public virtual ShipmentItem GetShipmentItemById(int shipmentItemId) var si = _siRepository.GetById(shipmentItemId); return si; } - + /// /// Inserts a shipment item /// @@ -220,23 +220,23 @@ public virtual ShipmentItem GetShipmentItemById(int shipmentItemId) public virtual void InsertShipmentItem(ShipmentItem shipmentItem) { if (shipmentItem == null) - throw new ArgumentNullException("shipmentItem"); + throw new ArgumentNullException("shipmentItem"); _siRepository.Insert(shipmentItem); - if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) - { - _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); - } - else - { - var shipment = _shipmentRepository.Table - .Expand(x => x.Order) - .FirstOrDefault(x => x.Id == shipmentItem.ShipmentId); - - if (shipment != null) - _eventPublisher.PublishOrderUpdated(shipment.Order); - } + if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) + { + _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); + } + else + { + var shipment = _shipmentRepository.Table + .Expand(x => x.Order) + .FirstOrDefault(x => x.Id == shipmentItem.ShipmentId); + + if (shipment != null) + _eventPublisher.PublishOrderUpdated(shipment.Order); + } } /// @@ -246,25 +246,25 @@ public virtual void InsertShipmentItem(ShipmentItem shipmentItem) public virtual void UpdateShipmentItem(ShipmentItem shipmentItem) { if (shipmentItem == null) - throw new ArgumentNullException("shipmentItem"); + throw new ArgumentNullException("shipmentItem"); _siRepository.Update(shipmentItem); - if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) - { - _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); - } - else - { - var shipment = _shipmentRepository.Table - .Expand(x => x.Order) - .FirstOrDefault(x => x.Id == shipmentItem.ShipmentId); - - if (shipment != null) - _eventPublisher.PublishOrderUpdated(shipment.Order); - } + if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) + { + _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); + } + else + { + var shipment = _shipmentRepository.Table + .Expand(x => x.Order) + .FirstOrDefault(x => x.Id == shipmentItem.ShipmentId); + + if (shipment != null) + _eventPublisher.PublishOrderUpdated(shipment.Order); + } } - - #endregion + + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs b/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs index b83fdc68ee..832354ccfa 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs @@ -5,7 +5,7 @@ namespace SmartStore.Services.Shipping { - public static class ShippingExtentions + public static class ShippingExtentions { public static bool IsShippingRateComputationMethodActive(this Provider srcm, ShippingSettings shippingSettings) { @@ -18,10 +18,10 @@ public static bool IsShippingRateComputationMethodActive(this Provider _shippingMethodRepository; - private readonly IRepository _storeMappingRepository; - private readonly IProductAttributeParser _productAttributeParser; - private readonly IProductService _productService; + private readonly IRepository _shippingMethodRepository; + private readonly IRepository _storeMappingRepository; + private readonly IProductAttributeParser _productAttributeParser; + private readonly IProductService _productService; private readonly ICheckoutAttributeParser _checkoutAttributeParser; - private readonly IGenericAttributeService _genericAttributeService; + private readonly IGenericAttributeService _genericAttributeService; private readonly ShippingSettings _shippingSettings; - private readonly ISettingService _settingService; - private readonly IProviderManager _providerManager; - private readonly ICommonServices _services; + private readonly ISettingService _settingService; + private readonly IProviderManager _providerManager; + private readonly ICommonServices _services; private readonly ICartRuleProvider _cartRuleProvider; public ShippingService( IRepository shippingMethodRepository, - IRepository storeMappingRepository, - IProductAttributeParser productAttributeParser, - IProductService productService, + IRepository storeMappingRepository, + IProductAttributeParser productAttributeParser, + IProductService productService, ICheckoutAttributeParser checkoutAttributeParser, - IGenericAttributeService genericAttributeService, + IGenericAttributeService genericAttributeService, ShippingSettings shippingSettings, - ISettingService settingService, - IProviderManager providerManager, - ICommonServices services, + ISettingService settingService, + IProviderManager providerManager, + ICommonServices services, ICartRuleProvider cartRuleProvider) { _shippingMethodRepository = shippingMethodRepository; - _storeMappingRepository = storeMappingRepository; + _storeMappingRepository = storeMappingRepository; _productAttributeParser = productAttributeParser; - _productService = productService; + _productService = productService; _checkoutAttributeParser = checkoutAttributeParser; - _genericAttributeService = genericAttributeService; + _genericAttributeService = genericAttributeService; _shippingSettings = shippingSettings; - _settingService = settingService; - _providerManager = providerManager; - _services = services; + _settingService = settingService; + _providerManager = providerManager; + _services = services; _cartRuleProvider = cartRuleProvider; - T = NullLocalizer.Instance; - Logger = NullLogger.Instance; - QuerySettings = DbQuerySettings.Default; - } + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; + QuerySettings = DbQuerySettings.Default; + } - public Localizer T { get; set; } - public ILogger Logger { get; set; } - public DbQuerySettings QuerySettings { get; set; } + public Localizer T { get; set; } + public ILogger Logger { get; set; } + public DbQuerySettings QuerySettings { get; set; } - #region Shipping rate computation methods + #region Shipping rate computation methods - /// - /// Load active shipping rate computation methods - /// - /// Load records allows only in specified store; pass 0 to load all records - /// Shipping rate computation methods - public virtual IEnumerable> LoadActiveShippingRateComputationMethods(int storeId = 0) + /// + /// Load active shipping rate computation methods + /// + /// Load records allows only in specified store; pass 0 to load all records + /// Shipping rate computation methods + public virtual IEnumerable> LoadActiveShippingRateComputationMethods(int storeId = 0) { - var allMethods = LoadAllShippingRateComputationMethods(storeId); - - var activeMethods = allMethods - .Where(p => p.Value.IsActive && _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)); - - if (!activeMethods.Any()) - { - var fallbackMethod = allMethods.FirstOrDefault(x => x.IsShippingRateComputationMethodActive(_shippingSettings)); - - if (fallbackMethod == null) - fallbackMethod = allMethods.FirstOrDefault(); - - if (fallbackMethod != null) - { - _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Clear(); - _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Add(fallbackMethod.Metadata.SystemName); - _settingService.SaveSetting(_shippingSettings); - - return new Provider[] { fallbackMethod }; - } - else - { - if (DataSettings.DatabaseIsInstalled()) - throw new SmartException(T("Shipping.OneActiveMethodProviderRequired")); - } - } - - return activeMethods; + var allMethods = LoadAllShippingRateComputationMethods(storeId); + + var activeMethods = allMethods + .Where(p => p.Value.IsActive && _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)); + + if (!activeMethods.Any()) + { + var fallbackMethod = allMethods.FirstOrDefault(x => x.IsShippingRateComputationMethodActive(_shippingSettings)); + + if (fallbackMethod == null) + fallbackMethod = allMethods.FirstOrDefault(); + + if (fallbackMethod != null) + { + _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Clear(); + _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Add(fallbackMethod.Metadata.SystemName); + _settingService.SaveSetting(_shippingSettings); + + return new Provider[] { fallbackMethod }; + } + else + { + if (DataSettings.DatabaseIsInstalled()) + throw new SmartException(T("Shipping.OneActiveMethodProviderRequired")); + } + } + + return activeMethods; } /// @@ -113,7 +113,7 @@ public virtual IEnumerable> LoadActiveS /// Found Shipping rate computation method public virtual Provider LoadShippingRateComputationMethodBySystemName(string systemName, int storeId = 0) { - return _providerManager.GetProvider(systemName, storeId); + return _providerManager.GetProvider(systemName, storeId); } /// @@ -123,7 +123,7 @@ public virtual Provider LoadShippingRateComputat /// Shipping rate computation methods public virtual IEnumerable> LoadAllShippingRateComputationMethods(int storeId = 0) { - return _providerManager.GetAllProviders(storeId); + return _providerManager.GetAllProviders(storeId); } #endregion @@ -156,48 +156,48 @@ public virtual ShippingMethod GetShippingMethodById(int shippingMethodId) return _shippingMethodRepository.GetById(shippingMethodId); } - public virtual IList GetAllShippingMethods(GetShippingOptionRequest request = null, int storeId = 0) + public virtual IList GetAllShippingMethods(GetShippingOptionRequest request = null, int storeId = 0) { - var query = - from sm in _shippingMethodRepository.Table - select sm; - - if (!QuerySettings.IgnoreMultiStore && storeId > 0) - { - query = - from x in query - join sm in _storeMappingRepository.TableUntracked - on new { c1 = x.Id, c2 = "ShippingMethod" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm - from sm in x_sm.DefaultIfEmpty() - where !x.LimitedToStores || storeId == sm.StoreId - select x; - - query = - from x in query - group x by x.Id into grp - orderby grp.Key - select grp.FirstOrDefault(); - } - - var allMethods = query.OrderBy(x => x.DisplayOrder).ToList(); - - if (request == null) - { - return allMethods; - } - - var activeShippingMethods = allMethods.Where(s => - { + var query = + from sm in _shippingMethodRepository.Table + select sm; + + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + query = + from x in query + join sm in _storeMappingRepository.TableUntracked + on new { c1 = x.Id, c2 = "ShippingMethod" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm + from sm in x_sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == sm.StoreId + select x; + + query = + from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + } + + var allMethods = query.OrderBy(x => x.DisplayOrder).ToList(); + + if (request == null) + { + return allMethods; + } + + var activeShippingMethods = allMethods.Where(s => + { // Rule sets. if (!_cartRuleProvider.RuleMatches(s)) { return false; } - return true; - }); + return true; + }); - return activeShippingMethods.ToList(); + return activeShippingMethods.ToList(); } /// @@ -228,7 +228,7 @@ public virtual void UpdateShippingMethod(ShippingMethod shippingMethod) #region Workflow - public virtual decimal GetShoppingCartItemWeight(OrganizedShoppingCartItem shoppingCartItem) + public virtual decimal GetShoppingCartItemWeight(OrganizedShoppingCartItem shoppingCartItem) { if (shoppingCartItem == null) throw new ArgumentNullException("shoppingCartItem"); @@ -243,19 +243,19 @@ public virtual decimal GetShoppingCartItemWeight(OrganizedShoppingCartItem shopp { var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); - foreach (var pvaValue in pvaValues) - { - if (pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) - { - var linkedProduct = _productService.GetProductById(pvaValue.LinkedProductId); - if (linkedProduct != null && linkedProduct.IsShipEnabled) - attributesTotalWeight += (linkedProduct.Weight * pvaValue.Quantity); - } - else - { - attributesTotalWeight += pvaValue.WeightAdjustment; - } - } + foreach (var pvaValue in pvaValues) + { + if (pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) + { + var linkedProduct = _productService.GetProductById(pvaValue.LinkedProductId); + if (linkedProduct != null && linkedProduct.IsShipEnabled) + attributesTotalWeight += (linkedProduct.Weight * pvaValue.Quantity); + } + else + { + attributesTotalWeight += pvaValue.WeightAdjustment; + } + } } weight = shoppingCartItem.Item.Product.Weight + attributesTotalWeight; @@ -263,7 +263,7 @@ public virtual decimal GetShoppingCartItemWeight(OrganizedShoppingCartItem shopp return weight; } - public virtual decimal GetShoppingCartItemTotalWeight(OrganizedShoppingCartItem shoppingCartItem) + public virtual decimal GetShoppingCartItemTotalWeight(OrganizedShoppingCartItem shoppingCartItem) { if (shoppingCartItem == null) throw new ArgumentNullException("shoppingCartItem"); @@ -272,84 +272,84 @@ public virtual decimal GetShoppingCartItemTotalWeight(OrganizedShoppingCartItem return totalWeight; } - public virtual decimal GetShoppingCartTotalWeight(IList cart, bool includeFreeShippingProducts = true) + public virtual decimal GetShoppingCartTotalWeight(IList cart, bool includeFreeShippingProducts = true) { - var totalWeight = decimal.Zero; - var customer = cart.GetCustomer(); - - // shopping cart items - foreach (var cartItem in cart) - { - var product = cartItem.Item.Product; - if (product != null) - { - if (!includeFreeShippingProducts && product.IsFreeShipping) - { - // skip product - } - else - { - totalWeight += GetShoppingCartItemTotalWeight(cartItem); - } - } - } + var totalWeight = decimal.Zero; + var customer = cart.GetCustomer(); + + // shopping cart items + foreach (var cartItem in cart) + { + var product = cartItem.Item.Product; + if (product != null) + { + if (!includeFreeShippingProducts && product.IsFreeShipping) + { + // skip product + } + else + { + totalWeight += GetShoppingCartItemTotalWeight(cartItem); + } + } + } // checkout attributes if (customer != null) { - var checkoutAttributesXml = customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); - if (!String.IsNullOrEmpty(checkoutAttributesXml)) - { - var caValues = _checkoutAttributeParser.ParseCheckoutAttributeValues(checkoutAttributesXml); - foreach (var caValue in caValues) - { - totalWeight += caValue.WeightAdjustment; - } - } + var checkoutAttributesXml = customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); + if (!String.IsNullOrEmpty(checkoutAttributesXml)) + { + var caValues = _checkoutAttributeParser.ParseCheckoutAttributeValues(checkoutAttributesXml); + foreach (var caValue in caValues) + { + totalWeight += caValue.WeightAdjustment; + } + } } return totalWeight; } - public virtual GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress, int storeId) + public virtual GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress, int storeId) { var request = new GetShippingOptionRequest(); - request.StoreId = storeId; + request.StoreId = storeId; request.Customer = cart.GetCustomer(); request.ShippingAddress = shippingAddress; request.CountryFrom = null; request.StateProvinceFrom = null; request.ZipPostalCodeFrom = string.Empty; - request.Items = new List(); + request.Items = new List(); - foreach (var sc in cart) - { - if (sc.Item.IsShipEnabled) - request.Items.Add(sc); - } + foreach (var sc in cart) + { + if (sc.Item.IsShipEnabled) + request.Items.Add(sc); + } return request; } - public virtual GetShippingOptionResponse GetShippingOptions( - IList cart, - Address shippingAddress, - string allowedShippingRateComputationMethodSystemName = "", - int storeId = 0) + public virtual GetShippingOptionResponse GetShippingOptions( + IList cart, + Address shippingAddress, + string allowedShippingRateComputationMethodSystemName = "", + int storeId = 0) { if (cart == null) throw new ArgumentNullException("cart"); var result = new GetShippingOptionResponse(); - + //create a package var getShippingOptionRequest = CreateShippingOptionRequest(cart, shippingAddress, storeId); var shippingRateComputationMethods = LoadActiveShippingRateComputationMethods(storeId) - .Where(srcm => - String.IsNullOrWhiteSpace(allowedShippingRateComputationMethodSystemName) || + .Where(srcm => + String.IsNullOrWhiteSpace(allowedShippingRateComputationMethodSystemName) || allowedShippingRateComputationMethodSystemName.Equals(srcm.Metadata.SystemName, StringComparison.InvariantCultureIgnoreCase)) .ToList(); @@ -372,15 +372,15 @@ public virtual GetShippingOptionResponse GetShippingOptions( //log errors if (!getShippingOptionResponse.Success) { - var hasItemsToShip = getShippingOptionRequest.Items != null && getShippingOptionRequest.Items.Count > 0; + var hasItemsToShip = getShippingOptionRequest.Items != null && getShippingOptionRequest.Items.Count > 0; - foreach (string error in getShippingOptionResponse.Errors) + foreach (string error in getShippingOptionResponse.Errors) { result.AddError(error); - if (hasItemsToShip) - { - Logger.Warn(error); - } + if (hasItemsToShip) + { + Logger.Warn(error); + } } } } @@ -392,12 +392,12 @@ public virtual GetShippingOptionResponse GetShippingOptions( result.Errors.Clear(); } - //no shipping options loaded - if (result.ShippingOptions.Count == 0 && result.Errors.Count == 0) - { - result.Errors.Add(T("Checkout.ShippingOptionCouldNotBeLoaded")); - } - + //no shipping options loaded + if (result.ShippingOptions.Count == 0 && result.Errors.Count == 0) + { + result.Errors.Add(T("Checkout.ShippingOptionCouldNotBeLoaded")); + } + return result; } diff --git a/src/Libraries/SmartStore.Services/Shipping/Tracking/GeneralShipmentTracker.cs b/src/Libraries/SmartStore.Services/Shipping/Tracking/GeneralShipmentTracker.cs index 33713acfbf..2bb952546a 100644 --- a/src/Libraries/SmartStore.Services/Shipping/Tracking/GeneralShipmentTracker.cs +++ b/src/Libraries/SmartStore.Services/Shipping/Tracking/GeneralShipmentTracker.cs @@ -18,7 +18,7 @@ public partial class GeneralShipmentTracker : IShipmentTracker private readonly ITypeFinder _typeFinder; #endregion - + #region Ctor /// @@ -31,7 +31,7 @@ public GeneralShipmentTracker(ITypeFinder typeFinder) } #endregion - + #region Utilities /// @@ -40,7 +40,7 @@ public GeneralShipmentTracker(ITypeFinder typeFinder) /// All available shipment trackers protected virtual IList GetAllTrackers() { - return _typeFinder.FindClassesOfType(ignoreInactivePlugins: true) + return _typeFinder.FindClassesOfType(ignoreInactivePlugins: true) //exclude this one .Where(x => x != typeof(GeneralShipmentTracker)) .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as IShipmentTracker) @@ -51,9 +51,9 @@ protected virtual IShipmentTracker GetTrackerByTrackingNumber(string trackingNum { return GetAllTrackers().FirstOrDefault(c => c.IsMatch(trackingNumber)); } - + #endregion - + #region Methods /// diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 4f7c50a562..377945466a 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Services SmartStore.Services - v4.6.1 + v4.7.2 512 @@ -69,39 +69,43 @@ ..\..\packages\AngleSharp.0.9.11\lib\net45\AngleSharp.dll - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll - True + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll - - ..\..\packages\CronExpressionDescriptor.1.21.2\lib\net45\CronExpressionDescriptor.dll - True + + ..\..\packages\CronExpressionDescriptor.2.15.0\lib\netstandard2.0\CronExpressionDescriptor.dll - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll True - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll True ..\..\packages\EPPlus.4.5.3.1\lib\net40\EPPlus.dll - - ..\..\packages\ImageProcessor.2.7.0.100\lib\net452\ImageProcessor.dll + + ..\..\packages\ImageProcessor.2.8.0\lib\net452\ImageProcessor.dll + + + ..\..\packages\ImageProcessor.Plugins.WebP.1.3.0\lib\net452\ImageProcessor.Plugins.WebP.dll ..\..\packages\LumenWorksCsvReader.4.0.0\lib\net461\LumenWorks.Framework.IO.dll - ..\..\packages\MaxMind.Db.2.4.0\lib\net45\MaxMind.Db.dll + ..\..\packages\MaxMind.Db.2.6.1\lib\net45\MaxMind.Db.dll - ..\..\packages\MaxMind.GeoIP2.3.0.0\lib\net45\MaxMind.GeoIP2.dll + ..\..\packages\MaxMind.GeoIP2.3.2.0\lib\net45\MaxMind.GeoIP2.dll + + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll @@ -115,12 +119,11 @@ ..\..\packages\ncrontab.3.3.0\lib\net35\NCrontab.dll True - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - - ..\..\packages\NReco.PdfGenerator.1.1.15\lib\net20\NReco.PdfGenerator.dll - True + + ..\..\packages\NReco.PdfGenerator.1.2.0\lib\net45\NReco.PdfGenerator.dll ..\..\packages\NuGet.Core.2.14.0\lib\net40-Client\NuGet.Core.dll @@ -137,15 +140,18 @@ - - ..\..\packages\System.Linq.Dynamic.1.0.7\lib\net40\System.Linq.Dynamic.dll - True + + ..\..\packages\System.Linq.Dynamic.Core.1.2.1\lib\net46\System.Linq.Dynamic.Core.dll + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + - - ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll @@ -171,9 +177,8 @@ - - ..\..\packages\UAParser.2.1.0.0\lib\net40-Client\UAParser.dll - True + + ..\..\packages\UAParser.3.1.44\lib\net45\UAParser.dll @@ -184,6 +189,9 @@ Properties\AssemblyVersionInfo.cs + + + @@ -207,6 +215,8 @@ + + @@ -348,11 +358,27 @@ + + + + + + + + + + + + + + + - - - + + + + @@ -360,49 +386,49 @@ - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -433,6 +459,7 @@ + @@ -520,12 +547,10 @@ - - - - - - + + + + @@ -558,8 +583,6 @@ - - @@ -748,7 +771,7 @@ - + True True Reference.map @@ -776,32 +799,30 @@ SettingsSingleFileGenerator Settings.Designer.cs - - Designer - - + + Reference.map - + MSDiscoCodeGenerator Reference.cs - + Dynamic - Web References\EuropaCheckVatService\ - http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl + Web References\EuropeCheckVatService\ + https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl Settings - Nop_Services_EuropaCheckVatService_checkVatService + SmartStore_Services_EuropeCheckVatService_checkVatService - + diff --git a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs index 02e9f61346..ccf1a4584d 100644 --- a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs @@ -6,143 +6,143 @@ namespace SmartStore.Services.Stores { - /// - /// Store mapping service interface - /// - public partial interface IStoreMappingService - { - /// - /// Deletes a store mapping record - /// - /// Store mapping record - void DeleteStoreMapping(StoreMapping storeMapping); - - /// - /// Gets a store mapping record - /// - /// Store mapping record identifier - /// Store mapping record - StoreMapping GetStoreMappingById(int storeMappingId); - - /// - /// Gets store mapping records - /// - /// Type - /// Entity - /// Store mapping records - IList GetStoreMappings(T entity) where T : BaseEntity, IStoreMappingSupported; - - /// - /// Gets store mapping records - /// - /// Could be null - /// Could be 0 - /// Store mapping record query - IQueryable GetStoreMappingsFor(string entityName, int entityId); - - /// - /// Save the store mapping for an entity - /// - /// Entity type - /// The entity - /// Array of selected store ids - void SaveStoreMappings(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreMappingSupported; - - /// - /// Inserts a store mapping record - /// - /// Store mapping - void InsertStoreMapping(StoreMapping storeMapping); - - /// - /// Inserts a store mapping record - /// - /// Type - /// Store id - /// Entity - void InsertStoreMapping(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported; - - /// - /// Updates the store mapping record - /// - /// Store mapping - void UpdateStoreMapping(StoreMapping storeMapping); - - /// - /// Find store identifiers with granted access (mapped to the entity) - /// - /// Entity name to check - /// Entity id to check - /// Store identifiers - int[] GetStoresIdsWithAccess(string entityName, int entityId); - - /// - /// Checks whether an entity can be accessed in a store (mapped to this store) - /// - /// Entity name to check - /// Entity id to check - /// true - authorized; otherwise, false - bool Authorize(string entityName, int entityId); - - /// - /// Checks whether an entity can be accessed in a store (mapped to this store) - /// - /// Entity name to check - /// Entity id to check - /// Store identifier to check against - /// true - authorized; otherwise, false - bool Authorize(string entityName, int entityId, int storeId); - } - - public static class IStoreMappingServiceExtensions - { - /// - /// Find store identifiers with granted access (mapped to the entity) - /// - /// Type - /// Entity - /// Store identifiers - public static int[] GetStoresIdsWithAccess(this IStoreMappingService svc, T entity) where T : BaseEntity, IStoreMappingSupported - { - if (entity == null) - return new int[0]; - - return svc.GetStoresIdsWithAccess(entity.GetEntityName(), entity.Id); - } - - /// - /// Authorize whether entity could be accessed in the current store (mapped to this store) - /// - /// Type - /// Entity - /// true - authorized; otherwise, false - public static bool Authorize(this IStoreMappingService svc, T entity) where T : BaseEntity, IStoreMappingSupported - { - if (entity == null) - return false; - - if (!entity.LimitedToStores) - return true; - - return svc.Authorize(entity.GetEntityName(), entity.Id); - } - - /// - /// Authorize whether entity could be accessed in a store (mapped to this store) - /// - /// Type - /// Entity - /// Store identifier to check against - /// true - authorized; otherwise, false - public static bool Authorize(this IStoreMappingService svc, T entity, int storeId) where T : BaseEntity, IStoreMappingSupported - { - if (entity == null) - return false; - - if (!entity.LimitedToStores) - return true; - - return svc.Authorize(entity.GetEntityName(), entity.Id, storeId); - } - } + /// + /// Store mapping service interface + /// + public partial interface IStoreMappingService + { + /// + /// Deletes a store mapping record + /// + /// Store mapping record + void DeleteStoreMapping(StoreMapping storeMapping); + + /// + /// Gets a store mapping record + /// + /// Store mapping record identifier + /// Store mapping record + StoreMapping GetStoreMappingById(int storeMappingId); + + /// + /// Gets store mapping records + /// + /// Type + /// Entity + /// Store mapping records + IList GetStoreMappings(T entity) where T : BaseEntity, IStoreMappingSupported; + + /// + /// Gets store mapping records + /// + /// Could be null + /// Could be 0 + /// Store mapping record query + IQueryable GetStoreMappingsFor(string entityName, int entityId); + + /// + /// Save the store mapping for an entity + /// + /// Entity type + /// The entity + /// Array of selected store ids + void SaveStoreMappings(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreMappingSupported; + + /// + /// Inserts a store mapping record + /// + /// Store mapping + void InsertStoreMapping(StoreMapping storeMapping); + + /// + /// Inserts a store mapping record + /// + /// Type + /// Store id + /// Entity + void InsertStoreMapping(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported; + + /// + /// Updates the store mapping record + /// + /// Store mapping + void UpdateStoreMapping(StoreMapping storeMapping); + + /// + /// Find store identifiers with granted access (mapped to the entity) + /// + /// Entity name to check + /// Entity id to check + /// Store identifiers + int[] GetStoresIdsWithAccess(string entityName, int entityId); + + /// + /// Checks whether an entity can be accessed in a store (mapped to this store) + /// + /// Entity name to check + /// Entity id to check + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId); + + /// + /// Checks whether an entity can be accessed in a store (mapped to this store) + /// + /// Entity name to check + /// Entity id to check + /// Store identifier to check against + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId, int storeId); + } + + public static class IStoreMappingServiceExtensions + { + /// + /// Find store identifiers with granted access (mapped to the entity) + /// + /// Type + /// Entity + /// Store identifiers + public static int[] GetStoresIdsWithAccess(this IStoreMappingService svc, T entity) where T : BaseEntity, IStoreMappingSupported + { + if (entity == null) + return new int[0]; + + return svc.GetStoresIdsWithAccess(entity.GetEntityName(), entity.Id); + } + + /// + /// Authorize whether entity could be accessed in the current store (mapped to this store) + /// + /// Type + /// Entity + /// true - authorized; otherwise, false + public static bool Authorize(this IStoreMappingService svc, T entity) where T : BaseEntity, IStoreMappingSupported + { + if (entity == null) + return false; + + if (!entity.LimitedToStores) + return true; + + return svc.Authorize(entity.GetEntityName(), entity.Id); + } + + /// + /// Authorize whether entity could be accessed in a store (mapped to this store) + /// + /// Type + /// Entity + /// Store identifier to check against + /// true - authorized; otherwise, false + public static bool Authorize(this IStoreMappingService svc, T entity, int storeId) where T : BaseEntity, IStoreMappingSupported + { + if (entity == null) + return false; + + if (!entity.LimitedToStores) + return true; + + return svc.Authorize(entity.GetEntityName(), entity.Id, storeId); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Stores/IStoreService.cs b/src/Libraries/SmartStore.Services/Stores/IStoreService.cs index 90a6da14ce..1cef9569da 100644 --- a/src/Libraries/SmartStore.Services/Stores/IStoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/IStoreService.cs @@ -4,62 +4,62 @@ namespace SmartStore.Services.Stores { - /// - /// Store service interface - /// - public partial interface IStoreService - { - /// - /// Deletes a store - /// - /// Store - void DeleteStore(Store store); + /// + /// Store service interface + /// + public partial interface IStoreService + { + /// + /// Deletes a store + /// + /// Store + void DeleteStore(Store store); - /// - /// Gets all stores - /// - /// Store collection - IList GetAllStores(); + /// + /// Gets all stores + /// + /// Store collection + IList GetAllStores(); - /// - /// Gets a store - /// - /// Store identifier - /// Store - Store GetStoreById(int storeId); + /// + /// Gets a store + /// + /// Store identifier + /// Store + Store GetStoreById(int storeId); - /// - /// Inserts a store - /// - /// Store - void InsertStore(Store store); + /// + /// Inserts a store + /// + /// Store + void InsertStore(Store store); - /// - /// Updates the store - /// - /// Store - void UpdateStore(Store store); + /// + /// Updates the store + /// + /// Store + void UpdateStore(Store store); - /// - /// True if there's only one store. Otherwise False. - /// - bool IsSingleStoreMode(); + /// + /// True if there's only one store. Otherwise False. + /// + bool IsSingleStoreMode(); - /// - /// True if the store data is valid. Otherwise False. - /// - /// Store entity - bool IsStoreDataValid(Store store); + /// + /// True if the store data is valid. Otherwise False. + /// + /// Store entity + bool IsStoreDataValid(Store store); - /// - /// Gets the store host name - /// - /// The store to get the host name for - /// - /// If null, checks whether all pages should be secured per . - /// If true, returns the secure url, but only if SSL is enabled for the store. - /// - /// The host name - string GetHost(Store store, bool? secure = null); - } + /// + /// Gets the store host name + /// + /// The store to get the host name for + /// + /// If null, checks whether all pages should be secured per . + /// If true, returns the secure url, but only if SSL is enabled for the store. + /// + /// The host name + string GetHost(Store store, bool? secure = null); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs index d5578486ca..07f903029b 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs @@ -8,240 +8,247 @@ namespace SmartStore.Services.Stores { - public partial class StoreMappingService : IStoreMappingService - { - /// - /// 0 = segment (EntityName.IdRange) - /// - const string STOREMAPPING_SEGMENT_KEY = "storemapping:range-{0}"; - const string STOREMAPPING_SEGMENT_PATTERN = "storemapping:range-*"; - - private readonly IRepository _storeMappingRepository; - private readonly IStoreContext _storeContext; - private readonly IStoreService _storeService; - private readonly ICacheManager _cacheManager; - - public StoreMappingService( - ICacheManager cacheManager, - IStoreContext storeContext, - IStoreService storeService, - IRepository storeMappingRepository) - { - _cacheManager = cacheManager; - _storeContext = storeContext; - _storeService = storeService; - _storeMappingRepository = storeMappingRepository; - - QuerySettings = DbQuerySettings.Default; - } - - public DbQuerySettings QuerySettings { get; set; } - - public virtual void DeleteStoreMapping(StoreMapping storeMapping) - { - Guard.NotNull(storeMapping, nameof(storeMapping)); - - _storeMappingRepository.Delete(storeMapping); - - ClearCacheSegment(storeMapping.EntityName, storeMapping.EntityId); - } - - public virtual StoreMapping GetStoreMappingById(int storeMappingId) - { - if (storeMappingId == 0) - return null; - - var storeMapping = _storeMappingRepository.GetById(storeMappingId); - return storeMapping; - } - - public virtual IList GetStoreMappings(T entity) where T : BaseEntity, IStoreMappingSupported - { - Guard.NotNull(entity, nameof(entity)); - - int entityId = entity.Id; - string entityName = entity.GetEntityName(); - - var query = from sm in _storeMappingRepository.Table - where sm.EntityId == entityId && - sm.EntityName == entityName - select sm; - var storeMappings = query.ToList(); - return storeMappings; - } - - public virtual IQueryable GetStoreMappingsFor(string entityName, int entityId) - { - var query = _storeMappingRepository.Table; - - if (entityName.HasValue()) - query = query.Where(x => x.EntityName == entityName); - - if (entityId != 0) - query = query.Where(x => x.EntityId == entityId); - - return query; - } - - public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreMappingSupported - { - var existingStoreMappings = GetStoreMappings(entity); - var allStores = _storeService.GetAllStores(); - - foreach (var store in allStores) - { - if (selectedStoreIds != null && selectedStoreIds.Contains(store.Id)) - { - if (!existingStoreMappings.Any(x => x.StoreId == store.Id)) - InsertStoreMapping(entity, store.Id); - } - else - { - var storeMappingToDelete = existingStoreMappings.FirstOrDefault(x => x.StoreId == store.Id); - if (storeMappingToDelete != null) - DeleteStoreMapping(storeMappingToDelete); - } - } - } - - public virtual void InsertStoreMapping(StoreMapping storeMapping) - { - Guard.NotNull(storeMapping, nameof(storeMapping)); - - _storeMappingRepository.Insert(storeMapping); - - ClearCacheSegment(storeMapping.EntityName, storeMapping.EntityId); - } - - public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported - { - Guard.NotNull(entity, nameof(entity)); - - if (storeId == 0) - throw new ArgumentOutOfRangeException(nameof(storeId)); - - int entityId = entity.Id; - string entityName = entity.GetEntityName(); - - var storeMapping = new StoreMapping - { - EntityId = entityId, - EntityName = entityName, - StoreId = storeId - }; - - InsertStoreMapping(storeMapping); - } - - public virtual void UpdateStoreMapping(StoreMapping storeMapping) - { - Guard.NotNull(storeMapping, nameof(storeMapping)); - - _storeMappingRepository.Update(storeMapping); - - ClearCacheSegment(storeMapping.EntityName, storeMapping.EntityId); - } - - public virtual int[] GetStoresIdsWithAccess(string entityName, int entityId) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - if (entityId <= 0) - return new int[0]; - - var cacheSegment = GetCacheSegment(entityName, entityId); - - if (!cacheSegment.TryGetValue(entityId, out var storeIds)) - { - return Array.Empty(); - } - - return storeIds; - } - - public bool Authorize(string entityName, int entityId) - { - return Authorize(entityName, entityId, _storeContext.CurrentStore.Id); - } - - public virtual bool Authorize(string entityName, int entityId, int storeId) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - if (entityId <= 0) - return false; - - if (storeId <= 0) - // return true if no store specified/found - return true; - - if (QuerySettings.IgnoreMultiStore) - return true; - - // Permission granted only when the id list contains the passed storeId - return GetStoresIdsWithAccess(entityName, entityId).Any(x => x == storeId); - } - - #region Cache segmenting - - protected virtual IDictionary GetCacheSegment(string entityName, int entityId) - { - Guard.NotEmpty(entityName, nameof(entityName)); - - var segmentKey = GetSegmentKeyPart(entityName, entityId, out var minEntityId, out var maxEntityId); - var cacheKey = BuildCacheSegmentKey(segmentKey); - - return _cacheManager.Get(cacheKey, () => - { - var query = from sm in _storeMappingRepository.TableUntracked - where - sm.EntityId >= minEntityId && - sm.EntityId <= maxEntityId && - sm.EntityName == entityName - select sm; - - var mappings = query.ToLookup(x => x.EntityId, x => x.StoreId); + public partial class StoreMappingService : IStoreMappingService + { + /// + /// 0 = segment (EntityName.IdRange) + /// + const string STOREMAPPING_SEGMENT_KEY = "storemapping:range-{0}"; + internal const string STOREMAPPING_SEGMENT_PATTERN = "storemapping:range-*"; + + private readonly IRepository _storeMappingRepository; + private readonly IStoreContext _storeContext; + private readonly IStoreService _storeService; + private readonly ICacheManager _cacheManager; + + public StoreMappingService( + ICacheManager cacheManager, + IStoreContext storeContext, + IStoreService storeService, + IRepository storeMappingRepository) + { + _cacheManager = cacheManager; + _storeContext = storeContext; + _storeService = storeService; + _storeMappingRepository = storeMappingRepository; + + QuerySettings = DbQuerySettings.Default; + } + + public DbQuerySettings QuerySettings { get; set; } + + public virtual void DeleteStoreMapping(StoreMapping storeMapping) + { + Guard.NotNull(storeMapping, nameof(storeMapping)); + + _storeMappingRepository.Delete(storeMapping); + + ClearCacheSegment(storeMapping.EntityName, storeMapping.EntityId); + } + + public virtual StoreMapping GetStoreMappingById(int storeMappingId) + { + if (storeMappingId == 0) + return null; + + var storeMapping = _storeMappingRepository.GetById(storeMappingId); + return storeMapping; + } + + public virtual IList GetStoreMappings(T entity) where T : BaseEntity, IStoreMappingSupported + { + Guard.NotNull(entity, nameof(entity)); + + int entityId = entity.Id; + string entityName = entity.GetEntityName(); + + var query = from sm in _storeMappingRepository.Table + where sm.EntityId == entityId && + sm.EntityName == entityName + select sm; + var storeMappings = query.ToList(); + return storeMappings; + } + + public virtual IQueryable GetStoreMappingsFor(string entityName, int entityId) + { + var query = _storeMappingRepository.Table; + + if (entityName.HasValue()) + query = query.Where(x => x.EntityName == entityName); + + if (entityId != 0) + query = query.Where(x => x.EntityId == entityId); + + return query; + } + + public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreMappingSupported + { + var existingStoreMappings = GetStoreMappings(entity); + var allStores = _storeService.GetAllStores(); + var selectedIds = selectedStoreIds ?? Array.Empty(); + + entity.LimitedToStores = selectedIds.Length == 1 && selectedIds[0] == 0 + ? false + : selectedIds.Any(); + + foreach (var store in allStores) + { + if (selectedIds.Contains(store.Id)) + { + if (!existingStoreMappings.Any(x => x.StoreId == store.Id)) + InsertStoreMapping(entity, store.Id); + } + else + { + var storeMappingToDelete = existingStoreMappings.FirstOrDefault(x => x.StoreId == store.Id); + if (storeMappingToDelete != null) + DeleteStoreMapping(storeMappingToDelete); + } + } + + _storeMappingRepository.Context.SaveChanges(); + } + + public virtual void InsertStoreMapping(StoreMapping storeMapping) + { + Guard.NotNull(storeMapping, nameof(storeMapping)); + + _storeMappingRepository.Insert(storeMapping); + + ClearCacheSegment(storeMapping.EntityName, storeMapping.EntityId); + } - var dict = new Dictionary(mappings.Count); + public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported + { + Guard.NotNull(entity, nameof(entity)); + + if (storeId == 0) + throw new ArgumentOutOfRangeException(nameof(storeId)); + + int entityId = entity.Id; + string entityName = entity.GetEntityName(); + + var storeMapping = new StoreMapping + { + EntityId = entityId, + EntityName = entityName, + StoreId = storeId + }; + + InsertStoreMapping(storeMapping); + } + + public virtual void UpdateStoreMapping(StoreMapping storeMapping) + { + Guard.NotNull(storeMapping, nameof(storeMapping)); + + _storeMappingRepository.Update(storeMapping); + + ClearCacheSegment(storeMapping.EntityName, storeMapping.EntityId); + } + + public virtual int[] GetStoresIdsWithAccess(string entityName, int entityId) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + if (entityId <= 0) + return new int[0]; + + var cacheSegment = GetCacheSegment(entityName, entityId); + + if (!cacheSegment.TryGetValue(entityId, out var storeIds)) + { + return Array.Empty(); + } + + return storeIds; + } + + public bool Authorize(string entityName, int entityId) + { + return Authorize(entityName, entityId, _storeContext.CurrentStore.Id); + } + + public virtual bool Authorize(string entityName, int entityId, int storeId) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + if (entityId <= 0) + return false; + + if (storeId <= 0) + // return true if no store specified/found + return true; + + if (QuerySettings.IgnoreMultiStore) + return true; + + // Permission granted only when the id list contains the passed storeId + return GetStoresIdsWithAccess(entityName, entityId).Any(x => x == storeId); + } + + #region Cache segmenting + + protected virtual IDictionary GetCacheSegment(string entityName, int entityId) + { + Guard.NotEmpty(entityName, nameof(entityName)); + + var segmentKey = GetSegmentKeyPart(entityName, entityId, out var minEntityId, out var maxEntityId); + var cacheKey = BuildCacheSegmentKey(segmentKey); + + return _cacheManager.Get(cacheKey, () => + { + var query = from sm in _storeMappingRepository.TableUntracked + where + sm.EntityId >= minEntityId && + sm.EntityId <= maxEntityId && + sm.EntityName == entityName + select sm; - foreach (var sm in mappings) - { - dict[sm.Key] = sm.ToArray(); - } - - return dict; - }); - } - - /// - /// Clears the cached segment from the cache - /// - protected virtual void ClearCacheSegment(string entityName, int entityId) - { - try - { - var segmentKey = GetSegmentKeyPart(entityName, entityId); - _cacheManager.Remove(BuildCacheSegmentKey(segmentKey)); - } - catch { } - } - - private string BuildCacheSegmentKey(string segment) - { - return String.Format(STOREMAPPING_SEGMENT_KEY, segment); - } - - private string GetSegmentKeyPart(string entityName, int entityId) - { - return GetSegmentKeyPart(entityName, entityId, out _, out _); - } - - private string GetSegmentKeyPart(string entityName, int entityId, out int minId, out int maxId) - { - maxId = entityId.GetRange(1000, out minId); - return (entityName + "." + maxId.ToString()).ToLowerInvariant(); - } - - #endregion - } + var mappings = query.ToLookup(x => x.EntityId, x => x.StoreId); + + var dict = new Dictionary(mappings.Count); + + foreach (var sm in mappings) + { + dict[sm.Key] = sm.ToArray(); + } + + return dict; + }); + } + + /// + /// Clears the cached segment from the cache + /// + protected virtual void ClearCacheSegment(string entityName, int entityId) + { + try + { + var segmentKey = GetSegmentKeyPart(entityName, entityId); + _cacheManager.Remove(BuildCacheSegmentKey(segmentKey)); + } + catch { } + } + + private string BuildCacheSegmentKey(string segment) + { + return String.Format(STOREMAPPING_SEGMENT_KEY, segment); + } + + private string GetSegmentKeyPart(string entityName, int entityId) + { + return GetSegmentKeyPart(entityName, entityId, out _, out _); + } + + private string GetSegmentKeyPart(string entityName, int entityId, out int minId, out int maxId) + { + (minId, maxId) = entityId.GetRange(1000); + return (entityName + "." + minId.ToString()).ToLowerInvariant(); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Stores/StoreService.cs b/src/Libraries/SmartStore.Services/Stores/StoreService.cs index f8cde15ab0..a7add0c0f1 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -7,108 +7,108 @@ namespace SmartStore.Services.Stores { - public partial class StoreService : IStoreService - { - private readonly IRepository _storeRepository; - - private bool? _isSingleStoreMode = null; - - public StoreService(IRepository storeRepository) - { - _storeRepository = storeRepository; - } - - public virtual IList GetAllStores() - { - var query = _storeRepository.Table - .Expand(x => x.PrimaryStoreCurrency) - .Expand(x => x.PrimaryExchangeRateCurrency) - .OrderBy(x => x.DisplayOrder) - .ThenBy(x => x.Name); - - var stores = query.ToListCached("db.store.all"); - return stores; - } - - public virtual Store GetStoreById(int storeId) - { - if (storeId == 0) - return null; - - return _storeRepository.GetByIdCached(storeId, "db.store.id-" + storeId); - } - - public virtual void InsertStore(Store store) - { - Guard.NotNull(store, nameof(store)); - - _storeRepository.Insert(store); - } - - public virtual void UpdateStore(Store store) - { - Guard.NotNull(store, nameof(store)); - - _storeRepository.Update(store); - } - - public virtual void DeleteStore(Store store) - { - Guard.NotNull(store, nameof(store)); - - var allStores = GetAllStores(); - if (allStores.Count == 1) - throw new Exception("You cannot delete the only configured store."); - - _storeRepository.Delete(store); - } - - public virtual bool IsSingleStoreMode() - { - if (!_isSingleStoreMode.HasValue) - { - _isSingleStoreMode = (_storeRepository.TableUntracked.Count() <= 1); - } - - return _isSingleStoreMode.Value; - } - - public virtual bool IsStoreDataValid(Store store) - { - Guard.NotNull(store, nameof(store)); - - if (store.Url.IsEmpty()) - return false; - - try - { - var uri = new Uri(store.Url); - var domain = uri.DnsSafeHost.EmptyNull().ToLower(); - - switch (domain) - { - case "www.yourstore.com": - case "yourstore.com": - case "www.mystore.com": - case "mystore.com": - case "www.mein-shop.de": - case "mein-shop.de": - return false; - default: - return store.Url.IsWebUrl(); - } - } - catch - { - return false; - } - } - - public string GetHost(Store store, bool? secure = null) - { - Guard.NotNull(store, nameof(store)); - - return store.GetHost(secure ?? store.ForceSslForAllPages); - } - } + public partial class StoreService : IStoreService + { + private readonly IRepository _storeRepository; + + private bool? _isSingleStoreMode = null; + + public StoreService(IRepository storeRepository) + { + _storeRepository = storeRepository; + } + + public virtual IList GetAllStores() + { + var query = _storeRepository.Table + .Expand(x => x.PrimaryStoreCurrency) + .Expand(x => x.PrimaryExchangeRateCurrency) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Name); + + var stores = query.ToListCached("db.store.all"); + return stores; + } + + public virtual Store GetStoreById(int storeId) + { + if (storeId == 0) + return null; + + return _storeRepository.GetByIdCached(storeId, "db.store.id-" + storeId); + } + + public virtual void InsertStore(Store store) + { + Guard.NotNull(store, nameof(store)); + + _storeRepository.Insert(store); + } + + public virtual void UpdateStore(Store store) + { + Guard.NotNull(store, nameof(store)); + + _storeRepository.Update(store); + } + + public virtual void DeleteStore(Store store) + { + Guard.NotNull(store, nameof(store)); + + var allStores = GetAllStores(); + if (allStores.Count == 1) + throw new Exception("You cannot delete the only configured store."); + + _storeRepository.Delete(store); + } + + public virtual bool IsSingleStoreMode() + { + if (!_isSingleStoreMode.HasValue) + { + _isSingleStoreMode = (_storeRepository.TableUntracked.Count() <= 1); + } + + return _isSingleStoreMode.Value; + } + + public virtual bool IsStoreDataValid(Store store) + { + Guard.NotNull(store, nameof(store)); + + if (store.Url.IsEmpty()) + return false; + + try + { + var uri = new Uri(store.Url); + var domain = uri.DnsSafeHost.EmptyNull().ToLower(); + + switch (domain) + { + case "www.yourstore.com": + case "yourstore.com": + case "www.mystore.com": + case "mystore.com": + case "www.mein-shop.de": + case "mein-shop.de": + return false; + default: + return store.Url.IsWebUrl(); + } + } + catch + { + return false; + } + } + + public string GetHost(Store store, bool? secure = null) + { + Guard.NotNull(store, nameof(store)); + + return store.GetHost(secure ?? store.ForceSslForAllPages); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs index 28598352c2..5432120b91 100644 --- a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs +++ b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs @@ -7,65 +7,65 @@ namespace SmartStore.Services.Tasks { - public static class CronExpression - { - public static bool IsValid(string expression) - { - try - { - CrontabSchedule.Parse(expression); - return true; - } - catch { } + public static class CronExpression + { + public static bool IsValid(string expression) + { + try + { + CrontabSchedule.Parse(expression); + return true; + } + catch { } - return false; - } + return false; + } - public static DateTime GetNextSchedule(string expression, DateTime baseTime) - { - return GetFutureSchedules(expression, baseTime, 1).FirstOrDefault(); - } + public static DateTime GetNextSchedule(string expression, DateTime baseTime) + { + return GetFutureSchedules(expression, baseTime, 1).FirstOrDefault(); + } - public static DateTime GetNextSchedule(string expression, DateTime baseTime, DateTime endTime) - { - return GetFutureSchedules(expression, baseTime, endTime, 1).FirstOrDefault(); - } + public static DateTime GetNextSchedule(string expression, DateTime baseTime, DateTime endTime) + { + return GetFutureSchedules(expression, baseTime, endTime, 1).FirstOrDefault(); + } - public static IEnumerable GetFutureSchedules(string expression, DateTime baseTime, int max = 10) - { - return GetFutureSchedules(expression, baseTime, DateTime.MaxValue); - } + public static IEnumerable GetFutureSchedules(string expression, DateTime baseTime, int max = 10) + { + return GetFutureSchedules(expression, baseTime, DateTime.MaxValue); + } - public static IEnumerable GetFutureSchedules(string expression, DateTime baseTime, DateTime endTime, int max = 10) - { - Guard.NotEmpty(expression, nameof(expression)); + public static IEnumerable GetFutureSchedules(string expression, DateTime baseTime, DateTime endTime, int max = 10) + { + Guard.NotEmpty(expression, nameof(expression)); - var schedule = CrontabSchedule.Parse(expression); - return schedule.GetNextOccurrences(baseTime, endTime).Take(max); - } + var schedule = CrontabSchedule.Parse(expression); + return schedule.GetNextOccurrences(baseTime, endTime).Take(max); + } - public static string GetFriendlyDescription(string expression) - { - var options = new Options - { - DayOfWeekStartIndexZero = true, - ThrowExceptionOnParseError = true, - Verbose = false, - Use24HourTimeFormat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.AMDesignator.IsEmpty() - }; + public static string GetFriendlyDescription(string expression) + { + var options = new Options + { + DayOfWeekStartIndexZero = true, + ThrowExceptionOnParseError = true, + Verbose = false, + Use24HourTimeFormat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.AMDesignator.IsEmpty() + }; - if (expression.HasValue()) - { - try - { - return ExpressionDescriptor.GetDescription(expression, options); - } - catch { } - } + if (expression.HasValue()) + { + try + { + return ExpressionDescriptor.GetDescription(expression, options); + } + catch { } + } - return "?"; - } + return "?"; + } - } + } } diff --git a/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs index be442aa61a..89bb1a7acc 100644 --- a/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs @@ -56,30 +56,30 @@ public partial interface IScheduleTaskService /// Task void UpdateTask(ScheduleTask task); - /// - /// Inserts a new task definition to the database or returns an existing one - /// - /// The concrete implementation of the task - /// Wraps the newly created instance - /// A newly created or existing task instance - /// - /// This method does NOT update an already exising task - /// - ScheduleTask GetOrAddTask(Action newAction) where T : ITask; - - /// - /// Calculates - according to their cron expressions - all task future schedules - /// and saves them to the database. - /// - /// When true, determines stale tasks and fixes their states to idle. - void CalculateFutureSchedules(IEnumerable tasks, bool isAppStart = false); - - /// - /// Calculates the next schedule according to the task's cron expression - /// - /// ScheduleTask - /// The next schedule or null if the task is disabled - DateTime? GetNextSchedule(ScheduleTask task); + /// + /// Inserts a new task definition to the database or returns an existing one + /// + /// The concrete implementation of the task + /// Wraps the newly created instance + /// A newly created or existing task instance + /// + /// This method does NOT update an already exising task + /// + ScheduleTask GetOrAddTask(Action newAction) where T : ITask; + + /// + /// Calculates - according to their cron expressions - all task future schedules + /// and saves them to the database. + /// + /// When true, determines stale tasks and fixes their states to idle. + void CalculateFutureSchedules(IEnumerable tasks, bool isAppStart = false); + + /// + /// Calculates the next schedule according to the task's cron expression + /// + /// ScheduleTask + /// The next schedule or null if the task is disabled + DateTime? GetNextSchedule(ScheduleTask task); #region Schedule task history @@ -168,32 +168,32 @@ IPagedList GetHistoryEntries( } public static class IScheduleTaskServiceExtensions - { - public static ScheduleTask GetTaskByType(this IScheduleTaskService service) where T : ITask - { - return service.GetTaskByType(typeof(T)); - } - - public static ScheduleTask GetTaskByType(this IScheduleTaskService service, Type taskType) - { - Guard.NotNull(taskType, nameof(taskType)); - - var name = taskType.AssemblyQualifiedNameWithoutVersion(); - - return service.GetTaskByType(name); - } - - public static bool TryDeleteTask(this IScheduleTaskService service) where T : ITask - { - var task = service.GetTaskByType(typeof(T)); - - if (task != null) - { - service.DeleteTask(task); - return true; - } - - return false; - } - } + { + public static ScheduleTask GetTaskByType(this IScheduleTaskService service) where T : ITask + { + return service.GetTaskByType(typeof(T)); + } + + public static ScheduleTask GetTaskByType(this IScheduleTaskService service, Type taskType) + { + Guard.NotNull(taskType, nameof(taskType)); + + var name = taskType.AssemblyQualifiedNameWithoutVersion(); + + return service.GetTaskByType(name); + } + + public static bool TryDeleteTask(this IScheduleTaskService service) where T : ITask + { + var task = service.GetTaskByType(typeof(T)); + + if (task != null) + { + service.DeleteTask(task); + return true; + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ITask.cs b/src/Libraries/SmartStore.Services/Tasks/ITask.cs index 01e576c330..43055fa729 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITask.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITask.cs @@ -18,25 +18,27 @@ public partial interface ITask void Execute(TaskExecutionContext ctx); } - /// - /// Interface that should be implemented by background tasks - /// - public interface IAsyncTask : ITask - { - /// - /// Executes a task asynchronously - /// - /// - /// The execution context - /// - Task ExecuteAsync(TaskExecutionContext ctx); - } + /// + /// Interface that should be implemented by background tasks + /// + public interface IAsyncTask : ITask + { + /// + /// Executes a task asynchronously + /// + /// + /// The execution context + /// + Task ExecuteAsync(TaskExecutionContext ctx); + } - public abstract class AsyncTask : IAsyncTask - { - public void Execute(TaskExecutionContext ctx) => - throw new NotSupportedException(); + public abstract class AsyncTask : IAsyncTask + { + public void Execute(TaskExecutionContext ctx) + { + throw new NotSupportedException(); + } - public abstract Task ExecuteAsync(TaskExecutionContext ctx); - } + public abstract Task ExecuteAsync(TaskExecutionContext ctx); + } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs index 5f0f126359..f18f72916e 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs @@ -7,8 +7,8 @@ namespace SmartStore.Services.Tasks public interface ITaskExecutor { Task ExecuteAsync( - ScheduleTask entity, - IDictionary taskParameters = null, - bool throwOnError = false); + ScheduleTask entity, + IDictionary taskParameters = null, + bool throwOnError = false); } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs index 0070ad48f6..af80694d8c 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs @@ -17,58 +17,58 @@ public interface ITaskScheduler /// int SweepIntervalMinutes { get; set; } - /// - /// The fully qualified base url - /// + /// + /// The fully qualified base url + /// string BaseUrl { get; set; } - /// - /// Gets a value indicating whether the scheduler is active and periodically sweeps all tasks. - /// + /// + /// Gets a value indicating whether the scheduler is active and periodically sweeps all tasks. + /// bool IsActive { get; } - /// - /// Gets the unique key by which a instance - /// can be retrieved from an instance - /// - /// The schedule task id - /// A unique string key - string GetAsyncStateKey(int scheduleTaskId); + /// + /// Gets the unique key by which a instance + /// can be retrieved from an instance + /// + /// The schedule task id + /// A unique string key + string GetAsyncStateKey(int scheduleTaskId); - /// - /// Starts/initializes the scheduler - /// - void Start(); + /// + /// Starts/initializes the scheduler + /// + void Start(); - /// - /// Stops the scheduler - /// + /// + /// Stops the scheduler + /// void Stop(); - /// - /// Executes a single task immediately - /// - /// - /// Optional task parameters + /// + /// Executes a single task immediately + /// + /// + /// Optional task parameters void RunSingleTask(int scheduleTaskId, IDictionary taskParameters = null); - /// - /// Verifies the authentication token which is generated right before the HTTP endpoint gets called. - /// - /// The authentication token to verify - /// true if the validation succeeds, false otherwise - /// - /// The task scheduler sends the token as a HTTP request header item. - /// The called endpoint (e.g. a controller action) is reponsible for invoking - /// this method and quitting the tasks's execution - preferrably with HTTP 403 - - /// if the verification fails. - /// + /// + /// Verifies the authentication token which is generated right before the HTTP endpoint gets called. + /// + /// The authentication token to verify + /// true if the validation succeeds, false otherwise + /// + /// The task scheduler sends the token as a HTTP request header item. + /// The called endpoint (e.g. a controller action) is reponsible for invoking + /// this method and quitting the tasks's execution - preferrably with HTTP 403 - + /// if the verification fails. + /// bool VerifyAuthToken(string authToken); } public static class ITaskSchedulerExtensions { - internal static void SetBaseUrl(this ITaskScheduler scheduler, IStoreService storeService, HttpContextBase httpContext) + internal static void SetBaseUrl(this ITaskScheduler scheduler, IStoreService storeService, HttpContextBase httpContext) { string url = ""; @@ -83,7 +83,7 @@ internal static void SetBaseUrl(this ITaskScheduler scheduler, IStoreService sto if (url.IsEmpty()) { - url = WebHelper.GetAbsoluteUrl(VirtualPathUtility.ToAbsolute("~/TaskScheduler"), httpContext.Request); + url = WebHelper.GetAbsoluteUrl(VirtualPathUtility.ToAbsolute("~/TaskScheduler"), httpContext.Request); } scheduler.BaseUrl = url; diff --git a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs index b1ed3e917c..c9990757c2 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs @@ -33,20 +33,20 @@ public ScheduleTaskService( { _taskRepository = taskRepository; _taskHistoryRepository = taskHistoryRepository; - _dtHelper = dtHelper; + _dtHelper = dtHelper; _env = env; _commonSettings = commonSettings; - T = NullLocalizer.Instance; - Logger = NullLogger.Instance; + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; } - public Localizer T { get; set; } - public ILogger Logger { get; set; } + public Localizer T { get; set; } + public ILogger Logger { get; set; } public virtual void DeleteTask(ScheduleTask task) { - Guard.NotNull(task, nameof(task)); + Guard.NotNull(task, nameof(task)); _taskRepository.Delete(task); } @@ -56,49 +56,49 @@ public virtual ScheduleTask GetTaskById(int taskId) if (taskId == 0) return null; - return Retry.Run( - () => _taskRepository.GetById(taskId), - 3, TimeSpan.FromMilliseconds(100), - RetryOnDeadlockException); - } + return Retry.Run( + () => _taskRepository.GetById(taskId), + 3, TimeSpan.FromMilliseconds(100), + RetryOnDeadlockException); + } public virtual ScheduleTask GetTaskByType(string type) { - try - { - if (type.HasValue()) - { - var query = _taskRepository.Table - .Where(t => t.Type == type) - .OrderByDescending(t => t.Id); - - var task = query.FirstOrDefault(); - return task; - } - } - catch (Exception exc) - { - // Do not throw an exception if the underlying provider failed on Open. - exc.Dump(); - } - - return null; + try + { + if (type.HasValue()) + { + var query = _taskRepository.Table + .Where(t => t.Type == type) + .OrderByDescending(t => t.Id); + + var task = query.FirstOrDefault(); + return task; + } + } + catch (Exception exc) + { + // Do not throw an exception if the underlying provider failed on Open. + exc.Dump(); + } + + return null; } - public virtual IList GetAllTasks(bool includeDisabled = false) + public virtual IList GetAllTasks(bool includeDisabled = false) { var query = _taskRepository.Table; - if (!includeDisabled) + if (!includeDisabled) { query = query.Where(t => t.Enabled); } query = query.OrderByDescending(t => t.Enabled); - return Retry.Run( - () => query.ToList(), - 3, TimeSpan.FromMilliseconds(100), - RetryOnDeadlockException); - } + return Retry.Run( + () => query.ToList(), + 3, TimeSpan.FromMilliseconds(100), + RetryOnDeadlockException); + } public async virtual Task> GetPendingTasksAsync() { @@ -135,76 +135,76 @@ from t in _taskRepository.Table .ToList(); return pendingTasks; - } + } public virtual void InsertTask(ScheduleTask task) { - Guard.NotNull(task, nameof(task)); + Guard.NotNull(task, nameof(task)); - _taskRepository.Insert(task); + _taskRepository.Insert(task); } public virtual void UpdateTask(ScheduleTask task) { - Guard.NotNull(task, nameof(task)); + Guard.NotNull(task, nameof(task)); - try - { + try + { _taskRepository.Update(task); - } - catch (Exception ex) - { - Logger.Error(ex); - throw; - } + } + catch (Exception ex) + { + Logger.Error(ex); + throw; + } } - public ScheduleTask GetOrAddTask(Action newAction) where T : ITask - { - Guard.NotNull(newAction, nameof(newAction)); - - var type = typeof(T); - - if (type.IsAbstract || type.IsInterface || type.IsNotPublic) - { - throw new InvalidOperationException("Only concrete public task types can be registered."); - } - - var scheduleTask = this.GetTaskByType(); - - if (scheduleTask == null) - { - scheduleTask = new ScheduleTask { Type = type.AssemblyQualifiedNameWithoutVersion() }; - newAction(scheduleTask); - InsertTask(scheduleTask); - } - - return scheduleTask; - } - - public void CalculateFutureSchedules(IEnumerable tasks, bool isAppStart = false) - { - Guard.NotNull(tasks, nameof(tasks)); - - foreach (var task in tasks) - { - task.NextRunUtc = GetNextSchedule(task); - if (isAppStart) - { - FixTypeName(task); - } - else - { - UpdateTask(task); - } - } - - if (isAppStart) - { - // On app start this method's execution is thread-safe, making it sufficient - // to commit all changes in one go. - _taskRepository.Context.SaveChanges(); - } + public ScheduleTask GetOrAddTask(Action newAction) where T : ITask + { + Guard.NotNull(newAction, nameof(newAction)); + + var type = typeof(T); + + if (type.IsAbstract || type.IsInterface || type.IsNotPublic) + { + throw new InvalidOperationException("Only concrete public task types can be registered."); + } + + var scheduleTask = this.GetTaskByType(); + + if (scheduleTask == null) + { + scheduleTask = new ScheduleTask { Type = type.AssemblyQualifiedNameWithoutVersion() }; + newAction(scheduleTask); + InsertTask(scheduleTask); + } + + return scheduleTask; + } + + public void CalculateFutureSchedules(IEnumerable tasks, bool isAppStart = false) + { + Guard.NotNull(tasks, nameof(tasks)); + + foreach (var task in tasks) + { + task.NextRunUtc = GetNextSchedule(task); + if (isAppStart) + { + FixTypeName(task); + } + else + { + UpdateTask(task); + } + } + + if (isAppStart) + { + // On app start this method's execution is thread-safe, making it sufficient + // to commit all changes in one go. + _taskRepository.Context.SaveChanges(); + } if (isAppStart) { @@ -245,49 +245,49 @@ public void CalculateFutureSchedules(IEnumerable tasks, bool isApp } } - private void FixTypeName(ScheduleTask task) - { - // In versions prior V3 a double space could exist in ScheduleTask type name. - if (task.Type.IndexOf(", ") > 0) - { - task.Type = task.Type.Replace(", ", ", "); - } - } - - public virtual DateTime? GetNextSchedule(ScheduleTask task) - { - if (task.Enabled) - { - try - { - var localTimeZone = _dtHelper.DefaultStoreTimeZone; - var baseTime = TimeZoneInfo.ConvertTime(DateTime.UtcNow, localTimeZone); - var next = CronExpression.GetNextSchedule(task.CronExpression, baseTime); - var utcTime = _dtHelper.ConvertToUtcTime(next, localTimeZone); - - return utcTime; - } - catch (Exception ex) - { - Logger.ErrorFormat(ex, "Could not calculate next schedule time for task '{0}'", task.Name); - } - } - - return null; - } - - private static void RetryOnDeadlockException(int attemp, Exception ex) - { - var isDeadLockException = - (ex as EntityCommandExecutionException).IsDeadlockException() || - (ex as SqlException).IsDeadlockException(); - - if (!isDeadLockException) - { - // We only want to retry on deadlock stuff. - throw ex; - } - } + private void FixTypeName(ScheduleTask task) + { + // In versions prior V3 a double space could exist in ScheduleTask type name. + if (task.Type.IndexOf(", ") > 0) + { + task.Type = task.Type.Replace(", ", ", "); + } + } + + public virtual DateTime? GetNextSchedule(ScheduleTask task) + { + if (task.Enabled) + { + try + { + var localTimeZone = _dtHelper.DefaultStoreTimeZone; + var baseTime = TimeZoneInfo.ConvertTime(DateTime.UtcNow, localTimeZone); + var next = CronExpression.GetNextSchedule(task.CronExpression, baseTime); + var utcTime = _dtHelper.ConvertToUtcTime(next, localTimeZone); + + return utcTime; + } + catch (Exception ex) + { + Logger.ErrorFormat(ex, "Could not calculate next schedule time for task '{0}'", task.Name); + } + } + + return null; + } + + private static void RetryOnDeadlockException(int attemp, Exception ex) + { + var isDeadLockException = + (ex as EntityCommandExecutionException).IsDeadlockException() || + (ex as SqlException).IsDeadlockException(); + + if (!isDeadLockException) + { + // We only want to retry on deadlock stuff. + throw ex; + } + } #region Schedule task history @@ -299,6 +299,17 @@ protected virtual IQueryable GetHistoryEntriesQuery( { var query = _taskHistoryRepository.TableUntracked; + if (lastEntryOnly) + { + query = + from th in query + group th by th.ScheduleTaskId into grp + select grp + .OrderByDescending(x => x.StartedOnUtc) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } + if (taskId != 0) { query = query.Where(x => x.ScheduleTaskId == taskId); @@ -313,17 +324,6 @@ protected virtual IQueryable GetHistoryEntriesQuery( query = query.Where(x => x.IsRunning == isRunning.Value); } - if (lastEntryOnly) - { - query = - from th in query - group th by th.ScheduleTaskId into grp - select grp - .OrderByDescending(x => x.StartedOnUtc) - .ThenByDescending(x => x.Id) - .FirstOrDefault(); - } - query = query .OrderByDescending(x => x.StartedOnUtc) .ThenByDescending(x => x.Id); diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index fa52d51967..0c79140495 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -7,14 +7,14 @@ namespace SmartStore.Services.Tasks { - public delegate void ProgressCallback(int value, int maximum, string message); - - /// - /// Provides the context for the Execute method of the interface. - /// - public class TaskExecutionContext - { - private readonly IComponentContext _componentContext; + public delegate void ProgressCallback(int value, int maximum, string message); + + /// + /// Provides the context for the Execute method of the interface. + /// + public class TaskExecutionContext + { + private readonly IComponentContext _componentContext; private readonly ScheduleTaskHistory _originalTaskHistory; internal TaskExecutionContext(IComponentContext componentContext, ScheduleTaskHistory originalTaskHistory) @@ -25,19 +25,19 @@ internal TaskExecutionContext(IComponentContext componentContext, ScheduleTaskHi } public T Resolve(object key = null) where T : class - { - if (key == null) - { - return _componentContext.Resolve(); - } + { + if (key == null) + { + return _componentContext.Resolve(); + } - return _componentContext.ResolveKeyed(key); - } + return _componentContext.ResolveKeyed(key); + } - public T ResolveNamed(string name) where T : class - { - return _componentContext.ResolveNamed(name); - } + public T ResolveNamed(string name) where T : class + { + return _componentContext.ResolveNamed(name); + } /// /// A cancellation token for the running task. @@ -49,84 +49,84 @@ public T ResolveNamed(string name) where T : class public IDictionary Parameters { get; set; } - /// - /// Persists a task's progress information to the database - /// - /// Progress value (numerator) - /// Progress maximum (denominator) - /// Progress message. Can be null. - /// if true, saves the updated task entity immediately, or lazily with the next database commit otherwise. - public void SetProgress(int value, int maximum, string message, bool immediately = false) - { - if (value == 0 && maximum == 0) - { - SetProgress(null, message, immediately); - } - else - { - float fraction = (float)value / (float)Math.Max(maximum, 1f); - int percentage = (int)Math.Round(fraction * 100f, 0); - - SetProgress(Math.Min(Math.Max(percentage, 0), 100), message, immediately); - } - } - - /// - /// Persists a task's progress information to the database - /// - /// Percentual progress. Can be null or a value between 0 and 100. - /// Progress message. Can be null. - /// if true, saves the updated task entity immediately, or lazily with the next database commit otherwise. - public virtual void SetProgress(int? progress, string message, bool immediately = false) - { + /// + /// Persists a task's progress information to the database + /// + /// Progress value (numerator) + /// Progress maximum (denominator) + /// Progress message. Can be null. + /// if true, saves the updated task entity immediately, or lazily with the next database commit otherwise. + public void SetProgress(int value, int maximum, string message, bool immediately = false) + { + if (value == 0 && maximum == 0) + { + SetProgress(null, message, immediately); + } + else + { + float fraction = (float)value / (float)Math.Max(maximum, 1f); + int percentage = (int)Math.Round(fraction * 100f, 0); + + SetProgress(Math.Min(Math.Max(percentage, 0), 100), message, immediately); + } + } + + /// + /// Persists a task's progress information to the database + /// + /// Percentual progress. Can be null or a value between 0 and 100. + /// Progress message. Can be null. + /// if true, saves the updated task entity immediately, or lazily with the next database commit otherwise. + public virtual void SetProgress(int? progress, string message, bool immediately = false) + { if (progress.HasValue) { Guard.InRange(progress.Value, 0, 100, nameof(progress)); } - // Update cloned entity. - ScheduleTaskHistory.ProgressPercent = progress; + // Update cloned entity. + ScheduleTaskHistory.ProgressPercent = progress; ScheduleTaskHistory.ProgressMessage = message; // Update attached entity. _originalTaskHistory.ProgressPercent = progress; _originalTaskHistory.ProgressMessage = message; - if (immediately) - { + if (immediately) + { // Dont't let this abort the task on failure. try { - var dbContext = _componentContext.Resolve(); - //dbContext.ChangeState(_originalTask, System.Data.Entity.EntityState.Modified); - dbContext.SaveChanges(); - } - catch { } - } - } - - /// - /// Creates a transient context, which can be passed to methods requiring a instance. - /// Such methods are usually executed by the task executor in the background. If a manual invocation of such - /// methods is required, this is the way to go. - /// - /// The component context - /// A transient context - public static TaskExecutionContext CreateTransientContext(IComponentContext componentContext) - { - return CreateTransientContext(componentContext, CancellationToken.None); - } - - /// - /// Creates a transient context, which can be passed to methods requiring a instance. - /// Such methods are usually executed by the task executor in the background. If a manual invocation of such - /// methods is required, this is the way to go. - /// - /// The component context - /// The cancellation token - /// A transient context - public static TaskExecutionContext CreateTransientContext(IComponentContext componentContext, CancellationToken cancellationToken) - { + var dbContext = _componentContext.Resolve(); + //dbContext.ChangeState(_originalTask, System.Data.Entity.EntityState.Modified); + dbContext.SaveChanges(); + } + catch { } + } + } + + /// + /// Creates a transient context, which can be passed to methods requiring a instance. + /// Such methods are usually executed by the task executor in the background. If a manual invocation of such + /// methods is required, this is the way to go. + /// + /// The component context + /// A transient context + public static TaskExecutionContext CreateTransientContext(IComponentContext componentContext) + { + return CreateTransientContext(componentContext, CancellationToken.None); + } + + /// + /// Creates a transient context, which can be passed to methods requiring a instance. + /// Such methods are usually executed by the task executor in the background. If a manual invocation of such + /// methods is required, this is the way to go. + /// + /// The component context + /// The cancellation token + /// A transient context + public static TaskExecutionContext CreateTransientContext(IComponentContext componentContext, CancellationToken cancellationToken) + { var originalHistoryEntry = new ScheduleTaskHistory { ScheduleTask = new ScheduleTask @@ -137,24 +137,24 @@ public static TaskExecutionContext CreateTransientContext(IComponentContext comp } }; - var context = new TransientTaskExecutionContext(componentContext, originalHistoryEntry); - context.CancellationToken = cancellationToken; + var context = new TransientTaskExecutionContext(componentContext, originalHistoryEntry); + context.CancellationToken = cancellationToken; context.ScheduleTaskHistory = originalHistoryEntry.Clone(); - return context; - } - } - - internal class TransientTaskExecutionContext : TaskExecutionContext - { - public TransientTaskExecutionContext(IComponentContext componentContext, ScheduleTaskHistory originalHistoryEntry) - : base(componentContext, originalHistoryEntry) - { - } - - public override void SetProgress(int? progress, string message, bool immediately = false) - { - base.SetProgress(progress, message, false /* skip committing to DB */); - } - } + return context; + } + } + + internal class TransientTaskExecutionContext : TaskExecutionContext + { + public TransientTaskExecutionContext(IComponentContext componentContext, ScheduleTaskHistory originalHistoryEntry) + : base(componentContext, originalHistoryEntry) + { + } + + public override void SetProgress(int? progress, string message, bool immediately = false) + { + base.SetProgress(progress, message, false /* skip committing to DB */); + } + } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs index 05676a0490..e79a3e5a9a 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -17,36 +17,36 @@ public class TaskExecutor : ITaskExecutor { private readonly IScheduleTaskService _scheduledTaskService; private readonly Func _taskResolver; - private readonly IComponentContext _componentContext; - private readonly IAsyncState _asyncState; + private readonly IComponentContext _componentContext; + private readonly IAsyncState _asyncState; private readonly IApplicationEnvironment _env; public const string CurrentCustomerIdParamName = "CurrentCustomerId"; - public const string CurrentStoreIdParamName = "CurrentStoreId"; + public const string CurrentStoreIdParamName = "CurrentStoreId"; - public TaskExecutor( - IScheduleTaskService scheduledTaskService, - IComponentContext componentContext, - IAsyncState asyncState, - Func taskResolver, + public TaskExecutor( + IScheduleTaskService scheduledTaskService, + IComponentContext componentContext, + IAsyncState asyncState, + Func taskResolver, IApplicationEnvironment env) { _scheduledTaskService = scheduledTaskService; - _componentContext = componentContext; - _asyncState = asyncState; + _componentContext = componentContext; + _asyncState = asyncState; _taskResolver = taskResolver; _env = env; Logger = NullLogger.Instance; - T = NullLocalizer.Instance; - } + T = NullLocalizer.Instance; + } public ILogger Logger { get; set; } - public Localizer T { get; set; } + public Localizer T { get; set; } - public async Task ExecuteAsync( - ScheduleTask entity, - IDictionary taskParameters = null, + public async Task ExecuteAsync( + ScheduleTask entity, + IDictionary taskParameters = null, bool throwOnError = false) { if (AsyncRunner.AppShutdownCancellationToken.IsCancellationRequested) @@ -66,11 +66,11 @@ public async Task ExecuteAsync( } bool faulted = false; - bool canceled = false; + bool canceled = false; string lastError = null; ITask task = null; - string stateName = null; - Type taskType = null; + string stateName = null; + Type taskType = null; Exception exception = null; var historyEntry = new ScheduleTaskHistory @@ -83,61 +83,61 @@ public async Task ExecuteAsync( try { - taskType = Type.GetType(entity.Type); - if (taskType == null) - { - Logger.DebugFormat("Invalid scheduled task type: {0}", entity.Type.NaIfEmpty()); - } + taskType = Type.GetType(entity.Type); + if (taskType == null) + { + Logger.DebugFormat("Invalid scheduled task type: {0}", entity.Type.NaIfEmpty()); + } - if (taskType == null) - return; + if (taskType == null) + return; - if (!PluginManager.IsActivePluginAssembly(taskType.Assembly)) - return; + if (!PluginManager.IsActivePluginAssembly(taskType.Assembly)) + return; entity.ScheduleTaskHistory.Add(historyEntry); _scheduledTaskService.UpdateTask(entity); } catch - { - return; - } + { + return; + } try { // Task history entry has been successfully added, now we execute the task. - // Create task instance. - task = _taskResolver(taskType); - stateName = entity.Id.ToString(); + // Create task instance. + task = _taskResolver(taskType); + stateName = entity.Id.ToString(); - // Create & set a composite CancellationTokenSource which also contains the global app shoutdown token. - var cts = CancellationTokenSource.CreateLinkedTokenSource(AsyncRunner.AppShutdownCancellationToken, new CancellationTokenSource().Token); - _asyncState.SetCancelTokenSource(cts, stateName); + // Create & set a composite CancellationTokenSource which also contains the global app shoutdown token. + var cts = CancellationTokenSource.CreateLinkedTokenSource(AsyncRunner.AppShutdownCancellationToken, new CancellationTokenSource().Token); + _asyncState.SetCancelTokenSource(cts, stateName); - var ctx = new TaskExecutionContext(_componentContext, historyEntry) - { + var ctx = new TaskExecutionContext(_componentContext, historyEntry) + { ScheduleTaskHistory = historyEntry.Clone(), - CancellationToken = cts.Token, - Parameters = taskParameters ?? new Dictionary() - }; - - Logger.DebugFormat("Executing scheduled task: {0}", entity.Type); - - if (task is IAsyncTask asyncTask) - { - await asyncTask.ExecuteAsync(ctx); - } - else - { - task.Execute(ctx); - } + CancellationToken = cts.Token, + Parameters = taskParameters ?? new Dictionary() + }; + + Logger.DebugFormat("Executing scheduled task: {0}", entity.Type); + + if (task is IAsyncTask asyncTask) + { + await asyncTask.ExecuteAsync(ctx); + } + else + { + task.Execute(ctx); + } } catch (Exception ex) { exception = ex; faulted = true; - canceled = ex is OperationCanceledException; - lastError = ex.ToAllMessages(true); + canceled = ex is OperationCanceledException; + lastError = ex.ToAllMessages(true); if (canceled) { @@ -145,7 +145,7 @@ public async Task ExecuteAsync( } else { - Logger.Error(ex, string.Concat(T("Admin.System.ScheduleTasks.RunningError", entity.Name), ": ", ex.Message)); + Logger.Error(ex, string.Concat(T("Admin.System.ScheduleTasks.RunningError", entity.Name), ": ", ex.Message)); } } finally @@ -158,19 +158,19 @@ public async Task ExecuteAsync( historyEntry.ProgressMessage = null; historyEntry.Error = lastError; historyEntry.FinishedOnUtc = now; - - if (faulted) - { - if ((!canceled && entity.StopOnError) || task == null) - { - entity.Enabled = false; + + if (faulted) + { + if ((!canceled && entity.StopOnError) || task == null) + { + entity.Enabled = false; updateTask = true; - } - } - else - { + } + } + else + { historyEntry.SucceededOnUtc = now; - } + } try { @@ -190,8 +190,8 @@ public async Task ExecuteAsync( } if (entity.Enabled) - { - entity.NextRunUtc = _scheduledTaskService.GetNextSchedule(entity); + { + entity.NextRunUtc = _scheduledTaskService.GetNextSchedule(entity); updateTask = true; } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs index 72eaabfc7c..f286db5350 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs @@ -10,137 +10,134 @@ namespace SmartStore.Services.Tasks { - public class DefaultTaskScheduler : DisposableObject, ITaskScheduler, IRegisteredObject + public class DefaultTaskScheduler : DisposableObject, ITaskScheduler, IRegisteredObject { - private readonly ICacheManager _cache; + private readonly ICacheManager _cache; - private bool _intervalFixed; - private int _sweepInterval; - private string _baseUrl; + private bool _intervalFixed; + private int _sweepInterval; + private string _baseUrl; private System.Timers.Timer _timer; private bool _shuttingDown; - private int _errCount; + private int _errCount; public DefaultTaskScheduler(ICacheManager cache) { - _cache = cache; + _cache = cache; - _sweepInterval = 1; - _timer = new System.Timers.Timer(); + _sweepInterval = 1; + _timer = new System.Timers.Timer(); _timer.Elapsed += Elapsed; - Logger = NullLogger.Instance; + Logger = NullLogger.Instance; HostingEnvironment.RegisterObject(this); } - public ILogger Logger - { - get; - set; - } + public ILogger Logger + { + get; + set; + } - public int SweepIntervalMinutes + public int SweepIntervalMinutes { - get { return _sweepInterval; } - set { _sweepInterval = value; } + get => _sweepInterval; + set => _sweepInterval = value; } public string BaseUrl { // TODO: HTTPS? - get { return _baseUrl; } + get => _baseUrl; set { CheckUrl(value); _baseUrl = value.TrimEnd('/', '\\'); - } + } } public void Start() { - if (_timer.Enabled) - return; - - lock (_timer) + if (_timer.Enabled) + return; + + lock (_timer) { CheckUrl(_baseUrl); - _timer.Interval = GetFixedInterval(); + _timer.Interval = GetFixedInterval(); _timer.Start(); } } - private double GetFixedInterval() - { - // Gets seconds to next sweep minute - int seconds = (_sweepInterval * 60) - DateTime.Now.Second; - return seconds * 1000; - } + private double GetFixedInterval() + { + // Gets seconds to next sweep minute + int seconds = (_sweepInterval * 60) - DateTime.Now.Second; + return seconds * 1000; + } public void Stop() { - if (!_timer.Enabled) - return; - - lock (_timer) + if (!_timer.Enabled) + return; + + lock (_timer) { _timer.Stop(); } } - public bool IsActive + public bool IsActive => _timer.Enabled; + + public string GetAsyncStateKey(int scheduleTaskId) { - get { return _timer.Enabled; } + return scheduleTaskId.ToString(); } - public string GetAsyncStateKey(int scheduleTaskId) - { - return scheduleTaskId.ToString(); - } - - private string CreateAuthToken() - { - string authToken = Guid.NewGuid().ToString(); + private string CreateAuthToken() + { + string authToken = Guid.NewGuid().ToString(); - _cache.Put(GenerateAuthTokenCacheKey(authToken), true, TimeSpan.FromMinutes(1)); + _cache.Put(GenerateAuthTokenCacheKey(authToken), true, TimeSpan.FromMinutes(1)); - return authToken; - } + return authToken; + } - private string GenerateAuthTokenCacheKey(string authToken) - { - return "Scheduler:AuthToken:" + authToken; - } + private string GenerateAuthTokenCacheKey(string authToken) + { + return "Scheduler:AuthToken:" + authToken; + } public bool VerifyAuthToken(string authToken) { if (authToken.IsEmpty()) return false; - var cacheKey = GenerateAuthTokenCacheKey(authToken); - if (_cache.Contains(cacheKey)) - { - _cache.Remove(cacheKey); - return true; - } + var cacheKey = GenerateAuthTokenCacheKey(authToken); + if (_cache.Contains(cacheKey)) + { + _cache.Remove(cacheKey); + return true; + } - return false; + return false; } public void RunSingleTask(int scheduleTaskId, IDictionary taskParameters = null) { - taskParameters = taskParameters ?? new Dictionary(); + taskParameters = taskParameters ?? new Dictionary(); - // User executes task in backend explicitly - taskParameters["Explicit"] = "true"; + // User executes task in backend explicitly + taskParameters["Explicit"] = "true"; var qs = new QueryString(); - taskParameters.Each(x => qs.Add(x.Key, x.Value)); - var query = qs.ToString(); + taskParameters.Each(x => qs.Add(x.Key, x.Value)); + var query = qs.ToString(); - CallEndpoint(new Uri("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query))); + CallEndpoint(new Uri("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query))); } - private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) { if (!System.Threading.Monitor.TryEnter(_timer)) return; @@ -149,13 +146,13 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) { if (_timer.Enabled) { - if (!_intervalFixed) - { - _timer.Interval = TimeSpan.FromMinutes(_sweepInterval).TotalMilliseconds; - _intervalFixed = true; - } + if (!_intervalFixed) + { + _timer.Interval = TimeSpan.FromMinutes(_sweepInterval).TotalMilliseconds; + _intervalFixed = true; + } - CallEndpoint(new Uri(_baseUrl + "/Sweep")); + CallEndpoint(new Uri(_baseUrl + "/Sweep")); } } finally @@ -169,73 +166,73 @@ protected internal virtual void CallEndpoint(Uri uri) if (_shuttingDown) return; - var req = WebHelper.CreateHttpRequestForSafeLocalCall(uri); - req.Method = "POST"; + var req = WebHelper.CreateHttpRequestForSafeLocalCall(uri); + req.Method = "POST"; req.ContentType = "text/plain"; - req.ContentLength = 0; - req.Timeout = 10000; // 10 sec. + req.ContentLength = 0; + req.Timeout = 10000; // 10 sec. string authToken = CreateAuthToken(); req.Headers.Add("X-AUTH-TOKEN", authToken); - req.GetResponseAsync().ContinueWith(t => - { - if (t.IsFaulted) - { - HandleException(t.Exception, uri); - _errCount++; - if (_errCount >= 10) - { - // 10 failed attempts in succession. Stop the timer! - this.Stop(); - Logger.Info("Stopping TaskScheduler sweep timer. Too many failed requests in succession."); - } - } - else - { - _errCount = 0; - var response = t.Result; - - //_logger.DebugFormat("TaskScheduler Sweep called successfully: {0}", response.GetResponseStream().AsString()); - - response.Dispose(); - } - }); - } - - private void HandleException(AggregateException exception, Uri uri) - { - string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(uri.OriginalString); - var wex = exception.InnerExceptions.OfType().FirstOrDefault(); - - if (wex == null) - { - Logger.Error(exception.InnerException, msg); - } - else if (wex.Response == null) - { - Logger.Error(wex, msg); - } - else - { - using (var response = wex.Response as HttpWebResponse) - { - if (response != null) - { - var statusCode = (int)response.StatusCode; - if (statusCode < 500) - { - // Any internal server error (>= 500) already handled by TaskSchedulerController's exception filter - msg += " HTTP {0}, {1}".FormatCurrent(statusCode, response.StatusDescription); - Logger.Error(msg); - } - } - - } - } - } - - private void CheckUrl(string url) + req.GetResponseAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + HandleException(t.Exception, uri); + _errCount++; + if (_errCount >= 10) + { + // 10 failed attempts in succession. Stop the timer! + this.Stop(); + Logger.Info("Stopping TaskScheduler sweep timer. Too many failed requests in succession."); + } + } + else + { + _errCount = 0; + var response = t.Result; + + //_logger.DebugFormat("TaskScheduler Sweep called successfully: {0}", response.GetResponseStream().AsString()); + + response.Dispose(); + } + }); + } + + private void HandleException(AggregateException exception, Uri uri) + { + string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(uri.OriginalString); + var wex = exception.InnerExceptions.OfType().FirstOrDefault(); + + if (wex == null) + { + Logger.Error(exception.InnerException, msg); + } + else if (wex.Response == null) + { + Logger.Error(wex, msg); + } + else + { + using (var response = wex.Response as HttpWebResponse) + { + if (response != null) + { + var statusCode = (int)response.StatusCode; + if (statusCode < 500) + { + // Any internal server error (>= 500) already handled by TaskSchedulerController's exception filter + msg += " HTTP {0}, {1}".FormatCurrent(statusCode, response.StatusDescription); + Logger.Error(msg); + } + } + + } + } + } + + private void CheckUrl(string url) { if (!url.IsWebUrl()) { @@ -254,7 +251,7 @@ protected override void OnDispose(bool disposing) void IRegisteredObject.Stop(bool immediate) { _shuttingDown = true; - HostingEnvironment.UnregisterObject(this); + HostingEnvironment.UnregisterObject(this); } } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskSchedulerStarter.cs b/src/Libraries/SmartStore.Services/Tasks/TaskSchedulerStarter.cs index d3d5cc1906..c2de669aea 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskSchedulerStarter.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskSchedulerStarter.cs @@ -16,16 +16,16 @@ public sealed class TaskSchedulerStarter : IPostApplicationStart private readonly IEventPublisher _eventPublisher; public TaskSchedulerStarter( - ITaskScheduler taskScheduler, - IScheduleTaskService taskService, - IStoreService storeService, + ITaskScheduler taskScheduler, + IScheduleTaskService taskService, + IStoreService storeService, IEventPublisher eventPublisher) { _taskScheduler = taskScheduler; _taskService = taskService; _storeService = storeService; _eventPublisher = eventPublisher; - + Logger = NullLogger.Instance; } diff --git a/src/Libraries/SmartStore.Services/Tax/CalculateTaxResult.cs b/src/Libraries/SmartStore.Services/Tax/CalculateTaxResult.cs index bca952c015..4431af2868 100644 --- a/src/Libraries/SmartStore.Services/Tax/CalculateTaxResult.cs +++ b/src/Libraries/SmartStore.Services/Tax/CalculateTaxResult.cs @@ -22,13 +22,7 @@ public CalculateTaxResult() /// public IList Errors { get; set; } - public bool Success - { - get - { - return this.Errors.Count == 0; - } - } + public bool Success => this.Errors.Count == 0; public void AddError(string error) { diff --git a/src/Libraries/SmartStore.Services/Tax/FreeTaxProvider.cs b/src/Libraries/SmartStore.Services/Tax/FreeTaxProvider.cs index 679cef0249..22737cf44d 100644 --- a/src/Libraries/SmartStore.Services/Tax/FreeTaxProvider.cs +++ b/src/Libraries/SmartStore.Services/Tax/FreeTaxProvider.cs @@ -4,17 +4,17 @@ namespace SmartStore.Services.Tax { - [SystemName("Tax.Free")] - [FriendlyName("Free tax rate provider")] - [DisplayOrder(0)] + [SystemName("Tax.Free")] + [FriendlyName("Free tax rate provider")] + [DisplayOrder(0)] public class FreeTaxProvider : ITaxProvider { - - public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) + + public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) { var result = new CalculateTaxResult() { - TaxRate = decimal.Zero + TaxRate = decimal.Zero }; return result; } diff --git a/src/Libraries/SmartStore.Services/Tax/ITaxService.cs b/src/Libraries/SmartStore.Services/Tax/ITaxService.cs index 95bbe83faf..f38335ce69 100644 --- a/src/Libraries/SmartStore.Services/Tax/ITaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/ITaxService.cs @@ -33,14 +33,14 @@ public partial interface ITaxService /// /// Tax providers IEnumerable> LoadAllTaxProviders(); - + /// /// Gets tax rate /// - /// Product + /// Product /// Customer /// Tax rate decimal GetTaxRate(Product product, Customer customer); @@ -52,23 +52,23 @@ public partial interface ITaxService /// Customer /// Tax rate decimal GetTaxRate(int taxCategoryId, Customer customer); - + /// /// Gets tax rate /// - /// Product + /// Product /// Tax category identifier /// Customer /// Tax rate - decimal GetTaxRate(Product product, int taxCategoryId, Customer customer); - + decimal GetTaxRate(Product product, int taxCategoryId, Customer customer); + /// /// Gets price /// - /// Product + /// Product /// Price /// Tax rate /// Price @@ -84,140 +84,140 @@ public partial interface ITaxService /// Price decimal GetProductPrice(Product product, decimal price, Customer customer, out decimal taxRate); - /// - /// Gets price - /// - /// Product - /// Price - /// Customer - /// Currency - /// Tax rate - /// Price - decimal GetProductPrice(Product product, decimal price, Customer customer, Currency currency, out decimal taxRate); - - /// - /// Gets price - /// - /// Product - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax rate - /// Price - decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate); - - /// - /// Gets price - /// - /// Product - /// Tax category identifier - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Currency - /// A value indicating whether price already includes tax - /// Tax rate - /// Price - decimal GetProductPrice(Product product, - int taxCategoryId, - decimal price, + /// + /// Gets price + /// + /// Product + /// Price + /// Customer + /// Currency + /// Tax rate + /// Price + decimal GetProductPrice(Product product, decimal price, Customer customer, Currency currency, out decimal taxRate); + + /// + /// Gets price + /// + /// Product + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax rate + /// Price + decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate); + + /// + /// Gets price + /// + /// Product + /// Tax category identifier + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Currency + /// A value indicating whether price already includes tax + /// Tax rate + /// Price + decimal GetProductPrice(Product product, + int taxCategoryId, + decimal price, bool includingTax, - Customer customer, - Currency currency, - bool priceIncludesTax, - out decimal taxRate); - - - - - /// - /// Gets the shipping price - /// - /// Price - /// Customer - /// Shipping price - decimal GetShippingPrice(decimal price, Customer customer); - - /// - /// Gets the shipping price - /// - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Shipping price - decimal GetShippingPrice(decimal price, bool includingTax, Customer customer); - - /// - /// Gets the shipping price - /// - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax rate - /// Shipping price - decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, out decimal taxRate); - - /// - /// Gets the shipping price - /// - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax category id - /// Tax rate - /// Shipping price - decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate); - - - - - /// - /// Gets payment method additional handling fee - /// - /// Price - /// Customer - /// Payment fee - decimal GetPaymentMethodAdditionalFee(decimal price, Customer customer); - - /// - /// Gets payment method additional handling fee - /// - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Payment fee - decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer); - - /// - /// Gets payment method additional handling fee - /// - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax rate - /// Payment fee - decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, out decimal taxRate); - - /// - /// Gets payment method additional handling fee - /// - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax category id - /// Tax rate - /// Payment fee - decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate); - - - - - /// - /// Gets checkout attribute value price - /// - /// Checkout attribute value - /// Price - decimal GetCheckoutAttributePrice(CheckoutAttributeValue cav); + Customer customer, + Currency currency, + bool priceIncludesTax, + out decimal taxRate); + + + + + /// + /// Gets the shipping price + /// + /// Price + /// Customer + /// Shipping price + decimal GetShippingPrice(decimal price, Customer customer); + + /// + /// Gets the shipping price + /// + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Shipping price + decimal GetShippingPrice(decimal price, bool includingTax, Customer customer); + + /// + /// Gets the shipping price + /// + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax rate + /// Shipping price + decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, out decimal taxRate); + + /// + /// Gets the shipping price + /// + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax category id + /// Tax rate + /// Shipping price + decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate); + + + + + /// + /// Gets payment method additional handling fee + /// + /// Price + /// Customer + /// Payment fee + decimal GetPaymentMethodAdditionalFee(decimal price, Customer customer); + + /// + /// Gets payment method additional handling fee + /// + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Payment fee + decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer); + + /// + /// Gets payment method additional handling fee + /// + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax rate + /// Payment fee + decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, out decimal taxRate); + + /// + /// Gets payment method additional handling fee + /// + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax category id + /// Tax rate + /// Payment fee + decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate); + + + + + /// + /// Gets checkout attribute value price + /// + /// Checkout attribute value + /// Price + decimal GetCheckoutAttributePrice(CheckoutAttributeValue cav); /// /// Gets checkout attribute value price @@ -251,7 +251,7 @@ decimal GetCheckoutAttributePrice(CheckoutAttributeValue cav, - + /// /// Gets VAT Number status @@ -276,7 +276,7 @@ VatNumberStatus GetVatNumberStatus(string fullVatNumber, /// VAT number /// VAT Number status VatNumberStatus GetVatNumberStatus(string twoLetterIsoCode, string vatNumber); - + /// /// Gets VAT Number status /// @@ -285,7 +285,7 @@ VatNumberStatus GetVatNumberStatus(string fullVatNumber, /// Name (if received) /// Address (if received) /// VAT Number status - VatNumberStatus GetVatNumberStatus(string twoLetterIsoCode, string vatNumber, + VatNumberStatus GetVatNumberStatus(string twoLetterIsoCode, string vatNumber, out string name, out string address); /// @@ -297,7 +297,7 @@ VatNumberStatus GetVatNumberStatus(string twoLetterIsoCode, string vatNumber, /// Address /// Exception /// VAT number status - VatNumberStatus DoVatCheck(string twoLetterIsoCode, string vatNumber, + VatNumberStatus DoVatCheck(string twoLetterIsoCode, string vatNumber, out string name, out string address, out Exception exception); diff --git a/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs b/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs index 4038498b9b..30bbe8f8b2 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs @@ -31,21 +31,21 @@ public virtual void DeleteTaxCategory(TaxCategory taxCategory) public virtual IList GetAllTaxCategories() { - var query = from tc in _taxCategoryRepository.Table - orderby tc.DisplayOrder - select tc; + var query = from tc in _taxCategoryRepository.Table + orderby tc.DisplayOrder + select tc; - var taxCategories = query.ToListCached("db.taxcategory.all"); - return taxCategories; - } + var taxCategories = query.ToListCached("db.taxcategory.all"); + return taxCategories; + } public virtual TaxCategory GetTaxCategoryById(int taxCategoryId) { if (taxCategoryId == 0) return null; - return _taxCategoryRepository.GetByIdCached(taxCategoryId, "db.taxcategory.id-" + taxCategoryId); - } + return _taxCategoryRepository.GetByIdCached(taxCategoryId, "db.taxcategory.id-" + taxCategoryId); + } public virtual void InsertTaxCategory(TaxCategory taxCategory) { diff --git a/src/Libraries/SmartStore.Services/Tax/TaxService.cs b/src/Libraries/SmartStore.Services/Tax/TaxService.cs index bc74ecadc8..241b85f9fb 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxService.cs @@ -19,55 +19,55 @@ namespace SmartStore.Services.Tax /// Tax service /// public partial class TaxService : ITaxService - { - #region Nested classes + { + #region Nested classes - private class TaxAddressKey : Tuple // - { - public TaxAddressKey(int customerId, bool productIsEsd) - : base(customerId, productIsEsd) - { - } - } + private class TaxAddressKey : Tuple // + { + public TaxAddressKey(int customerId, bool productIsEsd) + : base(customerId, productIsEsd) + { + } + } - #endregion + #endregion - #region Fields + #region Fields - private static readonly DateTime _euEsdRegulationStart = new DateTime(2015, 01, 01); + private static readonly DateTime _euEsdRegulationStart = new DateTime(2015, 01, 01); - private readonly IAddressService _addressService; + private readonly IAddressService _addressService; private readonly IWorkContext _workContext; private readonly TaxSettings _taxSettings; - private readonly ShoppingCartSettings _cartSettings; + private readonly ShoppingCartSettings _cartSettings; private readonly IPluginFinder _pluginFinder; private readonly IDictionary _cachedTaxRates; - private readonly IDictionary _cachedTaxAddresses; - private readonly IProviderManager _providerManager; - private readonly IGeoCountryLookup _geoCountryLookup; + private readonly IDictionary _cachedTaxAddresses; + private readonly IProviderManager _providerManager; + private readonly IGeoCountryLookup _geoCountryLookup; #endregion #region Ctor public TaxService( - IAddressService addressService, + IAddressService addressService, IWorkContext workContext, TaxSettings taxSettings, - ShoppingCartSettings cartSettings, + ShoppingCartSettings cartSettings, IPluginFinder pluginFinder, - IGeoCountryLookup geoCountryLookup, - IProviderManager providerManager) + IGeoCountryLookup geoCountryLookup, + IProviderManager providerManager) { this._addressService = addressService; - this._workContext = workContext; - this._taxSettings = taxSettings; - this._cartSettings = cartSettings; - this._pluginFinder = pluginFinder; - this._cachedTaxRates = new Dictionary(); - this._cachedTaxAddresses = new Dictionary(); - this._providerManager = providerManager; - this._geoCountryLookup = geoCountryLookup; + this._workContext = workContext; + this._taxSettings = taxSettings; + this._cartSettings = cartSettings; + this._pluginFinder = pluginFinder; + this._cachedTaxRates = new Dictionary(); + this._cachedTaxAddresses = new Dictionary(); + this._providerManager = providerManager; + this._geoCountryLookup = geoCountryLookup; } #endregion @@ -119,95 +119,95 @@ protected CalculateTaxRequest CreateCalculateTaxRequest(Product product, int tax return calculateTaxRequest; } - /// - /// Gets a value indicating whether the given customer is a consumer (NOT a business/company) within the EU - /// - /// Customer - /// true if the customer is a consumer, false otherwise - /// - /// A customer is assumed to be a consumer if the default tax address doesn't include a company name, - /// OR if a company name was specified but the EU VAT number for this record is invalid. - /// - protected virtual bool IsEuConsumer(Customer customer) - { - if (customer == null) - return false; - - var address = customer.BillingAddress; - if (address != null && address.Company.IsEmpty()) - { - // BillingAddress is explicitly set, but no CompanyName in there: so we assume a consumer - return true; - } + /// + /// Gets a value indicating whether the given customer is a consumer (NOT a business/company) within the EU + /// + /// Customer + /// true if the customer is a consumer, false otherwise + /// + /// A customer is assumed to be a consumer if the default tax address doesn't include a company name, + /// OR if a company name was specified but the EU VAT number for this record is invalid. + /// + protected virtual bool IsEuConsumer(Customer customer) + { + if (customer == null) + return false; + + var address = customer.BillingAddress; + if (address != null && address.Company.IsEmpty()) + { + // BillingAddress is explicitly set, but no CompanyName in there: so we assume a consumer + return true; + } // No Country or BillingAddress set: try to resolve country from IP address var subjectToVat = _geoCountryLookup.LookupCountry(customer.LastIpAddress)?.IsInEu == true; - if (!subjectToVat) - { - return false; - } + if (!subjectToVat) + { + return false; + } - // It's EU: check VAT number status - var vatStatus = (VatNumberStatus)customer.VatNumberStatusId; - // companies with invalid VAT numbers are assumed to be consumers - return vatStatus != VatNumberStatus.Valid; - } + // It's EU: check VAT number status + var vatStatus = (VatNumberStatus)customer.VatNumberStatusId; + // companies with invalid VAT numbers are assumed to be consumers + return vatStatus != VatNumberStatus.Valid; + } protected virtual Address GetTaxAddress(Customer customer, Product product = null) { - int customerId = customer != null ? customer.Id : 0; - Address address = null; - - bool productIsEsd = product != null ? product.IsEsd : false; - - var cacheKey = new TaxAddressKey(customerId, productIsEsd); - if (_cachedTaxAddresses.TryGetValue(cacheKey, out address)) - { - return address; - } - - var basedOn = _taxSettings.TaxBasedOn; - - // According to the new EU VAT regulations for electronic services from 2015 on, - // VAT must be charged in the EU country the customer originates from (BILLING address). - // In addition to this, the IP addresses' origin should also be checked for verification. - if (DateTime.UtcNow > _euEsdRegulationStart) - { - if (_taxSettings.EuVatEnabled && productIsEsd) - { - if (IsEuConsumer(customer)) - { - basedOn = TaxBasedOn.BillingAddress; - } - } - } - - if (basedOn == TaxBasedOn.BillingAddress && (customer == null || customer.BillingAddress == null)) - { - basedOn = TaxBasedOn.DefaultAddress; - } - if (basedOn == TaxBasedOn.ShippingAddress && (customer == null || customer.ShippingAddress == null)) - { - basedOn = TaxBasedOn.DefaultAddress; - } - - switch (basedOn) - { - case TaxBasedOn.BillingAddress: - address = customer.BillingAddress; - break; - case TaxBasedOn.ShippingAddress: - address = customer.ShippingAddress; - break; - case TaxBasedOn.DefaultAddress: - default: - address = _addressService.GetAddressById(_taxSettings.DefaultTaxAddressId); - break; - } - - _cachedTaxAddresses[cacheKey] = address; - - return address; + int customerId = customer != null ? customer.Id : 0; + Address address = null; + + bool productIsEsd = product != null ? product.IsEsd : false; + + var cacheKey = new TaxAddressKey(customerId, productIsEsd); + if (_cachedTaxAddresses.TryGetValue(cacheKey, out address)) + { + return address; + } + + var basedOn = _taxSettings.TaxBasedOn; + + // According to the new EU VAT regulations for electronic services from 2015 on, + // VAT must be charged in the EU country the customer originates from (BILLING address). + // In addition to this, the IP addresses' origin should also be checked for verification. + if (DateTime.UtcNow > _euEsdRegulationStart) + { + if (_taxSettings.EuVatEnabled && productIsEsd) + { + if (IsEuConsumer(customer)) + { + basedOn = TaxBasedOn.BillingAddress; + } + } + } + + if (basedOn == TaxBasedOn.BillingAddress && (customer == null || customer.BillingAddress == null)) + { + basedOn = TaxBasedOn.DefaultAddress; + } + if (basedOn == TaxBasedOn.ShippingAddress && (customer == null || customer.ShippingAddress == null)) + { + basedOn = TaxBasedOn.DefaultAddress; + } + + switch (basedOn) + { + case TaxBasedOn.BillingAddress: + address = customer.BillingAddress; + break; + case TaxBasedOn.ShippingAddress: + address = customer.ShippingAddress; + break; + case TaxBasedOn.DefaultAddress: + default: + address = _addressService.GetAddressById(_taxSettings.DefaultTaxAddressId); + break; + } + + _cachedTaxAddresses[cacheKey] = address; + + return address; } /// @@ -228,10 +228,10 @@ protected decimal CalculatePrice(decimal price, decimal percent, bool increase, result = price * (1 + percent / 100); } else - { - var decreaseValue = (price) / (100 + percent) * percent; + { + var decreaseValue = (price) / (100 + percent) * percent; result = price - decreaseValue; - } + } // Gross > Net RoundFix result = result.RoundIfEnabledFor(currency); @@ -252,7 +252,7 @@ public virtual Provider LoadActiveTaxProvider() if (taxProvider == null) { taxProvider = LoadAllTaxProviders().FirstOrDefault(); - } + } return taxProvider; } @@ -263,7 +263,7 @@ public virtual Provider LoadActiveTaxProvider() /// Found tax provider public virtual Provider LoadTaxProviderBySystemName(string systemName) { - return _providerManager.GetProvider(systemName); + return _providerManager.GetProvider(systemName); } /// @@ -272,7 +272,7 @@ public virtual Provider LoadTaxProviderBySystemName(string systemN /// Tax providers public virtual IEnumerable> LoadAllTaxProviders() { - return _providerManager.GetAllProviders(); + return _providerManager.GetAllProviders(); } @@ -325,31 +325,31 @@ public virtual decimal GetTaxRate(Product product, int taxCategoryId, Customer c protected virtual decimal GetTaxRateCore(Product product, int taxCategoryId, Customer customer) { - // active tax provider - var activeTaxProvider = LoadActiveTaxProvider(); - if (activeTaxProvider == null) - { - return decimal.Zero; - } + // active tax provider + var activeTaxProvider = LoadActiveTaxProvider(); + if (activeTaxProvider == null) + { + return decimal.Zero; + } if (IsTaxExempt(product, customer)) { return decimal.Zero; } - // tax request + // tax request var calculateTaxRequest = CreateCalculateTaxRequest(product, taxCategoryId, customer); - #region Legacy - ////make EU VAT exempt validation (the European Union Value Added Tax) (VATFIX) + #region Legacy + ////make EU VAT exempt validation (the European Union Value Added Tax) (VATFIX) //if (_taxSettings.EuVatEnabled && IsVatExempt(calculateTaxRequest.Address, calculateTaxRequest.Customer)) //{ // //return zero if VAT is not chargeable // return decimal.Zero; - //} - #endregion + //} + #endregion - //get tax rate + //get tax rate var calculateTaxResult = activeTaxProvider.Value.GetTaxRate(calculateTaxRequest); if (calculateTaxResult.Success) { @@ -390,61 +390,61 @@ public virtual decimal GetProductPrice(Product product, decimal price, Customer return GetProductPrice(product, price, includingTax, customer, out taxRate); } - public virtual decimal GetProductPrice(Product product, decimal price, Customer customer, Currency currency, out decimal taxRate) - { - var includingTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; - var priceIncludesTax = _taxSettings.PricesIncludeTax; - var taxCategoryId = product.TaxCategoryId; // 0; // (VATFIX) + public virtual decimal GetProductPrice(Product product, decimal price, Customer customer, Currency currency, out decimal taxRate) + { + var includingTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var priceIncludesTax = _taxSettings.PricesIncludeTax; + var taxCategoryId = product.TaxCategoryId; // 0; // (VATFIX) - return GetProductPrice(product, taxCategoryId, price, includingTax, customer, currency, priceIncludesTax, out taxRate); - } + return GetProductPrice(product, taxCategoryId, price, includingTax, customer, currency, priceIncludesTax, out taxRate); + } - /// - /// Gets price - /// - /// Product - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax rate - /// Price - public virtual decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate) + /// + /// Gets price + /// + /// Product + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax rate + /// Price + public virtual decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate) { var priceIncludesTax = _taxSettings.PricesIncludeTax; var taxCategoryId = product.TaxCategoryId; // 0; // (VATFIX) return GetProductPrice(product, taxCategoryId, price, includingTax, customer, _workContext.WorkingCurrency, priceIncludesTax, out taxRate); - } + } - /// - /// Gets price - /// - /// Product - /// Tax category identifier - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// A value indicating whether price already includes tax - /// Tax rate - /// Price - public virtual decimal GetProductPrice( - Product product, - int taxCategoryId, - decimal price, - bool includingTax, - Customer customer, - Currency currency, - bool priceIncludesTax, - out decimal taxRate) - { - // don't calculate if price is 0 - if (price == decimal.Zero) - { - taxRate = decimal.Zero; - return decimal.Zero; - } - - taxRate = GetTaxRate(product, taxCategoryId, customer); + /// + /// Gets price + /// + /// Product + /// Tax category identifier + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// A value indicating whether price already includes tax + /// Tax rate + /// Price + public virtual decimal GetProductPrice( + Product product, + int taxCategoryId, + decimal price, + bool includingTax, + Customer customer, + Currency currency, + bool priceIncludesTax, + out decimal taxRate) + { + // don't calculate if price is 0 + if (price == decimal.Zero) + { + taxRate = decimal.Zero; + return decimal.Zero; + } + + taxRate = GetTaxRate(product, taxCategoryId, customer); // Admin: GROSS prices if (priceIncludesTax) @@ -473,7 +473,7 @@ public virtual decimal GetProductPrice( - public virtual decimal GetShippingPrice(decimal price, Customer customer) + public virtual decimal GetShippingPrice(decimal price, Customer customer) { var includingTax = (_workContext.TaxDisplayType == TaxDisplayType.IncludingTax); return GetShippingPrice(price, includingTax, customer); @@ -487,32 +487,32 @@ public virtual decimal GetShippingPrice(decimal price, bool includingTax, Custom public virtual decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, out decimal taxRate) { - return GetShippingPrice(price, includingTax, customer, _taxSettings.ShippingTaxClassId, out taxRate); - } + return GetShippingPrice(price, includingTax, customer, _taxSettings.ShippingTaxClassId, out taxRate); + } - public virtual decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate) - { - taxRate = decimal.Zero; + public virtual decimal GetShippingPrice(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate) + { + taxRate = decimal.Zero; - if (!_taxSettings.ShippingIsTaxable) - return price; + if (!_taxSettings.ShippingIsTaxable) + return price; - var result = GetProductPrice( - null, - taxCategoryId, - price, - includingTax, - customer, - _workContext.WorkingCurrency, - _taxSettings.ShippingPriceIncludesTax, - out taxRate); + var result = GetProductPrice( + null, + taxCategoryId, + price, + includingTax, + customer, + _workContext.WorkingCurrency, + _taxSettings.ShippingPriceIncludesTax, + out taxRate); - return result; - } + return result; + } - public virtual decimal GetPaymentMethodAdditionalFee(decimal price, Customer customer) + public virtual decimal GetPaymentMethodAdditionalFee(decimal price, Customer customer) { var includingTax = (_workContext.TaxDisplayType == TaxDisplayType.IncludingTax); return GetPaymentMethodAdditionalFee(price, includingTax, customer); @@ -524,12 +524,12 @@ public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includi return GetPaymentMethodAdditionalFee(price, includingTax, customer, out taxRate); } - public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, out decimal taxRate) - { - return GetPaymentMethodAdditionalFee(price, includingTax, customer, _taxSettings.PaymentMethodAdditionalFeeTaxClassId, out taxRate); - } + public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, out decimal taxRate) + { + return GetPaymentMethodAdditionalFee(price, includingTax, customer, _taxSettings.PaymentMethodAdditionalFeeTaxClassId, out taxRate); + } - public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate) + public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includingTax, Customer customer, int taxCategoryId, out decimal taxRate) { taxRate = decimal.Zero; @@ -537,16 +537,16 @@ public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includi return price; var result = GetProductPrice( - null, - taxCategoryId, - price, - includingTax, - customer, - _workContext.WorkingCurrency, - _taxSettings.PaymentMethodAdditionalFeeIncludesTax, - out taxRate); + null, + taxCategoryId, + price, + includingTax, + customer, + _workContext.WorkingCurrency, + _taxSettings.PaymentMethodAdditionalFeeIncludesTax, + out taxRate); - return result; + return result; } @@ -605,8 +605,8 @@ public virtual decimal GetCheckoutAttributePrice(CheckoutAttributeValue cav, taxRate = decimal.Zero; var priceIncludesTax = _taxSettings.PricesIncludeTax; - var taxClassId = cav.CheckoutAttribute.TaxCategoryId; - var price = cav.PriceAdjustment; + var taxClassId = cav.CheckoutAttribute.TaxCategoryId; + var price = cav.PriceAdjustment; if (cav.CheckoutAttribute.IsTaxExempt) { @@ -710,23 +710,25 @@ public virtual VatNumberStatus DoVatCheck(string twoLetterIsoCode, string vatNum name = string.Empty; address = string.Empty; - if (vatNumber == null) - vatNumber = string.Empty; - vatNumber = vatNumber.Trim().Replace(" ", ""); + vatNumber = vatNumber.EmptyNull().Replace(" ", ""); if (twoLetterIsoCode == null) + { twoLetterIsoCode = string.Empty; + } if (!String.IsNullOrEmpty(twoLetterIsoCode)) - //The service returns INVALID_INPUT for country codes that are not uppercase. + { + // The service returns INVALID_INPUT for country codes that are not uppercase. twoLetterIsoCode = twoLetterIsoCode.ToUpper(); + } - EuropaCheckVatService.checkVatService s = null; + EuropeCheckVatService.checkVatService s = null; try { bool valid; - s = new EuropaCheckVatService.checkVatService(); + s = new EuropeCheckVatService.checkVatService(); s.checkVat(ref twoLetterIsoCode, ref vatNumber, out valid, out name, out address); exception = null; return valid ? VatNumberStatus.Valid : VatNumberStatus.Invalid; diff --git a/src/Libraries/SmartStore.Services/Themes/ThemeValidationException.cs b/src/Libraries/SmartStore.Services/Themes/ThemeValidationException.cs index 060775b69b..fda9d06f1f 100644 --- a/src/Libraries/SmartStore.Services/Themes/ThemeValidationException.cs +++ b/src/Libraries/SmartStore.Services/Themes/ThemeValidationException.cs @@ -3,16 +3,16 @@ namespace SmartStore.Services.Themes { - public class ThemeValidationException : Exception - { - public ThemeValidationException(string message, IDictionary attemptedVars) - : base(message) - { - Guard.NotNull(attemptedVars, nameof(attemptedVars)); + public class ThemeValidationException : Exception + { + public ThemeValidationException(string message, IDictionary attemptedVars) + : base(message) + { + Guard.NotNull(attemptedVars, nameof(attemptedVars)); - AttemptedVars = attemptedVars; - } + AttemptedVars = attemptedVars; + } - public IDictionary AttemptedVars { get; private set; } - } + public IDictionary AttemptedVars { get; private set; } + } } diff --git a/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs b/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs index 79f000af32..5f9b98f6a3 100644 --- a/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs +++ b/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs @@ -21,30 +21,30 @@ public class ThemeVariablesService : IThemeVariablesService private const string THEMEVARS_BY_THEME_KEY = "SmartStore.themevars.theme-{0}-{1}"; private const string THEMEVARS_PATTERN_KEY = "SmartStore.themevars."; - private readonly IRepository _rsVariables; + private readonly IRepository _rsVariables; private readonly IThemeRegistry _themeRegistry; private readonly IRequestCache _requestCache; private readonly IEventPublisher _eventPublisher; - private readonly Lazy _themeFileResolver; - private readonly HttpContextBase _httpContext; - - public ThemeVariablesService( - IRepository rsVariables, - IThemeRegistry themeRegistry, - IRequestCache requestCache, - IEventPublisher eventPublisher, - Lazy themeFileResolver, - HttpContextBase httpContext) + private readonly Lazy _themeFileResolver; + private readonly HttpContextBase _httpContext; + + public ThemeVariablesService( + IRepository rsVariables, + IThemeRegistry themeRegistry, + IRequestCache requestCache, + IEventPublisher eventPublisher, + Lazy themeFileResolver, + HttpContextBase httpContext) { _rsVariables = rsVariables; _themeRegistry = themeRegistry; _requestCache = requestCache; _eventPublisher = eventPublisher; - _themeFileResolver = themeFileResolver; - _httpContext = httpContext; - } + _themeFileResolver = themeFileResolver; + _httpContext = httpContext; + } - public virtual ExpandoObject GetThemeVariables(string themeName, int storeId) + public virtual ExpandoObject GetThemeVariables(string themeName, int storeId) { if (themeName.IsEmpty()) return null; @@ -60,17 +60,18 @@ public virtual ExpandoObject GetThemeVariables(string themeName, int storeId) // first get all default (static) var values from manifest... var manifest = _themeRegistry.GetThemeManifest(themeName); - manifest.Variables.Values.Each(v => { + manifest.Variables.Values.Each(v => + { dict.Add(v.Name, v.DefaultValue); }); // ...then merge with persisted runtime records var query = from v in _rsVariables.TableUntracked - where v.StoreId == storeId && v.Theme.Equals(themeName, StringComparison.OrdinalIgnoreCase) + where v.StoreId == storeId && v.Theme.Equals(themeName, StringComparison.OrdinalIgnoreCase) select v; - query.ToList().Each(v => - { + query.ToList().Each(v => + { if (v.Value.HasValue() && dict.ContainsKey(v.Name)) { dict[v.Name] = v.Value; @@ -83,155 +84,155 @@ public virtual ExpandoObject GetThemeVariables(string themeName, int storeId) public virtual void DeleteThemeVariables(string themeName, int storeId) { - DeleteThemeVariablesInternal(themeName, storeId); - } - - private void DeleteThemeVariablesInternal(string themeName, int storeId) - { - Guard.NotEmpty(themeName, nameof(themeName)); - - var query = from v in _rsVariables.Table - where v.StoreId == storeId && v.Theme == themeName - select v; - - if (query.Any()) - { - using (var scope = new DbContextScope(ctx: _rsVariables.Context, autoCommit: false)) - { - query.Each(v => - { - _rsVariables.Delete(v); - }); - - _requestCache.Remove(THEMEVARS_BY_THEME_KEY.FormatInvariant(themeName, storeId)); - - _rsVariables.Context.SaveChanges(); - } - } - } - - public virtual int SaveThemeVariables(string themeName, int storeId, IDictionary variables) - { - Guard.NotEmpty(themeName, nameof(themeName)); - Guard.Against(!_themeRegistry.ThemeManifestExists(themeName), "The theme '{0}' does not exist in the registry.".FormatInvariant(themeName)); - Guard.NotNull(variables, nameof(variables)); - - if (!variables.Any()) - return 0; - - var manifest = _themeRegistry.GetThemeManifest(themeName); - if (manifest == null) - { - throw new ArgumentException("Theme '{0}' does not exist".FormatInvariant(themeName), nameof(themeName)); - } - - // Get current for later restore on parse error - var currentVars = GetThemeVariables(themeName, storeId); - - // Save - var result = SaveThemeVariablesInternal(manifest, storeId, variables); - - if (result.TouchedVariablesCount > 0) - { - // Check for parsing error - string error = ValidateSass(manifest, storeId); - if (error.HasValue()) - { - // Restore previous vars - try - { - DeleteThemeVariablesInternal(themeName, storeId); - } - finally - { - // We do it here to absolutely ensure that this gets called - SaveThemeVariablesInternal(manifest, storeId, currentVars); - } - - throw new ThemeValidationException(error, variables); - } - } - - return result.TouchedVariablesCount; - } - - private SaveThemeVariablesResult SaveThemeVariablesInternal(ThemeManifest manifest, int storeId, IDictionary variables) + DeleteThemeVariablesInternal(themeName, storeId); + } + + private void DeleteThemeVariablesInternal(string themeName, int storeId) + { + Guard.NotEmpty(themeName, nameof(themeName)); + + var query = from v in _rsVariables.Table + where v.StoreId == storeId && v.Theme == themeName + select v; + + if (query.Any()) + { + using (var scope = new DbContextScope(ctx: _rsVariables.Context, autoCommit: false)) + { + query.Each(v => + { + _rsVariables.Delete(v); + }); + + _requestCache.Remove(THEMEVARS_BY_THEME_KEY.FormatInvariant(themeName, storeId)); + + _rsVariables.Context.SaveChanges(); + } + } + } + + public virtual int SaveThemeVariables(string themeName, int storeId, IDictionary variables) + { + Guard.NotEmpty(themeName, nameof(themeName)); + Guard.Against(!_themeRegistry.ThemeManifestExists(themeName), "The theme '{0}' does not exist in the registry.".FormatInvariant(themeName)); + Guard.NotNull(variables, nameof(variables)); + + if (!variables.Any()) + return 0; + + var manifest = _themeRegistry.GetThemeManifest(themeName); + if (manifest == null) + { + throw new ArgumentException("Theme '{0}' does not exist".FormatInvariant(themeName), nameof(themeName)); + } + + // Get current for later restore on parse error + var currentVars = GetThemeVariables(themeName, storeId); + + // Save + var result = SaveThemeVariablesInternal(manifest, storeId, variables); + + if (result.TouchedVariablesCount > 0) + { + // Check for parsing error + string error = ValidateSass(manifest, storeId); + if (error.HasValue()) + { + // Restore previous vars + try + { + DeleteThemeVariablesInternal(themeName, storeId); + } + finally + { + // We do it here to absolutely ensure that this gets called + SaveThemeVariablesInternal(manifest, storeId, currentVars); + } + + throw new ThemeValidationException(error, variables); + } + } + + return result.TouchedVariablesCount; + } + + private SaveThemeVariablesResult SaveThemeVariablesInternal(ThemeManifest manifest, int storeId, IDictionary variables) { - var result = new SaveThemeVariablesResult(); + var result = new SaveThemeVariablesResult(); var infos = manifest.Variables; - using (var scope = new DbContextScope(ctx: _rsVariables.Context, autoCommit: false)) - { - var unsavedVars = new List(); - var savedThemeVars = _rsVariables.Table - .Where(v => v.StoreId == storeId && v.Theme == manifest.ThemeName) - .ToDictionary(x => x.Name); - - bool touched = false; - - foreach (var v in variables) - { - ThemeVariableInfo info; - if (!infos.TryGetValue(v.Key, out info)) - { - // var not specified in metadata so don't save - // TODO: (MC) delete from db also if it exists - continue; - } - - var value = v.Value == null ? string.Empty : v.Value.ToString(); - - var savedThemeVar = savedThemeVars.Get(v.Key); - if (savedThemeVar != null) - { - if (value.IsEmpty() || String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) - { - // it's either null or the default value, so delete - _rsVariables.Delete(savedThemeVar); - result.Deleted.Add(savedThemeVar); - touched = true; - } - else - { - // update entity - if (!savedThemeVar.Value.Equals(value, StringComparison.OrdinalIgnoreCase)) - { - savedThemeVar.Value = value; - result.Updated.Add(savedThemeVar); - touched = true; - } - } - } - else - { - if (value.HasValue() && !String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) - { - // insert entity (only when not default value) - unsavedVars.Add(v.Key); - savedThemeVar = new ThemeVariable - { - Theme = manifest.ThemeName, - Name = v.Key, - Value = value, - StoreId = storeId - }; - _rsVariables.Insert(savedThemeVar); - result.Inserted.Add(savedThemeVar); - touched = true; - } - } - } - - if (touched) - { - _rsVariables.Context.SaveChanges(); - } - } + using (var scope = new DbContextScope(ctx: _rsVariables.Context, autoCommit: false)) + { + var unsavedVars = new List(); + var savedThemeVars = _rsVariables.Table + .Where(v => v.StoreId == storeId && v.Theme == manifest.ThemeName) + .ToDictionary(x => x.Name); + + bool touched = false; + + foreach (var v in variables) + { + ThemeVariableInfo info; + if (!infos.TryGetValue(v.Key, out info)) + { + // var not specified in metadata so don't save + // TODO: (MC) delete from db also if it exists + continue; + } + + var value = v.Value == null ? string.Empty : v.Value.ToString(); + + var savedThemeVar = savedThemeVars.Get(v.Key); + if (savedThemeVar != null) + { + if (value.IsEmpty() || String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) + { + // it's either null or the default value, so delete + _rsVariables.Delete(savedThemeVar); + result.Deleted.Add(savedThemeVar); + touched = true; + } + else + { + // update entity + if (!savedThemeVar.Value.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + savedThemeVar.Value = value; + result.Updated.Add(savedThemeVar); + touched = true; + } + } + } + else + { + if (value.HasValue() && !String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) + { + // insert entity (only when not default value) + unsavedVars.Add(v.Key); + savedThemeVar = new ThemeVariable + { + Theme = manifest.ThemeName, + Name = v.Key, + Value = value, + StoreId = storeId + }; + _rsVariables.Insert(savedThemeVar); + result.Inserted.Add(savedThemeVar); + touched = true; + } + } + } + + if (touched) + { + _rsVariables.Context.SaveChanges(); + } + } return result; } - public int ImportVariables(string themeName, int storeId, string configurationXml) + public int ImportVariables(string themeName, int storeId, string configurationXml) { Guard.NotEmpty(themeName, nameof(themeName)); Guard.NotEmpty(configurationXml, nameof(configurationXml)); @@ -279,7 +280,7 @@ public string ExportVariables(string themeName, int storeId) using (var xmlWriter = XmlWriter.Create(sb)) { - + xmlWriter.WriteStartDocument(); xmlWriter.WriteStartElement("ThemeVars"); xmlWriter.WriteAttributeString("for", themeName); @@ -311,88 +312,85 @@ public string ExportVariables(string themeName, int storeId) } - /// - /// Validates the result SASS file by calling it's url. - /// - /// Theme name - /// Stored Id - /// The error message when a parsing error occured, null otherwise - private string ValidateSass(ThemeManifest manifest, int storeId) - { - string error = string.Empty; - - var virtualPath = "~/Themes/{0}/Content/theme.scss".FormatCurrent(manifest.ThemeName); - var resolver = _themeFileResolver.Value; - var file = resolver.Resolve(virtualPath); - if (file != null) - { - virtualPath = file.ResultVirtualPath; - } - - var url = "{0}?storeId={1}&theme={2}&validate=1".FormatInvariant( - WebHelper.GetAbsoluteUrl(virtualPath, _httpContext.Request), - storeId, - manifest.ThemeName); - - var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); - WebResponse response = null; - - try - { - response = request.GetResponse(); - } - catch (WebException ex) - { - if (ex.Response is HttpWebResponse) - { - var webResponse = (HttpWebResponse)ex.Response; - var statusCode = webResponse.StatusCode; - - if (statusCode == HttpStatusCode.InternalServerError) - { - // catch only 500, as this indicates a parsing error. - var stream = webResponse.GetResponseStream(); - - using (var streamReader = new StreamReader(stream)) - { - // read the content (the error message has been put there) - error = streamReader.ReadToEnd(); - streamReader.Close(); - stream.Close(); - } - } - } - } - catch (Exception ex) - { - error = ex.Message; - } - finally - { - if (response != null) - response.Close(); - } - - return error; - } - } - - class SaveThemeVariablesResult - { - public SaveThemeVariablesResult() - { - Inserted = new List(); - Updated = new List(); - Deleted = new List(); - } - - public IList Inserted { get; private set; } - public IList Updated { get; private set; } - public IList Deleted { get; private set; } - - public int TouchedVariablesCount - { - get { return Inserted.Count + Updated.Count + Deleted.Count; } - } - } + /// + /// Validates the result SASS file by calling it's url. + /// + /// Theme name + /// Stored Id + /// The error message when a parsing error occured, null otherwise + private string ValidateSass(ThemeManifest manifest, int storeId) + { + string error = string.Empty; + + var virtualPath = "~/Themes/{0}/Content/theme.scss".FormatCurrent(manifest.ThemeName); + var resolver = _themeFileResolver.Value; + var file = resolver.Resolve(virtualPath); + if (file != null) + { + virtualPath = file.ResultVirtualPath; + } + + var url = "{0}?storeId={1}&theme={2}&validate=1".FormatInvariant( + WebHelper.GetAbsoluteUrl(virtualPath, _httpContext.Request), + storeId, + manifest.ThemeName); + + var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); + WebResponse response = null; + + try + { + response = request.GetResponse(); + } + catch (WebException ex) + { + if (ex.Response is HttpWebResponse) + { + var webResponse = (HttpWebResponse)ex.Response; + var statusCode = webResponse.StatusCode; + + if (statusCode == HttpStatusCode.InternalServerError) + { + // catch only 500, as this indicates a parsing error. + var stream = webResponse.GetResponseStream(); + + using (var streamReader = new StreamReader(stream)) + { + // read the content (the error message has been put there) + error = streamReader.ReadToEnd(); + streamReader.Close(); + stream.Close(); + } + } + } + } + catch (Exception ex) + { + error = ex.Message; + } + finally + { + if (response != null) + response.Close(); + } + + return error; + } + } + + class SaveThemeVariablesResult + { + public SaveThemeVariablesResult() + { + Inserted = new List(); + Updated = new List(); + Deleted = new List(); + } + + public IList Inserted { get; private set; } + public IList Updated { get; private set; } + public IList Deleted { get; private set; } + + public int TouchedVariablesCount => Inserted.Count + Updated.Count + Deleted.Count; + } } diff --git a/src/Libraries/SmartStore.Services/Topics/ITopicService.cs b/src/Libraries/SmartStore.Services/Topics/ITopicService.cs index ccdd2e3588..7eede60f7a 100644 --- a/src/Libraries/SmartStore.Services/Topics/ITopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/ITopicService.cs @@ -22,28 +22,28 @@ public partial interface ITopicService /// Topic Topic GetTopicById(int topicId); - /// - /// Gets a topic - /// - /// The topic system name - /// Store identifier - /// Whether to check for permission (ACL). If true and check fails, null is returned. - /// Topic - Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true); + /// + /// Gets a topic + /// + /// The topic system name + /// Store identifier + /// Whether to check for permission (ACL). If true and check fails, null is returned. + /// Topic + Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true); - /// - /// Gets all topics - /// - /// Store identifier; pass 0 to load all records - /// Whether to load hidden records - /// Topics - IPagedList GetAllTopics(int storeId = 0, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); + /// + /// Gets all topics + /// + /// Store identifier; pass 0 to load all records + /// Whether to load hidden records + /// Topics + IPagedList GetAllTopics(int storeId = 0, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); - /// - /// Inserts a topic - /// - /// Topic - void InsertTopic(Topic topic); + /// + /// Inserts a topic + /// + /// Topic + void InsertTopic(Topic topic); /// /// Updates the topic diff --git a/src/Libraries/SmartStore.Services/Topics/TopicService.cs b/src/Libraries/SmartStore.Services/Topics/TopicService.cs index ecc51a491c..b91a15d2e1 100644 --- a/src/Libraries/SmartStore.Services/Topics/TopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/TopicService.cs @@ -15,36 +15,36 @@ namespace SmartStore.Services.Topics { public partial class TopicService : ITopicService, IXmlSitemapPublisher { - private readonly ICommonServices _services; - private readonly IRepository _topicRepository; - private readonly IRepository _storeMappingRepository; - private readonly IStoreMappingService _storeMappingService; - private readonly IRepository _aclRepository; - - public TopicService( - ICommonServices services, - IRepository topicRepository, - IRepository storeMappingRepository, - IStoreMappingService storeMappingService, - IRepository aclRepository, - SeoSettings seoSettings) + private readonly ICommonServices _services; + private readonly IRepository _topicRepository; + private readonly IRepository _storeMappingRepository; + private readonly IStoreMappingService _storeMappingService; + private readonly IRepository _aclRepository; + + public TopicService( + ICommonServices services, + IRepository topicRepository, + IRepository storeMappingRepository, + IStoreMappingService storeMappingService, + IRepository aclRepository, + SeoSettings seoSettings) { - _services = services; - _topicRepository = topicRepository; - _storeMappingRepository = storeMappingRepository; - _storeMappingService = storeMappingService; - _aclRepository = aclRepository; + _services = services; + _topicRepository = topicRepository; + _storeMappingRepository = storeMappingRepository; + _storeMappingService = storeMappingService; + _aclRepository = aclRepository; - this.QuerySettings = DbQuerySettings.Default; - } + this.QuerySettings = DbQuerySettings.Default; + } - public DbQuerySettings QuerySettings { get; set; } + public DbQuerySettings QuerySettings { get; set; } public virtual void DeleteTopic(Topic topic) { - Guard.NotNull(topic, nameof(topic)); + Guard.NotNull(topic, nameof(topic)); - _topicRepository.Delete(topic); + _topicRepository.Delete(topic); } public virtual Topic GetTopicById(int topicId) @@ -55,131 +55,131 @@ public virtual Topic GetTopicById(int topicId) return _topicRepository.GetByIdCached(topicId, "db.topic.id-" + topicId); } - public virtual Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true) + public virtual Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true) { - if (systemName.IsEmpty()) - return null; + if (systemName.IsEmpty()) + return null; + + var query = BuildTopicsQuery(systemName, storeId, !checkPermission); + var rolesIdent = checkPermission + ? "0" + : _services.WorkContext.CurrentCustomer.GetRolesIdent(); + + var result = query.FirstOrDefaultCached("db.topic.bysysname-{0}-{1}-{2}".FormatInvariant(systemName, storeId, rolesIdent)); - var query = BuildTopicsQuery(systemName, storeId, !checkPermission); - var rolesIdent = checkPermission - ? "0" - : _services.WorkContext.CurrentCustomer.GetRolesIdent(); - - var result = query.FirstOrDefaultCached("db.topic.bysysname-{0}-{1}-{2}".FormatInvariant(systemName, storeId, rolesIdent)); + return result; + } - return result; + public virtual IPagedList GetAllTopics(int storeId = 0, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) + { + var query = BuildTopicsQuery(null, storeId, showHidden); + return new PagedList(query, pageIndex, pageSize); } - public virtual IPagedList GetAllTopics(int storeId = 0, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) + protected virtual IQueryable BuildTopicsQuery(string systemName, int storeId, bool showHidden = false) { - var query = BuildTopicsQuery(null, storeId, showHidden); - return new PagedList(query, pageIndex, pageSize); - } - - protected virtual IQueryable BuildTopicsQuery(string systemName, int storeId, bool showHidden = false) - { - var entityName = nameof(Topic); - var joinApplied = false; - - var query = _topicRepository.Table.Where(x => showHidden || x.IsPublished); - - if (systemName.HasValue()) - { - query = query.Where(x => x.SystemName == systemName); - } - - // Store mapping - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = from t in query - join m in _storeMappingRepository.Table - on new { c1 = t.Id, c2 = "Topic" } equals new { c1 = m.EntityId, c2 = m.EntityName } into tm - from m in tm.DefaultIfEmpty() - where !t.LimitedToStores || storeId == m.StoreId - select t; - - joinApplied = true; - } - - // ACL (access control list) - if (!showHidden && !QuerySettings.IgnoreAcl) - { + var entityName = nameof(Topic); + var joinApplied = false; + + var query = _topicRepository.Table.Where(x => showHidden || x.IsPublished); + + if (systemName.HasValue()) + { + query = query.Where(x => x.SystemName == systemName); + } + + // Store mapping + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = from t in query + join m in _storeMappingRepository.Table + on new { c1 = t.Id, c2 = "Topic" } equals new { c1 = m.EntityId, c2 = m.EntityName } into tm + from m in tm.DefaultIfEmpty() + where !t.LimitedToStores || storeId == m.StoreId + select t; + + joinApplied = true; + } + + // ACL (access control list) + if (!showHidden && !QuerySettings.IgnoreAcl) + { var allowedCustomerRolesIds = _services.WorkContext.CurrentCustomer.GetRoleIds(); - query = from c in query - join a in _aclRepository.Table - on new { c1 = c.Id, c2 = entityName } equals new { c1 = a.EntityId, c2 = a.EntityName } into ca - from a in ca.DefaultIfEmpty() - where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(a.CustomerRoleId) - select c; + query = from c in query + join a in _aclRepository.Table + on new { c1 = c.Id, c2 = entityName } equals new { c1 = a.EntityId, c2 = a.EntityName } into ca + from a in ca.DefaultIfEmpty() + where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(a.CustomerRoleId) + select c; - joinApplied = true; - } + joinApplied = true; + } - if (joinApplied) - { - // Only distinct topics (group by ID) - query = from t in query - group t by t.Id into tGroup - orderby tGroup.Key - select tGroup.FirstOrDefault(); - } + if (joinApplied) + { + // Only distinct topics (group by ID) + query = from t in query + group t by t.Id into tGroup + orderby tGroup.Key + select tGroup.FirstOrDefault(); + } - query = query.OrderBy(t => t.Priority).ThenBy(t => t.SystemName); + query = query.OrderBy(t => t.Priority).ThenBy(t => t.SystemName); - return query; - } + return query; + } public virtual void InsertTopic(Topic topic) { - Guard.NotNull(topic, nameof(topic)); + Guard.NotNull(topic, nameof(topic)); - _topicRepository.Insert(topic); + _topicRepository.Insert(topic); } public virtual void UpdateTopic(Topic topic) { - Guard.NotNull(topic, nameof(topic)); + Guard.NotNull(topic, nameof(topic)); - _topicRepository.Update(topic); + _topicRepository.Update(topic); } #region XML Sitemap public XmlSitemapProvider PublishXmlSitemap(XmlSitemapBuildContext context) - { - if (!context.LoadSetting().XmlSitemapIncludesTopics) - return null; - - var query = GetAllTopics(context.RequestStoreId).AlterQuery(q => - { - return q.Where(t => t.IncludeInSitemap && !t.RenderAsWidget); - }).SourceQuery; - - return new TopicXmlSitemapResult { Query = query }; - } - - class TopicXmlSitemapResult : XmlSitemapProvider - { - public IQueryable Query { get; set; } - - public override int GetTotalCount() - { - return Query.Count(); - } - - public override IEnumerable Enlist() - { - var topics = Query.Select(x => new { x.Id }).ToList(); - foreach (var x in topics) - { - yield return new NamedEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }; - } - } - - public override int Order => 200; - } - - #endregion - } + { + if (!context.LoadSetting().XmlSitemapIncludesTopics) + return null; + + var query = GetAllTopics(context.RequestStoreId).AlterQuery(q => + { + return q.Where(t => t.IncludeInSitemap && !t.RenderAsWidget); + }).SourceQuery; + + return new TopicXmlSitemapResult { Query = query }; + } + + class TopicXmlSitemapResult : XmlSitemapProvider + { + public IQueryable Query { get; set; } + + public override int GetTotalCount() + { + return Query.Count(); + } + + public override IEnumerable Enlist() + { + var topics = Query.Select(x => new { x.Id }).ToList(); + foreach (var x in topics) + { + yield return new NamedEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }; + } + } + + public override int Order => 200; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/Reference.cs b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/Reference.cs similarity index 98% rename from src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/Reference.cs rename to src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/Reference.cs index 02ecf2d637..ce14537202 100644 --- a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/Reference.cs +++ b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/Reference.cs @@ -13,7 +13,7 @@ // #pragma warning disable 1591 -namespace SmartStore.Services.EuropaCheckVatService { +namespace SmartStore.Services.EuropeCheckVatService { using System; using System.Web.Services; using System.Diagnostics; @@ -23,10 +23,10 @@ namespace SmartStore.Services.EuropaCheckVatService { /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1055.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="checkVatBinding", Namespace="urn:ec.europa.eu:taxud:vies:services:checkVat:types")] + [System.Web.Services.WebServiceBindingAttribute(Name="checkVatBinding", Namespace="urn:ec.europa.eu:taxud:vies:services:checkVat")] public partial class checkVatService : System.Web.Services.Protocols.SoapHttpClientProtocol { private System.Threading.SendOrPostCallback checkVatOperationCompleted; @@ -37,7 +37,7 @@ public partial class checkVatService : System.Web.Services.Protocols.SoapHttpCli /// public checkVatService() { - this.Url = global::SmartStore.Services.Properties.Settings.Default.Nop_Services_EuropaCheckVatService_checkVatService; + this.Url = global::SmartStore.Services.Properties.Settings.Default.SmartStore_Services_EuropeCheckVatService_checkVatService; if ((this.IsLocalFileSystemWebService(this.Url) == true)) { this.UseDefaultCredentials = true; this.useDefaultCredentialsSetExplicitly = false; @@ -222,7 +222,7 @@ private bool IsLocalFileSystemWebService(string url) { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1064.2")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.8.3752.0")] [System.SerializableAttribute()] [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:ec.europa.eu:taxud:vies:services:checkVat:types")] public enum matchCode { @@ -234,14 +234,18 @@ public enum matchCode { /// [System.Xml.Serialization.XmlEnumAttribute("2")] Item2, + + /// + [System.Xml.Serialization.XmlEnumAttribute("3")] + Item3, } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1055.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void checkVatCompletedEventHandler(object sender, checkVatCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1055.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class checkVatCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -303,11 +307,11 @@ public string address { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1055.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] public delegate void checkVatApproxCompletedEventHandler(object sender, checkVatApproxCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1055.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class checkVatApproxCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { diff --git a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/Reference.map b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/Reference.map similarity index 68% rename from src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/Reference.map rename to src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/Reference.map index 357ddc8242..5d20fb7ab0 100644 --- a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/Reference.map +++ b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/Reference.map @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/checkVatService.wsdl b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/checkVatService.wsdl similarity index 57% rename from src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/checkVatService.wsdl rename to src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/checkVatService.wsdl index 9cbb53a2ef..25691c25ff 100644 --- a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/checkVatService.wsdl +++ b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/checkVatService.wsdl @@ -1,40 +1,29 @@ - + - Specific disclaimer for this service ----------------------------------------- The - objective of this Internet site is to allow persons involved in the intra-Community supply of - goods or of services to obtain confirmation of the validity of the VAT identification number of - any specified person, in accordance to article 27 of Council Regulation (EC) No. 1798/2003 of 7 - October 2003. Any other use and any extraction and use of the data which is not in conformity - with the objective of this site is strictly forbidden. Any retransmission of the contents of - this site, whether for a commercial purpose or otherwise, as well as any more general use other - than as far as is necessary to support the activity of a legitimate user (for example: to draw - up their own invoices) is expressly forbidden. In addition, any copying or reproduction of the - contents of this site is strictly forbidden. The European Commission maintains this website to - enhance the access by taxable persons making intra-Community supplies to verification of their - customers VAT identification numbers. Our goal is to supply instantaneous and accurate - information. However the Commission accepts no responsibility or liability whatsoever with - regard to the information obtained using this site. This information: - is obtained from Member - States databases over which the Commission services have no control and for which the Commission - assumes no responsibility; it is the responsibility of the Member States to keep their databases - complete, accurate and up to date; - is not professional or legal advice (if you need specific - advice, you should always consult a suitably qualified professional); - does not in itself give - a right to exempt intra-Community supplies from Value Added Tax; - does not change any - obligations imposed on taxable persons in relation to intra-Community supplies. It is our goal - to minimise disruption caused by technical errors. However some data or information on our site - may have been created or structured in files or formats which are not error-free and we cannot - guarantee that our service will not be interrupted or otherwise affected by such problems. The - Commission accepts no responsibility with regard to such problems incurred as a result of using - this site or any linked external sites. This disclaimer is not intended to limit the liability - of the Commission in contravention of any requirements laid down in applicable national law nor - to exclude its liability for matters which may not be excluded under that law. Usage: The - countryCode input parameter must follow the pattern [A-Z]{2} The vatNumber input parameter must - follow the [0-9A-Za-z\+\*\.]{2,12} In case of problem, the returned FaultString can take the - following specific values: - INVALID_INPUT: The provided CountryCode is invalid or the VAT - number is empty; - SERVICE_UNAVAILABLE: The SOAP service is unavailable, try again later; - - MS_UNAVAILABLE: The Member State service is unavailable, try again later or with another Member - State; - TIMEOUT: The Member State service could not be reach in time, try again later or with - another Member State; - SERVER_BUSY: The service can't process your request. Try again latter. + The objective of this Internet site is to allow persons involved in the intra-Community supply of goods or of services to obtain confirmation of the validity of the VAT identification number of any specified person, in accordance to article 31 of Council Regulation (EC) No. 904/2010 of 7 October 2010. \n + Any other use and any extraction and use of the data which is not in conformity with the objective of this site is strictly forbidden. + Any retransmission of the contents of this site, whether for a commercial purpose or otherwise, as well as any more general use other than as far as is necessary to support the activity of a legitimate user (for example: to draw up their own invoices) is expressly forbidden. In addition, any copying or reproduction of the contents of this site is strictly forbidden. + The European Commission maintains this website to enhance the access by taxable persons making intra-Community supplies to verification of their customers' VAT identification numbers. Our goal is to supply instantaneous and accurate information. + However the Commission accepts no responsibility or liability whatsoever with regard to the information obtained using this site. This information: + - is obtained from Member States' databases over which the Commission services have no control and for which the Commission assumes no responsibility; it is the responsibility of the Member States to keep their databases complete, accurate and up to date; + - is not professional or legal advice (if you need specific advice, you should always consult a suitably qualified professional); + - does not in itself give a right to exempt intra-Community supplies from Value Added Tax; + - does not change any obligations imposed on taxable persons in relation to intra-Community supplies. + It is our goal to minimise disruption caused by technical errors. However some data or information on our site may have been created or structured in files or formats which are not error-free and we cannot guarantee that our service will not be interrupted or otherwise affected by such problems. The Commission accepts no responsibility with regard to such problems incurred as a result of using this site or any linked external sites. + This disclaimer is not intended to limit the liability of the Commission in contravention of any requirements laid down in applicable national law nor to exclude its liability for matters which may not be excluded under that law. + Collecting or handling personal data falls under the Data Protection Notice. This data protection declaration explains the Processing in the VIES-on-the-web Internet Website of VAT Identification Numbers for intra-Community Transaction on Goods or Services. Details of your legal rights associated with the collection, processing and use of this data are also provided: http://ec.europa.eu/dpo-register/details.htm?id=40647 . + + Usage: + The countryCode input parameter must follow the pattern [A-Z]{2} + The vatNumber input parameter must follow the pattern [0-9A-Za-z\+\*\.]{2,12} + In case of problems, the returned FaultString can take the following specific values: + - INVALID_INPUT: The provided CountryCode is invalid or the VAT number is empty; + - GLOBAL_MAX_CONCURRENT_REQ: Your Request for VAT validation has not been processed; the maximum number of concurrent requests has been reached. Please re-submit your request later or contact TAXUD-VIESWEB@ec.europa.eu for further information": Your request cannot be processed due to high traffic on the web application. Please try again later; + - MS_MAX_CONCURRENT_REQ: Your Request for VAT validation has not been processed; the maximum number of concurrent requests for this Member State has been reached. Please re-submit your request later or contact TAXUD-VIESWEB@ec.europa.eu for further information": Your request cannot be processed due to high traffic towards the Member State you are trying to reach. Please try again later. + - SERVICE_UNAVAILABLE: an error was encountered either at the network level or the Web application level, try again later; + - MS_UNAVAILABLE: The application at the Member State is not replying or not available. Please refer to the Technical Information page to check the status of the requested Member State, try again later; + - TIMEOUT: The application did not receive a reply within the allocated time period, try again later. @@ -112,6 +101,11 @@ INVALID + + + NOT_PROCESSED + + diff --git a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/matchCode.datasource b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/matchCode.datasource similarity index 80% rename from src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/matchCode.datasource rename to src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/matchCode.datasource index 39c1b02382..8c0fe5afe2 100644 --- a/src/Libraries/SmartStore.Services/Web References/EuropaCheckVatService/matchCode.datasource +++ b/src/Libraries/SmartStore.Services/Web References/EuropeCheckVatService/matchCode.datasource @@ -6,5 +6,5 @@ cause the file to be unrecognizable by the program. --> - SmartStore.Services.EuropaCheckVatService.matchCode, Web References.EuropaCheckVatService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + SmartStore.Services.EuropeCheckVatService.matchCode, Web References.EuropeCheckVatService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/app.config b/src/Libraries/SmartStore.Services/app.config index 08f9834796..446c32df9a 100644 --- a/src/Libraries/SmartStore.Services/app.config +++ b/src/Libraries/SmartStore.Services/app.config @@ -9,10 +9,7 @@ - - http://ec.europa.eu/taxation_customs/vies/services/checkVatService - - + http://ec.europa.eu/taxation_customs/vies/services/checkVatService @@ -29,11 +26,7 @@ - - - - - + @@ -41,7 +34,19 @@ - + + + + + + + + + + + + + @@ -51,4 +56,4 @@ - + diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index be5db3727d..4046ac6a7f 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -1,27 +1,30 @@  - - - - + + + + - + + - - + + - + + - - + + - - - + + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs b/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs index 21cce48dbe..810c633b6f 100644 --- a/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs +++ b/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs @@ -3,63 +3,63 @@ namespace SmartStore.AmazonPay { - public class AmazonPaySettings : ISettings - { - public AmazonPaySettings() - { - Marketplace = "de"; - DataFetching = AmazonPayDataFetchingType.Ipn; - TransactionType = AmazonPayTransactionType.Authorize; - AuthorizeMethod = AmazonPayAuthorizeMethod.Omnichronous; - SaveEmailAndPhone = AmazonPaySaveDataType.OnlyIfEmpty; - AddOrderNotes = true; - InformCustomerAboutErrors = true; - InformCustomerAddErrors = true; - PollingMaxOrderCreationDays = 31; + public class AmazonPaySettings : ISettings + { + public AmazonPaySettings() + { + Marketplace = "de"; + DataFetching = AmazonPayDataFetchingType.Ipn; + TransactionType = AmazonPayTransactionType.Authorize; + AuthorizeMethod = AmazonPayAuthorizeMethod.Omnichronous; + SaveEmailAndPhone = AmazonPaySaveDataType.OnlyIfEmpty; + AddOrderNotes = true; + InformCustomerAboutErrors = true; + InformCustomerAddErrors = true; + PollingMaxOrderCreationDays = 31; - PayButtonColor = "Gold"; - PayButtonSize = "small"; - AuthButtonType = "LwA"; - AuthButtonColor = "Gold"; - AuthButtonSize = "medium"; - } + PayButtonColor = "Gold"; + PayButtonSize = "small"; + AuthButtonType = "LwA"; + AuthButtonColor = "Gold"; + AuthButtonSize = "medium"; + } - public bool UseSandbox { get; set; } + public bool UseSandbox { get; set; } - public string SellerId { get; set; } - public string AccessKey { get; set; } - public string SecretKey { get; set; } - public string ClientId { get; set; } - public string Marketplace { get; set; } - - public AmazonPayDataFetchingType DataFetching { get; set; } - public AmazonPayTransactionType TransactionType { get; set; } - public AmazonPayAuthorizeMethod AuthorizeMethod { get; set; } + public string SellerId { get; set; } + public string AccessKey { get; set; } + public string SecretKey { get; set; } + public string ClientId { get; set; } + public string Marketplace { get; set; } - public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } - public bool ShowPayButtonForAdminOnly { get; set; } - public bool ShowButtonInMiniShoppingCart { get; set; } + public AmazonPayDataFetchingType DataFetching { get; set; } + public AmazonPayTransactionType TransactionType { get; set; } + public AmazonPayAuthorizeMethod AuthorizeMethod { get; set; } - public int PollingMaxOrderCreationDays { get; set; } + public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } + public bool ShowPayButtonForAdminOnly { get; set; } + public bool ShowButtonInMiniShoppingCart { get; set; } - public decimal AdditionalFee { get; set; } - public bool AdditionalFeePercentage { get; set; } + public int PollingMaxOrderCreationDays { get; set; } - public bool AddOrderNotes { get; set; } + public decimal AdditionalFee { get; set; } + public bool AdditionalFeePercentage { get; set; } - public bool InformCustomerAboutErrors { get; set; } - public bool InformCustomerAddErrors { get; set; } + public bool AddOrderNotes { get; set; } - public string PayButtonColor { get; set; } - public string PayButtonSize { get; set; } + public bool InformCustomerAboutErrors { get; set; } + public bool InformCustomerAddErrors { get; set; } - public string AuthButtonType { get; set; } - public string AuthButtonColor { get; set; } - public string AuthButtonSize { get; set; } + public string PayButtonColor { get; set; } + public string PayButtonSize { get; set; } - public bool CanSaveEmailAndPhone(string value) - { - return (SaveEmailAndPhone == AmazonPaySaveDataType.Always || (SaveEmailAndPhone == AmazonPaySaveDataType.OnlyIfEmpty && value.IsEmpty())); - } - } + public string AuthButtonType { get; set; } + public string AuthButtonColor { get; set; } + public string AuthButtonSize { get; set; } + + public bool CanSaveEmailAndPhone(string value) + { + return (SaveEmailAndPhone == AmazonPaySaveDataType.Always || (SaveEmailAndPhone == AmazonPaySaveDataType.OnlyIfEmpty && value.IsEmpty())); + } + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs index 35fbe06a06..f3f9e19667 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs @@ -13,109 +13,109 @@ namespace SmartStore.AmazonPay.Controllers { public class AmazonPayCheckoutController : AmazonPayControllerBase - { - private readonly HttpContextBase _httpContext; - private readonly IAmazonPayService _apiService; - private readonly IGenericAttributeService _genericAttributeService; + { + private readonly HttpContextBase _httpContext; + private readonly IAmazonPayService _apiService; + private readonly IGenericAttributeService _genericAttributeService; private readonly IOrderProcessingService _orderProcessingService; public AmazonPayCheckoutController( - HttpContextBase httpContext, - IAmazonPayService apiService, - IGenericAttributeService genericAttributeService, + HttpContextBase httpContext, + IAmazonPayService apiService, + IGenericAttributeService genericAttributeService, IOrderProcessingService orderProcessingService) - { - _httpContext = httpContext; - _apiService = apiService; - _genericAttributeService = genericAttributeService; + { + _httpContext = httpContext; + _apiService = apiService; + _genericAttributeService = genericAttributeService; _orderProcessingService = orderProcessingService; - } - - public ActionResult OrderReferenceCreated(string orderReferenceId/*, string accessToken*/) - { - var success = false; - var error = string.Empty; - - try - { - var state = _httpContext.GetAmazonPayState(Services.Localization); - state.OrderReferenceId = orderReferenceId; - - //if (accessToken.HasValue()) - //{ - // state.AccessToken = accessToken; - //} - - if (state.OrderReferenceId.IsEmpty()) - { - success = false; - error = T("Plugins.Payments.AmazonPay.MissingOrderReferenceId"); - } - - if (state.AccessToken.IsEmpty()) - { - success = false; - error = error.Grow(T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"), " "); - } - } - catch (Exception ex) - { - error = ex.Message; - } - - return new JsonResult { Data = new { success = success, error = error } }; - } - - public ActionResult BillingAddress() - { - return RedirectToAction("ShippingAddress", "Checkout", new { area = "" }); - } - - public ActionResult ShippingAddress() - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.Address, TempData); - - return GetActionResult(model); - } - - public ActionResult PaymentMethod() - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.PaymentMethod, TempData); - - return GetActionResult(model); - } - - [HttpPost] - public ActionResult PaymentMethod(FormCollection form) - { - // Display biling address on confirm page. - _apiService.GetBillingAddress(); - - var customer = Services.WorkContext.CurrentCustomer; - if (customer.BillingAddress == null) - { - NotifyError(T("Plugins.Payments.AmazonPay.MissingBillingAddress")); - return RedirectToAction("Cart", "ShoppingCart", new { area = "" }); - } - - return RedirectToAction("Confirm", "Checkout", new { area = "" }); - } - - public ActionResult PaymentInfo() - { - return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); - } - - public ActionResult CheckoutCompleted() - { - var note = _httpContext.Session["AmazonPayCheckoutCompletedNote"] as string; - if (note.HasValue()) - { - return Content(note); - } - - return new EmptyResult(); - } + } + + public ActionResult OrderReferenceCreated(string orderReferenceId/*, string accessToken*/) + { + var success = false; + var error = string.Empty; + + try + { + var state = _httpContext.GetAmazonPayState(Services.Localization); + state.OrderReferenceId = orderReferenceId; + + //if (accessToken.HasValue()) + //{ + // state.AccessToken = accessToken; + //} + + if (state.OrderReferenceId.IsEmpty()) + { + success = false; + error = T("Plugins.Payments.AmazonPay.MissingOrderReferenceId"); + } + + if (state.AccessToken.IsEmpty()) + { + success = false; + error = error.Grow(T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"), " "); + } + } + catch (Exception ex) + { + error = ex.Message; + } + + return new JsonResult { Data = new { success = success, error = error } }; + } + + public ActionResult BillingAddress() + { + return RedirectToAction("ShippingAddress", "Checkout", new { area = "" }); + } + + public ActionResult ShippingAddress() + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.Address, TempData); + + return GetActionResult(model); + } + + public ActionResult PaymentMethod() + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.PaymentMethod, TempData); + + return GetActionResult(model); + } + + [HttpPost] + public ActionResult PaymentMethod(FormCollection form) + { + // Display biling address on confirm page. + _apiService.GetBillingAddress(); + + var customer = Services.WorkContext.CurrentCustomer; + if (customer.BillingAddress == null) + { + NotifyError(T("Plugins.Payments.AmazonPay.MissingBillingAddress")); + return RedirectToAction("Cart", "ShoppingCart", new { area = "" }); + } + + return RedirectToAction("Confirm", "Checkout", new { area = "" }); + } + + public ActionResult PaymentInfo() + { + return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); + } + + public ActionResult CheckoutCompleted() + { + var note = _httpContext.Session["AmazonPayCheckoutCompletedNote"] as string; + if (note.HasValue()) + { + return Content(note); + } + + return new EmptyResult(); + } #region Confirmation flow diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs index 669cc4c009..014492316e 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs @@ -19,141 +19,143 @@ namespace SmartStore.AmazonPay.Controllers { - public class AmazonPayController : PaymentControllerBase - { + public class AmazonPayController : PaymentControllerBase + { private readonly HttpContextBase _httpContext; private readonly IAmazonPayService _apiService; - private readonly Lazy _scheduleTaskService; - private readonly Lazy _openAuthenticationService; - private readonly Lazy _externalAuthenticationSettings; + private readonly Lazy _scheduleTaskService; + private readonly Lazy _openAuthenticationService; + private readonly Lazy _externalAuthenticationSettings; - public AmazonPayController( + public AmazonPayController( HttpContextBase httpContext, IAmazonPayService apiService, - Lazy scheduleTaskService, - Lazy openAuthenticationService, - Lazy externalAuthenticationSettings) - { + Lazy scheduleTaskService, + Lazy openAuthenticationService, + Lazy externalAuthenticationSettings) + { _httpContext = httpContext; - _apiService = apiService; - _scheduleTaskService = scheduleTaskService; - _openAuthenticationService = openAuthenticationService; - _externalAuthenticationSettings = externalAuthenticationSettings; - } - - [NonAction] - public override IList ValidatePaymentForm(FormCollection form) - { - var warnings = new List(); - return warnings; - } - - [NonAction] - public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) - { - var paymentInfo = new ProcessPaymentRequest(); - return paymentInfo; - } - - [AdminAuthorize, LoadSetting] - public ActionResult Configure(AmazonPaySettings settings) - { - var model = new ConfigurationModel(); - - MiniMapper.Map(settings, model); - _apiService.SetupConfiguration(model); - - return View(model); - } - - [HttpPost, AdminAuthorize] - public ActionResult Configure(ConfigurationModel model, FormCollection form) - { - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - - if (!ModelState.IsValid) - return Configure(settings); - - ModelState.Clear(); - - model.AccessKey = model.AccessKey.TrimSafe(); - model.ClientId = model.ClientId.TrimSafe(); - model.SecretKey = model.SecretKey.TrimSafe(); - model.SellerId = model.SellerId.TrimSafe(); - - MiniMapper.Map(model, settings); - - using (Services.Settings.BeginScope()) - { - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); - } - - using (Services.Settings.BeginScope()) - { - Services.Settings.SaveSetting(settings, x => x.DataFetching, 0, false); - Services.Settings.SaveSetting(settings, x => x.PollingMaxOrderCreationDays, 0, false); - } - - var task = _scheduleTaskService.Value.GetTaskByType(); - if (task != null) - { - task.Enabled = settings.DataFetching == AmazonPayDataFetchingType.Polling; - - _scheduleTaskService.Value.UpdateTask(task); - } - - NotifySuccess(T("Plugins.Payments.AmazonPay.ConfigSaveNote")); - - return RedirectToConfiguration(AmazonPayPlugin.SystemName); - } - - [HttpPost, AdminAuthorize] - public ActionResult SaveAccessData(string accessData) - { - try - { - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - _apiService.ShareKeys(accessData, storeScope); - - NotifySuccess(T("Plugins.Payments.AmazonPay.SaveAccessDataSucceeded")); - } - catch (Exception exception) - { - NotifyError(exception.Message); - } - - return RedirectToConfiguration(AmazonPayPlugin.SystemName); - } - - [ValidateInput(false)] - public ActionResult ShareKey(string payload) - { - Response.AddHeader("Access-Control-Allow-Origin", "https://payments.amazon.com"); - Response.AddHeader("Access-Control-Allow-Methods", "GET, POST"); - Response.AddHeader("Access-Control-Allow-Headers", "Content-Type"); - - try - { - _apiService.ShareKeys(payload, 0); - } - catch (Exception exception) - { - Response.StatusCode = 400; - return Json(new { result = "error", message = exception.Message }); - } - - return Json(new { result = "success" }); - } - - [HttpPost] - [ValidateInput(false)] - public ActionResult IPNHandler() - { - _apiService.ProcessIpn(Request); - return Content("OK"); - } + _apiService = apiService; + _scheduleTaskService = scheduleTaskService; + _openAuthenticationService = openAuthenticationService; + _externalAuthenticationSettings = externalAuthenticationSettings; + } + + [NonAction] + public override IList ValidatePaymentForm(FormCollection form) + { + var warnings = new List(); + return warnings; + } + + [NonAction] + public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) + { + var paymentInfo = new ProcessPaymentRequest(); + return paymentInfo; + } + + [AdminAuthorize, LoadSetting] + public ActionResult Configure(AmazonPaySettings settings) + { + var model = new ConfigurationModel(); + + MiniMapper.Map(settings, model); + _apiService.SetupConfiguration(model); + + return View(model); + } + + [HttpPost, AdminAuthorize] + [ValidateAntiForgeryToken] + public ActionResult Configure(ConfigurationModel model, FormCollection form) + { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + if (!ModelState.IsValid) + return Configure(settings); + + ModelState.Clear(); + + model.AccessKey = model.AccessKey.TrimSafe(); + model.ClientId = model.ClientId.TrimSafe(); + model.SecretKey = model.SecretKey.TrimSafe(); + model.SellerId = model.SellerId.TrimSafe(); + + MiniMapper.Map(model, settings); + + using (Services.Settings.BeginScope()) + { + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } + + using (Services.Settings.BeginScope()) + { + Services.Settings.SaveSetting(settings, x => x.DataFetching, 0, false); + Services.Settings.SaveSetting(settings, x => x.PollingMaxOrderCreationDays, 0, false); + } + + var task = _scheduleTaskService.Value.GetTaskByType(); + if (task != null) + { + task.Enabled = settings.DataFetching == AmazonPayDataFetchingType.Polling; + + _scheduleTaskService.Value.UpdateTask(task); + } + + NotifySuccess(T("Plugins.Payments.AmazonPay.ConfigSaveNote")); + + return RedirectToConfiguration(AmazonPayPlugin.SystemName); + } + + [HttpPost, AdminAuthorize] + [ValidateAntiForgeryToken] + public ActionResult SaveAccessData(string accessData) + { + try + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + _apiService.ShareKeys(accessData, storeScope); + + NotifySuccess(T("Plugins.Payments.AmazonPay.SaveAccessDataSucceeded")); + } + catch (Exception exception) + { + NotifyError(exception.Message); + } + + return RedirectToConfiguration(AmazonPayPlugin.SystemName); + } + + [ValidateInput(false)] + public ActionResult ShareKey(string payload) + { + Response.AddHeader("Access-Control-Allow-Origin", "https://payments.amazon.com"); + Response.AddHeader("Access-Control-Allow-Methods", "GET, POST"); + Response.AddHeader("Access-Control-Allow-Headers", "Content-Type"); + + try + { + _apiService.ShareKeys(payload, 0); + } + catch (Exception exception) + { + Response.StatusCode = 400; + return Json(new { result = "error", message = exception.Message }); + } + + return Json(new { result = "success" }); + } + + [HttpPost] + [ValidateInput(false)] + public ActionResult IPNHandler() + { + _apiService.ProcessIpn(Request); + return Content("OK"); + } // For payment simulation in sandbox mode. Admin and sandbox only. public ActionResult SetSellerNote(string paymentState) @@ -194,7 +196,7 @@ public ActionResult SetSellerNote(string paymentState) } var result = _apiService.SetOrderReferenceDetails(null, settings, state, sellerNote, out var errorMesage); - + var sb = new StringBuilder(); sb.Append(result ? "ok. " : "failure. "); sb.AppendLine(sellerNote); @@ -206,52 +208,52 @@ public ActionResult SetSellerNote(string paymentState) #region Authentication [ChildActionOnly] - public ActionResult AuthenticationPublicInfo() - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.AuthenticationPublicInfo, TempData); + public ActionResult AuthenticationPublicInfo() + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.AuthenticationPublicInfo, TempData); if (model != null) { return View(model); } return new EmptyResult(); - } - - public ActionResult AuthenticationButtonHandler() - { - var returnUrl = Session["AmazonAuthReturnUrl"] as string; - - var processor = _openAuthenticationService.Value.LoadExternalAuthenticationMethodBySystemName(AmazonPayPlugin.SystemName, Services.StoreContext.CurrentStore.Id); - if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings.Value)) - { - NotifyError(T("Plugins.Payments.AmazonPay.AuthenticationNotActive")); - return new RedirectResult(Url.LogOn(returnUrl)); - } - - var result = _apiService.Authorize(returnUrl); - switch (result.AuthenticationStatus) - { - case OpenAuthenticationStatus.Error: - result.Errors.Each(x => NotifyError(x)); - return new RedirectResult(Url.LogOn(returnUrl)); - case OpenAuthenticationStatus.AssociateOnLogon: - return new RedirectResult(Url.LogOn(returnUrl)); - case OpenAuthenticationStatus.AutoRegisteredEmailValidation: - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl }); - case OpenAuthenticationStatus.AutoRegisteredAdminApproval: - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl }); - case OpenAuthenticationStatus.AutoRegisteredStandard: - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard, returnUrl }); - default: - if (result.Result != null) - return result.Result; - - if (HttpContext.Request.IsAuthenticated) - return RedirectToReferrer(returnUrl, "~/"); - - return new RedirectResult(Url.LogOn(returnUrl)); - } - } + } + + public ActionResult AuthenticationButtonHandler() + { + var returnUrl = Session["AmazonAuthReturnUrl"] as string; + + var processor = _openAuthenticationService.Value.LoadExternalAuthenticationMethodBySystemName(AmazonPayPlugin.SystemName, Services.StoreContext.CurrentStore.Id); + if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings.Value)) + { + NotifyError(T("Plugins.Payments.AmazonPay.AuthenticationNotActive")); + return new RedirectResult(Url.LogOn(returnUrl)); + } + + var result = _apiService.Authorize(returnUrl); + switch (result.AuthenticationStatus) + { + case OpenAuthenticationStatus.Error: + result.Errors.Each(x => NotifyError(x)); + return new RedirectResult(Url.LogOn(returnUrl)); + case OpenAuthenticationStatus.AssociateOnLogon: + return new RedirectResult(Url.LogOn(returnUrl)); + case OpenAuthenticationStatus.AutoRegisteredEmailValidation: + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl }); + case OpenAuthenticationStatus.AutoRegisteredAdminApproval: + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl }); + case OpenAuthenticationStatus.AutoRegisteredStandard: + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard, returnUrl }); + default: + if (result.Result != null) + return result.Result; + + if (HttpContext.Request.IsAuthenticated) + return RedirectToReferrer(returnUrl, "~/"); + + return new RedirectResult(Url.LogOn(returnUrl)); + } + } #endregion } diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayControllerBase.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayControllerBase.cs index b9f4858333..423463b7d7 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayControllerBase.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayControllerBase.cs @@ -5,25 +5,25 @@ namespace SmartStore.AmazonPay.Controllers { - public abstract partial class AmazonPayControllerBase : PublicControllerBase - { - protected ActionResult GetActionResult(AmazonPayViewModel model) - { - switch (model.Result) - { - case AmazonPayResultType.None: - return new EmptyResult(); + public abstract partial class AmazonPayControllerBase : PublicControllerBase + { + protected ActionResult GetActionResult(AmazonPayViewModel model) + { + switch (model.Result) + { + case AmazonPayResultType.None: + return new EmptyResult(); - case AmazonPayResultType.PluginView: - return View(model); + case AmazonPayResultType.PluginView: + return View(model); - case AmazonPayResultType.Unauthorized: - return new HttpUnauthorizedResult(); + case AmazonPayResultType.Unauthorized: + return new HttpUnauthorizedResult(); - case AmazonPayResultType.Redirect: - default: - return RedirectToAction(model.RedirectAction, model.RedirectController, new { area = "" }); - } - } - } + case AmazonPayResultType.Redirect: + default: + return RedirectToAction(model.RedirectAction, model.RedirectController, new { area = "" }); + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs index 794078cebd..9da9735274 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs @@ -4,57 +4,57 @@ namespace SmartStore.AmazonPay.Controllers { public class AmazonPayShoppingCartController : AmazonPayControllerBase - { + { private readonly IAmazonPayService _apiService; - public AmazonPayShoppingCartController(IAmazonPayService apiService) - { - _apiService = apiService; - } - - public ActionResult PayButtonHandler() - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.PayButtonHandler, TempData); - return GetActionResult(model); - } - - [ChildActionOnly] - public ActionResult ShoppingCart() - { - if (ControllerContext.ParentActionViewContext.RequestContext.RouteData.IsRouteEqual("ShoppingCart", "Cart")) - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.ShoppingCart, TempData); - - return GetActionResult(model); - } - - return new EmptyResult(); - } - - [ChildActionOnly] - public ActionResult OrderReviewData(bool renderAmazonPayView) - { - if (renderAmazonPayView) - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.OrderReviewData, TempData); - - return View(model); - } - - return new EmptyResult(); - } - - [ChildActionOnly] - public ActionResult MiniShoppingCart(bool renderAmazonPayView) - { - if (renderAmazonPayView) - { - var model = _apiService.CreateViewModel(AmazonPayRequestType.MiniShoppingCart, TempData); - - return GetActionResult(model); - } - - return new EmptyResult(); - } + public AmazonPayShoppingCartController(IAmazonPayService apiService) + { + _apiService = apiService; + } + + public ActionResult PayButtonHandler() + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.PayButtonHandler, TempData); + return GetActionResult(model); + } + + [ChildActionOnly] + public ActionResult ShoppingCart() + { + if (ControllerContext.ParentActionViewContext.RequestContext.RouteData.IsRouteEqual("ShoppingCart", "Cart")) + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.ShoppingCart, TempData); + + return GetActionResult(model); + } + + return new EmptyResult(); + } + + [ChildActionOnly] + public ActionResult OrderReviewData(bool renderAmazonPayView) + { + if (renderAmazonPayView) + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.OrderReviewData, TempData); + + return View(model); + } + + return new EmptyResult(); + } + + [ChildActionOnly] + public ActionResult MiniShoppingCart(bool renderAmazonPayView) + { + if (renderAmazonPayView) + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.MiniShoppingCart, TempData); + + return GetActionResult(model); + } + + return new EmptyResult(); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs b/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs index b163acf6e8..080b9f19eb 100644 --- a/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs @@ -8,21 +8,18 @@ namespace SmartStore.AmazonPay { - public class DependencyRegistrar : IDependencyRegistrar - { - public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) - { - builder.RegisterType().As().InstancePerRequest(); + public class DependencyRegistrar : IDependencyRegistrar + { + public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) + { + builder.RegisterType().As().InstancePerRequest(); - if (isActiveModule) - { - builder.RegisterType().AsActionFilterFor().InstancePerRequest(); - } - } + if (isActiveModule) + { + builder.RegisterType().AsActionFilterFor().InstancePerRequest(); + } + } - public int Order - { - get { return 1; } - } - } + public int Order => 1; + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 7449e587bb..937624a4ec 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Login and Pay with Amazon SystemName: SmartStore.AmazonPay Group: Payment -Version: 3.2.2 -MinAppVersion: 3.2.2 +Version: 4.2.0 +MinAppVersion: 4.2.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.AmazonPay.dll diff --git a/src/Plugins/SmartStore.AmazonPay/Events/EventConsumer.cs b/src/Plugins/SmartStore.AmazonPay/Events/EventConsumer.cs index 3d6e180d4f..0ad0c626a7 100644 --- a/src/Plugins/SmartStore.AmazonPay/Events/EventConsumer.cs +++ b/src/Plugins/SmartStore.AmazonPay/Events/EventConsumer.cs @@ -13,81 +13,81 @@ namespace SmartStore.AmazonPay.Events { - public class EventConsumer : IConsumer - { - private readonly IPluginFinder _pluginFinder; - private readonly ICommonServices _services; - private readonly IOrderService _orderService; - private readonly Lazy _amazonPayService; - - public EventConsumer( - IPluginFinder pluginFinder, - ICommonServices services, - IOrderService orderService, - Lazy amazonPayService) - { - _pluginFinder = pluginFinder; - _services = services; - _orderService = orderService; - _amazonPayService = amazonPayService; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - public void HandleEvent(MessageModelCreatedEvent message) - { - if (message.MessageContext.MessageTemplate.Name != MessageTemplateNames.OrderPlacedCustomer) - return; - - var storeId = _services.StoreContext.CurrentStore.Id; - - if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayPlugin.SystemName, storeId)) - return; - - dynamic model = message.Model; - - if (model.Order == null) - return; - - var orderId = model.Order.ID; - - if (orderId is int id) - { - var order = _orderService.GetOrderById(id); - - var isAmazonPayment = (order != null && order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayPlugin.SystemName)); - var tokenValue = (isAmazonPayment ? _services.Localization.GetResource("Plugins.Payments.AmazonPay.BillingAddressMessageNote") : ""); - - model.AmazonPay = new Dictionary - { - { "BillingAddressMessageNote", tokenValue } - }; - } - } - - public void HandleEvent(OrderPaidEvent eventMessage) - { - if (eventMessage?.Order == null) - return; - - if (!eventMessage.Order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayPlugin.SystemName)) - return; - - if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayPlugin.SystemName, eventMessage.Order.StoreId)) - return; - - try - { - var settings = _services.Settings.LoadSetting(eventMessage.Order.StoreId); - - _amazonPayService.Value.CloseOrderReference(settings, eventMessage.Order); - } - catch (Exception exception) - { - Logger.Error(exception); - } - } - } + public class EventConsumer : IConsumer + { + private readonly IPluginFinder _pluginFinder; + private readonly ICommonServices _services; + private readonly IOrderService _orderService; + private readonly Lazy _amazonPayService; + + public EventConsumer( + IPluginFinder pluginFinder, + ICommonServices services, + IOrderService orderService, + Lazy amazonPayService) + { + _pluginFinder = pluginFinder; + _services = services; + _orderService = orderService; + _amazonPayService = amazonPayService; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void HandleEvent(MessageModelCreatedEvent message) + { + if (message.MessageContext.MessageTemplate.Name != MessageTemplateNames.OrderPlacedCustomer) + return; + + var storeId = _services.StoreContext.CurrentStore.Id; + + if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayPlugin.SystemName, storeId)) + return; + + dynamic model = message.Model; + + if (model.Order == null) + return; + + var orderId = model.Order.ID; + + if (orderId is int id) + { + var order = _orderService.GetOrderById(id); + + var isAmazonPayment = (order != null && order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayPlugin.SystemName)); + var tokenValue = (isAmazonPayment ? _services.Localization.GetResource("Plugins.Payments.AmazonPay.BillingAddressMessageNote") : ""); + + model.AmazonPay = new Dictionary + { + { "BillingAddressMessageNote", tokenValue } + }; + } + } + + public void HandleEvent(OrderPaidEvent eventMessage) + { + if (eventMessage?.Order == null) + return; + + if (!eventMessage.Order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayPlugin.SystemName)) + return; + + if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayPlugin.SystemName, eventMessage.Order.StoreId)) + return; + + try + { + var settings = _services.Settings.LoadSetting(eventMessage.Order.StoreId); + + _amazonPayService.Value.CloseOrderReference(settings, eventMessage.Order); + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs b/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs index ef444da26e..eb5dd84142 100644 --- a/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs +++ b/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs @@ -6,71 +6,71 @@ namespace SmartStore.AmazonPay.Filters { - public class AmazonPayCheckoutFilter : IActionFilter - { - private static readonly string[] s_interceptableActions = new string[] { "BillingAddress", "ShippingAddress", "ShippingMethod", "PaymentMethod" }; - private readonly Lazy _apiService; - - public AmazonPayCheckoutFilter(Lazy apiService) - { - _apiService = apiService; - } - - private static bool IsInterceptableAction(string actionName) - { - return s_interceptableActions.Contains(actionName, StringComparer.OrdinalIgnoreCase); - } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) - return; - - var actionName = filterContext.ActionDescriptor.ActionName; - - if (!IsInterceptableAction(actionName)) - return; - - if (actionName.IsCaseInsensitiveEqual("ShippingMethod") || actionName.IsCaseInsensitiveEqual("PaymentMethod")) - { - if (!filterContext.HttpContext.HasAmazonPayState()) - return; - } - - if (actionName.IsCaseInsensitiveEqual("ShippingMethod")) - { - var model = _apiService.Value.CreateViewModel(AmazonPayRequestType.ShippingMethod, filterContext.Controller.TempData); - - if (model.Result == AmazonPayResultType.Redirect) - { - // Shipping to selected address not possible. - var urlHelper = new UrlHelper(filterContext.HttpContext.Request.RequestContext); - var url = urlHelper.Action("ShippingAddress", "Checkout", new { area = "" }); - - filterContext.Result = new RedirectResult(url, false); - } - } - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) - return; - - var actionName = filterContext.ActionDescriptor.ActionName; + public class AmazonPayCheckoutFilter : IActionFilter + { + private static readonly string[] s_interceptableActions = new string[] { "BillingAddress", "ShippingAddress", "ShippingMethod", "PaymentMethod" }; + private readonly Lazy _apiService; + + public AmazonPayCheckoutFilter(Lazy apiService) + { + _apiService = apiService; + } + + private static bool IsInterceptableAction(string actionName) + { + return s_interceptableActions.Contains(actionName, StringComparer.OrdinalIgnoreCase); + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + return; + + var actionName = filterContext.ActionDescriptor.ActionName; + + if (!IsInterceptableAction(actionName)) + return; + + if (actionName.IsCaseInsensitiveEqual("ShippingMethod") || actionName.IsCaseInsensitiveEqual("PaymentMethod")) + { + if (!filterContext.HttpContext.HasAmazonPayState()) + return; + } + + if (actionName.IsCaseInsensitiveEqual("ShippingMethod")) + { + var model = _apiService.Value.CreateViewModel(AmazonPayRequestType.ShippingMethod, filterContext.Controller.TempData); + + if (model.Result == AmazonPayResultType.Redirect) + { + // Shipping to selected address not possible. + var urlHelper = new UrlHelper(filterContext.HttpContext.Request.RequestContext); + var url = urlHelper.Action("ShippingAddress", "Checkout", new { area = "" }); + + filterContext.Result = new RedirectResult(url, false); + } + } + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + return; + + var actionName = filterContext.ActionDescriptor.ActionName; if (actionName.IsCaseInsensitiveEqual("ShippingMethod")) return; if (!IsInterceptableAction(actionName)) - return; + return; if (!filterContext.HttpContext.HasAmazonPayState()) return; var routeValues = new RouteValueDictionary(new { action = actionName, controller = "AmazonPayCheckout" }); - filterContext.Result = new RedirectToRouteResult("SmartStore.AmazonPay", routeValues); - } - } + filterContext.Result = new RedirectToRouteResult("SmartStore.AmazonPay", routeValues); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs index 77c391b448..e709b6b39b 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs @@ -4,49 +4,49 @@ namespace SmartStore.AmazonPay.Models { - public class AmazonPayViewModel : ModelBase + public class AmazonPayViewModel : ModelBase { - public AmazonPayViewModel() - { - IsShippable = true; - RedirectAction = "Cart"; - RedirectController = "ShoppingCart"; - Result = AmazonPayResultType.PluginView; - BillingAddress = new AddressModel(); - } - - public string SystemName => AmazonPayPlugin.SystemName; - - public string SellerId { get; set; } - public string ClientId { get; set; } - - /// - /// Amazon widget script URL - /// - public string WidgetUrl { get; set; } - public string ButtonHandlerUrl { get; set; } - - public bool IsShippable { get; set; } - public bool IsRecurring { get; set; } - - public string LanguageCode { get; set; } - public AmazonPayRequestType Type { get; set; } - public AmazonPayResultType Result { get; set; } - - public string RedirectAction { get; set; } - public string RedirectController { get; set; } - - public string OrderReferenceId { get; set; } - public string AddressConsentToken { get; set; } - public string Warning { get; set; } - public bool Logout { get; set; } - - public string ButtonType { get; set; } - public string ButtonColor { get; set; } - public string ButtonSize { get; set; } - - public string ShippingMethod { get; set; } - public AddressModel BillingAddress { get; set; } + public AmazonPayViewModel() + { + IsShippable = true; + RedirectAction = "Cart"; + RedirectController = "ShoppingCart"; + Result = AmazonPayResultType.PluginView; + BillingAddress = new AddressModel(); + } + + public string SystemName => AmazonPayPlugin.SystemName; + + public string SellerId { get; set; } + public string ClientId { get; set; } + + /// + /// Amazon widget script URL + /// + public string WidgetUrl { get; set; } + public string ButtonHandlerUrl { get; set; } + + public bool IsShippable { get; set; } + public bool IsRecurring { get; set; } + + public string LanguageCode { get; set; } + public AmazonPayRequestType Type { get; set; } + public AmazonPayResultType Result { get; set; } + + public string RedirectAction { get; set; } + public string RedirectController { get; set; } + + public string OrderReferenceId { get; set; } + public string AddressConsentToken { get; set; } + public string Warning { get; set; } + public bool Logout { get; set; } + + public string ButtonType { get; set; } + public string ButtonColor { get; set; } + public string ButtonSize { get; set; } + + public string ShippingMethod { get; set; } + public AddressModel BillingAddress { get; set; } // Confirmation flow. public bool IsConfirmed { get; set; } diff --git a/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs index cf304df151..45a1c1cd25 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs @@ -7,122 +7,122 @@ namespace SmartStore.AmazonPay.Models { - public class ConfigurationModel : ModelBase - { - public string[] ConfigGroups { get; set; } - public string PrimaryStoreCurrencyCode { get; set; } + public class ConfigurationModel : ModelBase + { + public string[] ConfigGroups { get; set; } + public string PrimaryStoreCurrencyCode { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.UseSandbox")] - public bool UseSandbox { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.UseSandbox")] + public bool UseSandbox { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SellerId")] - public string SellerId { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SellerId")] + public string SellerId { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AccessKey")] - public string AccessKey { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AccessKey")] + public string AccessKey { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SecretKey")] - [DataType(DataType.Password)] - public string SecretKey { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SecretKey")] + [DataType(DataType.Password)] + public string SecretKey { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ClientId")] - public string ClientId { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ClientId")] + public string ClientId { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.Marketplace")] - public string Marketplace { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.Marketplace")] + public string Marketplace { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.DataFetching")] - public AmazonPayDataFetchingType DataFetching { get; set; } - public List DataFetchings { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.DataFetching")] + public AmazonPayDataFetchingType DataFetching { get; set; } + public List DataFetchings { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.IpnUrl")] - public string IpnUrl { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.IpnUrl")] + public string IpnUrl { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PollingMaxOrderCreationDays")] - public int PollingMaxOrderCreationDays { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PollingMaxOrderCreationDays")] + public int PollingMaxOrderCreationDays { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.TransactionType")] - public AmazonPayTransactionType TransactionType { get; set; } - public List TransactionTypes { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.TransactionType")] + public AmazonPayTransactionType TransactionType { get; set; } + public List TransactionTypes { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthorizeMethod")] - public AmazonPayAuthorizeMethod AuthorizeMethod { get; set; } - public SelectList AuthorizeMethods { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthorizeMethod")] + public AmazonPayAuthorizeMethod AuthorizeMethod { get; set; } + public SelectList AuthorizeMethods { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SaveEmailAndPhone")] - public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } - public List SaveEmailAndPhones { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SaveEmailAndPhone")] + public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } + public List SaveEmailAndPhones { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ShowPayButtonForAdminOnly")] - public bool ShowPayButtonForAdminOnly { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ShowPayButtonForAdminOnly")] + public bool ShowPayButtonForAdminOnly { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ShowButtonInMiniShoppingCart")] - public bool ShowButtonInMiniShoppingCart { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ShowButtonInMiniShoppingCart")] + public bool ShowButtonInMiniShoppingCart { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] - public decimal AdditionalFee { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] + public decimal AdditionalFee { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] - public bool AdditionalFeePercentage { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] + public bool AdditionalFeePercentage { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AddOrderNotes")] - public bool AddOrderNotes { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AddOrderNotes")] + public bool AddOrderNotes { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.InformCustomerAboutErrors")] - public bool InformCustomerAboutErrors { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.InformCustomerAboutErrors")] + public bool InformCustomerAboutErrors { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.InformCustomerAddErrors")] - public bool InformCustomerAddErrors { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.InformCustomerAddErrors")] + public bool InformCustomerAddErrors { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PayButtonColor")] - public string PayButtonColor { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PayButtonColor")] + public string PayButtonColor { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PayButtonSize")] - public string PayButtonSize { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PayButtonSize")] + public string PayButtonSize { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonType")] - public string AuthButtonType { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonType")] + public string AuthButtonType { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonColor")] - public string AuthButtonColor { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonColor")] + public string AuthButtonColor { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonSize")] - public string AuthButtonSize { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonSize")] + public string AuthButtonSize { get; set; } - #region Registration data + #region Registration data - public string RegisterUrl { get; set; } - public string SoftwareVersion { get; set; } - public string PluginVersion { get; set; } - public string LeadCode { get; set; } - public string PlatformId { get; set; } - public string PublicKey { get; set; } - public string KeyShareUrl { get; set; } - public string LanguageLocale { get; set; } + public string RegisterUrl { get; set; } + public string SoftwareVersion { get; set; } + public string PluginVersion { get; set; } + public string LeadCode { get; set; } + public string PlatformId { get; set; } + public string PublicKey { get; set; } + public string KeyShareUrl { get; set; } + public string LanguageLocale { get; set; } - /// - /// Including all domains and sub domains where the login button appears. SSL mandatory. - /// - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginDomains")] - public HashSet MerchantLoginDomains { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginDomains")] - public HashSet CurrentMerchantLoginDomains { get; set; } + /// + /// Including all domains and sub domains where the login button appears. SSL mandatory. + /// + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginDomains")] + public HashSet MerchantLoginDomains { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginDomains")] + public HashSet CurrentMerchantLoginDomains { get; set; } - /// - /// Used to populate Allowed Return URLs on the Login with Amazon application. SSL mandatory. Max 512 characters. - /// - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginRedirectUrls")] - public HashSet MerchantLoginRedirectUrls { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginRedirectUrls")] - public HashSet CurrentMerchantLoginRedirectUrls { get; set; } + /// + /// Used to populate Allowed Return URLs on the Login with Amazon application. SSL mandatory. Max 512 characters. + /// + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginRedirectUrls")] + public HashSet MerchantLoginRedirectUrls { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginRedirectUrls")] + public HashSet CurrentMerchantLoginRedirectUrls { get; set; } - public string MerchantStoreDescription { get; set; } - public string MerchantPrivacyNoticeUrl { get; set; } - public string MerchantCountry { get; set; } - public string MerchantSandboxIpnUrl { get; set; } - public string MerchantProductionIpnUrl { get; set; } + public string MerchantStoreDescription { get; set; } + public string MerchantPrivacyNoticeUrl { get; set; } + public string MerchantCountry { get; set; } + public string MerchantSandboxIpnUrl { get; set; } + public string MerchantProductionIpnUrl { get; set; } - #endregion - } + #endregion + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Plugin.cs b/src/Plugins/SmartStore.AmazonPay/Plugin.cs index ff643fed61..8b6904f964 100644 --- a/src/Plugins/SmartStore.AmazonPay/Plugin.cs +++ b/src/Plugins/SmartStore.AmazonPay/Plugin.cs @@ -3,11 +3,13 @@ using System.Web.Routing; using SmartStore.AmazonPay.Controllers; using SmartStore.AmazonPay.Services; +using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services; using SmartStore.Services.Authentication.External; +using SmartStore.Services.Cms; using SmartStore.Services.Customers; using SmartStore.Services.Orders; using SmartStore.Services.Payments; @@ -15,189 +17,181 @@ namespace SmartStore.AmazonPay { - [DependentWidgets("Widgets.AmazonPay")] - [FriendlyName("Amazon Pay")] - [DisplayOrder(-1)] - public class AmazonPayPlugin : PaymentPluginBase, IExternalAuthenticationMethod, IConfigurable, ICookiePublisher - { - private readonly IAmazonPayService _apiService; - private readonly ICommonServices _services; - private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private readonly IScheduleTaskService _scheduleTaskService; - - public AmazonPayPlugin( - IAmazonPayService apiService, - ICommonServices services, - IOrderTotalCalculationService orderTotalCalculationService, - IScheduleTaskService scheduleTaskService) - { - _apiService = apiService; - _services = services; - _orderTotalCalculationService = orderTotalCalculationService; - _scheduleTaskService = scheduleTaskService; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - public static string SystemName - { - get { return "SmartStore.AmazonPay"; } - } - - /// - /// Gets CookieInfos for display in CookieManager dialog. - /// - /// CookieInfo containing plugin name, cookie purpose description & cookie type - public CookieInfo GetCookieInfo() - { - var cookieInfo = new CookieInfo - { - Name = _services.Localization.GetResource("Plugins.FriendlyName.Widgets.AmazonPay"), - Description = _services.Localization.GetResource("Plugins.Payments.AmazonPay.CookieInfo"), - CookieType = CookieType.Required - }; - - return cookieInfo; - } - - public override void Install() - { - _services.Settings.SaveSetting(new AmazonPaySettings()); - _services.Localization.ImportPluginResourcesFromXml(PluginDescriptor); - - // Polling task every 30 minutes. - _scheduleTaskService.GetOrAddTask(x => - { - x.Name = _services.Localization.GetResource("Plugins.Payments.AmazonPay.TaskName"); - x.CronExpression = "*/30 * * * *"; - }); - - base.Install(); - } - - public override void Uninstall() - { - _scheduleTaskService.TryDeleteTask(); - _services.Settings.DeleteSetting(); - _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); - - base.Uninstall(); - } - - public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = _apiService.ProcessPayment(processPaymentRequest); - return result; - } - - public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) - { - _apiService.PostProcessPayment(postProcessPaymentRequest); - } - - public override decimal GetAdditionalHandlingFee(IList cart) - { - var result = decimal.Zero; - - try - { - var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - - result = this.CalculateAdditionalFee(_orderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); - } - catch (Exception exception) - { - Logger.Error(exception); - } - - return result; - } - - public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) - { - var result = _apiService.Capture(capturePaymentRequest); - return result; - } - - public override RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequest) - { - var result = _apiService.Refund(refundPaymentRequest); - return result; - } - - public override VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) - { - var result = _apiService.Void(voidPaymentRequest); - return result; - } - - - public override void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) - { - actionName = "Configure"; - controllerName = "AmazonPay"; - routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; - } - - public void GetPublicInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) - { - actionName = "AuthenticationPublicInfo"; - controllerName = "AmazonPay"; - routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; - } - - public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) - { - try - { - var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - if (settings.ShowPayButtonForAdminOnly && !_services.WorkContext.CurrentCustomer.IsAdmin()) - { - actionName = controllerName = null; - routeValues = null; - return; - } - } - catch (Exception exception) - { - Logger.Error(exception); - } - - actionName = "ShoppingCart"; - controllerName = "AmazonPayShoppingCart"; - routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; - } - - public override Type GetControllerType() - { - return typeof(AmazonPayController); - } - - public override bool SupportCapture - { - get { return true; } - } - - public override bool SupportPartiallyRefund - { - get { return true; } - } - - public override bool SupportRefund - { - get { return true; } - } - - public override bool SupportVoid - { - get { return true; } - } - - public override PaymentMethodType PaymentMethodType - { - get { return PaymentMethodType.StandardAndButton; } - } - } + [DependentWidgets("Widgets.AmazonPay")] + [FriendlyName("Amazon Pay")] + [DisplayOrder(-1)] + public class AmazonPayPlugin : PaymentPluginBase, IExternalAuthenticationMethod, IConfigurable, ICookiePublisher + { + private readonly IAmazonPayService _apiService; + private readonly ICommonServices _services; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly IScheduleTaskService _scheduleTaskService; + private readonly IWidgetService _widgetService; + private readonly WidgetSettings _widgetSettings; + + public AmazonPayPlugin( + IAmazonPayService apiService, + ICommonServices services, + IOrderTotalCalculationService orderTotalCalculationService, + IScheduleTaskService scheduleTaskService, + IWidgetService widgetService, + WidgetSettings widgetSettings) + { + _apiService = apiService; + _services = services; + _orderTotalCalculationService = orderTotalCalculationService; + _scheduleTaskService = scheduleTaskService; + _widgetService = widgetService; + _widgetSettings = widgetSettings; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public static string SystemName => "SmartStore.AmazonPay"; + + /// + /// Gets CookieInfos for display in CookieManager dialog. + /// + /// CookieInfo containing plugin name, cookie purpose description & cookie type + public IEnumerable GetCookieInfo() + { + var widget = _widgetService.LoadWidgetBySystemName("Widgets.AmazonPay"); + if (!widget.IsWidgetActive(_widgetSettings)) + return null; + + var cookieInfo = new CookieInfo + { + Name = _services.Localization.GetResource("Plugins.FriendlyName.Widgets.AmazonPay"), + Description = _services.Localization.GetResource("Plugins.Payments.AmazonPay.CookieInfo"), + CookieType = CookieType.Required + }; + + return new List { cookieInfo }; + } + + public override void Install() + { + _services.Settings.SaveSetting(new AmazonPaySettings()); + _services.Localization.ImportPluginResourcesFromXml(PluginDescriptor); + + // Polling task every 30 minutes. + _scheduleTaskService.GetOrAddTask(x => + { + x.Name = _services.Localization.GetResource("Plugins.Payments.AmazonPay.TaskName"); + x.CronExpression = "*/30 * * * *"; + }); + + base.Install(); + } + + public override void Uninstall() + { + _scheduleTaskService.TryDeleteTask(); + _services.Settings.DeleteSetting(); + _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); + + base.Uninstall(); + } + + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = _apiService.ProcessPayment(processPaymentRequest); + return result; + } + + public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) + { + _apiService.PostProcessPayment(postProcessPaymentRequest); + } + + public override decimal GetAdditionalHandlingFee(IList cart) + { + var result = decimal.Zero; + + try + { + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + + result = this.CalculateAdditionalFee(_orderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); + } + catch (Exception exception) + { + Logger.Error(exception); + } + + return result; + } + + public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) + { + var result = _apiService.Capture(capturePaymentRequest); + return result; + } + + public override RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequest) + { + var result = _apiService.Refund(refundPaymentRequest); + return result; + } + + public override VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) + { + var result = _apiService.Void(voidPaymentRequest); + return result; + } + + + public override void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "Configure"; + controllerName = "AmazonPay"; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; + } + + public void GetPublicInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "AuthenticationPublicInfo"; + controllerName = "AmazonPay"; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; + } + + public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + try + { + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + if (settings.ShowPayButtonForAdminOnly && !_services.WorkContext.CurrentCustomer.IsAdmin()) + { + actionName = controllerName = null; + routeValues = null; + return; + } + } + catch (Exception exception) + { + Logger.Error(exception); + } + + actionName = "ShoppingCart"; + controllerName = "AmazonPayShoppingCart"; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; + } + + public override Type GetControllerType() + { + return typeof(AmazonPayController); + } + + public override bool SupportCapture => true; + + public override bool SupportPartiallyRefund => true; + + public override bool SupportRefund => true; + + public override bool SupportVoid => true; + + public override PaymentMethodType PaymentMethodType => PaymentMethodType.StandardAndButton; + } } diff --git a/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs b/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs index d7d33434cb..3cfe49b1b8 100644 --- a/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs +++ b/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs @@ -1,30 +1,29 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.AmazonPay.Services; using SmartStore.Web.Framework.Routing; namespace SmartStore.AmazonPay { - public class RouteProvider : IRouteProvider - { - public void RegisterRoutes(RouteCollection routes) - { - routes.MapRoute("SmartStore.AmazonPay", - "Plugins/SmartStore.AmazonPay/{controller}/{action}", - new { controller = "AmazonPay" }, - new[] { "SmartStore.AmazonPay.Controllers" } - ) - .DataTokens["area"] = AmazonPayPlugin.SystemName; + public class RouteProvider : IRouteProvider + { + public void RegisterRoutes(RouteCollection routes) + { + routes.MapRoute("SmartStore.AmazonPay", + "Plugins/SmartStore.AmazonPay/{controller}/{action}", + new { controller = "AmazonPay" }, + new[] { "SmartStore.AmazonPay.Controllers" } + ) + .DataTokens["area"] = AmazonPayPlugin.SystemName; - // for backward compatibility (IPN!) - routes.MapRoute("SmartStore.AmazonPay.Legacy", - "Plugins/PaymentsAmazonPay/{action}", - new { controller = "AmazonPay" }, - new[] { "SmartStore.AmazonPay.Controllers" } - ) - .DataTokens["area"] = AmazonPayPlugin.SystemName; - } + // for backward compatibility (IPN!) + routes.MapRoute("SmartStore.AmazonPay.Legacy", + "Plugins/PaymentsAmazonPay/{action}", + new { controller = "AmazonPay" }, + new[] { "SmartStore.AmazonPay.Controllers" } + ) + .DataTokens["area"] = AmazonPayPlugin.SystemName; + } - public int Priority { get { return 0; } } - } + public int Priority => 0; + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs index d64719b0e7..49e0e261bb 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs @@ -4,29 +4,23 @@ namespace SmartStore.AmazonPay.Services { - [Serializable] - public class AmazonAuthenticationParameters : OpenAuthenticationParameters - { - private IList _claims; + [Serializable] + public class AmazonAuthenticationParameters : OpenAuthenticationParameters + { + private IList _claims; - public override string ProviderSystemName - { - get { return AmazonPayPlugin.SystemName; } - } + public override string ProviderSystemName => AmazonPayPlugin.SystemName; - public override IList UserClaims - { - get { return _claims; } - } + public override IList UserClaims => _claims; - public void AddClaim(UserClaims claim) - { - if (_claims == null) - { - _claims = new List(); - } + public void AddClaim(UserClaims claim) + { + if (_claims == null) + { + _claims = new List(); + } - _claims.Add(claim); - } - } + _claims.Add(claim); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs index 4e9144bc9a..bab0824eee 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs @@ -1,78 +1,78 @@ namespace SmartStore.AmazonPay.Services { - public enum AmazonPayRequestType - { - None = 0, - ShoppingCart, - Address, - PaymentMethod, - OrderReviewData, - ShippingMethod, - MiniShoppingCart, + public enum AmazonPayRequestType + { + None = 0, + ShoppingCart, + Address, + PaymentMethod, + OrderReviewData, + ShippingMethod, + MiniShoppingCart, - /// - /// Amazon Pay button clicked - /// - PayButtonHandler, + /// + /// Amazon Pay button clicked + /// + PayButtonHandler, - /// - /// Display authentication button on login page - /// - AuthenticationPublicInfo - } + /// + /// Display authentication button on login page + /// + AuthenticationPublicInfo + } - public enum AmazonPayTransactionType - { - None = 0, - Authorize, - AuthorizeAndCapture - } + public enum AmazonPayTransactionType + { + None = 0, + Authorize, + AuthorizeAndCapture + } - public enum AmazonPayAuthorizeMethod - { - Omnichronous = 0, - Asynchronous, - Synchronous - } + public enum AmazonPayAuthorizeMethod + { + Omnichronous = 0, + Asynchronous, + Synchronous + } - public enum AmazonPaySaveDataType - { - None = 0, - OnlyIfEmpty, - Always - } + public enum AmazonPaySaveDataType + { + None = 0, + OnlyIfEmpty, + Always + } - public enum AmazonPayDataFetchingType - { - None = 0, - Ipn, - Polling - } + public enum AmazonPayDataFetchingType + { + None = 0, + Ipn, + Polling + } - public enum AmazonPayResultType - { - None = 0, - PluginView, - Redirect, - Unauthorized - } + public enum AmazonPayResultType + { + None = 0, + PluginView, + Redirect, + Unauthorized + } - public enum AmazonPayMessage - { - MessageTyp = 0, - MessageId, - AuthorizationID, - CaptureID, - RefundID, - ReferenceID, - State, - StateUpdate, - Fee, - AuthorizedAmount, - CapturedAmount, - RefundedAmount, - CaptureNow, - Creation, - Expiration - } + public enum AmazonPayMessage + { + MessageTyp = 0, + MessageId, + AuthorizationID, + CaptureID, + RefundID, + ReferenceID, + State, + StateUpdate, + Fee, + AuthorizedAmount, + CapturedAmount, + RefundedAmount, + CaptureNow, + Creation, + Expiration + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs index 34826f65fd..88d41c6aad 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs @@ -21,7 +21,6 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Stores; using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; @@ -40,348 +39,348 @@ namespace SmartStore.AmazonPay.Services { public partial class AmazonPayService : IAmazonPayService - { - private readonly HttpContextBase _httpContext; - private readonly IRepository _orderRepository; - private readonly ICommonServices _services; - private readonly IPaymentService _paymentService; - private readonly IGenericAttributeService _genericAttributeService; - private readonly ICurrencyService _currencyService; - private readonly ICustomerService _customerService; - private readonly ICountryService _countryService; - private readonly IStateProvinceService _stateProvinceService; - private readonly IOrderService _orderService; - private readonly IOrderProcessingService _orderProcessingService; + { + private readonly HttpContextBase _httpContext; + private readonly IRepository _orderRepository; + private readonly ICommonServices _services; + private readonly IPaymentService _paymentService; + private readonly IGenericAttributeService _genericAttributeService; + private readonly ICurrencyService _currencyService; + private readonly ICustomerService _customerService; + private readonly ICountryService _countryService; + private readonly IStateProvinceService _stateProvinceService; + private readonly IOrderService _orderService; + private readonly IOrderProcessingService _orderProcessingService; private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private readonly IDateTimeHelper _dateTimeHelper; - private readonly IPluginFinder _pluginFinder; - private readonly Lazy _authorizer; - private readonly AddressSettings _addressSettings; - private readonly OrderSettings _orderSettings; - private readonly CompanyInformationSettings _companyInformationSettings; - - public AmazonPayService( - HttpContextBase httpContext, - IRepository orderRepository, - ICommonServices services, - IPaymentService paymentService, - IGenericAttributeService genericAttributeService, - ICurrencyService currencyService, - ICustomerService customerService, - ICountryService countryService, - IStateProvinceService stateProvinceService, - IOrderService orderService, - IOrderProcessingService orderProcessingService, + private readonly IDateTimeHelper _dateTimeHelper; + private readonly IPluginFinder _pluginFinder; + private readonly Lazy _authorizer; + private readonly AddressSettings _addressSettings; + private readonly OrderSettings _orderSettings; + private readonly CompanyInformationSettings _companyInformationSettings; + + public AmazonPayService( + HttpContextBase httpContext, + IRepository orderRepository, + ICommonServices services, + IPaymentService paymentService, + IGenericAttributeService genericAttributeService, + ICurrencyService currencyService, + ICustomerService customerService, + ICountryService countryService, + IStateProvinceService stateProvinceService, + IOrderService orderService, + IOrderProcessingService orderProcessingService, IOrderTotalCalculationService orderTotalCalculationService, - IDateTimeHelper dateTimeHelper, - IPluginFinder pluginFinder, - Lazy authorizer, - AddressSettings addressSettings, - OrderSettings orderSettings, - CompanyInformationSettings companyInformationSettings) - { - _httpContext = httpContext; - _orderRepository = orderRepository; - _services = services; - _paymentService = paymentService; - _genericAttributeService = genericAttributeService; - _currencyService = currencyService; - _customerService = customerService; - _countryService = countryService; - _stateProvinceService = stateProvinceService; - _orderService = orderService; - _orderProcessingService = orderProcessingService; + IDateTimeHelper dateTimeHelper, + IPluginFinder pluginFinder, + Lazy authorizer, + AddressSettings addressSettings, + OrderSettings orderSettings, + CompanyInformationSettings companyInformationSettings) + { + _httpContext = httpContext; + _orderRepository = orderRepository; + _services = services; + _paymentService = paymentService; + _genericAttributeService = genericAttributeService; + _currencyService = currencyService; + _customerService = customerService; + _countryService = countryService; + _stateProvinceService = stateProvinceService; + _orderService = orderService; + _orderProcessingService = orderProcessingService; _orderTotalCalculationService = orderTotalCalculationService; - _dateTimeHelper = dateTimeHelper; - _pluginFinder = pluginFinder; - _authorizer = authorizer; - _addressSettings = addressSettings; - _orderSettings = orderSettings; - _companyInformationSettings = companyInformationSettings; - - T = NullLocalizer.Instance; - Logger = NullLogger.Instance; - } - - public Localizer T { get; set; } - public ILogger Logger { get; set; } - - public void SetupConfiguration(ConfigurationModel model) - { - var store = _services.StoreContext.CurrentStore; - var language = _services.WorkContext.WorkingLanguage; - var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(AmazonPayPlugin.SystemName); - var allStores = _services.StoreService.GetAllStores(); - var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); - - model.IpnUrl = GetPluginUrl("IPNHandler", true); - model.ConfigGroups = T("Plugins.Payments.AmazonPay.ConfigGroups").Text.SplitSafe(";"); - model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; - - model.RegisterUrl = "https://payments-eu.amazon.com/register"; - model.SoftwareVersion = SmartStoreVersion.CurrentFullVersion; - if (descriptor != null) - { - model.PluginVersion = descriptor.Version.ToString(); - } - model.LeadCode = LeadCode; - model.PlatformId = PlatformId; - // Not implemented. Not available for europe at the moment. - model.PublicKey = string.Empty; - model.KeyShareUrl = GetPluginUrl("ShareKey", store.SslEnabled); - model.LanguageLocale = language.UniqueSeoCode.ToAmazonLanguageCode('_'); - model.MerchantStoreDescription = store.Name.Truncate(2048); - model.MerchantPrivacyNoticeUrl = WebHelper.GetAbsoluteUrl(urlHelper.Topic("privacyinfo"), _httpContext.Request, protocol: store.SslEnabled ? "https" : "http"); - model.MerchantSandboxIpnUrl = model.IpnUrl; - model.MerchantProductionIpnUrl = model.IpnUrl; - - model.MerchantLoginDomains = new HashSet(StringComparer.OrdinalIgnoreCase); - model.MerchantLoginRedirectUrls = new HashSet(StringComparer.OrdinalIgnoreCase); - model.CurrentMerchantLoginDomains = new HashSet(StringComparer.OrdinalIgnoreCase); - model.CurrentMerchantLoginRedirectUrls = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var entity in allStores) - { - // SSL required! - var shopUrl = entity.SslEnabled ? entity.SecureUrl : entity.Url; - if (shopUrl.HasValue()) - { - try - { - // Only protocol and domain name. - var uri = new Uri(shopUrl); - var loginDomain = uri.GetLeftPart(UriPartial.Scheme | UriPartial.Authority).EmptyNull().TrimEnd('/'); - model.MerchantLoginDomains.Add(loginDomain); - - if (entity.Id == store.Id) - { - model.CurrentMerchantLoginDomains.Add(loginDomain); - } - } - catch { } - - var payHandlerUrl = shopUrl.EnsureEndsWith("/") + "Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/PayButtonHandler"; - var authHandlerUrl = shopUrl.EnsureEndsWith("/") + "Plugins/SmartStore.AmazonPay/AmazonPay/AuthenticationButtonHandler"; - - model.MerchantLoginRedirectUrls.Add(payHandlerUrl); - model.MerchantLoginRedirectUrls.Add(authHandlerUrl); - - if (entity.Id == store.Id) - { - model.CurrentMerchantLoginRedirectUrls.Add(payHandlerUrl); - model.CurrentMerchantLoginRedirectUrls.Add(authHandlerUrl); - } - } - } - - if (_companyInformationSettings.CountryId != 0) - { - var merchantCountry = _countryService.GetCountryById(_companyInformationSettings.CountryId); - if (merchantCountry != null) - { - model.MerchantCountry = merchantCountry.GetLocalized(x => x.Name, language, false, false); - } - } - - model.DataFetchings = new List - { - new SelectListItem - { - Text = T("Common.Unspecified").Text, - Value = "" - }, - new SelectListItem - { - Selected = (model.DataFetching == AmazonPayDataFetchingType.Ipn), - Text = T("Plugins.Payments.AmazonPay.DataFetching.Ipn"), - Value = ((int)AmazonPayDataFetchingType.Ipn).ToString() - }, - new SelectListItem - { - Selected = (model.DataFetching == AmazonPayDataFetchingType.Polling), - Text = T("Plugins.Payments.AmazonPay.DataFetching.Polling"), - Value = ((int)AmazonPayDataFetchingType.Polling).ToString() - } - }; - - model.TransactionTypes = new List - { - new SelectListItem - { - Selected = (model.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture), - Text = T("Plugins.Payments.AmazonPay.TransactionType.AuthAndCapture"), - Value = ((int)AmazonPayTransactionType.AuthorizeAndCapture).ToString() - }, - new SelectListItem - { - Selected = (model.TransactionType == AmazonPayTransactionType.Authorize), - Text = T("Plugins.Payments.AmazonPay.TransactionType.Auth"), - Value = ((int)AmazonPayTransactionType.Authorize).ToString() - } - }; - - model.AuthorizeMethods = model.AuthorizeMethod.ToSelectList(); - - model.SaveEmailAndPhones = new List - { - new SelectListItem - { - Text = T("Common.Unspecified").Text, - Value = "" - }, - new SelectListItem - { - Selected = (model.SaveEmailAndPhone == AmazonPaySaveDataType.OnlyIfEmpty), - Text = T("Plugins.Payments.AmazonPay.AmazonPaySaveDataType.OnlyIfEmpty"), - Value = ((int)AmazonPaySaveDataType.OnlyIfEmpty).ToString() - }, - new SelectListItem - { - Selected = (model.SaveEmailAndPhone == AmazonPaySaveDataType.Always), - Text = T("Plugins.Payments.AmazonPay.AmazonPaySaveDataType.Always"), - Value = ((int)AmazonPaySaveDataType.Always).ToString() - } - }; - } - - public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDictionary tempData) - { - var model = new AmazonPayViewModel(); - model.Type = type; - - try - { - var store = _services.StoreContext.CurrentStore; - var customer = _services.WorkContext.CurrentCustomer; - var language = _services.WorkContext.WorkingLanguage; - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); - var storeLocation = _services.WebHelper.GetStoreLocation(store.SslEnabled); - var settings = _services.Settings.LoadSetting(store.Id); - - model.ButtonHandlerUrl = $"{storeLocation}Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/PayButtonHandler"; - model.LanguageCode = language.UniqueSeoCode.ToAmazonLanguageCode(); - - if (type == AmazonPayRequestType.PayButtonHandler) - { - if (cart.Count <= 0 || !_paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, customer, cart, store.Id)) + _dateTimeHelper = dateTimeHelper; + _pluginFinder = pluginFinder; + _authorizer = authorizer; + _addressSettings = addressSettings; + _orderSettings = orderSettings; + _companyInformationSettings = companyInformationSettings; + + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; + } + + public Localizer T { get; set; } + public ILogger Logger { get; set; } + + public void SetupConfiguration(ConfigurationModel model) + { + var store = _services.StoreContext.CurrentStore; + var language = _services.WorkContext.WorkingLanguage; + var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(AmazonPayPlugin.SystemName); + var allStores = _services.StoreService.GetAllStores(); + var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); + + model.IpnUrl = GetPluginUrl("IPNHandler", true); + model.ConfigGroups = T("Plugins.Payments.AmazonPay.ConfigGroups").Text.SplitSafe(";"); + model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; + + model.RegisterUrl = "https://payments-eu.amazon.com/register"; + model.SoftwareVersion = SmartStoreVersion.CurrentFullVersion; + if (descriptor != null) + { + model.PluginVersion = descriptor.Version.ToString(); + } + model.LeadCode = LeadCode; + model.PlatformId = PlatformId; + // Not implemented. Not available for europe at the moment. + model.PublicKey = string.Empty; + model.KeyShareUrl = GetPluginUrl("ShareKey", store.SslEnabled); + model.LanguageLocale = language.UniqueSeoCode.ToAmazonLanguageCode('_'); + model.MerchantStoreDescription = store.Name.Truncate(2048); + model.MerchantPrivacyNoticeUrl = WebHelper.GetAbsoluteUrl(urlHelper.Topic("privacyinfo"), _httpContext.Request, protocol: store.SslEnabled ? "https" : "http"); + model.MerchantSandboxIpnUrl = model.IpnUrl; + model.MerchantProductionIpnUrl = model.IpnUrl; + + model.MerchantLoginDomains = new HashSet(StringComparer.OrdinalIgnoreCase); + model.MerchantLoginRedirectUrls = new HashSet(StringComparer.OrdinalIgnoreCase); + model.CurrentMerchantLoginDomains = new HashSet(StringComparer.OrdinalIgnoreCase); + model.CurrentMerchantLoginRedirectUrls = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var entity in allStores) + { + // SSL required! + var shopUrl = entity.SslEnabled ? entity.SecureUrl : entity.Url; + if (shopUrl.HasValue()) + { + try + { + // Only protocol and domain name. + var uri = new Uri(shopUrl); + var loginDomain = uri.GetLeftPart(UriPartial.Scheme | UriPartial.Authority).EmptyNull().TrimEnd('/'); + model.MerchantLoginDomains.Add(loginDomain); + + if (entity.Id == store.Id) + { + model.CurrentMerchantLoginDomains.Add(loginDomain); + } + } + catch { } + + var payHandlerUrl = shopUrl.EnsureEndsWith("/") + "Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/PayButtonHandler"; + var authHandlerUrl = shopUrl.EnsureEndsWith("/") + "Plugins/SmartStore.AmazonPay/AmazonPay/AuthenticationButtonHandler"; + + model.MerchantLoginRedirectUrls.Add(payHandlerUrl); + model.MerchantLoginRedirectUrls.Add(authHandlerUrl); + + if (entity.Id == store.Id) + { + model.CurrentMerchantLoginRedirectUrls.Add(payHandlerUrl); + model.CurrentMerchantLoginRedirectUrls.Add(authHandlerUrl); + } + } + } + + if (_companyInformationSettings.CountryId != 0) + { + var merchantCountry = _countryService.GetCountryById(_companyInformationSettings.CountryId); + if (merchantCountry != null) + { + model.MerchantCountry = merchantCountry.GetLocalized(x => x.Name, language, false, false); + } + } + + model.DataFetchings = new List + { + new SelectListItem + { + Text = T("Common.Unspecified").Text, + Value = "" + }, + new SelectListItem + { + Selected = (model.DataFetching == AmazonPayDataFetchingType.Ipn), + Text = T("Plugins.Payments.AmazonPay.DataFetching.Ipn"), + Value = ((int)AmazonPayDataFetchingType.Ipn).ToString() + }, + new SelectListItem + { + Selected = (model.DataFetching == AmazonPayDataFetchingType.Polling), + Text = T("Plugins.Payments.AmazonPay.DataFetching.Polling"), + Value = ((int)AmazonPayDataFetchingType.Polling).ToString() + } + }; + + model.TransactionTypes = new List + { + new SelectListItem + { + Selected = (model.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture), + Text = T("Plugins.Payments.AmazonPay.TransactionType.AuthAndCapture"), + Value = ((int)AmazonPayTransactionType.AuthorizeAndCapture).ToString() + }, + new SelectListItem + { + Selected = (model.TransactionType == AmazonPayTransactionType.Authorize), + Text = T("Plugins.Payments.AmazonPay.TransactionType.Auth"), + Value = ((int)AmazonPayTransactionType.Authorize).ToString() + } + }; + + model.AuthorizeMethods = model.AuthorizeMethod.ToSelectList(); + + model.SaveEmailAndPhones = new List + { + new SelectListItem + { + Text = T("Common.Unspecified").Text, + Value = "" + }, + new SelectListItem + { + Selected = (model.SaveEmailAndPhone == AmazonPaySaveDataType.OnlyIfEmpty), + Text = T("Plugins.Payments.AmazonPay.AmazonPaySaveDataType.OnlyIfEmpty"), + Value = ((int)AmazonPaySaveDataType.OnlyIfEmpty).ToString() + }, + new SelectListItem + { + Selected = (model.SaveEmailAndPhone == AmazonPaySaveDataType.Always), + Text = T("Plugins.Payments.AmazonPay.AmazonPaySaveDataType.Always"), + Value = ((int)AmazonPaySaveDataType.Always).ToString() + } + }; + } + + public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDictionary tempData) + { + var model = new AmazonPayViewModel(); + model.Type = type; + + try + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var language = _services.WorkContext.WorkingLanguage; + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + var storeLocation = _services.WebHelper.GetStoreLocation(store.SslEnabled); + var settings = _services.Settings.LoadSetting(store.Id); + + model.ButtonHandlerUrl = $"{storeLocation}Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/PayButtonHandler"; + model.LanguageCode = language.UniqueSeoCode.ToAmazonLanguageCode(); + + if (type == AmazonPayRequestType.PayButtonHandler) + { + if (cart.Count <= 0 || !_paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, customer, cart, store.Id)) { - model.Result = AmazonPayResultType.Redirect; - return model; - } + model.Result = AmazonPayResultType.Redirect; + return model; + } if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) - { - model.Result = AmazonPayResultType.Unauthorized; - return model; - } - - var accessToken = _httpContext.Request.QueryString["access_token"]; - if (accessToken.IsEmpty()) - { - var msg = T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"); - Logger.Error(null, msg); - _services.Notifier.Error(new LocalizedString(msg)); - - model.Result = AmazonPayResultType.Redirect; - return model; - } - - // Create session state object. - var checkoutState = _httpContext.GetCheckoutState(); - if (checkoutState == null) - { - Logger.Warn("Checkout state is null in AmazonPayService.ValidateAndInitiateCheckout!"); - model.Result = AmazonPayResultType.Redirect; - return model; - } - - checkoutState.CustomProperties[AmazonPayPlugin.SystemName + ".CheckoutState"] = new AmazonPayCheckoutState { AccessToken = accessToken }; - - model.RedirectAction = "Index"; - model.RedirectController = "Checkout"; - model.Result = AmazonPayResultType.Redirect; - return model; - } - else if (type == AmazonPayRequestType.AuthenticationPublicInfo) - { - if (settings.SellerId.IsEmpty() || settings.AccessKey.IsEmpty() || settings.SecretKey.IsEmpty()) - { - return null; - } - - model.ButtonHandlerUrl = $"{storeLocation}Plugins/SmartStore.AmazonPay/AmazonPay/AuthenticationButtonHandler"; - - // Do not append returnUrl to button handler URL. Handler URLs must be whitelisted in Amazon Seller Central. - _httpContext.Session["AmazonAuthReturnUrl"] = _httpContext.Request.QueryString["returnUrl"]; - } - else if (type == AmazonPayRequestType.ShoppingCart || type == AmazonPayRequestType.MiniShoppingCart) - { - if (cart.Count <= 0 || !_paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, customer, cart, store.Id)) - { - model.Result = AmazonPayResultType.None; - return model; - } + { + model.Result = AmazonPayResultType.Unauthorized; + return model; + } + + var accessToken = _httpContext.Request.QueryString["access_token"]; + if (accessToken.IsEmpty()) + { + var msg = T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"); + Logger.Error(null, msg); + _services.Notifier.Error(new LocalizedString(msg)); + + model.Result = AmazonPayResultType.Redirect; + return model; + } + + // Create session state object. + var checkoutState = _httpContext.GetCheckoutState(); + if (checkoutState == null) + { + Logger.Warn("Checkout state is null in AmazonPayService.ValidateAndInitiateCheckout!"); + model.Result = AmazonPayResultType.Redirect; + return model; + } + + checkoutState.CustomProperties[AmazonPayPlugin.SystemName + ".CheckoutState"] = new AmazonPayCheckoutState { AccessToken = accessToken }; + + model.RedirectAction = "Index"; + model.RedirectController = "Checkout"; + model.Result = AmazonPayResultType.Redirect; + return model; + } + else if (type == AmazonPayRequestType.AuthenticationPublicInfo) + { + if (settings.SellerId.IsEmpty() || settings.AccessKey.IsEmpty() || settings.SecretKey.IsEmpty()) + { + return null; + } + + model.ButtonHandlerUrl = $"{storeLocation}Plugins/SmartStore.AmazonPay/AmazonPay/AuthenticationButtonHandler"; + + // Do not append returnUrl to button handler URL. Handler URLs must be whitelisted in Amazon Seller Central. + _httpContext.Session["AmazonAuthReturnUrl"] = _httpContext.Request.QueryString["returnUrl"]; + } + else if (type == AmazonPayRequestType.ShoppingCart || type == AmazonPayRequestType.MiniShoppingCart) + { + if (cart.Count <= 0 || !_paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, customer, cart, store.Id)) + { + model.Result = AmazonPayResultType.None; + return model; + } } - else - { - if (cart.Count <= 0) - { - model.Result = AmazonPayResultType.Redirect; - return model; - } - - if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) - { - model.Result = AmazonPayResultType.Unauthorized; - return model; - } - - var state = _httpContext.GetAmazonPayState(_services.Localization); - model.OrderReferenceId = state.OrderReferenceId; - model.AddressConsentToken = state.AccessToken; + else + { + if (cart.Count <= 0) + { + model.Result = AmazonPayResultType.Redirect; + return model; + } + + if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) + { + model.Result = AmazonPayResultType.Unauthorized; + return model; + } + + var state = _httpContext.GetAmazonPayState(_services.Localization); + model.OrderReferenceId = state.OrderReferenceId; + model.AddressConsentToken = state.AccessToken; model.IsConfirmed = state.IsConfirmed; model.FormData = state.FormData; model.SubmitForm = state.SubmitForm; state.SubmitForm = false; - if (type == AmazonPayRequestType.ShippingMethod || type == AmazonPayRequestType.PaymentMethod) - { - if (state.OrderReferenceId.IsEmpty() || state.AccessToken.IsEmpty()) - { - model.Result = AmazonPayResultType.Redirect; - return model; - } - } - } - - model.SellerId = settings.SellerId; - model.ClientId = settings.ClientId; - model.IsShippable = cart.RequiresShipping(); - model.IsRecurring = cart.IsRecurring(); - model.WidgetUrl = GetWidgetUrl(settings); - - if (type == AmazonPayRequestType.MiniShoppingCart || type == AmazonPayRequestType.ShoppingCart) - { - if (type == AmazonPayRequestType.MiniShoppingCart && !settings.ShowButtonInMiniShoppingCart) - { - model.Result = AmazonPayResultType.None; - return model; - } - if (settings.ShowPayButtonForAdminOnly && !customer.IsAdmin()) - { - model.Result = AmazonPayResultType.None; - return model; - } - - // AmazonPay review: The setting for payment button type has been removed. - model.ButtonType = "PwA"; - model.ButtonColor = settings.PayButtonColor; - model.ButtonSize = settings.PayButtonSize; - - var failureReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + if (type == AmazonPayRequestType.ShippingMethod || type == AmazonPayRequestType.PaymentMethod) + { + if (state.OrderReferenceId.IsEmpty() || state.AccessToken.IsEmpty()) + { + model.Result = AmazonPayResultType.Redirect; + return model; + } + } + } + + model.SellerId = settings.SellerId; + model.ClientId = settings.ClientId; + model.IsShippable = cart.RequiresShipping(); + model.IsRecurring = cart.IsRecurring(); + model.WidgetUrl = GetWidgetUrl(settings); + + if (type == AmazonPayRequestType.MiniShoppingCart || type == AmazonPayRequestType.ShoppingCart) + { + if (type == AmazonPayRequestType.MiniShoppingCart && !settings.ShowButtonInMiniShoppingCart) + { + model.Result = AmazonPayResultType.None; + return model; + } + if (settings.ShowPayButtonForAdminOnly && !customer.IsAdmin()) + { + model.Result = AmazonPayResultType.None; + return model; + } + + // AmazonPay review: The setting for payment button type has been removed. + model.ButtonType = "PwA"; + model.ButtonColor = settings.PayButtonColor; + model.ButtonSize = settings.PayButtonSize; + + var failureReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; if (failureReason.IsCaseInsensitiveEqual("AmazonRejected")) { model.Logout = true; @@ -393,1334 +392,1334 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthenticationStatusFailureMessage"))); } - _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); - } - else if (type == AmazonPayRequestType.AuthenticationPublicInfo) - { - model.ButtonType = settings.AuthButtonType; - model.ButtonColor = settings.AuthButtonColor; - model.ButtonSize = settings.AuthButtonSize; - } - else if (type == AmazonPayRequestType.Address) - { - if (!model.IsShippable) - { - model.RedirectAction = "ShippingMethod"; - model.RedirectController = "Checkout"; - model.Result = AmazonPayResultType.Redirect; - return model; - } - - var shippingToCountryNotAllowed = tempData[AmazonPayPlugin.SystemName + "ShippingToCountryNotAllowed"]; - - if (shippingToCountryNotAllowed != null && true == (bool)shippingToCountryNotAllowed) - { - model.Warning = T("Plugins.Payments.AmazonPay.ShippingToCountryNotAllowed"); - } - } - else if (type == AmazonPayRequestType.ShippingMethod) - { - model.RedirectAction = model.RedirectController = ""; - - if (model.IsShippable) - { - var client = CreateClient(settings); - var getOrderRequest = new GetOrderReferenceDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(model.OrderReferenceId) - .WithAccessToken(model.AddressConsentToken); - - var getOrderResponse = client.GetOrderReferenceDetails(getOrderRequest); - if (getOrderResponse.GetSuccess()) - { - // Billing address not available here. getOrderResponse.GetBillingAddressDetails() is null. - //if (FindAndApplyAddress(getOrderResponse, customer, model.IsShippable, true)) - var countryAllowsShipping = true; - var countryAllowsBilling = true; - - var address = CreateAddress( - getOrderResponse.GetEmail(), - getOrderResponse.GetBuyerShippingName(), - getOrderResponse.GetAddressLine1(), - getOrderResponse.GetAddressLine2(), - getOrderResponse.GetAddressLine3(), - getOrderResponse.GetCity(), - getOrderResponse.GetPostalCode(), - getOrderResponse.GetPhone(), - getOrderResponse.GetCountryCode(), - getOrderResponse.GetStateOrRegion(), - getOrderResponse.GetCounty(), - getOrderResponse.GetDistrict(), - out countryAllowsShipping, - out countryAllowsBilling); - - if (model.IsShippable && !countryAllowsShipping) - { - tempData[AmazonPayPlugin.SystemName + "ShippingToCountryNotAllowed"] = true; - model.RedirectAction = "ShippingAddress"; - model.RedirectController = "Checkout"; - model.Result = AmazonPayResultType.Redirect; - return model; - } - - if (address.Email.IsEmpty()) - { - address.Email = customer.Email; - } - - var existingAddress = customer.Addresses.ToList().FindAddress(address, true); - if (existingAddress == null) - { - customer.Addresses.Add(address); - customer.ShippingAddress = model.IsShippable ? address : null; - } - else - { - customer.ShippingAddress = model.IsShippable ? existingAddress : null; - } - - _customerService.UpdateCustomer(customer); - model.Result = AmazonPayResultType.None; - return model; - } - else - { - LogError(getOrderRequest, getOrderResponse); - } - } - } - else if (type == AmazonPayRequestType.PaymentMethod) - { + _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); + } + else if (type == AmazonPayRequestType.AuthenticationPublicInfo) + { + model.ButtonType = settings.AuthButtonType; + model.ButtonColor = settings.AuthButtonColor; + model.ButtonSize = settings.AuthButtonSize; + } + else if (type == AmazonPayRequestType.Address) + { + if (!model.IsShippable) + { + model.RedirectAction = "ShippingMethod"; + model.RedirectController = "Checkout"; + model.Result = AmazonPayResultType.Redirect; + return model; + } + + var shippingToCountryNotAllowed = tempData[AmazonPayPlugin.SystemName + "ShippingToCountryNotAllowed"]; + + if (shippingToCountryNotAllowed != null && true == (bool)shippingToCountryNotAllowed) + { + model.Warning = T("Plugins.Payments.AmazonPay.ShippingToCountryNotAllowed"); + } + } + else if (type == AmazonPayRequestType.ShippingMethod) + { + model.RedirectAction = model.RedirectController = ""; + + if (model.IsShippable) + { + var client = CreateClient(settings); + var getOrderRequest = new GetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(model.OrderReferenceId) + .WithAccessToken(model.AddressConsentToken); + + var getOrderResponse = client.GetOrderReferenceDetails(getOrderRequest); + if (getOrderResponse.GetSuccess()) + { + // Billing address not available here. getOrderResponse.GetBillingAddressDetails() is null. + //if (FindAndApplyAddress(getOrderResponse, customer, model.IsShippable, true)) + var countryAllowsShipping = true; + var countryAllowsBilling = true; + + var address = CreateAddress( + getOrderResponse.GetEmail(), + getOrderResponse.GetBuyerShippingName(), + getOrderResponse.GetAddressLine1(), + getOrderResponse.GetAddressLine2(), + getOrderResponse.GetAddressLine3(), + getOrderResponse.GetCity(), + getOrderResponse.GetPostalCode(), + getOrderResponse.GetPhone(), + getOrderResponse.GetCountryCode(), + getOrderResponse.GetStateOrRegion(), + getOrderResponse.GetCounty(), + getOrderResponse.GetDistrict(), + out countryAllowsShipping, + out countryAllowsBilling); + + if (model.IsShippable && !countryAllowsShipping) + { + tempData[AmazonPayPlugin.SystemName + "ShippingToCountryNotAllowed"] = true; + model.RedirectAction = "ShippingAddress"; + model.RedirectController = "Checkout"; + model.Result = AmazonPayResultType.Redirect; + return model; + } + + if (address.Email.IsEmpty()) + { + address.Email = customer.Email; + } + + var existingAddress = customer.Addresses.ToList().FindAddress(address, true); + if (existingAddress == null) + { + customer.Addresses.Add(address); + customer.ShippingAddress = model.IsShippable ? address : null; + } + else + { + customer.ShippingAddress = model.IsShippable ? existingAddress : null; + } + + _customerService.UpdateCustomer(customer); + model.Result = AmazonPayResultType.None; + return model; + } + else + { + LogError(getOrderRequest, getOrderResponse); + } + } + } + else if (type == AmazonPayRequestType.PaymentMethod) + { _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayPlugin.SystemName, store.Id); var paymentFailure = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; if (paymentFailure.IsCaseInsensitiveEqual("InvalidPaymentMethod")) - { - _services.Notifier.Warning(new LocalizedString(T("Payment.PayingFailed"))); - } - else if (paymentFailure.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) - { - _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); - } + { + _services.Notifier.Warning(new LocalizedString(T("Payment.PayingFailed"))); + } + else if (paymentFailure.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); + } else if (paymentFailure.IsCaseInsensitiveEqual("AuthenticationStatusAbandoned")) { _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthenticationStatusAbandonedMessage"))); } else - { - decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); - if (shoppingCartTotalBase.HasValue) - { - var client = CreateClient(settings); - var setOrderRequest = new SetOrderReferenceDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(model.OrderReferenceId) - .WithPlatformId(PlatformId) - .WithAmount(shoppingCartTotalBase.Value) - .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) - .WithStoreName(store.Name); - - var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); - if (!setOrderResponse.GetSuccess()) - { - LogError(setOrderRequest, setOrderResponse); - } - } - } - - // This is ugly... - var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; - if (paymentRequest == null) - { + { + decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); + if (shoppingCartTotalBase.HasValue) + { + var client = CreateClient(settings); + var setOrderRequest = new SetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(model.OrderReferenceId) + .WithPlatformId(PlatformId) + .WithAmount(shoppingCartTotalBase.Value) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithStoreName(store.Name); + + var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); + if (!setOrderResponse.GetSuccess()) + { + LogError(setOrderRequest, setOrderResponse); + } + } + } + + // This is ugly... + var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; + if (paymentRequest == null) + { _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest { OrderGuid = Guid.NewGuid() }; - } - } - else if (type == AmazonPayRequestType.OrderReviewData) - { - if (model.IsShippable) - { - var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, store.Id); + } + } + else if (type == AmazonPayRequestType.OrderReviewData) + { + if (model.IsShippable) + { + var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, store.Id); if (shippingOption != null) { model.ShippingMethod = shippingOption.Name; } - } - - if (customer.BillingAddress != null) - { - model.BillingAddress.PrepareModel(customer.BillingAddress, false, _addressSettings); - } - } - } - catch (Exception ex) - { - Logger.Error(ex); - _services.Notifier.Error(new LocalizedString(ex.Message)); - } - - return model; - } - - private void ProcessAuthorizationResult(AmazonPaySettings settings, Order order, AmazonPayData data) - { - var orderAttribute = DeserializeOrderAttribute(order); - - if (data.State.IsCaseInsensitiveEqual("Pending")) - { - return; - } - - var newResult = data.State.Grow(data.ReasonCode, " "); - - if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) - { - _orderProcessingService.MarkAsAuthorized(order); - } - - if (data.State.IsCaseInsensitiveEqual("Closed") && data.ReasonCode.IsCaseInsensitiveEqual("OrderReferenceCanceled") && _orderProcessingService.CanVoidOffline(order)) - { - // Cancelation at amazon seller central. - _orderProcessingService.VoidOffline(order); - } - else if (data.State.IsCaseInsensitiveEqual("Declined") && _orderProcessingService.CanVoidOffline(order)) - { - _orderProcessingService.VoidOffline(order); - } - - if (!newResult.IsCaseInsensitiveEqual(order.AuthorizationTransactionResult)) - { - order.AuthorizationTransactionResult = newResult; - - if (order.CaptureTransactionId.IsEmpty() && data.CaptureId.HasValue()) - { - // Captured at amazon seller central. - order.CaptureTransactionId = data.CaptureId; - } - - _orderService.UpdateOrder(order); - - AddOrderNote(settings, order, ToInfoString(data), true); - } - } - - private void ProcessCaptureResult(Client client, AmazonPaySettings settings, Order order, AmazonPayData data) - { - if (data.State.IsCaseInsensitiveEqual("Pending")) - { - return; - } - - var newResult = data.State.Grow(data.ReasonCode, " "); - - if (data.State.IsCaseInsensitiveEqual("Completed") && _orderProcessingService.CanMarkOrderAsPaid(order)) - { - _orderProcessingService.MarkOrderAsPaid(order); - } - else if (data.State.IsCaseInsensitiveEqual("Declined") && _orderProcessingService.CanVoidOffline(order)) - { - var authDetailsRequest = new GetAuthorizationDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonAuthorizationId(order.AuthorizationTransactionId); - - var authDetailsResponse = client.GetAuthorizationDetails(authDetailsRequest); - if (authDetailsResponse.GetSuccess()) - { - if (authDetailsResponse.GetAuthorizationState().IsCaseInsensitiveEqual("Open")) - { - _orderProcessingService.VoidOffline(order); - } - } - else - { - LogError(authDetailsRequest, authDetailsResponse); - } - } - - if (!newResult.IsCaseInsensitiveEqual(order.CaptureTransactionResult)) - { - order.CaptureTransactionResult = newResult; - _orderService.UpdateOrder(order); - - AddOrderNote(settings, order, ToInfoString(data), true); - } - } - - private void ProcessRefundResult(Client client, AmazonPaySettings settings, Order order, AmazonPayData data) - { - if (data.State.IsCaseInsensitiveEqual("Pending")) - { - return; - } - - if (data.RefundedAmount != null && data.RefundedAmount.Amount != decimal.Zero) - { - // Totally refunded amount. - // We could only process it once cause otherwise order.RefundedAmount would getting wrong. - if (order.RefundedAmount == decimal.Zero) - { - decimal refundAmount = data.RefundedAmount.Amount; - decimal receivable = order.OrderTotal - refundAmount; - - if (receivable <= decimal.Zero) - { - if (_orderProcessingService.CanRefundOffline(order)) - { - _orderProcessingService.RefundOffline(order); - - if (settings.DataFetching == AmazonPayDataFetchingType.Polling) - { - AddOrderNote(settings, order, ToInfoString(data), true); - } - } - } - else - { - if (_orderProcessingService.CanPartiallyRefundOffline(order, refundAmount)) - { - _orderProcessingService.PartiallyRefundOffline(order, refundAmount); - - if (settings.DataFetching == AmazonPayDataFetchingType.Polling) - { - AddOrderNote(settings, order, ToInfoString(data), true); - } - } - } - } - } - - if (settings.DataFetching == AmazonPayDataFetchingType.Ipn) - { - AddOrderNote(settings, order, ToInfoString(data), true); - } - } - - private void PollingLoop(PollingLoopData data, Func poll) - { - try - { - var sleepMillSec = 8000; - var loopMillSec = 90000; - var startTime = DateTime.Now.TimeOfDay; - - for (int i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) - { - // Inside the loop cause other instances are also updating the order. - data.Order = _orderService.GetOrderById(data.OrderId); - - if (data.Settings == null) - data.Settings = _services.Settings.LoadSetting(data.Order.StoreId); - - if (data.Client == null) - data.Client = CreateClient(data.Settings); - - if (!poll()) - break; - - Thread.Sleep(sleepMillSec); - } - } - catch (Exception exception) - { - Logger.Error(exception); - } - } - private void EarlyPolling(int orderId, AmazonPaySettings settings) - { - // The Authorization object moves to the Open state after remaining in the Pending state for 30 seconds. - var d = new PollingLoopData(orderId); - d.Settings = settings; - - PollingLoop(d, () => - { - if (d.Order.AuthorizationTransactionId.IsEmpty()) - return false; - - var authDetailsRequest = new GetAuthorizationDetailsRequest() - .WithMerchantId(d.Settings.SellerId) - .WithAmazonAuthorizationId(d.Order.AuthorizationTransactionId); - - var authDetailsResponse = d.Client.GetAuthorizationDetails(authDetailsRequest); - if (!authDetailsResponse.GetSuccess()) - return false; - - var details = GetDetails(authDetailsResponse); - if (!details.State.IsCaseInsensitiveEqual("pending")) - { - ProcessAuthorizationResult(d.Settings, d.Order, details); - return false; - } - - return true; - }); - - - PollingLoop(d, () => - { - if (d.Order.CaptureTransactionId.IsEmpty()) - return false; - - var captureDetailsRequest = new GetCaptureDetailsRequest() - .WithMerchantId(d.Settings.SellerId) - .WithAmazonCaptureId(d.Order.CaptureTransactionId); - - var captureDetailsResponse = d.Client.GetCaptureDetails(captureDetailsRequest); - if (!captureDetailsResponse.GetSuccess()) - return false; - - var details = GetDetails(captureDetailsResponse); - ProcessCaptureResult(d.Client, d.Settings, d.Order, details); - - return details.State.IsCaseInsensitiveEqual("pending"); - }); - } - - public void CloseOrderReference(AmazonPaySettings settings, Order order) - { - // You can still perform captures against any open authorizations, but you cannot create any new authorizations on the - // Order Reference object. You can still execute refunds against the Order Reference object. - var orderAttribute = DeserializeOrderAttribute(order); - if (!orderAttribute.OrderReferenceClosed && orderAttribute.OrderReferenceId.HasValue()) - { - var client = CreateClient(settings); - var closeRequest = new CloseOrderReferenceRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); - - var closeResponse = client.CloseOrderReference(closeRequest); - if (closeResponse.GetSuccess()) - { - orderAttribute.OrderReferenceClosed = true; - SerializeOrderAttribute(orderAttribute, order); - } - else - { - LogError(closeRequest, closeResponse, true); - } - } - } - - public void AddCustomerOrderNoteLoop(AmazonPayActionState state) - { - try - { - var sleepMillSec = 4000; - var loopMillSec = 40000; - var startTime = DateTime.Now.TimeOfDay; - - for (var i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) - { - var order = _orderService.GetOrderByGuid(state.OrderGuid); - if (order != null) - { - var sb = new StringBuilder(T("Plugins.Payments.AmazonPay.AuthorizationHardDeclineMessage")); - - if (state.Errors != null) - { - foreach (var error in state.Errors) - { - sb.AppendFormat("

{0}

", error); - } - } - - var orderNote = new OrderNote - { - DisplayToCustomer = true, - Note = sb.ToString(), - CreatedOnUtc = DateTime.UtcNow, - }; - - order.OrderNotes.Add(orderNote); - _orderService.UpdateOrder(order); - - _services.MessageFactory.SendNewOrderNoteAddedCustomerNotification(orderNote, _services.WorkContext.WorkingLanguage.Id); - break; - } - - Thread.Sleep(sleepMillSec); - } - } - catch (Exception ex) - { - Logger.Error(ex); - } - } - - public void GetBillingAddress() - { - var store = _services.StoreContext.CurrentStore; - var customer = _services.WorkContext.CurrentCustomer; - var settings = _services.Settings.LoadSetting(store.Id); - var state = _httpContext.GetAmazonPayState(_services.Localization); - var client = CreateClient(settings); - - var getOrderRequest = new GetOrderReferenceDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId) - .WithAccessToken(state.AccessToken); - - var getOrderResponse = client.GetOrderReferenceDetails(getOrderRequest); - if (getOrderResponse.GetSuccess()) - { - var details = getOrderResponse.GetBillingAddressDetails(); - if (details != null) - { - var countryAllowsShipping = true; - var countryAllowsBilling = true; - var email = getOrderResponse.GetEmail(); - - var address = CreateAddress( - email, - details.GetName(), - details.GetAddressLine1(), - details.GetAddressLine2(), - details.GetAddressLine3(), - details.GetCity(), - details.GetPostalCode(), - details.GetPhone(), - details.GetCountryCode(), - details.GetStateOrRegion(), - details.GetCounty(), - details.GetDistrict(), - out countryAllowsShipping, - out countryAllowsBilling); - - // We must ignore countryAllowsBilling because the customer cannot choose another billing address in Amazon checkout. - //if (!countryAllowsBilling) - // return; - - var existingAddress = customer.Addresses.ToList().FindAddress(address, true); - if (existingAddress == null) - { - customer.Addresses.Add(address); - customer.BillingAddress = address; - } - else - { - customer.BillingAddress = existingAddress; - } - - if (settings.CanSaveEmailAndPhone(customer.Email)) - { - customer.Email = email; - } - _customerService.UpdateCustomer(customer); - - if (settings.CanSaveEmailAndPhone(customer.GetAttribute(SystemCustomerAttributeNames.Phone, store.Id))) - { - var phone = details.GetPhone(); - if (phone.IsEmpty()) - { - phone = getOrderResponse.GetPhone(); - } - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Phone, phone); - } - } - else - { - // No billing address at Amazon? We cannot proceed. - } - } - else - { - LogError(getOrderRequest, getOrderResponse); - } - } - - public bool SetOrderReferenceDetails( - Client client, - AmazonPaySettings settings, - AmazonPayCheckoutState state, - string sellerNote, - out string errorMessage) - { - errorMessage = null; + } - if (client == null) + if (customer.BillingAddress != null) + { + model.BillingAddress.PrepareModel(customer.BillingAddress, false, _addressSettings); + } + } + } + catch (Exception ex) { - client = CreateClient(settings); + Logger.Error(ex); + _services.Notifier.Error(new LocalizedString(ex.Message)); } - var store = _services.StoreContext.CurrentStore; - var customer = _services.WorkContext.CurrentCustomer; - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); - decimal? cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); + return model; + } - var setOrderRequest = new SetOrderReferenceDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId) - .WithPlatformId(PlatformId) - .WithAmount(cartTotal) - .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) - .WithStoreName(store.Name); + private void ProcessAuthorizationResult(AmazonPaySettings settings, Order order, AmazonPayData data) + { + var orderAttribute = DeserializeOrderAttribute(order); - var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; - if (paymentRequest != null) + if (data.State.IsCaseInsensitiveEqual("Pending")) { - setOrderRequest = setOrderRequest.WithSellerOrderId(paymentRequest.OrderGuid.ToString()); + return; } - if (sellerNote != null) + var newResult = data.State.Grow(data.ReasonCode, " "); + + if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) { - setOrderRequest = setOrderRequest.WithSellerNote(sellerNote); + _orderProcessingService.MarkAsAuthorized(order); } - var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); - if (!setOrderResponse.GetSuccess()) + if (data.State.IsCaseInsensitiveEqual("Closed") && data.ReasonCode.IsCaseInsensitiveEqual("OrderReferenceCanceled") && _orderProcessingService.CanVoidOffline(order)) { - errorMessage = LogError(setOrderRequest, setOrderResponse); - return false; + // Cancelation at amazon seller central. + _orderProcessingService.VoidOffline(order); + } + else if (data.State.IsCaseInsensitiveEqual("Declined") && _orderProcessingService.CanVoidOffline(order)) + { + _orderProcessingService.VoidOffline(order); } - if (setOrderResponse.GetHasConstraint()) + if (!newResult.IsCaseInsensitiveEqual(order.AuthorizationTransactionResult)) { - var ids = setOrderResponse.GetConstraintIdList(); - var descriptions = setOrderResponse.GetDescriptionList(); + order.AuthorizationTransactionResult = newResult; - foreach (var id in ids) + if (order.CaptureTransactionId.IsEmpty() && data.CaptureId.HasValue()) { - var idx = ids.IndexOf(id); - if (idx < descriptions.Count) - { - errorMessage = $"{descriptions[idx]} ({id})"; - Logger.Error(errorMessage); - } - - if (id.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) - { - // Must be redirected to checkout payment page. - return false; - } + // Captured at amazon seller central. + order.CaptureTransactionId = data.CaptureId; } - } - return true; + _orderService.UpdateOrder(order); + + AddOrderNote(settings, order, ToInfoString(data), true); + } } - public bool ConfirmOrderReference() + private void ProcessCaptureResult(Client client, AmazonPaySettings settings, Order order, AmazonPayData data) { - try + if (data.State.IsCaseInsensitiveEqual("Pending")) { - var store = _services.StoreContext.CurrentStore; - var settings = _services.Settings.LoadSetting(store.Id); - var state = _httpContext.GetAmazonPayState(_services.Localization); - var client = CreateClient(settings); - - // Only call SetOrderReferenceDetailsRequest if it's in draft state, otherwise it returns InvalidOrderReferenceStatus error. - if (!state.DetailsSet && !SetOrderReferenceDetails(client, settings, state, null, out var _)) - { - return false; - } - - state.DetailsSet = true; + return; + } - var confirmationResultUrl = _services.WebHelper.GetStoreLocation() + "Plugins/SmartStore.AmazonPay/AmazonPayCheckout/ConfirmationResult"; + var newResult = data.State.Grow(data.ReasonCode, " "); - // Confirm order. This already generates the payment object at Amazon. - // Can be called multiple times, for example to move the order reference object from suspended to open state. - var confirmRequest = new ConfirmOrderReferenceRequest() + if (data.State.IsCaseInsensitiveEqual("Completed") && _orderProcessingService.CanMarkOrderAsPaid(order)) + { + _orderProcessingService.MarkOrderAsPaid(order); + } + else if (data.State.IsCaseInsensitiveEqual("Declined") && _orderProcessingService.CanVoidOffline(order)) + { + var authDetailsRequest = new GetAuthorizationDetailsRequest() .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId) - .WithSuccessUrl(confirmationResultUrl); + .WithAmazonAuthorizationId(order.AuthorizationTransactionId); - var confirmResponse = client.ConfirmOrderReference(confirmRequest); - if (confirmResponse.GetSuccess()) + var authDetailsResponse = client.GetAuthorizationDetails(authDetailsRequest); + if (authDetailsResponse.GetSuccess()) { - state.IsConfirmed = true; - return true; + if (authDetailsResponse.GetAuthorizationState().IsCaseInsensitiveEqual("Open")) + { + _orderProcessingService.VoidOffline(order); + } } else { - LogError(confirmRequest, confirmResponse); - return false; + LogError(authDetailsRequest, authDetailsResponse); } } - catch (Exception ex) + + if (!newResult.IsCaseInsensitiveEqual(order.CaptureTransactionResult)) { - Logger.Error(ex); - return false; + order.CaptureTransactionResult = newResult; + _orderService.UpdateOrder(order); + + AddOrderNote(settings, order, ToInfoString(data), true); } } - public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) - { - var result = new ProcessPaymentResult(); - var orderNoteErrors = new List(); - var informCustomerAboutErrors = false; - var informCustomerAddErrors = false; - var isSynchronous = false; - string error = null; - - result.NewPaymentStatus = PaymentStatus.Pending; - - _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); - _httpContext.Session.SafeRemove("AmazonPayCheckoutCompletedNote"); - - try - { - var store = _services.StoreService.GetStoreById(request.StoreId); - var settings = _services.Settings.LoadSetting(store.Id); - var captureNow = settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture; - var state = _httpContext.GetAmazonPayState(_services.Localization); - var client = CreateClient(settings); - AuthorizeRequest authRequest = null; - AuthorizeResponse authResponse = null; + private void ProcessRefundResult(Client client, AmazonPaySettings settings, Order order, AmazonPayData data) + { + if (data.State.IsCaseInsensitiveEqual("Pending")) + { + return; + } - informCustomerAboutErrors = settings.InformCustomerAboutErrors; - informCustomerAddErrors = settings.InformCustomerAddErrors; - - // Authorize. - if (settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Omnichronous) - { - // First try synchronously. - authResponse = AuthorizePayment(settings, state, store, request, client, true, out authRequest); - - if (authResponse.GetAuthorizationState().IsCaseInsensitiveEqual("Declined") && - authResponse.GetReasonCode().IsCaseInsensitiveEqual("TransactionTimedOut")) - { - // Second try asynchronously. - // Transaction is always in pending state after return. - authResponse = AuthorizePayment(settings, state, store, request, client, false, out authRequest); - } - else - { - isSynchronous = true; - } - } - else - { - isSynchronous = settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Synchronous; - authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous, out authRequest); - } - - // Process authorization response. - if (authResponse.GetSuccess()) - { - var reason = authResponse.GetReasonCode(); - - result.AuthorizationTransactionId = authResponse.GetAuthorizationId(); - result.AuthorizationTransactionCode = authResponse.GetAuthorizationReferenceId(); - result.AuthorizationTransactionResult = authResponse.GetAuthorizationState(); - - if (captureNow) - { - var idList = authResponse.GetCaptureIdList(); - if (idList.Any()) - { - result.CaptureTransactionId = idList.First(); - } - } - - if (isSynchronous) - { - if (result.AuthorizationTransactionResult.IsCaseInsensitiveEqual("Open")) - { - result.NewPaymentStatus = PaymentStatus.Authorized; - } - else if (result.AuthorizationTransactionResult.IsCaseInsensitiveEqual("Closed")) - { - if (captureNow && reason.IsCaseInsensitiveEqual("MaxCapturesProcessed")) - { - result.NewPaymentStatus = PaymentStatus.Paid; - } - } - } - else - { - _httpContext.Session["AmazonPayCheckoutCompletedNote"] = T("Plugins.Payments.AmazonPay.AsyncPaymentAuthrizationNote").Text; - } - - if (reason.IsCaseInsensitiveEqual("InvalidPaymentMethod") || reason.IsCaseInsensitiveEqual("AmazonRejected") || - reason.IsCaseInsensitiveEqual("ProcessingFailure") || reason.IsCaseInsensitiveEqual("TransactionTimedOut") || - reason.IsCaseInsensitiveEqual("TransactionTimeout")) - { - error = authResponse.GetReasonDescription(); - error = error.HasValue() ? $"{reason}: {error}" : reason; - - if (reason.IsCaseInsensitiveEqual("AmazonRejected")) - { - // Must be logged out and redirected to shopping cart. - _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; - _httpContext.Response.RedirectToRoute("ShoppingCart"); - } - else if (reason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) - { - // Must be redirected to checkout payment page. - _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; + if (data.RefundedAmount != null && data.RefundedAmount.Amount != decimal.Zero) + { + // Totally refunded amount. + // We could only process it once cause otherwise order.RefundedAmount would getting wrong. + if (order.RefundedAmount == decimal.Zero) + { + decimal refundAmount = data.RefundedAmount.Amount; + decimal receivable = order.OrderTotal - refundAmount; - // Review: confirmation required to get order reference object from suspended to open state again. - state.IsConfirmed = false; - state.FormData = null; + if (receivable <= decimal.Zero) + { + if (_orderProcessingService.CanRefundOffline(order)) + { + _orderProcessingService.RefundOffline(order); - var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); - _httpContext.Response.Redirect(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" })); - } - } - } - else - { - error = LogError(authRequest, authResponse); - } - } - catch (Exception ex) - { - Logger.Error(ex); - error = ex.Message; - } - - if (error.HasValue()) - { - if (isSynchronous) - { - result.AddError(error); - } - else - { - orderNoteErrors.Add(error); - } - } - - // Customer needs to be informed of an Amazon error here. Hooking OrderPlaced.CustomerNotification won't work - // cause of asynchronous processing. Solution: we add a customer order note that is also send as an email. - if (informCustomerAboutErrors && orderNoteErrors.Any()) - { - var state = new AmazonPayActionState { OrderGuid = request.OrderGuid }; - - if (informCustomerAddErrors) - { - state.Errors = new List(); - state.Errors.AddRange(orderNoteErrors); - } - - AsyncRunner.Run((c, ct, o) => - { - var obj = o as AmazonPayActionState; - c.Resolve().AddCustomerOrderNoteLoop(obj); - }, - state); - } - - return result; - } - - public void PostProcessPayment(PostProcessPaymentRequest request) - { - // early polling... note we do not have the amazon billing address yet - //try - //{ - // int orderId = request.Order.Id; - // var settings = _services.Settings.LoadSetting(request.Order.StoreId); - - // if (orderId != 0 && settings.StatusFetching == AmazonPayStatusFetchingType.Polling) - // { - // AsyncRunner.Run((container, obj) => - // { - // var amazonService = container.Resolve(); - // amazonService.EarlyPolling(orderId, obj as AmazonPaySettings); - // }, - // settings, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - // } - //} - //catch (Exception exc) - //{ - // Logger.Error(exc); - //} - - try - { - var state = _httpContext.GetAmazonPayState(_services.Localization); - var orderAttribute = new AmazonPayOrderAttribute - { - OrderReferenceId = state.OrderReferenceId - }; - - if (request.Order.PaymentStatus == PaymentStatus.Paid) - { - var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var client = CreateClient(settings); - var closeRequest = new CloseOrderReferenceRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); - - var closeResponse = client.CloseOrderReference(closeRequest); - if (closeResponse.GetSuccess()) - { - orderAttribute.OrderReferenceClosed = true; - } - else - { - LogError(closeRequest, closeResponse, true); - } - } - - SerializeOrderAttribute(orderAttribute, request.Order); - } - catch (Exception exception) - { - Logger.Error(exception); - } - } - - public CapturePaymentResult Capture(CapturePaymentRequest request) - { - var result = new CapturePaymentResult - { - NewPaymentStatus = request.Order.PaymentStatus - }; - - try - { - var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var store = _services.StoreService.GetStoreById(request.Order.StoreId); - var client = CreateClient(settings); - - var captureRequest = new CaptureRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonAuthorizationId(request.Order.AuthorizationTransactionId) - .WithCaptureReferenceId(GetRandomId("Capture")) - .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) - .WithAmount(request.Order.OrderTotal); - - var captureResponse = client.Capture(captureRequest); - if (captureResponse.GetSuccess()) - { - var state = captureResponse.GetCaptureState(); - - result.CaptureTransactionId = captureResponse.GetCaptureId(); - result.CaptureTransactionResult = state.Grow(captureResponse.GetReasonCode(), " "); - - if (state.IsCaseInsensitiveEqual("completed")) - { - result.NewPaymentStatus = PaymentStatus.Paid; - } - } - else - { - var message = LogError(captureRequest, captureResponse); - result.AddError(message); - } - } - catch (Exception exception) - { - Logger.Error(exception); - result.AddError(exception.Message); - } - - return result; - } - - public RefundPaymentResult Refund(RefundPaymentRequest request) - { - var result = new RefundPaymentResult - { - NewPaymentStatus = request.Order.PaymentStatus - }; - - try - { - var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var store = _services.StoreService.GetStoreById(request.Order.StoreId); - var client = CreateClient(settings); - - var refundRequest = new RefundRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonCaptureId(request.Order.CaptureTransactionId) - .WithRefundReferenceId(GetRandomId("Refund")) - .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) - .WithAmount(request.AmountToRefund); - - var refundResponse = client.Refund(refundRequest); - if (refundResponse.GetSuccess()) - { - result.NewPaymentStatus = request.IsPartialRefund ? PaymentStatus.PartiallyRefunded : PaymentStatus.Refunded; - - var refundId = refundResponse.GetAmazonRefundId(); - if (refundId.HasValue() && request.Order.Id != 0) - { - _genericAttributeService.InsertAttribute(new GenericAttribute - { - EntityId = request.Order.Id, - KeyGroup = "Order", - Key = AmazonPayPlugin.SystemName + ".RefundId", - Value = refundId, - StoreId = request.Order.StoreId - }); - } - } - else - { - var message = LogError(refundRequest, refundResponse); - result.AddError(message); - } - } - catch (Exception exception) - { - Logger.Error(exception); - result.AddError(exception.Message); - } - - return result; - } - - public VoidPaymentResult Void(VoidPaymentRequest request) - { - var result = new VoidPaymentResult - { - NewPaymentStatus = request.Order.PaymentStatus - }; - - // redundant... cause payment infrastructure hides "void" and displays "refund" instead. - //if (request.Order.PaymentStatus == PaymentStatus.Paid) - //{ - // var refundRequest = new RefundPaymentRequest() - // { - // Order = request.Order, - // IsPartialRefund = false, - // AmountToRefund = request.Order.OrderTotal - // }; - - // var refundResult = Refund(refundRequest); - - // result.Errors.AddRange(refundResult.Errors); - // return result; - //} - - try - { - if (request.Order.PaymentStatus == PaymentStatus.Pending || request.Order.PaymentStatus == PaymentStatus.Authorized) - { - var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var orderAttribute = DeserializeOrderAttribute(request.Order); - var client = CreateClient(settings); - - var cancelRequest = new CancelOrderReferenceRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); - - var cancelResponse = client.CancelOrderReference(cancelRequest); - if (cancelResponse.GetSuccess()) - { - result.NewPaymentStatus = PaymentStatus.Voided; - } - else - { - var message = LogError(cancelRequest, cancelResponse); - result.AddError(message); - } - } - } - catch (Exception exception) - { - Logger.Error(exception); - result.AddError(exception.Message); - } - - return result; - } - - public void ProcessIpn(HttpRequestBase request) - { - string json = null; - try - { - using (var reader = new StreamReader(request.InputStream)) - { - json = reader.ReadToEnd(); - } - - var parser = new IpnHandler(request.Headers, json); - var type = parser.GetNotificationType(); - AmazonPayData data = null; - Order order = null; - string errorId = null; - var isAuthorize = false; - var isCapture = false; - var isRefund = false; - - if (type.IsCaseInsensitiveEqual("PaymentAuthorize")) - { - isAuthorize = true; - var response = parser.GetAuthorizeResponse(); - data = GetDetails(response); - } - else if (type.IsCaseInsensitiveEqual("PaymentCapture")) - { - isCapture = true; - var response = parser.GetCaptureResponse(); - data = GetDetails(response); - } - else if (type.IsCaseInsensitiveEqual("PaymentRefund")) - { - isRefund = true; - var response = parser.GetRefundResponse(); - data = GetDetails(response); - } - else - { - // Ignore, e.g. OrderReferenceNotification. - return; - } - - if (data == null) - { - Logger.Error($"No IPN details for notification type {type}"); - return; - } - - data.MessageType = type; - data.MessageId = parser.GetNotificationReferenceId(); - - // Find order. - if (isAuthorize) - { - if ((order = _orderService.GetOrderByPaymentAuthorization(AmazonPayPlugin.SystemName, data.AuthorizationId)) == null) - errorId = $"AuthorizationId {data.AuthorizationId.NaIfEmpty()}"; - } - else if (isCapture) - { - if ((order = _orderService.GetOrderByPaymentCapture(AmazonPayPlugin.SystemName, data.CaptureId)) == null) - order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); - - if (order == null) - errorId = $"CaptureId {data.CaptureId.NaIfEmpty()}"; - } - else if (isRefund) - { - var attribute = _genericAttributeService.GetAttributes(AmazonPayPlugin.SystemName + ".RefundId", "Order") - .Where(x => x.Value == data.RefundId) - .FirstOrDefault(); - - if (attribute == null || (order = _orderService.GetOrderById(attribute.EntityId)) == null) - order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); - - if (order == null) - errorId = $"RefundId {data.RefundId.NaIfEmpty()}"; - } - - if (errorId.HasValue()) - { - Logger.Warn(T("Plugins.Payments.AmazonPay.OrderNotFound", errorId)); - } - - if (order == null || !_paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, order.StoreId)) - { - return; - } + if (settings.DataFetching == AmazonPayDataFetchingType.Polling) + { + AddOrderNote(settings, order, ToInfoString(data), true); + } + } + } + else + { + if (_orderProcessingService.CanPartiallyRefundOffline(order, refundAmount)) + { + _orderProcessingService.PartiallyRefundOffline(order, refundAmount); - var settings = _services.Settings.LoadSetting(order.StoreId); - if (settings.DataFetching != AmazonPayDataFetchingType.Ipn) - { - return; - } - - if (isAuthorize) - { - ProcessAuthorizationResult(settings, order, data); - } - else if (isCapture) - { - var client = CreateClient(settings); - ProcessCaptureResult(client, settings, order, data); - } - else if (isRefund) - { - var client = CreateClient(settings); - ProcessRefundResult(client, settings, order, data); - } - } - catch (Exception exception) - { - Logger.Error(exception, json); - } - } - - public void StartDataPolling() - { - try - { - // Ignore cancelled and completed (paid and shipped) orders. ignore old orders too. - var pollingMaxOrderCreationDays = _services.Settings.GetSettingByKey("AmazonPaySettings.PollingMaxOrderCreationDays", 31); - var isTooOld = DateTime.UtcNow.AddDays(-(pollingMaxOrderCreationDays)); - - var query = - from x in _orderRepository.Table - where x.PaymentMethodSystemName == AmazonPayPlugin.SystemName && x.CreatedOnUtc > isTooOld && - !x.Deleted && x.OrderStatusId < (int)OrderStatus.Complete && x.PaymentStatusId != (int)PaymentStatus.Voided - orderby x.Id descending - select x; - - var orders = query.ToList(); - //"- start polling {0} orders".FormatWith(orders.Count).Dump(); - - foreach (var order in orders) - { - try - { - var settings = _services.Settings.LoadSetting(order.StoreId); - if (settings.DataFetching == AmazonPayDataFetchingType.Polling) - { - var client = CreateClient(settings); - - if (order.AuthorizationTransactionId.HasValue()) - { - var authDetailsRequest = new GetAuthorizationDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonAuthorizationId(order.AuthorizationTransactionId); - - var authDetailsResponse = client.GetAuthorizationDetails(authDetailsRequest); - if (authDetailsResponse.GetSuccess()) - { - var details = GetDetails(authDetailsResponse); - ProcessAuthorizationResult(settings, order, details); - } - else - { - LogError(authDetailsRequest, authDetailsResponse); - } - } - - if (order.CaptureTransactionId.HasValue()) - { - if (_orderProcessingService.CanMarkOrderAsPaid(order) || _orderProcessingService.CanVoidOffline(order) || - _orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) - { - var captureDetailsRequest = new GetCaptureDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonCaptureId(order.CaptureTransactionId); - - var captureDetailsResponse = client.GetCaptureDetails(captureDetailsRequest); - if (captureDetailsResponse.GetSuccess()) - { - var details = GetDetails(captureDetailsResponse); - ProcessCaptureResult(client, settings, order, details); - - if (_orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) - { - // Note status polling: we cannot use GetRefundDetails to reflect refund(s) made at Amazon seller central cause we - // do not have any refund-id and there is no api endpoint that provide them. So we only can process CaptureDetails.RefundedAmount. - ProcessRefundResult(client, settings, order, details); - } - } - else - { - LogError(captureDetailsRequest, captureDetailsResponse); - } - } - } - } - } - catch (Exception exception) - { - Logger.Error(exception); - } - } - } - catch (Exception exception) - { - Logger.Error(exception); - } - } - - public void ShareKeys(string payload, int storeId) - { - if (payload.IsEmpty()) - { - throw new SmartException(T("Plugins.Payments.AmazonPay.MissingPayloadParameter")); - } - - dynamic json = JObject.Parse(payload); - var settings = _services.Settings.LoadSetting(storeId); - - var encryptedPayload = (string)json.encryptedPayload; - if (encryptedPayload.HasValue()) - { - throw new SmartException(T("Plugins.Payments.AmazonPay.EncryptionNotSupported")); - } - else - { - settings.SellerId = (string)json.merchant_id; - settings.AccessKey = (string)json.access_key; - settings.SecretKey = (string)json.secret_key; - settings.ClientId = (string)json.client_id; - //settings.ClientSecret = (string)json.client_secret; - } - - using (_services.Settings.BeginScope()) - { - _services.Settings.SaveSetting(settings, x => x.SellerId, storeId, false); - _services.Settings.SaveSetting(settings, x => x.AccessKey, storeId, false); - _services.Settings.SaveSetting(settings, x => x.SecretKey, storeId, false); - _services.Settings.SaveSetting(settings, x => x.ClientId, storeId, false); - } - } - - #region IExternalProviderAuthorizer - - public AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null) - { - string error = null; - string email = null; - string name = null; - string userId = null; - var accessToken = _httpContext.Request.QueryString["access_token"]; - - if (accessToken.HasValue()) - { - var settings = _services.Settings.LoadSetting(); - var client = CreateClient(settings); - var jsonString = client.GetUserInfo(accessToken); - if (jsonString.HasValue()) - { - var json = JObject.Parse(jsonString); - - email = json.GetValue("email").ToString(); - name = json.GetValue("name").ToString(); - userId = json.GetValue("user_id").ToString(); - - if (email.IsEmpty() || name.IsEmpty() || userId.IsEmpty()) - { - error = T("Plugins.Payments.AmazonPay.IncompleteProfileDetails") + - $" Email: {email.NaIfEmpty()}, name: {name.NaIfEmpty()}, userId: {userId.NaIfEmpty()}."; - } - } - else - { - error = T("Plugins.Payments.AmazonPay.IncompleteProfileDetails"); - } - } - else - { - error = T("Plugins.Payments.AmazonPay.MissingAccessToken"); - } - - if (error.HasValue()) - { - var state = new AuthorizeState("", OpenAuthenticationStatus.Error); - state.AddError(error); - Logger.Error(error); - return state; - } - - string firstName, lastName; - name.ToFirstAndLastName(out firstName, out lastName); - - var claims = new UserClaims(); - claims.Name = new NameClaims(); - claims.Contact = new ContactClaims(); - claims.Contact.Email = email; - claims.Name.FullName = name; - claims.Name.First = firstName; - claims.Name.Last = lastName; - - var parameters = new AmazonAuthenticationParameters(); - parameters.ExternalIdentifier = userId; - parameters.AddClaim(claims); - - var result = _authorizer.Value.Authorize(parameters); - - return new AuthorizeState(returnUrl, result); - } - - #endregion - } + if (settings.DataFetching == AmazonPayDataFetchingType.Polling) + { + AddOrderNote(settings, order, ToInfoString(data), true); + } + } + } + } + } + + if (settings.DataFetching == AmazonPayDataFetchingType.Ipn) + { + AddOrderNote(settings, order, ToInfoString(data), true); + } + } + + private void PollingLoop(PollingLoopData data, Func poll) + { + try + { + var sleepMillSec = 8000; + var loopMillSec = 90000; + var startTime = DateTime.Now.TimeOfDay; + + for (int i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) + { + // Inside the loop cause other instances are also updating the order. + data.Order = _orderService.GetOrderById(data.OrderId); + + if (data.Settings == null) + data.Settings = _services.Settings.LoadSetting(data.Order.StoreId); + + if (data.Client == null) + data.Client = CreateClient(data.Settings); + + if (!poll()) + break; + + Thread.Sleep(sleepMillSec); + } + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + private void EarlyPolling(int orderId, AmazonPaySettings settings) + { + // The Authorization object moves to the Open state after remaining in the Pending state for 30 seconds. + var d = new PollingLoopData(orderId); + d.Settings = settings; + + PollingLoop(d, () => + { + if (d.Order.AuthorizationTransactionId.IsEmpty()) + return false; + + var authDetailsRequest = new GetAuthorizationDetailsRequest() + .WithMerchantId(d.Settings.SellerId) + .WithAmazonAuthorizationId(d.Order.AuthorizationTransactionId); + + var authDetailsResponse = d.Client.GetAuthorizationDetails(authDetailsRequest); + if (!authDetailsResponse.GetSuccess()) + return false; + + var details = GetDetails(authDetailsResponse); + if (!details.State.IsCaseInsensitiveEqual("pending")) + { + ProcessAuthorizationResult(d.Settings, d.Order, details); + return false; + } + + return true; + }); + + + PollingLoop(d, () => + { + if (d.Order.CaptureTransactionId.IsEmpty()) + return false; + + var captureDetailsRequest = new GetCaptureDetailsRequest() + .WithMerchantId(d.Settings.SellerId) + .WithAmazonCaptureId(d.Order.CaptureTransactionId); + + var captureDetailsResponse = d.Client.GetCaptureDetails(captureDetailsRequest); + if (!captureDetailsResponse.GetSuccess()) + return false; + + var details = GetDetails(captureDetailsResponse); + ProcessCaptureResult(d.Client, d.Settings, d.Order, details); + + return details.State.IsCaseInsensitiveEqual("pending"); + }); + } + + public void CloseOrderReference(AmazonPaySettings settings, Order order) + { + // You can still perform captures against any open authorizations, but you cannot create any new authorizations on the + // Order Reference object. You can still execute refunds against the Order Reference object. + var orderAttribute = DeserializeOrderAttribute(order); + if (!orderAttribute.OrderReferenceClosed && orderAttribute.OrderReferenceId.HasValue()) + { + var client = CreateClient(settings); + var closeRequest = new CloseOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); + + var closeResponse = client.CloseOrderReference(closeRequest); + if (closeResponse.GetSuccess()) + { + orderAttribute.OrderReferenceClosed = true; + SerializeOrderAttribute(orderAttribute, order); + } + else + { + LogError(closeRequest, closeResponse, true); + } + } + } + + public void AddCustomerOrderNoteLoop(AmazonPayActionState state) + { + try + { + var sleepMillSec = 4000; + var loopMillSec = 40000; + var startTime = DateTime.Now.TimeOfDay; + + for (var i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) + { + var order = _orderService.GetOrderByGuid(state.OrderGuid); + if (order != null) + { + var sb = new StringBuilder(T("Plugins.Payments.AmazonPay.AuthorizationHardDeclineMessage")); + + if (state.Errors != null) + { + foreach (var error in state.Errors) + { + sb.AppendFormat("

{0}

", error); + } + } + + var orderNote = new OrderNote + { + DisplayToCustomer = true, + Note = sb.ToString(), + CreatedOnUtc = DateTime.UtcNow, + }; + + order.OrderNotes.Add(orderNote); + _orderService.UpdateOrder(order); + + _services.MessageFactory.SendNewOrderNoteAddedCustomerNotification(orderNote, _services.WorkContext.WorkingLanguage.Id); + break; + } + + Thread.Sleep(sleepMillSec); + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + + public void GetBillingAddress() + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var settings = _services.Settings.LoadSetting(store.Id); + var state = _httpContext.GetAmazonPayState(_services.Localization); + var client = CreateClient(settings); + + var getOrderRequest = new GetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithAccessToken(state.AccessToken); + + var getOrderResponse = client.GetOrderReferenceDetails(getOrderRequest); + if (getOrderResponse.GetSuccess()) + { + var details = getOrderResponse.GetBillingAddressDetails(); + if (details != null) + { + var countryAllowsShipping = true; + var countryAllowsBilling = true; + var email = getOrderResponse.GetEmail(); + + var address = CreateAddress( + email, + details.GetName(), + details.GetAddressLine1(), + details.GetAddressLine2(), + details.GetAddressLine3(), + details.GetCity(), + details.GetPostalCode(), + details.GetPhone(), + details.GetCountryCode(), + details.GetStateOrRegion(), + details.GetCounty(), + details.GetDistrict(), + out countryAllowsShipping, + out countryAllowsBilling); + + // We must ignore countryAllowsBilling because the customer cannot choose another billing address in Amazon checkout. + //if (!countryAllowsBilling) + // return; + + var existingAddress = customer.Addresses.ToList().FindAddress(address, true); + if (existingAddress == null) + { + customer.Addresses.Add(address); + customer.BillingAddress = address; + } + else + { + customer.BillingAddress = existingAddress; + } + + if (settings.CanSaveEmailAndPhone(customer.Email)) + { + customer.Email = email; + } + _customerService.UpdateCustomer(customer); + + if (settings.CanSaveEmailAndPhone(customer.GetAttribute(SystemCustomerAttributeNames.Phone, store.Id))) + { + var phone = details.GetPhone(); + if (phone.IsEmpty()) + { + phone = getOrderResponse.GetPhone(); + } + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Phone, phone); + } + } + else + { + // No billing address at Amazon? We cannot proceed. + } + } + else + { + LogError(getOrderRequest, getOrderResponse); + } + } + + public bool SetOrderReferenceDetails( + Client client, + AmazonPaySettings settings, + AmazonPayCheckoutState state, + string sellerNote, + out string errorMessage) + { + errorMessage = null; + + if (client == null) + { + client = CreateClient(settings); + } + + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + decimal? cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); + + var setOrderRequest = new SetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithPlatformId(PlatformId) + .WithAmount(cartTotal) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithStoreName(store.Name); + + var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; + if (paymentRequest != null && paymentRequest.OrderGuid != Guid.Empty) + { + setOrderRequest = setOrderRequest.WithSellerOrderId(paymentRequest.OrderGuid.ToString()); + } + + if (sellerNote != null) + { + setOrderRequest = setOrderRequest.WithSellerNote(sellerNote); + } + + var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); + if (!setOrderResponse.GetSuccess()) + { + errorMessage = LogError(setOrderRequest, setOrderResponse); + return false; + } + + if (setOrderResponse.GetHasConstraint()) + { + var ids = setOrderResponse.GetConstraintIdList(); + var descriptions = setOrderResponse.GetDescriptionList(); + + foreach (var id in ids) + { + var idx = ids.IndexOf(id); + if (idx < descriptions.Count) + { + errorMessage = $"{descriptions[idx]} ({id})"; + Logger.Error(errorMessage); + } + + if (id.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + // Must be redirected to checkout payment page. + return false; + } + } + } + + return true; + } + + public bool ConfirmOrderReference() + { + try + { + var store = _services.StoreContext.CurrentStore; + var settings = _services.Settings.LoadSetting(store.Id); + var state = _httpContext.GetAmazonPayState(_services.Localization); + var client = CreateClient(settings); + + // Only call SetOrderReferenceDetailsRequest if it's in draft state, otherwise it returns InvalidOrderReferenceStatus error. + if (!state.DetailsSet && !SetOrderReferenceDetails(client, settings, state, null, out var _)) + { + return false; + } + + state.DetailsSet = true; + + var confirmationResultUrl = _services.WebHelper.GetStoreLocation() + "Plugins/SmartStore.AmazonPay/AmazonPayCheckout/ConfirmationResult"; + + // Confirm order. This already generates the payment object at Amazon. + // Can be called multiple times, for example to move the order reference object from suspended to open state. + var confirmRequest = new ConfirmOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithSuccessUrl(confirmationResultUrl); + + var confirmResponse = client.ConfirmOrderReference(confirmRequest); + if (confirmResponse.GetSuccess()) + { + state.IsConfirmed = true; + return true; + } + else + { + LogError(confirmRequest, confirmResponse); + return false; + } + } + catch (Exception ex) + { + Logger.Error(ex); + return false; + } + } + + public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) + { + var result = new ProcessPaymentResult(); + var orderNoteErrors = new List(); + var informCustomerAboutErrors = false; + var informCustomerAddErrors = false; + var isSynchronous = false; + string error = null; + + result.NewPaymentStatus = PaymentStatus.Pending; + + _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); + _httpContext.Session.SafeRemove("AmazonPayCheckoutCompletedNote"); + + try + { + var store = _services.StoreService.GetStoreById(request.StoreId); + var settings = _services.Settings.LoadSetting(store.Id); + var captureNow = settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture; + var state = _httpContext.GetAmazonPayState(_services.Localization); + var client = CreateClient(settings); + AuthorizeRequest authRequest = null; + AuthorizeResponse authResponse = null; + + informCustomerAboutErrors = settings.InformCustomerAboutErrors; + informCustomerAddErrors = settings.InformCustomerAddErrors; + + // Authorize. + if (settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Omnichronous) + { + // First try synchronously. + authResponse = AuthorizePayment(settings, state, store, request, client, true, out authRequest); + + if (authResponse.GetAuthorizationState().IsCaseInsensitiveEqual("Declined") && + authResponse.GetReasonCode().IsCaseInsensitiveEqual("TransactionTimedOut")) + { + // Second try asynchronously. + // Transaction is always in pending state after return. + authResponse = AuthorizePayment(settings, state, store, request, client, false, out authRequest); + } + else + { + isSynchronous = true; + } + } + else + { + isSynchronous = settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Synchronous; + authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous, out authRequest); + } + + // Process authorization response. + if (authResponse.GetSuccess()) + { + var reason = authResponse.GetReasonCode(); + + result.AuthorizationTransactionId = authResponse.GetAuthorizationId(); + result.AuthorizationTransactionCode = authResponse.GetAuthorizationReferenceId(); + result.AuthorizationTransactionResult = authResponse.GetAuthorizationState(); + + if (captureNow) + { + var idList = authResponse.GetCaptureIdList(); + if (idList.Any()) + { + result.CaptureTransactionId = idList.First(); + } + } + + if (isSynchronous) + { + if (result.AuthorizationTransactionResult.IsCaseInsensitiveEqual("Open")) + { + result.NewPaymentStatus = PaymentStatus.Authorized; + } + else if (result.AuthorizationTransactionResult.IsCaseInsensitiveEqual("Closed")) + { + if (captureNow && reason.IsCaseInsensitiveEqual("MaxCapturesProcessed")) + { + result.NewPaymentStatus = PaymentStatus.Paid; + } + } + } + else + { + _httpContext.Session["AmazonPayCheckoutCompletedNote"] = T("Plugins.Payments.AmazonPay.AsyncPaymentAuthrizationNote").Text; + } + + if (reason.IsCaseInsensitiveEqual("InvalidPaymentMethod") || reason.IsCaseInsensitiveEqual("AmazonRejected") || + reason.IsCaseInsensitiveEqual("ProcessingFailure") || reason.IsCaseInsensitiveEqual("TransactionTimedOut") || + reason.IsCaseInsensitiveEqual("TransactionTimeout")) + { + error = authResponse.GetReasonDescription(); + error = error.HasValue() ? $"{reason}: {error}" : reason; + + if (reason.IsCaseInsensitiveEqual("AmazonRejected")) + { + // Must be logged out and redirected to shopping cart. + _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; + _httpContext.Response.RedirectToRoute("ShoppingCart"); + } + else if (reason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) + { + // Must be redirected to checkout payment page. + _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; + + // Review: confirmation required to get order reference object from suspended to open state again. + state.IsConfirmed = false; + state.FormData = null; + + var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); + _httpContext.Response.Redirect(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" })); + } + } + } + else + { + error = LogError(authRequest, authResponse); + } + } + catch (Exception ex) + { + Logger.Error(ex); + error = ex.Message; + } + + if (error.HasValue()) + { + if (isSynchronous) + { + result.AddError(error); + } + else + { + orderNoteErrors.Add(error); + } + } + + // Customer needs to be informed of an Amazon error here. Hooking OrderPlaced.CustomerNotification won't work + // cause of asynchronous processing. Solution: we add a customer order note that is also send as an email. + if (informCustomerAboutErrors && orderNoteErrors.Any()) + { + var state = new AmazonPayActionState { OrderGuid = request.OrderGuid }; + + if (informCustomerAddErrors) + { + state.Errors = new List(); + state.Errors.AddRange(orderNoteErrors); + } + + AsyncRunner.Run((c, ct, o) => + { + var obj = o as AmazonPayActionState; + c.Resolve().AddCustomerOrderNoteLoop(obj); + }, + state); + } + + return result; + } + + public void PostProcessPayment(PostProcessPaymentRequest request) + { + // early polling... note we do not have the amazon billing address yet + //try + //{ + // int orderId = request.Order.Id; + // var settings = _services.Settings.LoadSetting(request.Order.StoreId); + + // if (orderId != 0 && settings.StatusFetching == AmazonPayStatusFetchingType.Polling) + // { + // AsyncRunner.Run((container, obj) => + // { + // var amazonService = container.Resolve(); + // amazonService.EarlyPolling(orderId, obj as AmazonPaySettings); + // }, + // settings, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + // } + //} + //catch (Exception exc) + //{ + // Logger.Error(exc); + //} + + try + { + var state = _httpContext.GetAmazonPayState(_services.Localization); + var orderAttribute = new AmazonPayOrderAttribute + { + OrderReferenceId = state.OrderReferenceId + }; + + if (request.Order.PaymentStatus == PaymentStatus.Paid) + { + var settings = _services.Settings.LoadSetting(request.Order.StoreId); + var client = CreateClient(settings); + var closeRequest = new CloseOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); + + var closeResponse = client.CloseOrderReference(closeRequest); + if (closeResponse.GetSuccess()) + { + orderAttribute.OrderReferenceClosed = true; + } + else + { + LogError(closeRequest, closeResponse, true); + } + } + + SerializeOrderAttribute(orderAttribute, request.Order); + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + + public CapturePaymentResult Capture(CapturePaymentRequest request) + { + var result = new CapturePaymentResult + { + NewPaymentStatus = request.Order.PaymentStatus + }; + + try + { + var settings = _services.Settings.LoadSetting(request.Order.StoreId); + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + var client = CreateClient(settings); + + var captureRequest = new CaptureRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonAuthorizationId(request.Order.AuthorizationTransactionId) + .WithCaptureReferenceId(GetRandomId("Capture")) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithAmount(request.Order.OrderTotal); + + var captureResponse = client.Capture(captureRequest); + if (captureResponse.GetSuccess()) + { + var state = captureResponse.GetCaptureState(); + + result.CaptureTransactionId = captureResponse.GetCaptureId(); + result.CaptureTransactionResult = state.Grow(captureResponse.GetReasonCode(), " "); + + if (state.IsCaseInsensitiveEqual("completed")) + { + result.NewPaymentStatus = PaymentStatus.Paid; + } + } + else + { + var message = LogError(captureRequest, captureResponse); + result.AddError(message); + } + } + catch (Exception exception) + { + Logger.Error(exception); + result.AddError(exception.Message); + } + + return result; + } + + public RefundPaymentResult Refund(RefundPaymentRequest request) + { + var result = new RefundPaymentResult + { + NewPaymentStatus = request.Order.PaymentStatus + }; + + try + { + var settings = _services.Settings.LoadSetting(request.Order.StoreId); + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + var client = CreateClient(settings); + + var refundRequest = new RefundRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonCaptureId(request.Order.CaptureTransactionId) + .WithRefundReferenceId(GetRandomId("Refund")) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithAmount(request.AmountToRefund); + + var refundResponse = client.Refund(refundRequest); + if (refundResponse.GetSuccess()) + { + result.NewPaymentStatus = request.IsPartialRefund ? PaymentStatus.PartiallyRefunded : PaymentStatus.Refunded; + + var refundId = refundResponse.GetAmazonRefundId(); + if (refundId.HasValue() && request.Order.Id != 0) + { + _genericAttributeService.InsertAttribute(new GenericAttribute + { + EntityId = request.Order.Id, + KeyGroup = "Order", + Key = AmazonPayPlugin.SystemName + ".RefundId", + Value = refundId, + StoreId = request.Order.StoreId + }); + } + } + else + { + var message = LogError(refundRequest, refundResponse); + result.AddError(message); + } + } + catch (Exception exception) + { + Logger.Error(exception); + result.AddError(exception.Message); + } + + return result; + } + + public VoidPaymentResult Void(VoidPaymentRequest request) + { + var result = new VoidPaymentResult + { + NewPaymentStatus = request.Order.PaymentStatus + }; + + // redundant... cause payment infrastructure hides "void" and displays "refund" instead. + //if (request.Order.PaymentStatus == PaymentStatus.Paid) + //{ + // var refundRequest = new RefundPaymentRequest() + // { + // Order = request.Order, + // IsPartialRefund = false, + // AmountToRefund = request.Order.OrderTotal + // }; + + // var refundResult = Refund(refundRequest); + + // result.Errors.AddRange(refundResult.Errors); + // return result; + //} + + try + { + if (request.Order.PaymentStatus == PaymentStatus.Pending || request.Order.PaymentStatus == PaymentStatus.Authorized) + { + var settings = _services.Settings.LoadSetting(request.Order.StoreId); + var orderAttribute = DeserializeOrderAttribute(request.Order); + var client = CreateClient(settings); + + var cancelRequest = new CancelOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); + + var cancelResponse = client.CancelOrderReference(cancelRequest); + if (cancelResponse.GetSuccess()) + { + result.NewPaymentStatus = PaymentStatus.Voided; + } + else + { + var message = LogError(cancelRequest, cancelResponse); + result.AddError(message); + } + } + } + catch (Exception exception) + { + Logger.Error(exception); + result.AddError(exception.Message); + } + + return result; + } + + public void ProcessIpn(HttpRequestBase request) + { + string json = null; + try + { + using (var reader = new StreamReader(request.InputStream)) + { + json = reader.ReadToEnd(); + } + + var parser = new IpnHandler(request.Headers, json); + var type = parser.GetNotificationType(); + AmazonPayData data = null; + Order order = null; + string errorId = null; + var isAuthorize = false; + var isCapture = false; + var isRefund = false; + + if (type.IsCaseInsensitiveEqual("PaymentAuthorize")) + { + isAuthorize = true; + var response = parser.GetAuthorizeResponse(); + data = GetDetails(response); + } + else if (type.IsCaseInsensitiveEqual("PaymentCapture")) + { + isCapture = true; + var response = parser.GetCaptureResponse(); + data = GetDetails(response); + } + else if (type.IsCaseInsensitiveEqual("PaymentRefund")) + { + isRefund = true; + var response = parser.GetRefundResponse(); + data = GetDetails(response); + } + else + { + // Ignore, e.g. OrderReferenceNotification. + return; + } + + if (data == null) + { + Logger.Error($"No IPN details for notification type {type}"); + return; + } + + data.MessageType = type; + data.MessageId = parser.GetNotificationReferenceId(); + + // Find order. + if (isAuthorize) + { + if ((order = _orderService.GetOrderByPaymentAuthorization(AmazonPayPlugin.SystemName, data.AuthorizationId)) == null) + errorId = $"AuthorizationId {data.AuthorizationId.NaIfEmpty()}"; + } + else if (isCapture) + { + if ((order = _orderService.GetOrderByPaymentCapture(AmazonPayPlugin.SystemName, data.CaptureId)) == null) + order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); + + if (order == null) + errorId = $"CaptureId {data.CaptureId.NaIfEmpty()}"; + } + else if (isRefund) + { + var attribute = _genericAttributeService.GetAttributes(AmazonPayPlugin.SystemName + ".RefundId", "Order") + .Where(x => x.Value == data.RefundId) + .FirstOrDefault(); + + if (attribute == null || (order = _orderService.GetOrderById(attribute.EntityId)) == null) + order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); + + if (order == null) + errorId = $"RefundId {data.RefundId.NaIfEmpty()}"; + } + + if (errorId.HasValue()) + { + Logger.Warn(T("Plugins.Payments.AmazonPay.OrderNotFound", errorId)); + } + + if (order == null || !_paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, order.StoreId)) + { + return; + } + + var settings = _services.Settings.LoadSetting(order.StoreId); + if (settings.DataFetching != AmazonPayDataFetchingType.Ipn) + { + return; + } + + if (isAuthorize) + { + ProcessAuthorizationResult(settings, order, data); + } + else if (isCapture) + { + var client = CreateClient(settings); + ProcessCaptureResult(client, settings, order, data); + } + else if (isRefund) + { + var client = CreateClient(settings); + ProcessRefundResult(client, settings, order, data); + } + } + catch (Exception exception) + { + Logger.Error(exception, json); + } + } + + public void StartDataPolling() + { + try + { + // Ignore cancelled and completed (paid and shipped) orders. ignore old orders too. + var pollingMaxOrderCreationDays = _services.Settings.GetSettingByKey("AmazonPaySettings.PollingMaxOrderCreationDays", 31); + var isTooOld = DateTime.UtcNow.AddDays(-(pollingMaxOrderCreationDays)); + + var query = + from x in _orderRepository.Table + where x.PaymentMethodSystemName == AmazonPayPlugin.SystemName && x.CreatedOnUtc > isTooOld && + !x.Deleted && x.OrderStatusId < (int)OrderStatus.Complete && x.PaymentStatusId != (int)PaymentStatus.Voided + orderby x.Id descending + select x; + + var orders = query.ToList(); + //"- start polling {0} orders".FormatWith(orders.Count).Dump(); + + foreach (var order in orders) + { + try + { + var settings = _services.Settings.LoadSetting(order.StoreId); + if (settings.DataFetching == AmazonPayDataFetchingType.Polling) + { + var client = CreateClient(settings); + + if (order.AuthorizationTransactionId.HasValue()) + { + var authDetailsRequest = new GetAuthorizationDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonAuthorizationId(order.AuthorizationTransactionId); + + var authDetailsResponse = client.GetAuthorizationDetails(authDetailsRequest); + if (authDetailsResponse.GetSuccess()) + { + var details = GetDetails(authDetailsResponse); + ProcessAuthorizationResult(settings, order, details); + } + else + { + LogError(authDetailsRequest, authDetailsResponse); + } + } + + if (order.CaptureTransactionId.HasValue()) + { + if (_orderProcessingService.CanMarkOrderAsPaid(order) || _orderProcessingService.CanVoidOffline(order) || + _orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) + { + var captureDetailsRequest = new GetCaptureDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonCaptureId(order.CaptureTransactionId); + + var captureDetailsResponse = client.GetCaptureDetails(captureDetailsRequest); + if (captureDetailsResponse.GetSuccess()) + { + var details = GetDetails(captureDetailsResponse); + ProcessCaptureResult(client, settings, order, details); + + if (_orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) + { + // Note status polling: we cannot use GetRefundDetails to reflect refund(s) made at Amazon seller central cause we + // do not have any refund-id and there is no api endpoint that provide them. So we only can process CaptureDetails.RefundedAmount. + ProcessRefundResult(client, settings, order, details); + } + } + else + { + LogError(captureDetailsRequest, captureDetailsResponse); + } + } + } + } + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + + public void ShareKeys(string payload, int storeId) + { + if (payload.IsEmpty()) + { + throw new SmartException(T("Plugins.Payments.AmazonPay.MissingPayloadParameter")); + } + + dynamic json = JObject.Parse(payload); + var settings = _services.Settings.LoadSetting(storeId); + + var encryptedPayload = (string)json.encryptedPayload; + if (encryptedPayload.HasValue()) + { + throw new SmartException(T("Plugins.Payments.AmazonPay.EncryptionNotSupported")); + } + else + { + settings.SellerId = (string)json.merchant_id; + settings.AccessKey = (string)json.access_key; + settings.SecretKey = (string)json.secret_key; + settings.ClientId = (string)json.client_id; + //settings.ClientSecret = (string)json.client_secret; + } + + using (_services.Settings.BeginScope()) + { + _services.Settings.SaveSetting(settings, x => x.SellerId, storeId, false); + _services.Settings.SaveSetting(settings, x => x.AccessKey, storeId, false); + _services.Settings.SaveSetting(settings, x => x.SecretKey, storeId, false); + _services.Settings.SaveSetting(settings, x => x.ClientId, storeId, false); + } + } + + #region IExternalProviderAuthorizer + + public AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null) + { + string error = null; + string email = null; + string name = null; + string userId = null; + var accessToken = _httpContext.Request.QueryString["access_token"]; + + if (accessToken.HasValue()) + { + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + var client = CreateClient(settings); + var jsonString = client.GetUserInfo(accessToken); + if (jsonString.HasValue()) + { + var json = JObject.Parse(jsonString); + + email = json.GetValue("email").ToString(); + name = json.GetValue("name").ToString(); + userId = json.GetValue("user_id").ToString(); + + if (email.IsEmpty() || name.IsEmpty() || userId.IsEmpty()) + { + error = T("Plugins.Payments.AmazonPay.IncompleteProfileDetails") + + $" Email: {email.NaIfEmpty()}, name: {name.NaIfEmpty()}, userId: {userId.NaIfEmpty()}."; + } + } + else + { + error = T("Plugins.Payments.AmazonPay.IncompleteProfileDetails"); + } + } + else + { + error = T("Plugins.Payments.AmazonPay.MissingAccessToken"); + } + + if (error.HasValue()) + { + var state = new AuthorizeState("", OpenAuthenticationStatus.Error); + state.AddError(error); + Logger.Error(error); + return state; + } + + string firstName, lastName; + name.ToFirstAndLastName(out firstName, out lastName); + + var claims = new UserClaims(); + claims.Name = new NameClaims(); + claims.Contact = new ContactClaims(); + claims.Contact.Email = email; + claims.Name.FullName = name; + claims.Name.First = firstName; + claims.Name.Last = lastName; + + var parameters = new AmazonAuthenticationParameters(); + parameters.ExternalIdentifier = userId; + parameters.AddClaim(claims); + + var result = _authorizer.Value.Authorize(parameters); + + return new AuthorizeState(returnUrl, result); + } + + #endregion + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs index 1fbf843352..9353f2538b 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs @@ -22,415 +22,415 @@ namespace SmartStore.AmazonPay.Services /// Helper with utilities to keep the AmazonPayService tidy. ///
public partial class AmazonPayService - { - /// - /// Also named "spId". - /// - internal static string PlatformId => "A3OJ83WFYM72IY"; - - internal static string LeadCode => "SPEXDEAPA-SmartStore.Net-CP-DP"; - - internal static string GetWidgetUrl(AmazonPaySettings settings) - { - switch (settings.Marketplace.EmptyNull().ToLower()) - { - case "us": - return settings.UseSandbox - ? "https://static-na.payments-amazon.com/OffAmazonPayments/us/sandbox/js/Widgets.js" - : "https://static-na.payments-amazon.com/OffAmazonPayments/us/js/Widgets.js"; - case "uk": - return settings.UseSandbox - ? "https://static-eu.payments-amazon.com/OffAmazonPayments/gbp/sandbox/lpa/js/Widgets.js" - : "https://static-eu.payments-amazon.com/OffAmazonPayments/gbp/lpa/js/Widgets.js"; - case "jp": - return settings.UseSandbox - ? "https://static-fe.payments-amazon.com/OffAmazonPayments/jp/sandbox/lpa/js/Widgets.js" - : "https://static-fe.payments-amazon.com/OffAmazonPayments/jp/lpa/js/Widgets.js"; - default: - return settings.UseSandbox - ? "https://static-eu.payments-amazon.com/OffAmazonPayments/eur/sandbox/lpa/js/Widgets.js" - : "https://static-eu.payments-amazon.com/OffAmazonPayments/eur/lpa/js/Widgets.js"; - } - } - - private string GetPluginUrl(string action, bool useSsl = false) - { - var pluginUrl = "{0}Plugins/SmartStore.AmazonPay/AmazonPay/{1}".FormatInvariant(_services.WebHelper.GetStoreLocation(useSsl), action); - return pluginUrl; - } - - private void SerializeOrderAttribute(AmazonPayOrderAttribute attribute, Order order) - { - if (attribute != null) - { - var sb = new StringBuilder(); - using (var writer = new StringWriter(sb)) - { - var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); - serializer.Serialize(writer, attribute); - - _genericAttributeService.SaveAttribute(order, AmazonPayPlugin.SystemName + ".OrderAttribute", sb.ToString(), order.StoreId); - } - } - } - - private AmazonPayOrderAttribute DeserializeOrderAttribute(Order order) - { - var serialized = order.GetAttribute(AmazonPayPlugin.SystemName + ".OrderAttribute", _genericAttributeService, order.StoreId); - - if (!serialized.HasValue()) - { - var attribute = new AmazonPayOrderAttribute(); - - // legacy < v.1.14 - attribute.OrderReferenceId = order.GetAttribute(AmazonPayPlugin.SystemName + ".OrderReferenceId", order.StoreId); - - return attribute; - } - - using (var reader = new StringReader(serialized)) - { - var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); - return (AmazonPayOrderAttribute)serializer.Deserialize(reader); - } - } - - private void AddOrderNote(AmazonPaySettings settings, Order order, string anyString = null, bool isIpn = false) - { - try - { - if (!settings.AddOrderNotes || order == null || anyString.IsEmpty()) - return; - - var sb = new StringBuilder(); - var faviconUrl = "{0}Plugins/{1}/Content/images/favicon.png".FormatInvariant(_services.WebHelper.GetStoreLocation(), AmazonPayPlugin.SystemName); - - sb.AppendFormat("", faviconUrl); - sb.Append(T("Plugins.Payments.AmazonPay.AmazonDataProcessed")); - sb.Append(":
"); - sb.Append(anyString); - - if (isIpn) - { - order.HasNewPaymentNotification = true; - } - - order.OrderNotes.Add(new OrderNote - { - Note = sb.ToString(), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - - _orderService.UpdateOrder(order); - } - catch (Exception exception) - { - Logger.Error(exception); - } - } - - private Regions.currencyCode ConvertCurrency(string currencyCode) - { - switch (currencyCode.EmptyNull().ToLower()) - { - case "usd": - return Regions.currencyCode.USD; - case "gbp": - return Regions.currencyCode.GBP; - case "jpy": - return Regions.currencyCode.JPY; - case "aud": - return Regions.currencyCode.AUD; - case "zar": - return Regions.currencyCode.ZAR; - case "chf": - return Regions.currencyCode.CHF; - case "nok": - return Regions.currencyCode.NOK; - case "dkk": - return Regions.currencyCode.DKK; - case "sek": - return Regions.currencyCode.SEK; - case "nzd": - return Regions.currencyCode.NZD; - case "hkd": - return Regions.currencyCode.HKD; - default: - return Regions.currencyCode.EUR; - } - } - - private Address CreateAddress( - string email, - string buyerName, - string addressLine1, - string addressLine2, - string addressLine3, - string city, - string postalCode, - string phone, - string countryCode, - string stateRegion, - string county, - string destrict, - out bool countryAllowsShipping, - out bool countryAllowsBilling) - { - countryAllowsShipping = countryAllowsBilling = true; - - var address = new Address(); - address.CreatedOnUtc = DateTime.UtcNow; - address.Email = email; - address.ToFirstAndLastName(buyerName); - address.Address1 = addressLine1.EmptyNull().Trim().Truncate(4000); - address.Address2 = addressLine2.EmptyNull().Trim().Truncate(4000); - address.Address2 = address.Address2.Grow(addressLine3.EmptyNull().Trim(), ", ").Truncate(4000); - address.City = county.Grow(destrict, " ").Grow(city, " ").EmptyNull().Trim().Truncate(4000); - address.ZipPostalCode = postalCode.EmptyNull().Trim().Truncate(4000); - address.PhoneNumber = phone.EmptyNull().Trim().Truncate(4000); - - if (countryCode.HasValue()) - { - var country = _countryService.GetCountryByTwoOrThreeLetterIsoCode(countryCode); - if (country != null) - { - address.CountryId = country.Id; - countryAllowsShipping = country.AllowsShipping; - countryAllowsBilling = country.AllowsBilling; - } - } - - if (stateRegion.HasValue()) - { - var stateProvince = _stateProvinceService.GetStateProvinceByAbbreviation(stateRegion); - if (stateProvince != null) - { - address.StateProvinceId = stateProvince.Id; - } - } - - // Normalize. - if (address.Address1.IsEmpty() && address.Address2.HasValue()) - { - address.Address1 = address.Address2; - address.Address2 = null; - } - else if (address.Address1.HasValue() && address.Address1 == address.Address2) - { - address.Address2 = null; - } - - if (address.CountryId == 0) - { - address.CountryId = null; - } - - if (address.StateProvinceId == 0) - { - address.StateProvinceId = null; - } - - return address; - } - - //private void GetAddress(OrderReferenceDetailsResponse details, Address address, out bool countryAllowsShipping, out bool countryAllowsBilling) - //{ - // countryAllowsShipping = countryAllowsBilling = true; - - // address.Email = details.GetEmail(); - // address.ToFirstAndLastName(details.GetBuyerName()); - // address.Address1 = details.GetAddressLine1().EmptyNull().Trim().Truncate(4000); - // address.Address2 = details.GetAddressLine2().EmptyNull().Trim().Truncate(4000); - // address.Address2 = address.Address2.Grow(details.GetAddressLine3().EmptyNull().Trim(), ", ").Truncate(4000); - // address.City = details.GetCity().EmptyNull().Trim().Truncate(4000); - // address.ZipPostalCode = details.GetPostalCode().EmptyNull().Trim().Truncate(4000); - // address.PhoneNumber = details.GetPhone().EmptyNull().Trim().Truncate(4000); - - // var countryCode = details.GetCountryCode(); - // if (countryCode.HasValue()) - // { - // var country = _countryService.GetCountryByTwoOrThreeLetterIsoCode(countryCode); - // if (country != null) - // { - // address.CountryId = country.Id; - // countryAllowsShipping = country.AllowsShipping; - // countryAllowsBilling = country.AllowsBilling; - // } - // } - - // var stateRegion = details.GetStateOrRegion(); - // if (stateRegion.HasValue()) - // { - // var stateProvince = _stateProvinceService.GetStateProvinceByAbbreviation(stateRegion); - // if (stateProvince != null) - // { - // address.StateProvinceId = stateProvince.Id; - // } - // } - - // // Normalize. - // if (address.Address1.IsEmpty() && address.Address2.HasValue()) - // { - // address.Address1 = address.Address2; - // address.Address2 = null; - // } - // else if (address.Address1.HasValue() && address.Address1 == address.Address2) - // { - // address.Address2 = null; - // } - - // if (address.CountryId == 0) - // { - // address.CountryId = null; - // } - - // if (address.StateProvinceId == 0) - // { - // address.StateProvinceId = null; - // } - //} - - //private bool FindAndApplyAddress(OrderReferenceDetailsResponse details, Customer customer, bool isShippable, bool forceToTakeAmazonAddress) - //{ - // // PlaceOrder requires billing address but we don't get one from Amazon here. so use shipping address instead until we get it from amazon. - // var countryAllowsShipping = true; - // var countryAllowsBilling = true; - - // var address = new Address(); - // address.CreatedOnUtc = DateTime.UtcNow; - - // GetAddress(details, address, out countryAllowsShipping, out countryAllowsBilling); - - // if (isShippable && !countryAllowsShipping) - // return false; - - // if (address.Email.IsEmpty()) - // { - // address.Email = customer.Email; - // } - - // if (forceToTakeAmazonAddress) - // { - // // First time to get in touch with an amazon address. - // var existingAddress = customer.Addresses.ToList().FindAddress(address, true); - // if (existingAddress == null) - // { - // customer.Addresses.Add(address); - // customer.BillingAddress = address; - // } - // else - // { - // customer.BillingAddress = existingAddress; - // } - // } - // else - // { - // if (customer.BillingAddress == null) - // { - // customer.Addresses.Add(address); - // customer.BillingAddress = address; - // } - - // GetAddress(details, customer.BillingAddress, out countryAllowsShipping, out countryAllowsBilling); - - // // But now we could have dublicates. - // var newAddressId = customer.BillingAddress.Id; - // var addresses = customer.Addresses.Where(x => x.Id != newAddressId).ToList(); - - // var existingAddress = addresses.FindAddress(customer.BillingAddress, false); - // if (existingAddress != null) - // { - // // Remove the new and take the old one. - // customer.RemoveAddress(customer.BillingAddress); - // customer.BillingAddress = existingAddress; - - // try - // { - // _addressService.DeleteAddress(newAddressId); - // } - // catch (Exception exception) - // { - // exception.Dump(); - // } - // } - // } - - // customer.ShippingAddress = (isShippable ? customer.BillingAddress : null); - - // return true; - //} - - private AmazonPayData GetDetails(AuthorizeResponse response) - { - var data = new AmazonPayData(); - data.MessageType = "GetAuthorizationDetails"; - data.MessageId = response.GetRequestId(); - data.AuthorizationId = response.GetAuthorizationId(); - data.ReferenceId = response.GetAuthorizationReferenceId(); - - var ids = response.GetCaptureIdList(); - if (ids.Any()) - { - data.CaptureId = ids.First(); - } - - data.Fee = new AmazonPayPrice(response.GetAuthorizationFee(), response.GetAuthorizationFeeCurrencyCode()); - data.AuthorizedAmount = new AmazonPayPrice(response.GetAuthorizationAmount(), response.GetAuthorizationAmountCurrencyCode()); - data.CapturedAmount = new AmazonPayPrice(response.GetCapturedAmount(), response.GetCapturedAmountCurrencyCode()); - data.CaptureNow = response.GetCaptureNow(); - data.Creation = response.GetCreationTimestamp(); - data.Expiration = response.GetExpirationTimestamp(); - data.ReasonCode = response.GetReasonCode(); - data.ReasonDescription = response.GetReasonDescription(); - data.State = response.GetAuthorizationState(); - data.StateLastUpdate = response.GetLastUpdateTimestamp(); - - return data; - } - private AmazonPayData GetDetails(CaptureResponse response) - { - var data = new AmazonPayData(); - data.MessageType = "GetCaptureDetails"; - data.MessageId = response.GetRequestId(); - data.CaptureId = response.GetCaptureId(); - data.ReferenceId = response.GetCaptureReferenceId(); - data.Fee = new AmazonPayPrice(response.GetCaptureFee(), response.GetCaptureFeeCurrencyCode()); - data.CapturedAmount = new AmazonPayPrice(response.GetCaptureAmount(), response.GetCaptureAmountCurrencyCode()); - data.Creation = response.GetCreationTimestamp(); - data.ReasonCode = response.GetReasonCode(); - data.ReasonDescription = response.GetReasonDescription(); - data.State = response.GetCaptureState(); - data.StateLastUpdate = response.GetLastUpdatedTimestamp(); - - return data; - } - private AmazonPayData GetDetails(RefundResponse response) - { - var data = new AmazonPayData(); - data.MessageType = "GetRefundDetails"; - data.MessageId = response.GetRequestId(); - data.ReferenceId = response.GetRefundReferenceId(); - data.Creation = response.GetCreationTimestamp(); - data.Fee = new AmazonPayPrice(response.GetRefundFee(), response.GetRefundFeeCurrencyCode()); - data.RefundId = response.GetAmazonRefundId(); - data.RefundedAmount = new AmazonPayPrice(response.GetRefundAmount(), response.GetRefundAmountCurrencyCode()); - data.ReasonCode = response.GetReasonCode(); - data.ReasonDescription = response.GetReasonDescription(); - data.State = response.GetRefundState(); - data.StateLastUpdate = response.GetLastUpdateTimestamp(); - - return data; - } - - private string GetRandomId(string prefix) - { - var str = prefix + CommonHelper.GenerateRandomDigitCode(20); - return str.Truncate(32); - } - - private string LogError(DelegateRequest request, IResponse response, bool isWarning = false) - { + { + /// + /// Also named "spId". + /// + internal static string PlatformId => "A3OJ83WFYM72IY"; + + internal static string LeadCode => "SPEXDEAPA-SmartStore.Net-CP-DP"; + + internal static string GetWidgetUrl(AmazonPaySettings settings) + { + switch (settings.Marketplace.EmptyNull().ToLower()) + { + case "us": + return settings.UseSandbox + ? "https://static-na.payments-amazon.com/OffAmazonPayments/us/sandbox/js/Widgets.js" + : "https://static-na.payments-amazon.com/OffAmazonPayments/us/js/Widgets.js"; + case "uk": + return settings.UseSandbox + ? "https://static-eu.payments-amazon.com/OffAmazonPayments/gbp/sandbox/lpa/js/Widgets.js" + : "https://static-eu.payments-amazon.com/OffAmazonPayments/gbp/lpa/js/Widgets.js"; + case "jp": + return settings.UseSandbox + ? "https://static-fe.payments-amazon.com/OffAmazonPayments/jp/sandbox/lpa/js/Widgets.js" + : "https://static-fe.payments-amazon.com/OffAmazonPayments/jp/lpa/js/Widgets.js"; + default: + return settings.UseSandbox + ? "https://static-eu.payments-amazon.com/OffAmazonPayments/eur/sandbox/lpa/js/Widgets.js" + : "https://static-eu.payments-amazon.com/OffAmazonPayments/eur/lpa/js/Widgets.js"; + } + } + + private string GetPluginUrl(string action, bool useSsl = false) + { + var pluginUrl = "{0}Plugins/SmartStore.AmazonPay/AmazonPay/{1}".FormatInvariant(_services.WebHelper.GetStoreLocation(useSsl), action); + return pluginUrl; + } + + private void SerializeOrderAttribute(AmazonPayOrderAttribute attribute, Order order) + { + if (attribute != null) + { + var sb = new StringBuilder(); + using (var writer = new StringWriter(sb)) + { + var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); + serializer.Serialize(writer, attribute); + + _genericAttributeService.SaveAttribute(order, AmazonPayPlugin.SystemName + ".OrderAttribute", sb.ToString(), order.StoreId); + } + } + } + + private AmazonPayOrderAttribute DeserializeOrderAttribute(Order order) + { + var serialized = order.GetAttribute(AmazonPayPlugin.SystemName + ".OrderAttribute", _genericAttributeService, order.StoreId); + + if (!serialized.HasValue()) + { + var attribute = new AmazonPayOrderAttribute(); + + // legacy < v.1.14 + attribute.OrderReferenceId = order.GetAttribute(AmazonPayPlugin.SystemName + ".OrderReferenceId", order.StoreId); + + return attribute; + } + + using (var reader = new StringReader(serialized)) + { + var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); + return (AmazonPayOrderAttribute)serializer.Deserialize(reader); + } + } + + private void AddOrderNote(AmazonPaySettings settings, Order order, string anyString = null, bool isIpn = false) + { + try + { + if (!settings.AddOrderNotes || order == null || anyString.IsEmpty()) + return; + + var sb = new StringBuilder(); + var faviconUrl = "{0}Plugins/{1}/Content/images/favicon.png".FormatInvariant(_services.WebHelper.GetStoreLocation(), AmazonPayPlugin.SystemName); + + sb.AppendFormat("", faviconUrl); + sb.Append(T("Plugins.Payments.AmazonPay.AmazonDataProcessed")); + sb.Append(":
"); + sb.Append(anyString); + + if (isIpn) + { + order.HasNewPaymentNotification = true; + } + + order.OrderNotes.Add(new OrderNote + { + Note = sb.ToString(), + DisplayToCustomer = false, + CreatedOnUtc = DateTime.UtcNow + }); + + _orderService.UpdateOrder(order); + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + + private Regions.currencyCode ConvertCurrency(string currencyCode) + { + switch (currencyCode.EmptyNull().ToLower()) + { + case "usd": + return Regions.currencyCode.USD; + case "gbp": + return Regions.currencyCode.GBP; + case "jpy": + return Regions.currencyCode.JPY; + case "aud": + return Regions.currencyCode.AUD; + case "zar": + return Regions.currencyCode.ZAR; + case "chf": + return Regions.currencyCode.CHF; + case "nok": + return Regions.currencyCode.NOK; + case "dkk": + return Regions.currencyCode.DKK; + case "sek": + return Regions.currencyCode.SEK; + case "nzd": + return Regions.currencyCode.NZD; + case "hkd": + return Regions.currencyCode.HKD; + default: + return Regions.currencyCode.EUR; + } + } + + private Address CreateAddress( + string email, + string buyerName, + string addressLine1, + string addressLine2, + string addressLine3, + string city, + string postalCode, + string phone, + string countryCode, + string stateRegion, + string county, + string destrict, + out bool countryAllowsShipping, + out bool countryAllowsBilling) + { + countryAllowsShipping = countryAllowsBilling = true; + + var address = new Address(); + address.CreatedOnUtc = DateTime.UtcNow; + address.Email = email; + address.ToFirstAndLastName(buyerName); + address.Address1 = addressLine1.EmptyNull().Trim().Truncate(4000); + address.Address2 = addressLine2.EmptyNull().Trim().Truncate(4000); + address.Address2 = address.Address2.Grow(addressLine3.EmptyNull().Trim(), ", ").Truncate(4000); + address.City = county.Grow(destrict, " ").Grow(city, " ").EmptyNull().Trim().Truncate(4000); + address.ZipPostalCode = postalCode.EmptyNull().Trim().Truncate(4000); + address.PhoneNumber = phone.EmptyNull().Trim().Truncate(4000); + + if (countryCode.HasValue()) + { + var country = _countryService.GetCountryByTwoOrThreeLetterIsoCode(countryCode); + if (country != null) + { + address.CountryId = country.Id; + countryAllowsShipping = country.AllowsShipping; + countryAllowsBilling = country.AllowsBilling; + } + } + + if (stateRegion.HasValue()) + { + var stateProvince = _stateProvinceService.GetStateProvinceByAbbreviation(stateRegion); + if (stateProvince != null) + { + address.StateProvinceId = stateProvince.Id; + } + } + + // Normalize. + if (address.Address1.IsEmpty() && address.Address2.HasValue()) + { + address.Address1 = address.Address2; + address.Address2 = null; + } + else if (address.Address1.HasValue() && address.Address1 == address.Address2) + { + address.Address2 = null; + } + + if (address.CountryId == 0) + { + address.CountryId = null; + } + + if (address.StateProvinceId == 0) + { + address.StateProvinceId = null; + } + + return address; + } + + //private void GetAddress(OrderReferenceDetailsResponse details, Address address, out bool countryAllowsShipping, out bool countryAllowsBilling) + //{ + // countryAllowsShipping = countryAllowsBilling = true; + + // address.Email = details.GetEmail(); + // address.ToFirstAndLastName(details.GetBuyerName()); + // address.Address1 = details.GetAddressLine1().EmptyNull().Trim().Truncate(4000); + // address.Address2 = details.GetAddressLine2().EmptyNull().Trim().Truncate(4000); + // address.Address2 = address.Address2.Grow(details.GetAddressLine3().EmptyNull().Trim(), ", ").Truncate(4000); + // address.City = details.GetCity().EmptyNull().Trim().Truncate(4000); + // address.ZipPostalCode = details.GetPostalCode().EmptyNull().Trim().Truncate(4000); + // address.PhoneNumber = details.GetPhone().EmptyNull().Trim().Truncate(4000); + + // var countryCode = details.GetCountryCode(); + // if (countryCode.HasValue()) + // { + // var country = _countryService.GetCountryByTwoOrThreeLetterIsoCode(countryCode); + // if (country != null) + // { + // address.CountryId = country.Id; + // countryAllowsShipping = country.AllowsShipping; + // countryAllowsBilling = country.AllowsBilling; + // } + // } + + // var stateRegion = details.GetStateOrRegion(); + // if (stateRegion.HasValue()) + // { + // var stateProvince = _stateProvinceService.GetStateProvinceByAbbreviation(stateRegion); + // if (stateProvince != null) + // { + // address.StateProvinceId = stateProvince.Id; + // } + // } + + // // Normalize. + // if (address.Address1.IsEmpty() && address.Address2.HasValue()) + // { + // address.Address1 = address.Address2; + // address.Address2 = null; + // } + // else if (address.Address1.HasValue() && address.Address1 == address.Address2) + // { + // address.Address2 = null; + // } + + // if (address.CountryId == 0) + // { + // address.CountryId = null; + // } + + // if (address.StateProvinceId == 0) + // { + // address.StateProvinceId = null; + // } + //} + + //private bool FindAndApplyAddress(OrderReferenceDetailsResponse details, Customer customer, bool isShippable, bool forceToTakeAmazonAddress) + //{ + // // PlaceOrder requires billing address but we don't get one from Amazon here. so use shipping address instead until we get it from amazon. + // var countryAllowsShipping = true; + // var countryAllowsBilling = true; + + // var address = new Address(); + // address.CreatedOnUtc = DateTime.UtcNow; + + // GetAddress(details, address, out countryAllowsShipping, out countryAllowsBilling); + + // if (isShippable && !countryAllowsShipping) + // return false; + + // if (address.Email.IsEmpty()) + // { + // address.Email = customer.Email; + // } + + // if (forceToTakeAmazonAddress) + // { + // // First time to get in touch with an amazon address. + // var existingAddress = customer.Addresses.ToList().FindAddress(address, true); + // if (existingAddress == null) + // { + // customer.Addresses.Add(address); + // customer.BillingAddress = address; + // } + // else + // { + // customer.BillingAddress = existingAddress; + // } + // } + // else + // { + // if (customer.BillingAddress == null) + // { + // customer.Addresses.Add(address); + // customer.BillingAddress = address; + // } + + // GetAddress(details, customer.BillingAddress, out countryAllowsShipping, out countryAllowsBilling); + + // // But now we could have dublicates. + // var newAddressId = customer.BillingAddress.Id; + // var addresses = customer.Addresses.Where(x => x.Id != newAddressId).ToList(); + + // var existingAddress = addresses.FindAddress(customer.BillingAddress, false); + // if (existingAddress != null) + // { + // // Remove the new and take the old one. + // customer.RemoveAddress(customer.BillingAddress); + // customer.BillingAddress = existingAddress; + + // try + // { + // _addressService.DeleteAddress(newAddressId); + // } + // catch (Exception exception) + // { + // exception.Dump(); + // } + // } + // } + + // customer.ShippingAddress = (isShippable ? customer.BillingAddress : null); + + // return true; + //} + + private AmazonPayData GetDetails(AuthorizeResponse response) + { + var data = new AmazonPayData(); + data.MessageType = "GetAuthorizationDetails"; + data.MessageId = response.GetRequestId(); + data.AuthorizationId = response.GetAuthorizationId(); + data.ReferenceId = response.GetAuthorizationReferenceId(); + + var ids = response.GetCaptureIdList(); + if (ids.Any()) + { + data.CaptureId = ids.First(); + } + + data.Fee = new AmazonPayPrice(response.GetAuthorizationFee(), response.GetAuthorizationFeeCurrencyCode()); + data.AuthorizedAmount = new AmazonPayPrice(response.GetAuthorizationAmount(), response.GetAuthorizationAmountCurrencyCode()); + data.CapturedAmount = new AmazonPayPrice(response.GetCapturedAmount(), response.GetCapturedAmountCurrencyCode()); + data.CaptureNow = response.GetCaptureNow(); + data.Creation = response.GetCreationTimestamp(); + data.Expiration = response.GetExpirationTimestamp(); + data.ReasonCode = response.GetReasonCode(); + data.ReasonDescription = response.GetReasonDescription(); + data.State = response.GetAuthorizationState(); + data.StateLastUpdate = response.GetLastUpdateTimestamp(); + + return data; + } + private AmazonPayData GetDetails(CaptureResponse response) + { + var data = new AmazonPayData(); + data.MessageType = "GetCaptureDetails"; + data.MessageId = response.GetRequestId(); + data.CaptureId = response.GetCaptureId(); + data.ReferenceId = response.GetCaptureReferenceId(); + data.Fee = new AmazonPayPrice(response.GetCaptureFee(), response.GetCaptureFeeCurrencyCode()); + data.CapturedAmount = new AmazonPayPrice(response.GetCaptureAmount(), response.GetCaptureAmountCurrencyCode()); + data.Creation = response.GetCreationTimestamp(); + data.ReasonCode = response.GetReasonCode(); + data.ReasonDescription = response.GetReasonDescription(); + data.State = response.GetCaptureState(); + data.StateLastUpdate = response.GetLastUpdatedTimestamp(); + + return data; + } + private AmazonPayData GetDetails(RefundResponse response) + { + var data = new AmazonPayData(); + data.MessageType = "GetRefundDetails"; + data.MessageId = response.GetRequestId(); + data.ReferenceId = response.GetRefundReferenceId(); + data.Creation = response.GetCreationTimestamp(); + data.Fee = new AmazonPayPrice(response.GetRefundFee(), response.GetRefundFeeCurrencyCode()); + data.RefundId = response.GetAmazonRefundId(); + data.RefundedAmount = new AmazonPayPrice(response.GetRefundAmount(), response.GetRefundAmountCurrencyCode()); + data.ReasonCode = response.GetReasonCode(); + data.ReasonDescription = response.GetReasonDescription(); + data.State = response.GetRefundState(); + data.StateLastUpdate = response.GetLastUpdateTimestamp(); + + return data; + } + + private string GetRandomId(string prefix) + { + var str = prefix + CommonHelper.GenerateRandomDigitCode(20); + return str.Truncate(32); + } + + private string LogError(DelegateRequest request, IResponse response, bool isWarning = false) + { var message = string.Empty; if (response != null) @@ -442,137 +442,137 @@ private string LogError(DelegateRequest request, IResponse response, bool Logger.Log(isWarning ? LogLevel.Warning : LogLevel.Error, new Exception(response.GetJson()), message, null); } - return message; - } - - private string ToInfoString(AmazonPayData data) - { - var sb = new StringBuilder(); - - try - { - var strings = _services.Localization.GetResource("Plugins.Payments.AmazonPay.MessageStrings").SplitSafe(";"); - var state = data.State.Grow(data.ReasonCode, " "); - - if (data.ReasonDescription.HasValue()) - state = $"{state} ({data.ReasonDescription})"; - - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.MessageTyp)}: {data.MessageType.NaIfEmpty()}"); - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.State)}: {state}"); - - var stateDate = _dateTimeHelper.ConvertToUserTime(data.StateLastUpdate, DateTimeKind.Utc); - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.StateUpdate)}: {stateDate.ToString()}"); - - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.MessageId)}: {data.MessageId.NaIfEmpty()}"); - - if (data.AuthorizationId.HasValue()) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.AuthorizationID)}: {data.AuthorizationId}"); - - if (data.CaptureId.HasValue()) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CaptureID)}: {data.CaptureId}"); - - if (data.RefundId.HasValue()) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.RefundID)}: {data.RefundId}"); - - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.ReferenceID)}: {data.ReferenceId.NaIfEmpty()}"); - - if (data.Fee != null && data.Fee.Amount != decimal.Zero) - { - var signed = data.MessageType.IsCaseInsensitiveEqual("RefundNotification") || data.MessageType.IsCaseInsensitiveEqual("GetRefundDetails") ? "-" : ""; - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Fee)}: {signed}"); - } - - if (data.AuthorizedAmount != null && data.AuthorizedAmount.Amount != decimal.Zero) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.AuthorizedAmount)}: {data.AuthorizedAmount.ToString()}"); - - if (data.CapturedAmount != null && data.CapturedAmount.Amount != decimal.Zero) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CapturedAmount)}: {data.CapturedAmount.ToString()}"); - - if (data.RefundedAmount != null && data.RefundedAmount.Amount != decimal.Zero) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.RefundedAmount)}: {data.RefundedAmount.ToString()}"); - - if (data.CaptureNow.HasValue) - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CaptureNow)}: {data.CaptureNow.Value.ToString()}"); - - var creationDate = _dateTimeHelper.ConvertToUserTime(data.Creation, DateTimeKind.Utc); - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Creation)}: {creationDate.ToString()}"); - - if (data.Expiration.HasValue) - { - var expirationDate = _dateTimeHelper.ConvertToUserTime(data.Expiration.Value, DateTimeKind.Utc); - sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Expiration)}: {expirationDate.ToString()}"); - } - } - catch (Exception exception) - { - exception.Dump(); - } - - return sb.ToString(); - } - - /// - /// Creates an API client. - /// - /// AmazonPay settings - /// Currency code of primary store currency - /// AmazonPay client - private Client CreateClient(AmazonPaySettings settings) - { - var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(AmazonPayPlugin.SystemName); - var appVersion = descriptor != null ? descriptor.Version.ToString() : "1.0"; - - Regions.supportedRegions region; - switch (settings.Marketplace.EmptyNull().ToLower()) - { - case "us": - region = Regions.supportedRegions.us; - break; - case "uk": - region = Regions.supportedRegions.uk; - break; - case "jp": - region = Regions.supportedRegions.jp; - break; - default: - region = Regions.supportedRegions.de; - break; - } - - var config = new Configuration() - .WithAccessKey(settings.AccessKey) - .WithClientId(settings.ClientId) - .WithSecretKey(settings.SecretKey) - .WithSandbox(settings.UseSandbox) - .WithApplicationName("SmartStore.Net " + AmazonPayPlugin.SystemName) - .WithApplicationVersion(appVersion) - .WithRegion(region); - - var client = new Client(config); - return client; - } - - private AuthorizeResponse AuthorizePayment( - AmazonPaySettings settings, - AmazonPayCheckoutState state, - Store store, - ProcessPaymentRequest request, - Client client, - bool synchronously, + return message; + } + + private string ToInfoString(AmazonPayData data) + { + var sb = new StringBuilder(); + + try + { + var strings = _services.Localization.GetResource("Plugins.Payments.AmazonPay.MessageStrings").SplitSafe(";"); + var state = data.State.Grow(data.ReasonCode, " "); + + if (data.ReasonDescription.HasValue()) + state = $"{state} ({data.ReasonDescription})"; + + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.MessageTyp)}: {data.MessageType.NaIfEmpty()}"); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.State)}: {state}"); + + var stateDate = _dateTimeHelper.ConvertToUserTime(data.StateLastUpdate, DateTimeKind.Utc); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.StateUpdate)}: {stateDate.ToString()}"); + + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.MessageId)}: {data.MessageId.NaIfEmpty()}"); + + if (data.AuthorizationId.HasValue()) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.AuthorizationID)}: {data.AuthorizationId}"); + + if (data.CaptureId.HasValue()) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CaptureID)}: {data.CaptureId}"); + + if (data.RefundId.HasValue()) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.RefundID)}: {data.RefundId}"); + + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.ReferenceID)}: {data.ReferenceId.NaIfEmpty()}"); + + if (data.Fee != null && data.Fee.Amount != decimal.Zero) + { + var signed = data.MessageType.IsCaseInsensitiveEqual("RefundNotification") || data.MessageType.IsCaseInsensitiveEqual("GetRefundDetails") ? "-" : ""; + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Fee)}: {signed}"); + } + + if (data.AuthorizedAmount != null && data.AuthorizedAmount.Amount != decimal.Zero) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.AuthorizedAmount)}: {data.AuthorizedAmount.ToString()}"); + + if (data.CapturedAmount != null && data.CapturedAmount.Amount != decimal.Zero) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CapturedAmount)}: {data.CapturedAmount.ToString()}"); + + if (data.RefundedAmount != null && data.RefundedAmount.Amount != decimal.Zero) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.RefundedAmount)}: {data.RefundedAmount.ToString()}"); + + if (data.CaptureNow.HasValue) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CaptureNow)}: {data.CaptureNow.Value.ToString()}"); + + var creationDate = _dateTimeHelper.ConvertToUserTime(data.Creation, DateTimeKind.Utc); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Creation)}: {creationDate.ToString()}"); + + if (data.Expiration.HasValue) + { + var expirationDate = _dateTimeHelper.ConvertToUserTime(data.Expiration.Value, DateTimeKind.Utc); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Expiration)}: {expirationDate.ToString()}"); + } + } + catch (Exception exception) + { + exception.Dump(); + } + + return sb.ToString(); + } + + /// + /// Creates an API client. + /// + /// AmazonPay settings + /// Currency code of primary store currency + /// AmazonPay client + private Client CreateClient(AmazonPaySettings settings) + { + var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(AmazonPayPlugin.SystemName); + var appVersion = descriptor != null ? descriptor.Version.ToString() : "1.0"; + + Regions.supportedRegions region; + switch (settings.Marketplace.EmptyNull().ToLower()) + { + case "us": + region = Regions.supportedRegions.us; + break; + case "uk": + region = Regions.supportedRegions.uk; + break; + case "jp": + region = Regions.supportedRegions.jp; + break; + default: + region = Regions.supportedRegions.de; + break; + } + + var config = new Configuration() + .WithAccessKey(settings.AccessKey) + .WithClientId(settings.ClientId) + .WithSecretKey(settings.SecretKey) + .WithSandbox(settings.UseSandbox) + .WithApplicationName("Smartstore " + AmazonPayPlugin.SystemName) + .WithApplicationVersion(appVersion) + .WithRegion(region); + + var client = new Client(config); + return client; + } + + private AuthorizeResponse AuthorizePayment( + AmazonPaySettings settings, + AmazonPayCheckoutState state, + Store store, + ProcessPaymentRequest request, + Client client, + bool synchronously, out AuthorizeRequest authRequest) - { - authRequest = new AuthorizeRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId) - .WithAuthorizationReferenceId(GetRandomId("Authorize")) - .WithCaptureNow(settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture) - .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) - .WithAmount(request.OrderTotal); - - if (synchronously) - { - authRequest = authRequest.WithTransactionTimeout(0); - } + { + authRequest = new AuthorizeRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithAuthorizationReferenceId(GetRandomId("Authorize")) + .WithCaptureNow(settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithAmount(request.OrderTotal); + + if (synchronously) + { + authRequest = authRequest.WithTransactionTimeout(0); + } // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"InvalidPaymentMethod", "PaymentMethodUpdateTimeInMins":5}} @@ -584,7 +584,7 @@ private AuthorizeResponse AuthorizePayment( //} var authResponse = client.Authorize(authRequest); - return authResponse; - } - } + return authResponse; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs index 8836575653..a2ade0617a 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs @@ -5,11 +5,11 @@ namespace SmartStore.AmazonPay.Services { - [Serializable] - public class AmazonPayCheckoutState - { - public string OrderReferenceId { get; set; } - public string AccessToken { get; set; } + [Serializable] + public class AmazonPayCheckoutState + { + public string OrderReferenceId { get; set; } + public string AccessToken { get; set; } // Confirmation flow. public bool DetailsSet { get; set; } @@ -19,31 +19,31 @@ public class AmazonPayCheckoutState } - public class AmazonPayActionState - { - public Guid OrderGuid { get; set; } - public List Errors { get; set; } - } + public class AmazonPayActionState + { + public Guid OrderGuid { get; set; } + public List Errors { get; set; } + } - [Serializable] - public class AmazonPayOrderAttribute - { - public string OrderReferenceId { get; set; } - public bool OrderReferenceClosed { get; set; } - } + [Serializable] + public class AmazonPayOrderAttribute + { + public string OrderReferenceId { get; set; } + public bool OrderReferenceClosed { get; set; } + } - internal class PollingLoopData - { - public PollingLoopData(int orderId) - { - OrderId = orderId; - } + internal class PollingLoopData + { + public PollingLoopData(int orderId) + { + OrderId = orderId; + } - public int OrderId { get; private set; } - public Order Order { get; set; } - public AmazonPaySettings Settings { get; set; } - public Client Client { get; set; } - } + public int OrderId { get; private set; } + public Order Order { get; set; } + public AmazonPaySettings Settings { get; set; } + public Client Client { get; set; } + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs index f4adc780e2..05e0589571 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs @@ -9,16 +9,16 @@ namespace SmartStore.AmazonPay.Services { public partial interface IAmazonPayService : IExternalProviderAuthorizer - { - void SetupConfiguration(ConfigurationModel model); + { + void SetupConfiguration(ConfigurationModel model); - AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDictionary tempData); + AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDictionary tempData); - void CloseOrderReference(AmazonPaySettings settings, Order order); + void CloseOrderReference(AmazonPaySettings settings, Order order); - void AddCustomerOrderNoteLoop(AmazonPayActionState state); + void AddCustomerOrderNoteLoop(AmazonPayActionState state); - void GetBillingAddress(); + void GetBillingAddress(); bool SetOrderReferenceDetails( Client client, @@ -29,20 +29,20 @@ bool SetOrderReferenceDetails( bool ConfirmOrderReference(); - ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request); + ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request); - void PostProcessPayment(PostProcessPaymentRequest request); + void PostProcessPayment(PostProcessPaymentRequest request); - CapturePaymentResult Capture(CapturePaymentRequest request); + CapturePaymentResult Capture(CapturePaymentRequest request); - RefundPaymentResult Refund(RefundPaymentRequest request); + RefundPaymentResult Refund(RefundPaymentRequest request); - VoidPaymentResult Void(VoidPaymentRequest request); + VoidPaymentResult Void(VoidPaymentRequest request); - void ProcessIpn(HttpRequestBase request); + void ProcessIpn(HttpRequestBase request); - void StartDataPolling(); + void StartDataPolling(); - void ShareKeys(string payload, int storeId); - } + void ShareKeys(string payload, int storeId); + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs index 8df3aa6d48..5a4b3a4f25 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs @@ -2,39 +2,39 @@ namespace SmartStore.AmazonPay.Services.Internal { - internal class AmazonPayData - { - public string MessageType { get; set; } - public string MessageId { get; set; } - public string AuthorizationId { get; set; } - public string CaptureId { get; set; } - public string RefundId { get; set; } - public string ReferenceId { get; set; } + internal class AmazonPayData + { + public string MessageType { get; set; } + public string MessageId { get; set; } + public string AuthorizationId { get; set; } + public string CaptureId { get; set; } + public string RefundId { get; set; } + public string ReferenceId { get; set; } - public string ReasonCode { get; set; } - public string ReasonDescription { get; set; } - public string State { get; set; } - public DateTime StateLastUpdate { get; set; } + public string ReasonCode { get; set; } + public string ReasonDescription { get; set; } + public string State { get; set; } + public DateTime StateLastUpdate { get; set; } - public AmazonPayPrice Fee { get; set; } - public AmazonPayPrice AuthorizedAmount { get; set; } - public AmazonPayPrice CapturedAmount { get; set; } - public AmazonPayPrice RefundedAmount { get; set; } + public AmazonPayPrice Fee { get; set; } + public AmazonPayPrice AuthorizedAmount { get; set; } + public AmazonPayPrice CapturedAmount { get; set; } + public AmazonPayPrice RefundedAmount { get; set; } - public bool? CaptureNow { get; set; } - public DateTime Creation { get; set; } - public DateTime? Expiration { get; set; } + public bool? CaptureNow { get; set; } + public DateTime Creation { get; set; } + public DateTime? Expiration { get; set; } - public string AnyAmazonId - { - get - { - if (CaptureId.HasValue()) - return CaptureId; - if (AuthorizationId.HasValue()) - return AuthorizationId; - return RefundId; - } - } - } + public string AnyAmazonId + { + get + { + if (CaptureId.HasValue()) + return CaptureId; + if (AuthorizationId.HasValue()) + return AuthorizationId; + return RefundId; + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs index 1eb4378dfa..0956b119b8 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs @@ -10,132 +10,132 @@ namespace SmartStore.AmazonPay { - internal static class AmazonPayExtensions - { - internal static void ToFirstAndLastName(this string name, out string firstName, out string lastName) - { - if (!string.IsNullOrWhiteSpace(name)) - { - int index = name.LastIndexOf(' '); - if (index == -1) - { - firstName = ""; - lastName = name; - } - else - { - firstName = name.Substring(0, index); - lastName = name.Substring(index + 1); - } - - firstName = firstName.EmptyNull().Truncate(4000); - lastName = lastName.EmptyNull().Truncate(4000); - } - else - { - firstName = lastName = ""; - } - } - - internal static void ToFirstAndLastName(this Address address, string name) - { - string firstName, lastName; - name.ToFirstAndLastName(out firstName, out lastName); - - address.FirstName = firstName; - address.LastName = lastName; - } - - internal static Address FindAddress(this List
addresses, Address address, bool uncompleteToo) - { - var match = addresses.FindAddress(address.FirstName, address.LastName, - address.PhoneNumber, address.Email, address.FaxNumber, address.Company, - address.Address1, address.Address2, - address.City, address.StateProvinceId, address.ZipPostalCode, address.CountryId); - - if (match == null && uncompleteToo) - { - // Compare with ToAddress - match = addresses.FirstOrDefault(x => - x.FirstName == null && x.LastName == null && - x.Address1 == null && x.Address2 == null && - x.City == address.City && x.ZipPostalCode == address.ZipPostalCode && - x.PhoneNumber == null && - x.CountryId == address.CountryId && x.StateProvinceId == address.StateProvinceId - ); - } - - return match; - } - - internal static string ToAmazonLanguageCode(this string twoLetterLanguageCode, char delimiter = '-') - { - switch (twoLetterLanguageCode.EmptyNull().ToLower()) - { - case "en": - return $"en{delimiter}GB"; - case "fr": - return $"fr{delimiter}FR"; - case "it": - return $"it{delimiter}IT"; - case "es": - return $"es{delimiter}ES"; - case "de": - default: - return $"de{delimiter}DE"; - } - } - - internal static bool HasAmazonPayState(this HttpContextBase httpContext) - { - var checkoutState = httpContext.GetCheckoutState(); - var checkoutStateKey = AmazonPayPlugin.SystemName + ".CheckoutState"; - - if (checkoutState != null && checkoutState.CustomProperties.ContainsKey(checkoutStateKey)) - { - var state = checkoutState.CustomProperties[checkoutStateKey] as AmazonPayCheckoutState; - - return state != null && state.AccessToken.HasValue(); - } - - return false; - } - - internal static AmazonPayCheckoutState GetAmazonPayState(this HttpContextBase httpContext, ILocalizationService localizationService) - { - var checkoutState = httpContext.GetCheckoutState(); - - if (checkoutState == null) - throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); - - var state = checkoutState.CustomProperties.Get(AmazonPayPlugin.SystemName + ".CheckoutState") as AmazonPayCheckoutState; - - if (state == null) - throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); - - return state; - } + internal static class AmazonPayExtensions + { + internal static void ToFirstAndLastName(this string name, out string firstName, out string lastName) + { + if (!string.IsNullOrWhiteSpace(name)) + { + int index = name.LastIndexOf(' '); + if (index == -1) + { + firstName = ""; + lastName = name; + } + else + { + firstName = name.Substring(0, index); + lastName = name.Substring(index + 1); + } + + firstName = firstName.EmptyNull().Truncate(4000); + lastName = lastName.EmptyNull().Truncate(4000); + } + else + { + firstName = lastName = ""; + } + } + + internal static void ToFirstAndLastName(this Address address, string name) + { + string firstName, lastName; + name.ToFirstAndLastName(out firstName, out lastName); + + address.FirstName = firstName; + address.LastName = lastName; + } + + internal static Address FindAddress(this List
addresses, Address address, bool uncompleteToo) + { + var match = addresses.FindAddress(address.FirstName, address.LastName, + address.PhoneNumber, address.Email, address.FaxNumber, address.Company, + address.Address1, address.Address2, + address.City, address.StateProvinceId, address.ZipPostalCode, address.CountryId); + + if (match == null && uncompleteToo) + { + // Compare with ToAddress + match = addresses.FirstOrDefault(x => + x.FirstName == null && x.LastName == null && + x.Address1 == null && x.Address2 == null && + x.City == address.City && x.ZipPostalCode == address.ZipPostalCode && + x.PhoneNumber == null && + x.CountryId == address.CountryId && x.StateProvinceId == address.StateProvinceId + ); + } + + return match; + } + + internal static string ToAmazonLanguageCode(this string twoLetterLanguageCode, char delimiter = '-') + { + switch (twoLetterLanguageCode.EmptyNull().ToLower()) + { + case "en": + return $"en{delimiter}GB"; + case "fr": + return $"fr{delimiter}FR"; + case "it": + return $"it{delimiter}IT"; + case "es": + return $"es{delimiter}ES"; + case "de": + default: + return $"de{delimiter}DE"; + } + } + + internal static bool HasAmazonPayState(this HttpContextBase httpContext) + { + var checkoutState = httpContext.GetCheckoutState(); + var checkoutStateKey = AmazonPayPlugin.SystemName + ".CheckoutState"; + + if (checkoutState != null && checkoutState.CustomProperties.ContainsKey(checkoutStateKey)) + { + var state = checkoutState.CustomProperties[checkoutStateKey] as AmazonPayCheckoutState; + + return state != null && state.AccessToken.HasValue(); + } + + return false; + } + + internal static AmazonPayCheckoutState GetAmazonPayState(this HttpContextBase httpContext, ILocalizationService localizationService) + { + var checkoutState = httpContext.GetCheckoutState(); + + if (checkoutState == null) + throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); + + var state = checkoutState.CustomProperties.Get(AmazonPayPlugin.SystemName + ".CheckoutState") as AmazonPayCheckoutState; + + if (state == null) + throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); + + return state; + } internal static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) - { - // S02-9777218-8608106 OrderReferenceId - // S02-9777218-8608106-A088344 Auth ID - // S02-9777218-8608106-C088344 Capture ID - - if (amazonId.HasValue()) - { - string amazonOrderReferenceId = amazonId.Substring(0, amazonId.LastIndexOf('-')); - if (amazonOrderReferenceId.HasValue()) - { - var orders = orderRepository.Table - .Where(x => x.PaymentMethodSystemName == AmazonPayPlugin.SystemName && x.AuthorizationTransactionId.StartsWith(amazonOrderReferenceId)) - .ToList(); - - if (orders.Count() == 1) - return orders.FirstOrDefault(); - } - } - return null; - } - } + { + // S02-9777218-8608106 OrderReferenceId + // S02-9777218-8608106-A088344 Auth ID + // S02-9777218-8608106-C088344 Capture ID + + if (amazonId.HasValue()) + { + string amazonOrderReferenceId = amazonId.Substring(0, amazonId.LastIndexOf('-')); + if (amazonOrderReferenceId.HasValue()) + { + var orders = orderRepository.Table + .Where(x => x.PaymentMethodSystemName == AmazonPayPlugin.SystemName && x.AuthorizationTransactionId.StartsWith(amazonOrderReferenceId)) + .ToList(); + + if (orders.Count() == 1) + return orders.FirstOrDefault(); + } + } + return null; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs index cfa3fef830..2648d382c9 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs @@ -2,21 +2,21 @@ namespace SmartStore.AmazonPay.Services.Internal { - internal class AmazonPayPrice - { - public AmazonPayPrice(decimal amount, string currencyCode) - { - Amount = amount; - CurrencyCode = currencyCode; - } + internal class AmazonPayPrice + { + public AmazonPayPrice(decimal amount, string currencyCode) + { + Amount = amount; + CurrencyCode = currencyCode; + } - public decimal Amount { get; private set; } - public string CurrencyCode { get; private set; } + public decimal Amount { get; private set; } + public string CurrencyCode { get; private set; } - public override string ToString() - { - var str = Amount.ToString("0.00", CultureInfo.InvariantCulture); - return str.Grow(CurrencyCode, " "); - } - } + public override string ToString() + { + var str = Amount.ToString("0.00", CultureInfo.InvariantCulture); + return str.Grow(CurrencyCode, " "); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj index 8d6b74f6b6..90e38591eb 100644 --- a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj +++ b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj @@ -18,7 +18,7 @@ Properties SmartStore.AmazonPay SmartStore.AmazonPay - v4.6.1 + v4.7.2 512 @@ -81,40 +81,50 @@ ..\..\packages\AmazonPay.3.5.0\lib\net20\AmazonPay.dll True - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll False - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll False ..\..\packages\Common.Logging.2.0.0\lib\2.0\Common.Logging.dll True - - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + False ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - False + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + - + ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll False @@ -291,12 +301,6 @@ - - - Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}". - - - - + @@ -62,7 +62,7 @@ - + @@ -78,19 +78,15 @@ - + - - - - - + @@ -126,16 +122,12 @@ - + - - - - @@ -160,11 +152,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs index f336662023..0734edd100 100644 --- a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs +++ b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs @@ -3,27 +3,21 @@ namespace SmartStore.Clickatell { - public class AdminMenu : AdminMenuProvider - { - protected override void BuildMenuCore(TreeNode pluginsNode) - { - var menuItem = new MenuItem().ToBuilder() - .Text("Clickatell SMS Provider") - .ResKey("Plugins.FriendlyName.SmartStore.Clickatell") - .Icon("far fa-paper-plane") - .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.Clickatell", area = "Admin" }) - .ToItem(); + public class AdminMenu : AdminMenuProvider + { + protected override void BuildMenuCore(TreeNode pluginsNode) + { + var menuItem = new MenuItem().ToBuilder() + .Text("Clickatell SMS Provider") + .ResKey("Plugins.FriendlyName.SmartStore.Clickatell") + .Icon("far fa-paper-plane") + .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.Clickatell", area = "Admin" }) + .ToItem(); - pluginsNode.Prepend(menuItem); - } + pluginsNode.Prepend(menuItem); + } - public override int Ordinal - { - get - { - return 100; - } - } + public override int Ordinal => 100; - } + } } diff --git a/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs b/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs index cd183ffb6b..2c6a23810d 100644 --- a/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs +++ b/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs @@ -12,35 +12,29 @@ namespace SmartStore.Clickatell { - public class ClickatellSmsProvider : BasePlugin, IConfigurable + public class ClickatellSmsProvider : BasePlugin, IConfigurable { private readonly ClickatellSettings _clickatellSettings; private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; + private readonly ILogger _logger; - public ClickatellSmsProvider( - ClickatellSettings clickatellSettings, + public ClickatellSmsProvider( + ClickatellSettings clickatellSettings, ILocalizationService localizationService, - ILogger logger) + ILogger logger) { _clickatellSettings = clickatellSettings; _localizationService = localizationService; - _logger = logger; - } - - public static string SystemName - { - get - { - return "SmartStore.Clickatell"; - } - } - - public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + _logger = logger; + } + + public static string SystemName => "SmartStore.Clickatell"; + + public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "Configure"; controllerName = "SmsClickatell"; - routeValues = new RouteValueDictionary { { "area", SystemName } }; + routeValues = new RouteValueDictionary { { "area", SystemName } }; } public override void Install() @@ -57,77 +51,77 @@ public override void Uninstall() base.Uninstall(); } - public void SendSms(string text) - { - if (text.IsEmpty()) - return; - - string error = null; - HttpWebResponse webResponse = null; - - try - { - // https://www.clickatell.com/developers/api-documentation/rest-api-request-parameters/ - var request = (HttpWebRequest)WebRequest.Create("https://platform.clickatell.com/messages"); - request.Method = "POST"; - request.Accept = "application/json"; - request.ContentType = "application/json"; - request.Headers["Authorization"] = _clickatellSettings.ApiId; - - var data = new Dictionary - { - { "content", text }, - { "to", _clickatellSettings.PhoneNumber.SplitSafe(";") } - }; - - var json = JsonConvert.SerializeObject(data); - - // UTF8 is default encoding - var bytes = Encoding.UTF8.GetBytes(json); - request.ContentLength = bytes.Length; - - using (var stream = request.GetRequestStream()) - { - stream.Write(bytes, 0, bytes.Length); - } - - webResponse = request.GetResponse() as HttpWebResponse; - } - catch (WebException wexc) - { - webResponse = wexc.Response as HttpWebResponse; - } - catch (Exception exception) - { - error = exception.ToString(); - } - - if (webResponse != null) - { - using (var reader = new StreamReader(webResponse.GetResponseStream(), Encoding.UTF8)) - { - var rawResponse = reader.ReadToEnd(); - - if (webResponse.StatusCode == HttpStatusCode.OK || webResponse.StatusCode == HttpStatusCode.Accepted) - { - dynamic response = JObject.Parse(rawResponse); - - error = (string)response.error; - if (error.IsEmpty() && response.messages != null) - error = response.messages[0].error; - } - else - { - error = rawResponse; - } - } - } - - if (error.HasValue()) - { - _logger.Error(error); - throw new SmartException(error); - } - } - } + public void SendSms(string text) + { + if (text.IsEmpty()) + return; + + string error = null; + HttpWebResponse webResponse = null; + + try + { + // https://www.clickatell.com/developers/api-documentation/rest-api-request-parameters/ + var request = (HttpWebRequest)WebRequest.Create("https://platform.clickatell.com/messages"); + request.Method = "POST"; + request.Accept = "application/json"; + request.ContentType = "application/json"; + request.Headers["Authorization"] = _clickatellSettings.ApiId; + + var data = new Dictionary + { + { "content", text }, + { "to", _clickatellSettings.PhoneNumber.SplitSafe(";") } + }; + + var json = JsonConvert.SerializeObject(data); + + // UTF8 is default encoding + var bytes = Encoding.UTF8.GetBytes(json); + request.ContentLength = bytes.Length; + + using (var stream = request.GetRequestStream()) + { + stream.Write(bytes, 0, bytes.Length); + } + + webResponse = request.GetResponse() as HttpWebResponse; + } + catch (WebException wexc) + { + webResponse = wexc.Response as HttpWebResponse; + } + catch (Exception exception) + { + error = exception.ToString(); + } + + if (webResponse != null) + { + using (var reader = new StreamReader(webResponse.GetResponseStream(), Encoding.UTF8)) + { + var rawResponse = reader.ReadToEnd(); + + if (webResponse.StatusCode == HttpStatusCode.OK || webResponse.StatusCode == HttpStatusCode.Accepted) + { + dynamic response = JObject.Parse(rawResponse); + + error = (string)response.error; + if (error.IsEmpty() && response.messages != null) + error = response.messages[0].error; + } + else + { + error = rawResponse; + } + } + } + + if (error.HasValue()) + { + _logger.Error(error); + throw new SmartException(error); + } + } + } } diff --git a/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs b/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs index d9451d5e75..a79d2de550 100644 --- a/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs +++ b/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs @@ -10,67 +10,69 @@ namespace SmartStore.Clickatell.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class SmsClickatellController : PluginControllerBase { - private readonly IPluginFinder _pluginFinder; + private readonly IPluginFinder _pluginFinder; public SmsClickatellController(IPluginFinder pluginFinder) { _pluginFinder = pluginFinder; } - [LoadSetting] - public ActionResult Configure(ClickatellSettings settings) - { - var model = new SmsClickatellModel(); - MiniMapper.Map(settings, model); + [LoadSetting] + public ActionResult Configure(ClickatellSettings settings) + { + var model = new SmsClickatellModel(); + MiniMapper.Map(settings, model); - return View(model); + return View(model); } [HttpPost, SaveSetting, FormValueRequired("save")] + [ValidateAntiForgeryToken] public ActionResult Configure(ClickatellSettings settings, SmsClickatellModel model) { - if (!ModelState.IsValid) - { - return Configure(settings); - } + if (!ModelState.IsValid) + { + return Configure(settings); + } - MiniMapper.Map(model, settings); - settings.ApiId = model.ApiId.TrimSafe(); + MiniMapper.Map(model, settings); + settings.ApiId = model.ApiId.TrimSafe(); - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - return RedirectToConfiguration(ClickatellSmsProvider.SystemName); - } + return RedirectToConfiguration(ClickatellSmsProvider.SystemName); + } - [HttpPost, ActionName("Configure"), FormValueRequired("test-sms")] + [HttpPost, ActionName("Configure"), FormValueRequired("test-sms")] + [ValidateAntiForgeryToken] public ActionResult TestSms(SmsClickatellModel model) { try { if (model.TestMessage.IsEmpty()) { - model.TestSucceeded = false; - model.TestSmsResult = T("Plugins.Sms.Clickatell.EnterMessage"); + model.TestSucceeded = false; + model.TestSmsResult = T("Plugins.Sms.Clickatell.EnterMessage"); } else { var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName(ClickatellSmsProvider.SystemName); var plugin = pluginDescriptor.Instance() as ClickatellSmsProvider; - plugin.SendSms(model.TestMessage); + plugin.SendSms(model.TestMessage); - model.TestSucceeded = true; - model.TestSmsResult = T("Plugins.Sms.Clickatell.TestSuccess"); + model.TestSucceeded = true; + model.TestSmsResult = T("Plugins.Sms.Clickatell.TestSuccess"); } } catch (Exception exception) { - model.TestSucceeded = false; - model.TestSmsResult = T("Plugins.Sms.Clickatell.TestFailed"); - model.TestSmsDetailResult = exception.Message; + model.TestSucceeded = false; + model.TestSmsResult = T("Plugins.Sms.Clickatell.TestFailed"); + model.TestSmsDetailResult = exception.Message; } return View("Configure", model); diff --git a/src/Plugins/SmartStore.Clickatell/Description.txt b/src/Plugins/SmartStore.Clickatell/Description.txt index 003d6a572e..ed2cde9ffe 100644 --- a/src/Plugins/SmartStore.Clickatell/Description.txt +++ b/src/Plugins/SmartStore.Clickatell/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Clickatell SMS Provider SystemName: SmartStore.Clickatell Group: Mobile -Version: 3.2.2 -MinAppVersion: 3.2.2 +Version: 4.2.0 +MinAppVersion: 4.2.0 DisplayOrder: 1 FileName: SmartStore.Clickatell.dll ResourceRootKey: Plugins.Sms.Clickatell \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/Models/SmsClickatellModel.cs b/src/Plugins/SmartStore.Clickatell/Models/SmsClickatellModel.cs index e3588abc53..11e84e5925 100644 --- a/src/Plugins/SmartStore.Clickatell/Models/SmsClickatellModel.cs +++ b/src/Plugins/SmartStore.Clickatell/Models/SmsClickatellModel.cs @@ -3,10 +3,10 @@ namespace SmartStore.Clickatell.Models { - public class SmsClickatellModel : ModelBase + public class SmsClickatellModel : ModelBase { [SmartResourceDisplayName("Plugins.Sms.Clickatell.Fields.Enabled")] - public bool Enabled { get; set; } + public bool Enabled { get; set; } [SmartResourceDisplayName("Plugins.Sms.Clickatell.Fields.PhoneNumber")] public string PhoneNumber { get; set; } @@ -17,8 +17,8 @@ public class SmsClickatellModel : ModelBase [SmartResourceDisplayName("Plugins.Sms.Clickatell.Fields.TestMessage")] public string TestMessage { get; set; } - public bool TestSucceeded { get; set; } - public string TestSmsResult { get; set; } - public string TestSmsDetailResult { get; set; } - } + public bool TestSucceeded { get; set; } + public string TestSmsResult { get; set; } + public string TestSmsDetailResult { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs b/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs index 8ff1936f22..49fab1aa85 100644 --- a/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs +++ b/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs @@ -7,30 +7,30 @@ namespace SmartStore.Clickatell { - public class OrderPlacedEventConsumer : IConsumer + public class OrderPlacedEventConsumer : IConsumer { - private readonly ICommonServices _services; - private readonly ClickatellSettings _clickatellSettings; + private readonly ICommonServices _services; + private readonly ClickatellSettings _clickatellSettings; private readonly IPluginFinder _pluginFinder; private readonly IOrderService _orderService; public OrderPlacedEventConsumer( - ICommonServices services, - ClickatellSettings clickatellSettings, + ICommonServices services, + ClickatellSettings clickatellSettings, IPluginFinder pluginFinder, - IOrderService orderService) + IOrderService orderService) { - _services = services; + _services = services; _clickatellSettings = clickatellSettings; _pluginFinder = pluginFinder; _orderService = orderService; - T = NullLocalizer.Instance; - } + T = NullLocalizer.Instance; + } - public Localizer T { get; set; } + public Localizer T { get; set; } - public void HandleEvent(OrderPlacedEvent eventMessage) + public void HandleEvent(OrderPlacedEvent eventMessage) { if (!_clickatellSettings.Enabled) return; @@ -39,22 +39,22 @@ public void HandleEvent(OrderPlacedEvent eventMessage) if (descriptor == null) return; - var storeId = _services.StoreContext.CurrentStore.Id; + var storeId = _services.StoreContext.CurrentStore.Id; - if (!(storeId == 0 || _services.Settings.GetSettingByKey(descriptor.GetSettingKey("LimitedToStores")).ToIntArrayContains(storeId, true))) - return; + if (!(storeId == 0 || _services.Settings.GetSettingByKey(descriptor.GetSettingKey("LimitedToStores")).ToIntArrayContains(storeId, true))) + return; var plugin = descriptor.Instance() as ClickatellSmsProvider; if (plugin == null) return; - try - { - plugin.SendSms(T("Plugins.Sms.Clickatell.OrderPlacedMessage", eventMessage.Order.GetOrderNumber())); + try + { + plugin.SendSms(T("Plugins.Sms.Clickatell.OrderPlacedMessage", eventMessage.Order.GetOrderNumber())); - _orderService.AddOrderNote(eventMessage.Order, T("Plugins.Sms.Clickatell.SmsSentNote")); - } - catch { } + _orderService.AddOrderNote(eventMessage.Order, T("Plugins.Sms.Clickatell.SmsSentNote")); + } + catch { } } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/RouteProvider.cs b/src/Plugins/SmartStore.Clickatell/RouteProvider.cs index 1cc25597ed..6271b445db 100644 --- a/src/Plugins/SmartStore.Clickatell/RouteProvider.cs +++ b/src/Plugins/SmartStore.Clickatell/RouteProvider.cs @@ -4,24 +4,18 @@ namespace SmartStore.Clickatell { - public partial class RouteProvider : IRouteProvider + public partial class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { routes.MapRoute("SmartStore.Clickatell", - "Plugins/SmartStore.Clickatell/{action}", + "Plugins/SmartStore.Clickatell/{action}", new { controller = "SmsClickatell", action = "Configure" }, new[] { "SmartStore.Clickatell.Controllers" } ) - .DataTokens["area"] = ClickatellSmsProvider.SystemName; + .DataTokens["area"] = ClickatellSmsProvider.SystemName; } - public int Priority - { - get - { - return 0; - } - } + public int Priority => 0; } } diff --git a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj index 6a96ac359a..a7ccb144e0 100644 --- a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj +++ b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj @@ -20,7 +20,7 @@ Properties SmartStore.Clickatell SmartStore.Clickatell - v4.6.1 + v4.7.2 512 @@ -85,16 +85,16 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + False ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - False + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll @@ -105,9 +105,9 @@ - + ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll False diff --git a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml index 09992e2c48..6898a79b73 100644 --- a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml +++ b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml @@ -31,6 +31,8 @@ @using (Html.BeginForm()) { + @Html.AntiForgeryToken() +
diff --git a/src/Plugins/SmartStore.Clickatell/packages.config b/src/Plugins/SmartStore.Clickatell/packages.config index b00b3cc8dc..0470997544 100644 --- a/src/Plugins/SmartStore.Clickatell/packages.config +++ b/src/Plugins/SmartStore.Clickatell/packages.config @@ -3,7 +3,7 @@ - + - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/web.config b/src/Plugins/SmartStore.Clickatell/web.config index 81f0867c5c..06bf37d3c2 100644 --- a/src/Plugins/SmartStore.Clickatell/web.config +++ b/src/Plugins/SmartStore.Clickatell/web.config @@ -1,10 +1,10 @@ - + - - - + + + - + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + - + diff --git a/src/Plugins/SmartStore.DevTools/AdminMenu.cs b/src/Plugins/SmartStore.DevTools/AdminMenu.cs index 94a9ce5ed6..052ef44657 100644 --- a/src/Plugins/SmartStore.DevTools/AdminMenu.cs +++ b/src/Plugins/SmartStore.DevTools/AdminMenu.cs @@ -4,40 +4,40 @@ namespace SmartStore.DevTools { - public class AdminMenu : AdminMenuProvider - { - protected override void BuildMenuCore(TreeNode pluginsNode) - { - var menuItem = new MenuItem().ToBuilder() - .Text("Developer Tools") + public class AdminMenu : AdminMenuProvider + { + protected override void BuildMenuCore(TreeNode pluginsNode) + { + var menuItem = new MenuItem().ToBuilder() + .Text("Developer Tools") .ResKey("Plugins.FriendlyName.SmartStore.DevTools") .Icon("far fa-terminal") .PermissionNames(DevToolsPermissions.Read) .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.DevTools", area = "Admin" }) - .ToItem(); - - pluginsNode.Prepend(menuItem); + .ToItem(); - // uncomment to add to admin menu (see plugin sub-menu) - //var backendExtensionItem = new MenuItem().ToBuilder() - // .Text("Backend extension") - // .Icon("chart-area") - // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) - // .ToItem(); - //pluginsNode.Append(backendExtensionItem); + pluginsNode.Prepend(menuItem); - // uncomment to add a sub-menu (see plugin sub-menu) - //var subMenu = new MenuItem().ToBuilder() - // .Text("Sub Menu") - // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) - // .ToItem(); - //var subMenuNode = pluginsNode.Append(subMenu); + // uncomment to add to admin menu (see plugin sub-menu) + //var backendExtensionItem = new MenuItem().ToBuilder() + // .Text("Backend extension") + // .Icon("chart-area") + // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) + // .ToItem(); + //pluginsNode.Append(backendExtensionItem); - //var subMenuItem = new MenuItem().ToBuilder() - // .Text("Sub Menu Item 1") - // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) - // .ToItem(); - //subMenuNode.Append(subMenuItem); - } - } + // uncomment to add a sub-menu (see plugin sub-menu) + //var subMenu = new MenuItem().ToBuilder() + // .Text("Sub Menu") + // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) + // .ToItem(); + //var subMenuNode = pluginsNode.Append(subMenu); + + //var subMenuItem = new MenuItem().ToBuilder() + // .Text("Sub Menu Item 1") + // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) + // .ToItem(); + //subMenuNode.Append(subMenuItem); + } + } } diff --git a/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs b/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs index a089af9c47..1e48a0e388 100644 --- a/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs +++ b/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs @@ -14,16 +14,16 @@ namespace SmartStore.DevTools.Blocks /// You can, however, overwrite any method to fulfill your custom needs. /// [Block("sample", Icon = "far fa-terminal", FriendlyName = "Sample", DisplayOrder = 50, IsInternal = true)] // REMOVE IsInternal = true to display the block in the page builder - public class SampleBlockHandler : BlockHandlerBase - { + public class SampleBlockHandler : BlockHandlerBase + { public override SampleBlock Load(IBlockEntity entity, StoryViewMode viewMode) { var block = base.Load(entity, viewMode); - // By default 'BlockHandlerBase' stores block instance data as JSON in the 'Model' field of the 'PageStoryBlock' table. - // You can, however, store data anywhere you like and override loading behaviour in this method. + // By default 'BlockHandlerBase' stores block instance data as JSON in the 'Model' field of the 'PageStoryBlock' table. + // You can, however, store data anywhere you like and override loading behaviour in this method. - if (viewMode == StoryViewMode.Edit) + if (viewMode == StoryViewMode.Edit) { // You can prepare your model especially for the edit mode of the block, // e.g. add some SelectListItems for dropdownmenus which you will only need in the edit mode of the block @@ -40,48 +40,48 @@ public override SampleBlock Load(IBlockEntity entity, StoryViewMode viewMode) public override void Save(SampleBlock block, IBlockEntity entity) { - // By default 'BlockHandlerBase' stores block instance data as JSON in the 'Model' field of the 'PageStoryBlock' table. - // You can, however, store data anywhere you like and override persistance behaviour in this method. + // By default 'BlockHandlerBase' stores block instance data as JSON in the 'Model' field of the 'PageStoryBlock' table. + // You can, however, store data anywhere you like and override persistance behaviour in this method. - base.Save(block, entity); + base.Save(block, entity); } - /// - /// By default block templates (Edit & Public) will be searched in '{Area}\Views\Story\BlockTemplates\{BlockSystemName}' or '{Area}\Views\Shared\BlockTemplates\{BlockSystemName}', - /// while {Area} represents your plugin folder. - /// The public action can address a deviating route by overwriting RenderCore & GetRoute. - /// You can override this behaviour by e.g. calling a child action in your plugin controller instead of directly rendering a view. - /// For this to take effect you have to override both methods 'RenderCore()' and 'GetRoute()' - /// - //protected override void RenderCore(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) - //{ - // if (templates.First() == "Edit") - // { - // base.RenderCore(element, templates, htmlHelper, textWriter); - // } - // else - // { - // base.RenderByChildAction(element, templates, htmlHelper, textWriter); - // } - //} + /// + /// By default block templates (Edit & Public) will be searched in '{Area}\Views\Story\BlockTemplates\{BlockSystemName}' or '{Area}\Views\Shared\BlockTemplates\{BlockSystemName}', + /// while {Area} represents your plugin folder. + /// The public action can address a deviating route by overwriting RenderCore & GetRoute. + /// You can override this behaviour by e.g. calling a child action in your plugin controller instead of directly rendering a view. + /// For this to take effect you have to override both methods 'RenderCore()' and 'GetRoute()' + /// + //protected override void RenderCore(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) + //{ + // if (templates.First() == "Edit") + // { + // base.RenderCore(element, templates, htmlHelper, textWriter); + // } + // else + // { + // base.RenderByChildAction(element, templates, htmlHelper, textWriter); + // } + //} - //protected override RouteInfo GetRoute(IBlockContainer element, string template) - //{ - // var block = (SampleBlock)element.Block; - // return new RouteInfo("SampleBlock", "DevTools", new - // { - // block = element.Block, - // area = "SmartStore.DevTools" - // }); - //} - } + //protected override RouteInfo GetRoute(IBlockContainer element, string template) + //{ + // var block = (SampleBlock)element.Block; + // return new RouteInfo("SampleBlock", "DevTools", new + // { + // block = element.Block, + // area = "SmartStore.DevTools" + // }); + //} + } - /// - /// Any type that implements the 'IBlock' interface acts as a model. - /// - [Validator(typeof(SampleBlockValidator))] + /// + /// Any type that implements the 'IBlock' interface acts as a model. + /// + [Validator(typeof(SampleBlockValidator))] public class SampleBlock : IBlock - { + { [SmartResourceDisplayName("Plugins.SmartStore.DevTools.Block.Property")] public string Property { get; set; } diff --git a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs index 39d4ce980e..6ed295335a 100644 --- a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs +++ b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs @@ -1,5 +1,6 @@ using System.Web.Mvc; using SmartStore.ComponentModel; +using SmartStore.Core.Search.Facets; using SmartStore.Core.Security; using SmartStore.DevTools.Blocks; using SmartStore.DevTools.Models; @@ -24,6 +25,7 @@ public ActionResult Configure(ProfilerSettings settings) [AdminAuthorize, Permission(DevToolsPermissions.Update)] [HttpPost, ChildActionOnly, SaveSetting] + [ValidateAntiForgeryToken] public ActionResult Configure(ConfigurationModel model, ProfilerSettings settings) { if (!ModelState.IsValid) @@ -54,11 +56,11 @@ public ActionResult MachineName() [ChildActionOnly] public ActionResult WidgetZone(string widgetZone) { - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); if (settings.DisplayWidgetZones) - { + { ViewData["widgetZone"] = widgetZone; return View(); @@ -67,23 +69,23 @@ public ActionResult WidgetZone(string widgetZone) return new EmptyResult(); } - [ChildActionOnly] - public ActionResult SampleBlock(SampleBlock block) - { - // Do something here with your block instance and return a result that should be rendered by the Page Builder. - return View(block); - } + [ChildActionOnly] + public ActionResult SampleBlock(SampleBlock block) + { + // Do something here with your block instance and return a result that should be rendered by the Page Builder. + return View(block); + } - [AdminAuthorize, AdminThemed] - public ActionResult BackendExtension() - { - var model = new BackendExtensionModel - { - Welcome = "Hello world!" - }; + [AdminAuthorize, AdminThemed] + public ActionResult BackendExtension() + { + var model = new BackendExtensionModel + { + Welcome = "Hello world!" + }; - return View(model); - } + return View(model); + } [AdminAuthorize] public ActionResult ProductEditTab(int productId, FormCollection form) @@ -98,9 +100,16 @@ public ActionResult ProductEditTab(int productId, FormCollection form) return result; } - public ActionResult MyDemoWidget() - { - return Content("Hello world! This is a sample widget created for demonstration purposes by Dev-Tools plugin."); - } - } + public ActionResult MyDemoWidget() + { + return Content("Hello world! This is a sample widget created for demonstration purposes by Dev-Tools plugin."); + } + + [ChildActionOnly, AllowAnonymous] + public ActionResult MyCustomFacetTemplate(FacetGroup facetGroup, string templateName) + { + /// Just a "proxy" for our . + return PartialView(templateName, facetGroup); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs index e69164727e..0f51d70cd4 100644 --- a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs @@ -7,18 +7,18 @@ using SmartStore.Core.Logging; using SmartStore.DevTools.Filters; using SmartStore.DevTools.Services; -using SmartStore.Web.Controllers; +using SmartStore.Services.Search.Rendering; using SmartStore.Web.Framework.Controllers; namespace SmartStore.DevTools { - public class DependencyRegistrar : IDependencyRegistrar + public class DependencyRegistrar : IDependencyRegistrar { - public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) + public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { - if (isActiveModule) - { - builder.Register(c => + if (isActiveModule) + { + builder.Register(c => { var ctx = c.Resolve(); @@ -30,29 +30,29 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b }).InstancePerRequest(); if (DataSettings.DatabaseIsInstalled()) - { - // intercept ALL public store controller actions - builder.RegisterType().AsActionFilterFor(); - builder.RegisterType().AsActionFilterFor(); - builder.RegisterType().AsResultFilterFor(); + { + // intercept ALL public store controller actions + builder.RegisterType().AsActionFilterFor(); + builder.RegisterType().AsActionFilterFor(); + builder.RegisterType().AsResultFilterFor(); // Add an action to product detail offer actions //builder.RegisterType() // .AsActionFilterFor(x => x.ProductDetails(default(int), default(string), null)) // .InstancePerRequest(); - + //// intercept CatalogController's Product action //builder.RegisterType().AsResultFilterFor(x => x.Product(default(int), default(string))).InstancePerRequest(); //builder.RegisterType().AsActionFilterFor().InstancePerRequest(); //// intercept CheckoutController's Index action (to hijack the checkout or payment workflow) //builder.RegisterType().AsActionFilterFor(x => x.Index()).InstancePerRequest(); } - } - } - public int Order - { - get { return 1; } + // Register a custom view template for certain search facets. Register it as InstancePerRequest() if your selector has no Autofac dependencies. + //builder.RegisterType().As().InstancePerRequest(); + } } + + public int Order => 1; } } diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index 54399e9fcf..3913be2199 100644 --- a/src/Plugins/SmartStore.DevTools/Description.txt +++ b/src/Plugins/SmartStore.DevTools/Description.txt @@ -1,8 +1,8 @@ -FriendlyName: SmartStore.NET Developer Tools (MiniProfiler and other goodies) +FriendlyName: Smartstore Developer Tools (MiniProfiler and other goodies) SystemName: SmartStore.DevTools Group: Developer -Version: 3.2.2 -MinAppVersion: 3.2.2 +Version: 4.2.0 +MinAppVersion: 4.2.0 DisplayOrder: 1 FileName: SmartStore.DevTools.dll ResourceRootKey: Plugins.Developer.DevTools \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs b/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs index 404de14a40..6f7c34c4b9 100644 --- a/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs +++ b/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs @@ -5,50 +5,49 @@ using SmartStore.Core.Plugins; using SmartStore.Data; using SmartStore.Data.Setup; -using SmartStore.DevTools.Security; using SmartStore.Services; namespace SmartStore.DevTools { [DisplayOrder(10)] - [SystemName("Widgets.DevToolsDemo")] - [FriendlyName("Dev-Tools Demo Widget")] - public class DevToolsPlugin : BasePlugin, IConfigurable //, IWidget - { + [SystemName("Widgets.DevToolsDemo")] + [FriendlyName("Dev-Tools Demo Widget")] + public class DevToolsPlugin : BasePlugin, IConfigurable //, IWidget + { private readonly ICommonServices _services; - private readonly ICacheableRouteRegistrar _cacheableRouteRegistrar; + private readonly ICacheableRouteRegistrar _cacheableRouteRegistrar; - public DevToolsPlugin( + public DevToolsPlugin( ICommonServices services, - ICacheableRouteRegistrar cacheAbleRouteRegistrar) + ICacheableRouteRegistrar cacheAbleRouteRegistrar) { _services = services; - _cacheableRouteRegistrar = cacheAbleRouteRegistrar; + _cacheableRouteRegistrar = cacheAbleRouteRegistrar; - Logger = NullLogger.Instance; + Logger = NullLogger.Instance; } - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } - //public IList GetWidgetZones() => new List { "home_page_top" }; + //public IList GetWidgetZones() => new List { "home_page_top" }; - //public void GetDisplayWidgetRoute(string widgetZone, object model, int storeId, out string actionName, out string controllerName, out RouteValueDictionary routeValues) - //{ - // actionName = "MyDemoWidget"; - // controllerName = "DevTools"; + //public void GetDisplayWidgetRoute(string widgetZone, object model, int storeId, out string actionName, out string controllerName, out RouteValueDictionary routeValues) + //{ + // actionName = "MyDemoWidget"; + // controllerName = "DevTools"; - // routeValues = new RouteValueDictionary - // { - // { "Namespaces", "SmartStore.DevTools.Controllers" }, - // { "area", "SmartStore.DevTools" } - // }; - //} + // routeValues = new RouteValueDictionary + // { + // { "Namespaces", "SmartStore.DevTools.Controllers" }, + // { "area", "SmartStore.DevTools" } + // }; + //} - public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "Configure"; controllerName = "DevTools"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.DevTools" } }; + routeValues = new RouteValueDictionary() { { "area", "SmartStore.DevTools" } }; } public override void Install() @@ -58,9 +57,9 @@ public override void Install() _services.Settings.SaveSetting(new ProfilerSettings()); - base.Install(); + base.Install(); - Logger.Info(string.Format("Plugin installed: SystemName: {0}, Version: {1}, Description: '{2}'", PluginDescriptor.SystemName, PluginDescriptor.Version, PluginDescriptor.FriendlyName)); + Logger.Info(string.Format("Plugin installed: SystemName: {0}, Version: {1}, Description: '{2}'", PluginDescriptor.SystemName, PluginDescriptor.Version, PluginDescriptor.FriendlyName)); } public override void Uninstall() @@ -70,35 +69,35 @@ public override void Uninstall() _services.Settings.DeleteSetting(); - base.Uninstall(); + base.Uninstall(); } - private static bool? _hasPendingMigrations; - internal static bool HasPendingMigrations() - { - bool result = true; - - if (_hasPendingMigrations == null) - { - try - { - var migrator = new DbSeedingMigrator(); - result = migrator.GetPendingMigrations().Any(); - - if (result == false) - { - // Don't check again - _hasPendingMigrations = false; - } - } - catch { } - } - else - { - result = _hasPendingMigrations.Value; - } - - return result; - } - } + private static bool? _hasPendingMigrations; + internal static bool HasPendingMigrations() + { + bool result = true; + + if (_hasPendingMigrations == null) + { + try + { + var migrator = new DbSeedingMigrator(); + result = migrator.GetPendingMigrations().Any(); + + if (result == false) + { + // Don't check again + _hasPendingMigrations = false; + } + } + catch { } + } + else + { + result = _hasPendingMigrations.Value; + } + + return result; + } + } } diff --git a/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs b/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs index cb5f929564..38fb44d16a 100644 --- a/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs +++ b/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs @@ -6,7 +6,7 @@ namespace SmartStore.DevTools.Events { - public class CustomTab : IConsumer + public class CustomTab : IConsumer { private readonly ICommonServices _services; @@ -50,7 +50,7 @@ public void HandleEvent(ModelBoundEvent eventMessage) var model = eventMessage.BoundModel.CustomProperties["DevTools"] as BackendExtensionModel; if (model == null) return; - + // Do something with the model now: e.g. store it ;-) } } diff --git a/src/Plugins/SmartStore.DevTools/Examples/OutputCacheInvalidationObserver.cs b/src/Plugins/SmartStore.DevTools/Examples/OutputCacheInvalidationObserver.cs index 9c5bcda2a3..59cf71c361 100644 --- a/src/Plugins/SmartStore.DevTools/Examples/OutputCacheInvalidationObserver.cs +++ b/src/Plugins/SmartStore.DevTools/Examples/OutputCacheInvalidationObserver.cs @@ -1,89 +1,82 @@ -using SmartStore.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Infrastructure; -using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Topics; -using System; -using System.Collections.Generic; -using System.Linq; namespace SmartStore.DevTools.Examples { - public static class OutputCacheInvalidationObserver - { - public static void Execute() - { - // Register a handler which returns unique string tags for your custom entity. - // This is required if you ever call IDisplayControl.Announce(entity) for your entity, - // otherwise the system is not able to generate tags. - // Tags can be obtained via IDisplayControl.GetCacheControlTagsFor(entity). - // Invalidation by tags is done via IOutputCacheProvider.InvalidateByTag(string[] tags). - // InvalidateByTag() will remove any page from the output cache storage in which your custom entity is somehow displayed. - DisplayControl.RegisterHandlerFor(typeof(MyRecord), (x, c) => new[] { "mr" + x.Id }); + public static class OutputCacheInvalidationObserver + { + public static void Execute() + { + // Register a handler which returns unique string tags for your custom entity. + // This is required if you ever call IDisplayControl.Announce(entity) for your entity, + // otherwise the system is not able to generate tags. + // Tags can be obtained via IDisplayControl.GetCacheControlTagsFor(entity). + // Invalidation by tags is done via IOutputCacheProvider.InvalidateByTag(string[] tags). + // InvalidateByTag() will remove any page from the output cache storage in which your custom entity is somehow displayed. + DisplayControl.RegisterHandlerFor(typeof(MyRecord), (x, c) => new[] { "mr" + x.Id }); - // Register an output cache observe handler - var observer = EngineContext.Current.Resolve(); - observer.ObserveEntity(Observe); - } + // Register an output cache observe handler + var observer = EngineContext.Current.Resolve(); + observer.ObserveEntity(Observe); + } - private static void Observe(ObserveEntityContext ctx) - { - // If your plugin renders dynamic content to the frontend which won't change often once it is configured, you should add a corresponding route - // to the ouput cache to add the rendered output of the plugin to the cache. For more information on this see the Install/Uninstall method of this plugin + private static void Observe(ObserveEntityContext ctx) + { + // If your plugin renders dynamic content to the frontend which won't change often once it is configured, you should add a corresponding route + // to the ouput cache to add the rendered output of the plugin to the cache. For more information on this see the Install/Uninstall method of this plugin - // In the example below we assume the plugin has own entities which depend on system entities like Product, Category or Topic. - // In this case you must invalidate the cache items for the system entities as soon as the corresponding plugin entity is changed by the shop admin - // in order to remove the cached output of the affected pages in the frontend and thus be rebuilt + // In the example below we assume the plugin has own entities which depend on system entities like Product, Category or Topic. + // In this case you must invalidate the cache items for the system entities as soon as the corresponding plugin entity is changed by the shop admin + // in order to remove the cached output of the affected pages in the frontend and thus be rebuilt - var myRecord = ctx?.Entity as MyRecord; + var myRecord = ctx?.Entity as MyRecord; - if (myRecord != null) - { - IEnumerable tags = null; + if (myRecord != null) + { + IEnumerable tags = null; - var outputCacheProvider = ctx.OutputCacheProvider; + var outputCacheProvider = ctx.OutputCacheProvider; - // We assume the domain record from the plugin stores information about the type of the entity in EntityName and the corresponding Id in EntityId - var entityname = myRecord.EntityName; + // We assume the domain record from the plugin stores information about the type of the entity in EntityName and the corresponding Id in EntityId + var entityname = myRecord.EntityName; - // Collect the tags for the entities which must be invalidated due to changes of the plugin domain record - switch (entityname.ToLower()) - { - case "product": - var product = ctx.ServiceContainer.Resolve().GetProductById(myRecord.EntityId); - if (product != null) tags = ctx.DisplayControl.GetCacheControlTagsFor(product); - break; - case "category": - var category = ctx.ServiceContainer.Resolve().GetCategoryById(myRecord.EntityId); - if (category != null) tags = ctx.DisplayControl.GetCacheControlTagsFor(category); - break; - case "topic": - var topic = ctx.ServiceContainer.Resolve().GetTopicById(myRecord.EntityId); - if (topic != null) tags = ctx.DisplayControl.GetCacheControlTagsFor(topic); - break; - } + // Collect the tags for the entities which must be invalidated due to changes of the plugin domain record + switch (entityname.ToLower()) + { + case "product": + var product = ctx.ServiceContainer.Resolve().GetProductById(myRecord.EntityId); + if (product != null) tags = ctx.DisplayControl.GetCacheControlTagsFor(product); + break; + case "category": + var category = ctx.ServiceContainer.Resolve().GetCategoryById(myRecord.EntityId); + if (category != null) tags = ctx.DisplayControl.GetCacheControlTagsFor(category); + break; + case "topic": + var topic = ctx.ServiceContainer.Resolve().GetTopicById(myRecord.EntityId); + if (topic != null) tags = ctx.DisplayControl.GetCacheControlTagsFor(topic); + break; + } - // Invalidate cache items by the collected tags - if (tags != null && tags.Any()) - { - outputCacheProvider.InvalidateByTag(tags.ToArray()); - ctx.Handled = true; - } - } - } + // Invalidate cache items by the collected tags + if (tags != null && tags.Any()) + { + outputCacheProvider.InvalidateByTag(tags.ToArray()); + ctx.Handled = true; + } + } + } - internal class MyRecord : BaseEntity - { - public int EntityId - { - get { return 0; } - } + internal class MyRecord : BaseEntity + { + public int EntityId => 0; - public string EntityName - { - get { return String.Empty; } - } - } - } + public string EntityName => String.Empty; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Filters/MachineNameFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/MachineNameFilter.cs index fd505c3573..6d969ed842 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/MachineNameFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/MachineNameFilter.cs @@ -6,51 +6,51 @@ namespace SmartStore.DevTools.Filters { - public class MachineNameFilter : IResultFilter - { - private readonly ICommonServices _services; - private readonly Lazy _widgetProvider; - private readonly ProfilerSettings _profilerSettings; - - public MachineNameFilter( - ICommonServices services, - Lazy widgetProvider, - ProfilerSettings profilerSettings) - { - _services = services; - _widgetProvider = widgetProvider; - _profilerSettings = profilerSettings; - } - - public void OnResultExecuting(ResultExecutingContext filterContext) - { + public class MachineNameFilter : IResultFilter + { + private readonly ICommonServices _services; + private readonly Lazy _widgetProvider; + private readonly ProfilerSettings _profilerSettings; + + public MachineNameFilter( + ICommonServices services, + Lazy widgetProvider, + ProfilerSettings profilerSettings) + { + _services = services; + _widgetProvider = widgetProvider; + _profilerSettings = profilerSettings; + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { if (!_profilerSettings.DisplayMachineName) - return; - - if (filterContext.IsChildAction) - return; - - var result = filterContext.Result; - - // should only run on a full view rendering result or HTML ContentResult - if (!result.IsHtmlViewResult()) - return; - - if (!_services.WorkContext.CurrentCustomer.IsAdmin() && !filterContext.HttpContext.Request.IsLocal) - { - return; - } - - _widgetProvider.Value.RegisterAction( - new[] { "body_end_html_tag_before", "admin_content_after" }, - "MachineName", - "DevTools", - new { area = "SmartStore.DevTools" }); - } - - public void OnResultExecuted(ResultExecutedContext filterContext) - { - } - - } + return; + + if (filterContext.IsChildAction) + return; + + var result = filterContext.Result; + + // should only run on a full view rendering result or HTML ContentResult + if (!result.IsHtmlViewResult()) + return; + + if (!_services.WorkContext.CurrentCustomer.IsAdmin() && !filterContext.HttpContext.Request.IsLocal) + { + return; + } + + _widgetProvider.Value.RegisterAction( + new[] { "body_end_html_tag_before", "admin_content_after" }, + "MachineName", + "DevTools", + new { area = "SmartStore.DevTools" }); + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + + } } diff --git a/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs index 87214c76eb..71d65d003c 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs @@ -3,114 +3,114 @@ using System.Web; using System.Web.Mvc; using SmartStore.Services; +using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Web.Framework.UI; -using SmartStore.Services.Common; namespace SmartStore.DevTools.Filters { - public class ProfilerFilter : IActionFilter, IResultFilter - { - private readonly ICommonServices _services; - private readonly Lazy _widgetProvider; - private readonly Lazy _profilerSettings; - private readonly Lazy _mobileDeviceHelper; - - public ProfilerFilter( - ICommonServices services, - Lazy widgetProvider, - Lazy profilerSettings, - Lazy mobileDeviceHelper) - { - _services = services; - _widgetProvider = widgetProvider; - _profilerSettings = profilerSettings; - _mobileDeviceHelper = mobileDeviceHelper; - } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) - return; - - var tokens = filterContext.RouteData.DataTokens; - string area = tokens.ContainsKey("area") && !string.IsNullOrEmpty(tokens["area"].ToString()) ? - string.Concat(tokens["area"], ".") : - string.Empty; - - string controller = string.Concat(filterContext.Controller.ToString().Split('.').Last(), "."); - string action = filterContext.ActionDescriptor.ActionName; - - _services.Chronometer.StepStart("ActionFilter", "Action: " + area + controller + action); - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) - return; + public class ProfilerFilter : IActionFilter, IResultFilter + { + private readonly ICommonServices _services; + private readonly Lazy _widgetProvider; + private readonly Lazy _profilerSettings; + private readonly Lazy _mobileDeviceHelper; + + public ProfilerFilter( + ICommonServices services, + Lazy widgetProvider, + Lazy profilerSettings, + Lazy mobileDeviceHelper) + { + _services = services; + _widgetProvider = widgetProvider; + _profilerSettings = profilerSettings; + _mobileDeviceHelper = mobileDeviceHelper; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) + return; + + var tokens = filterContext.RouteData.DataTokens; + string area = tokens.ContainsKey("area") && !string.IsNullOrEmpty(tokens["area"].ToString()) ? + string.Concat(tokens["area"], ".") : + string.Empty; + + string controller = string.Concat(filterContext.Controller.ToString().Split('.').Last(), "."); + string action = filterContext.ActionDescriptor.ActionName; + + _services.Chronometer.StepStart("ActionFilter", "Action: " + area + controller + action); + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) + return; if (!filterContext.Result.IsHtmlViewResult()) _services.Chronometer.StepStop("ActionFilter"); } - public void OnResultExecuting(ResultExecutingContext filterContext) - { - if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) - return; - - // should only run on a full view rendering result - if (!filterContext.Result.IsHtmlViewResult()) - return; + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) + return; + + // should only run on a full view rendering result + if (!filterContext.Result.IsHtmlViewResult()) + return; - if (!this.ShouldProfile(filterContext.HttpContext)) - return; + if (!this.ShouldProfile(filterContext.HttpContext)) + return; - var viewResult = filterContext.Result as ViewResultBase; + var viewResult = filterContext.Result as ViewResultBase; - string viewName = viewResult?.ViewName; - if (viewName.IsEmpty()) - { - string action = (filterContext.RouteData.Values["action"] as string).EmptyNull(); - viewName = action; - } + string viewName = viewResult?.ViewName; + if (viewName.IsEmpty()) + { + string action = (filterContext.RouteData.Values["action"] as string).EmptyNull(); + viewName = action; + } _services.Chronometer.StepStart("ResultFilter", string.Format("{0}: {1}", viewResult is PartialViewResult ? "Partial" : "View", viewName)); if (!filterContext.IsChildAction) - { + { _widgetProvider.Value.RegisterAction( "head_html_tag", "MiniProfiler", "DevTools", new { area = "SmartStore.DevTools" }); } - } + } - public void OnResultExecuted(ResultExecutedContext filterContext) - { - if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) - return; + public void OnResultExecuted(ResultExecutedContext filterContext) + { + if (!_profilerSettings.Value.EnableMiniProfilerInPublicStore) + return; - // should only run on a full view rendering result - if (!filterContext.Result.IsHtmlViewResult()) - return; + // should only run on a full view rendering result + if (!filterContext.Result.IsHtmlViewResult()) + return; - if (!this.ShouldProfile(filterContext.HttpContext)) - return; + if (!this.ShouldProfile(filterContext.HttpContext)) + return; _services.Chronometer.StepStop("ResultFilter"); _services.Chronometer.StepStop("ActionFilter"); } - private bool ShouldProfile(HttpContextBase ctx) - { - if (_mobileDeviceHelper.Value.IsMobileDevice()) - return false; + private bool ShouldProfile(HttpContextBase ctx) + { + if (_mobileDeviceHelper.Value.IsMobileDevice()) + return false; - if (!_services.WorkContext.CurrentCustomer.IsAdmin()) - return ctx.Request.IsLocal; + if (!_services.WorkContext.CurrentCustomer.IsAdmin()) + return ctx.Request.IsLocal; - return true; - } - } + return true; + } + } } diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs index 3c010c46fd..624f28253b 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs @@ -1,35 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; +using System.Diagnostics; using System.Web.Mvc; using SmartStore.Core.Logging; -using SmartStore.Core.Localization; namespace SmartStore.DevTools.Filters { - public class SampleActionFilter : IActionFilter - { - private readonly INotifier _notifier; + public class SampleActionFilter : IActionFilter + { + private readonly INotifier _notifier; - public SampleActionFilter(INotifier notifier) - { - this._notifier = notifier; - } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - Debug.WriteLine("Executing: {0} - {1}".FormatInvariant(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName)); - _notifier.Information("Yeah, my plugin action filter works. NICE!"); - // Do something meaningful here ;-) - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - Debug.WriteLine("Executed: {0} - {1}".FormatInvariant(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName)); - } - } + public SampleActionFilter(INotifier notifier) + { + this._notifier = notifier; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + Debug.WriteLine("Executing: {0} - {1}".FormatInvariant(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName)); + _notifier.Information("Yeah, my plugin action filter works. NICE!"); + // Do something meaningful here ;-) + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + Debug.WriteLine("Executed: {0} - {1}".FormatInvariant(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName)); + } + } } diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleCheckoutFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleCheckoutFilter.cs index 59f4856a07..b2ff97632d 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleCheckoutFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleCheckoutFilter.cs @@ -1,35 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; +using System.Web.Mvc; using System.Web.Routing; -using System.Web.Mvc; using SmartStore.Core.Logging; -using SmartStore.Core.Localization; -using SmartStore.Utilities; namespace SmartStore.DevTools.Filters { - public class SampleCheckoutFilter : IActionFilter - { - private readonly INotifier _notifier; + public class SampleCheckoutFilter : IActionFilter + { + private readonly INotifier _notifier; - public SampleCheckoutFilter(INotifier notifier) - { - this._notifier = notifier; - } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - var routeValues = new { action = "MyBillingAddress" }; - filterContext.Result = new RedirectToRouteResult("Developer.DevTools.MyCheckout", new RouteValueDictionary(routeValues)); - } - } + public SampleCheckoutFilter(INotifier notifier) + { + this._notifier = notifier; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + var routeValues = new { action = "MyBillingAddress" }; + filterContext.Result = new RedirectToRouteResult("Developer.DevTools.MyCheckout", new RouteValueDictionary(routeValues)); + } + } } diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs index fb4307fc1a..185f65234e 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs @@ -1,36 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; -using System.Web.Mvc; -using SmartStore.Core.Logging; -using SmartStore.Core.Localization; -using SmartStore.Web.Models.Catalog; +using System.Web.Mvc; using SmartStore.Services; +using SmartStore.Web.Models.Catalog; namespace SmartStore.DevTools.Filters { - public class SampleProductDetailActionFilter : IActionFilter - { - private readonly ICommonServices _services; + public class SampleProductDetailActionFilter : IActionFilter + { + private readonly ICommonServices _services; private readonly UrlHelper _urlHelper; public SampleProductDetailActionFilter(ICommonServices services, UrlHelper urlHelper) - { + { _services = services; _urlHelper = urlHelper; } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { var result = filterContext.Result as ViewResultBase; if (result == null) { @@ -59,5 +50,5 @@ public void OnActionExecuted(ActionExecutedContext filterContext) Href = "https://www.smartstore.com" }; ; } - } + } } diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs index 941ef81a1a..d4d0c9fee8 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs @@ -1,39 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; +using System.Diagnostics; using System.Web.Mvc; using SmartStore.Web.Framework.Modelling; namespace SmartStore.DevTools.Filters { - public class SampleResultFilter : IResultFilter - { - public void OnResultExecuting(ResultExecutingContext filterContext) - { - Debug.WriteLine("OnResultExecuting"); + public class SampleResultFilter : IResultFilter + { + public void OnResultExecuting(ResultExecutingContext filterContext) + { + Debug.WriteLine("OnResultExecuting"); - var result = filterContext.Result as ViewResultBase; - if (result != null) - { - var model = result.Model as ModelBase; - if (model != null) - { - // Do something with model here! - // If you want to use view models from SmartStore.Web - // or SmartStore.Admin make sure you have added - // the project reference first. - } - } - } - - public void OnResultExecuted(ResultExecutedContext filterContext) - { - Debug.WriteLine("OnResultExecuted"); - } + var result = filterContext.Result as ViewResultBase; + if (result != null) + { + var model = result.Model as ModelBase; + if (model != null) + { + // Do something with model here! + // If you want to use view models from SmartStore.Web + // or SmartStore.Admin make sure you have added + // the project reference first. + } + } + } - } + public void OnResultExecuted(ResultExecutedContext filterContext) + { + Debug.WriteLine("OnResultExecuted"); + } + + } } diff --git a/src/Plugins/SmartStore.DevTools/Filters/WidgetZoneFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/WidgetZoneFilter.cs index 92b04c94b6..498341f28e 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/WidgetZoneFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/WidgetZoneFilter.cs @@ -1,121 +1,115 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Web; using System.Web.Mvc; -using SmartStore.Core.Logging; -using SmartStore.Core.Localization; -using SmartStore.DevTools.Services; -using SmartStore.Core; +using SmartStore.Core.Caching; using SmartStore.Services; using SmartStore.Services.Customers; -using SmartStore.Web.Framework.UI; using SmartStore.Utilities; -using System.IO; -using SmartStore.Core.Caching; +using SmartStore.Web.Framework.UI; namespace SmartStore.DevTools.Filters { - public class WidgetZoneFilter : IActionFilter, IResultFilter - { - private readonly ICommonServices _services; - private readonly Lazy _widgetProvider; - private readonly ProfilerSettings _profilerSettings; - private readonly IDisplayControl _displayControl; - - public WidgetZoneFilter( - ICommonServices services, - Lazy widgetProvider, - ProfilerSettings profilerSettings, - IDisplayControl displayControl) - { - _services = services; - _widgetProvider = widgetProvider; - _profilerSettings = profilerSettings; - _displayControl = displayControl; - } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - if (filterContext.IsChildAction) - return; - - if (!_profilerSettings.DisplayWidgetZones) - return; - - _displayControl.MarkRequestAsUncacheable(); - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - } - - public void OnResultExecuting(ResultExecutingContext filterContext) - { + public class WidgetZoneFilter : IActionFilter, IResultFilter + { + private readonly ICommonServices _services; + private readonly Lazy _widgetProvider; + private readonly ProfilerSettings _profilerSettings; + private readonly IDisplayControl _displayControl; + + public WidgetZoneFilter( + ICommonServices services, + Lazy widgetProvider, + ProfilerSettings profilerSettings, + IDisplayControl displayControl) + { + _services = services; + _widgetProvider = widgetProvider; + _profilerSettings = profilerSettings; + _displayControl = displayControl; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext.IsChildAction) + return; + + if (!_profilerSettings.DisplayWidgetZones) + return; + + _displayControl.MarkRequestAsUncacheable(); + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { if (!_profilerSettings.DisplayWidgetZones) return; - // should only run on a full view rendering result - var result = filterContext.Result as ViewResultBase; - if (result == null) - { - return; - } - - if (!this.ShouldRender(filterContext.HttpContext)) - { - return; - } - - if (!filterContext.IsChildAction) - { - _widgetProvider.Value.RegisterAction( - new Wildcard("*"), + // should only run on a full view rendering result + var result = filterContext.Result as ViewResultBase; + if (result == null) + { + return; + } + + if (!this.ShouldRender(filterContext.HttpContext)) + { + return; + } + + if (!filterContext.IsChildAction) + { + _widgetProvider.Value.RegisterAction( + new Wildcard("*"), "WidgetZone", - "DevTools", - new { area = "SmartStore.DevTools" }); - } - - var viewName = result.ViewName; + "DevTools", + new { area = "SmartStore.DevTools" }); + } + + var viewName = result.ViewName; - if (viewName.IsEmpty()) - { - string action = (filterContext.RouteData.Values["action"] as string).EmptyNull(); - viewName = action; + if (viewName.IsEmpty()) + { + string action = (filterContext.RouteData.Values["action"] as string).EmptyNull(); + viewName = action; if (action == "WidgetsByZone") { var model = result.Model as WidgetZoneModel; - + filterContext.Result = new ViewResult { ViewName = "~/Plugins/SmartStore.DevTools/Views/DevTools/WidgetZone.cshtml", }; - if(filterContext.RouteData.Values["widgetZone"] == null) + if (filterContext.RouteData.Values["widgetZone"] == null) { filterContext.RouteData.Values.Add("widgetZone", model.WidgetZone); } } - } - } - - public void OnResultExecuted(ResultExecutedContext filterContext) - { - } - - private bool ShouldRender(HttpContextBase ctx) - { - if (!_services.WorkContext.CurrentCustomer.IsAdmin()) - { - return ctx.Request.IsLocal; - } + } + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + + private bool ShouldRender(HttpContextBase ctx) + { + if (!_services.WorkContext.CurrentCustomer.IsAdmin()) + { + if (ctx.Request.Url.AbsolutePath == "/common/pdfreceiptfooter" || ctx.Request.Url.AbsolutePath == "/common/pdfreceiptheader") + { + return false; + } + return ctx.Request.IsLocal; + } - return true; - } + return true; + } - } + } } diff --git a/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml b/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml index f18f62b5d4..10fb461867 100644 --- a/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml @@ -3,7 +3,7 @@ Developer Tools - SmartStore.NET Developer Tools (MiniProfiler und andere Extras). + Smartstore Developer Tools (MiniProfiler und andere Extras). Developer Tools Demo Widget diff --git a/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml b/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml index 2f503eb23a..8d519b9476 100644 --- a/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml @@ -3,7 +3,7 @@ Developer Tools - SmartStore.NET Developer Tools (MiniProfiler and other goodies). + Smartstore Developer Tools (MiniProfiler and other goodies). Developer Tools Demo Widget diff --git a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs index 7de7bbe5e1..58f8d5edbf 100644 --- a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs +++ b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs @@ -2,10 +2,10 @@ namespace SmartStore.DevTools.Models { - public class BackendExtensionModel : ModelBase - { - public string Welcome { get; set; } - + [CustomModelPart] + public class BackendExtensionModel : ModelBase + { + public string Welcome { get; set; } public int ProductId { get; set; } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs index ab616910d6..9cb991484f 100644 --- a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs +++ b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs @@ -6,88 +6,88 @@ namespace SmartStore.DevTools { - public class ProfilerHttpModule : IHttpModule - { - private const string MP_KEY = "sm.miniprofiler.started"; - - public void Init(HttpApplication context) - { - if (DevToolsPlugin.HasPendingMigrations()) - { - return; - } + public class ProfilerHttpModule : IHttpModule + { + private const string MP_KEY = "sm.miniprofiler.started"; - context.AcquireRequestState += OnAcquireRequestState; - context.EndRequest += OnEndRequest; - } + public void Init(HttpApplication context) + { + if (DevToolsPlugin.HasPendingMigrations()) + { + return; + } - private static void OnAcquireRequestState(object sender, EventArgs e) - { - var app = (HttpApplication)sender; - if (!MiniProfilerStarted(app) && ShouldProfile(app)) - { - MiniProfiler.Start(); - if (app.Context != null && app.Context.Items != null) - { - app.Context.Items[MP_KEY] = true; - } - } - } + context.AcquireRequestState += OnAcquireRequestState; + context.EndRequest += OnEndRequest; + } - private static void OnEndRequest(object sender, EventArgs e) - { - var app = (HttpApplication)sender; - if (MiniProfilerStarted(app)) - { - MiniProfiler.Stop(); - } - } + private static void OnAcquireRequestState(object sender, EventArgs e) + { + var app = (HttpApplication)sender; + if (!MiniProfilerStarted(app) && ShouldProfile(app)) + { + MiniProfiler.StartNew(); + if (app.Context != null && app.Context.Items != null) + { + app.Context.Items[MP_KEY] = true; + } + } + } - internal static bool MiniProfilerStarted(HttpApplication app) - { - return app?.Context?.Items != null && app.Context.Items.Contains(MP_KEY); - } + private static void OnEndRequest(object sender, EventArgs e) + { + var app = (HttpApplication)sender; + if (MiniProfilerStarted(app)) + { + MiniProfiler.Current?.Stop(); + } + } - private static bool ShouldProfile(HttpApplication app) - { - if (app?.Context?.Request == null) - return false; + internal static bool MiniProfilerStarted(HttpApplication app) + { + return app?.Context?.Items != null && app.Context.Items.Contains(MP_KEY); + } - if (!DataSettings.DatabaseIsInstalled()) - { - return false; - } + private static bool ShouldProfile(HttpApplication app) + { + if (app?.Context?.Request == null) + return false; - var url = app.Context.Request.AppRelativeCurrentExecutionFilePath; - if (url.StartsWith("~/admin", StringComparison.InvariantCultureIgnoreCase) - || url.StartsWith("~/mini-profiler", StringComparison.InvariantCultureIgnoreCase) - || url.StartsWith("~/bundles", StringComparison.InvariantCultureIgnoreCase) - || url.StartsWith("~/plugin/", StringComparison.InvariantCultureIgnoreCase) - || url.StartsWith("~/taskscheduler", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } + if (!DataSettings.DatabaseIsInstalled()) + { + return false; + } - ProfilerSettings settings = null; + var url = app.Context.Request.AppRelativeCurrentExecutionFilePath; + if (url.StartsWith("~/admin", StringComparison.InvariantCultureIgnoreCase) + || url.StartsWith("~/mini-profiler", StringComparison.InvariantCultureIgnoreCase) + || url.StartsWith("~/bundles", StringComparison.InvariantCultureIgnoreCase) + || url.StartsWith("~/plugin/", StringComparison.InvariantCultureIgnoreCase) + || url.StartsWith("~/taskscheduler", StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } - if (EngineContext.Current.IsFullyInitialized) - { - try - { - settings = EngineContext.Current.Resolve(); - } - catch - { - return true; - } - } + ProfilerSettings settings = null; - return settings == null ? true : settings.EnableMiniProfilerInPublicStore; - } + if (EngineContext.Current.IsFullyInitialized) + { + try + { + settings = EngineContext.Current.Resolve(); + } + catch + { + return true; + } + } - public void Dispose() - { - // nothing to dispose - } - } + return settings == null ? true : settings.EnableMiniProfilerInPublicStore; + } + + public void Dispose() + { + // nothing to dispose + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/RouteProvider.cs b/src/Plugins/SmartStore.DevTools/RouteProvider.cs index 607fff468d..1d35543960 100644 --- a/src/Plugins/SmartStore.DevTools/RouteProvider.cs +++ b/src/Plugins/SmartStore.DevTools/RouteProvider.cs @@ -4,32 +4,26 @@ namespace SmartStore.DevTools { - - public class RouteProvider : IRouteProvider + + public class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { - routes.MapRoute("SmartStore.DevTools", - "Plugin/SmartStore.DevTools/{action}/{id}", - new { controller = "DevTools", action = "Configure", id = UrlParameter.Optional }, - new[] { "SmartStore.DevTools.Controllers" } - ) - .DataTokens["area"] = "SmartStore.DevTools"; + routes.MapRoute("SmartStore.DevTools", + "Plugin/SmartStore.DevTools/{action}/{id}", + new { controller = "DevTools", action = "Configure", id = UrlParameter.Optional }, + new[] { "SmartStore.DevTools.Controllers" } + ) + .DataTokens["area"] = "SmartStore.DevTools"; - //routes.MapRoute("SmartStore.DevTools.MyCheckout", - // "MyCheckout/{action}", - // new { controller = "MyCheckout", action = "MyBillingAddress" }, - // new[] { "SmartStore.DevTools.Controllers" } - //) - //.DataTokens["area"] = "SmartStore.DevTools"; - } - public int Priority - { - get - { - return 0; - } + //routes.MapRoute("SmartStore.DevTools.MyCheckout", + // "MyCheckout/{action}", + // new { controller = "MyCheckout", action = "MyBillingAddress" }, + // new[] { "SmartStore.DevTools.Controllers" } + //) + //.DataTokens["area"] = "SmartStore.DevTools"; } + public int Priority => 0; } } diff --git a/src/Plugins/SmartStore.DevTools/Services/CustomFacetTemplateSelector.cs b/src/Plugins/SmartStore.DevTools/Services/CustomFacetTemplateSelector.cs new file mode 100644 index 0000000000..7b5265df8f --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Services/CustomFacetTemplateSelector.cs @@ -0,0 +1,60 @@ +using System.Web.Routing; +using SmartStore.Core; +using SmartStore.Core.Search.Facets; +using SmartStore.Services.Customers; +using SmartStore.Services.Search.Rendering; + +namespace SmartStore.DevTools.Services +{ + public class CustomFacetTemplateSelector : IFacetTemplateSelector + { + private readonly IWorkContext _workContext; + + public CustomFacetTemplateSelector(IWorkContext workContext) + { + _workContext = workContext; + } + + // Order in case of multiple implementations (like MegaSearchPlus). + public int Ordinal => 99; + + public RouteInfo GetTemplateRoute(FacetGroup facetGroup) + { + // Provide template for catalog only. + if (facetGroup.Scope != "Catalog") + { + return null; + } + + // Provide template for specifications attributes only. + if (facetGroup.Kind != FacetGroupKind.Attribute) + { + return null; + } + + // Provide template for admin only (because this is a developer demo). + if (!_workContext.CurrentCustomer.IsAdmin()) + { + return null; + } + + // TODO: filter by what your template is made for. + // E.g. merchant configures a specification attribute in your plugin, then you can filter by facetGroup.Key == "attrid" + + if (facetGroup.Label.IsCaseInsensitiveEqual("Dimensions") || + facetGroup.Label.IsCaseInsensitiveEqual("Maße")) + { + var routeValues = new RouteValueDictionary(new + { + area = "SmartStore.DevTools", + templateName = "MyCustomFacetTemplate", + facetGroup + }); + + return new RouteInfo("MyCustomFacetTemplate", "DevTools", routeValues); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Services/MiniProfilerChronometer.cs b/src/Plugins/SmartStore.DevTools/Services/MiniProfilerChronometer.cs index 522f4d138b..e9bd9e6787 100644 --- a/src/Plugins/SmartStore.DevTools/Services/MiniProfilerChronometer.cs +++ b/src/Plugins/SmartStore.DevTools/Services/MiniProfilerChronometer.cs @@ -1,31 +1,24 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using SmartStore.Core.Logging; using StackExchange.Profiling; namespace SmartStore.DevTools.Services { - public class MiniProfilerChronometer : IChronometer - { - private readonly Dictionary> _steps = new Dictionary>(); - private MiniProfiler _profiler; + public class MiniProfilerChronometer : IChronometer + { + private readonly Dictionary> _steps = new Dictionary>(); + private MiniProfiler _profiler; - public MiniProfilerChronometer() - { - _profiler = MiniProfiler.Current; - } + public MiniProfilerChronometer() + { + _profiler = MiniProfiler.Current; + } - protected MiniProfiler Profiler - { - get - { - return _profiler ?? (_profiler = MiniProfiler.Current); - } - } + protected MiniProfiler Profiler => _profiler ?? (_profiler = MiniProfiler.Current); - public void StepStart(string key, string message) - { + public void StepStart(string key, string message) + { if (this.Profiler == null) { return; @@ -41,8 +34,8 @@ public void StepStart(string key, string message) stack.Push(step); } - public void StepStop(string key) - { + public void StepStop(string key) + { if (_steps.ContainsKey(key) && _steps[key].Count > 0) { var step = _steps[key].Pop(); @@ -54,20 +47,20 @@ public void StepStop(string key) } } - private void StopAll() - { - // Dispose any orphaned steps - foreach (var stack in _steps.Values) - { - stack.Each(x => x.Dispose()); - } + private void StopAll() + { + // Dispose any orphaned steps + foreach (var stack in _steps.Values) + { + stack.Each(x => x.Dispose()); + } - _steps.Clear(); - } + _steps.Clear(); + } - public void Dispose() - { - this.StopAll(); - } - } + public void Dispose() + { + this.StopAll(); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs b/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs index 275acdf2d7..06e572338c 100644 --- a/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs +++ b/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs @@ -3,9 +3,9 @@ namespace SmartStore.DevTools { public class ProfilerSettings : ISettings - { + { public bool EnableMiniProfilerInPublicStore { get; set; } public bool DisplayWidgetZones { get; set; } public bool DisplayMachineName { get; set; } - } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj index 8c43d89520..4a2abff1a9 100644 --- a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj +++ b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj @@ -20,7 +20,7 @@ Properties SmartStore.DevTools SmartStore.DevTools - v4.6.1 + v4.7.2 512 ..\..\ @@ -76,20 +76,20 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll False - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll False - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll False @@ -100,36 +100,72 @@ ..\..\packages\FluentValidation.Mvc5.7.4.0\lib\net45\FluentValidation.Mvc.dll False + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + False + + + ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.6.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.Abstractions.dll + False ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False - - ..\..\packages\MiniProfiler.3.2.0.157\lib\net40\MiniProfiler.dll + + ..\..\packages\MiniProfiler.4.2.1\lib\net461\MiniProfiler.dll True - - False - ..\..\packages\MiniProfiler.EF6.3.0.11\lib\net40\MiniProfiler.EntityFramework6.dll + + ..\..\packages\MiniProfiler.EF6.4.2.1\lib\net461\MiniProfiler.EF6.dll True - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - False + + ..\..\packages\MiniProfiler.Shared.4.2.1\lib\net461\MiniProfiler.Shared.dll + True + + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + False + + + + ..\..\packages\System.Diagnostics.DiagnosticSource.6.0.0\lib\net461\System.Diagnostics.DiagnosticSource.dll + False + + + ..\..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + False + + + + ..\..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + False + + + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + - + ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll False @@ -185,6 +221,7 @@ + @@ -262,6 +299,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/Plugins/SmartStore.DevTools/Starter.cs b/src/Plugins/SmartStore.DevTools/Starter.cs index 6511b8f581..9a186299a8 100644 --- a/src/Plugins/SmartStore.DevTools/Starter.cs +++ b/src/Plugins/SmartStore.DevTools/Starter.cs @@ -1,70 +1,38 @@ -using Microsoft.Web.Infrastructure.DynamicModuleHelper; -using SmartStore.Core; +using System; +using Microsoft.Web.Infrastructure.DynamicModuleHelper; using SmartStore.Core.Infrastructure; using SmartStore.Web.Framework; using StackExchange.Profiling; using StackExchange.Profiling.Storage; -using System; -using System.Collections.Generic; namespace SmartStore.DevTools { - public class ProfilerPreApplicationStart : IPreApplicationStart - { - public void Start() - { - DynamicModuleUtility.RegisterModule(typeof(ProfilerHttpModule)); - SmartUrlRoutingModule.RegisterRoutablePath("/mini-profiler-resources/(.*)"); - } - } - - public class ProfilerStartupTask : IApplicationStart - { - public void Start() - { - StackExchange.Profiling.MiniProfiler.Settings.MaxUnviewedProfiles = 5; - StackExchange.Profiling.MiniProfiler.Settings.Storage = new HttpRuntimeCacheStorage(TimeSpan.FromMinutes(1)); - //StackExchange.Profiling.MiniProfiler.Settings.Results_List_Authorize = (req) => true; + public class ProfilerPreApplicationStart : IPreApplicationStart + { + public void Start() + { + DynamicModuleUtility.RegisterModule(typeof(ProfilerHttpModule)); + SmartUrlRoutingModule.RegisterRoutablePath("/mini-profiler-resources/(.*)"); + } + } + + public class ProfilerStartupTask : IApplicationStart + { + public void Start() + { + StackExchange.Profiling.MiniProfiler.Configure(new MiniProfilerOptions + { + MaxUnviewedProfiles = 5, + Storage = new MemoryCacheStorage(TimeSpan.FromMinutes(1)), + //ResultsAuthorize = req => true, + }); StackExchange.Profiling.EntityFramework6.MiniProfilerEF6.Initialize(); - - // output cache invidation example - //OutputCacheInvalidationObserver.Execute(); - } - - public int Order - { - get { return int.MinValue; } - } - } - - internal class NullProfilerStorage : IStorage - { - public List GetUnviewedIds(string user) - { - return new List(); - } - - public IEnumerable List(int maxResults, DateTime? start = default(DateTime?), DateTime? finish = default(DateTime?), ListResultsOrder orderBy = ListResultsOrder.Descending) - { - return new List(); - } - - public MiniProfiler Load(Guid id) - { - return null; - } - - public void Save(MiniProfiler profiler) - { - } - public void SetUnviewed(string user, Guid id) - { - } + // output cache invidation example + //OutputCacheInvalidationObserver.Execute(); + } - public void SetViewed(string user, Guid id) - { - } - } + public int Order => int.MinValue; + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml index 0af50e879f..c325f2977c 100644 --- a/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml @@ -3,13 +3,13 @@ @{ Layout = "_AdminLayout"; - ViewBag.Title = "My SmartStore.NET backend extension page"; + ViewBag.Title = "My Smartstore backend extension page"; }
- My SmartStore.NET backend extension page + My Smartstore backend extension page
diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml index 5624434063..79603bcf48 100644 --- a/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml @@ -13,6 +13,8 @@ @using (Html.BeginForm()) { + @Html.AntiForgeryToken() +
diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml index ba456f87eb..bf83d685e0 100644 --- a/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml @@ -1,8 +1,10 @@ @model dynamic +@using StackExchange.Profiling + @if (base.ViewContext.IsBareBonePage()) { return; } -@StackExchange.Profiling.MiniProfiler.RenderIncludes(useExistingjQuery: true, showControls: true) \ No newline at end of file +@(StackExchange.Profiling.MiniProfiler.Current?.RenderIncludes(showControls: true)) \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/MyCustomFacetTemplate.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/MyCustomFacetTemplate.cshtml new file mode 100644 index 0000000000..632a142e51 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/MyCustomFacetTemplate.cshtml @@ -0,0 +1,20 @@ +@model FacetGroup + +@using SmartStore.Core.Search.Facets + +@{ + var facets = Model.Facets.Where(x => x.Published); + + //.... + + if (!facets.Any()) + { + return; + } +} + +
+
+ Hello - my custom facet template for "@(Model.Label)"! +
+
\ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Web.config b/src/Plugins/SmartStore.DevTools/Web.config index 21a2c8be1f..d631d4b983 100644 --- a/src/Plugins/SmartStore.DevTools/Web.config +++ b/src/Plugins/SmartStore.DevTools/Web.config @@ -4,7 +4,7 @@ - + - + @@ -59,7 +59,7 @@ - + @@ -79,19 +79,15 @@ - + - - - - - + @@ -115,20 +111,16 @@ - + - + - - - - @@ -153,11 +145,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/Plugins/SmartStore.DevTools/packages.config b/src/Plugins/SmartStore.DevTools/packages.config index ca9ebcd893..20dd1d3075 100644 --- a/src/Plugins/SmartStore.DevTools/packages.config +++ b/src/Plugins/SmartStore.DevTools/packages.config @@ -1,16 +1,25 @@  - - - + + + - + + + - - - + + + + + + + + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs index c9c33a8456..c6351de248 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs @@ -27,36 +27,37 @@ public ExternalAuthFacebookController( _openAuthenticationService = openAuthenticationService; _externalAuthenticationSettings = externalAuthenticationSettings; } - - [LoadSetting, AdminAuthorize, ChildActionOnly] + + [LoadSetting, AdminAuthorize, ChildActionOnly] [Permission(Permissions.Configuration.Authentication.Read)] public ActionResult Configure(FacebookExternalAuthSettings settings) { var model = new ConfigurationModel(); - MiniMapper.Map(settings, model); + MiniMapper.Map(settings, model); - var host = Services.StoreContext.CurrentStore.GetHost(true); - model.RedirectUrl = $"{host}Plugins/SmartStore.FacebookAuth/logincallback/"; + var host = Services.StoreContext.CurrentStore.GetHost(true); + model.RedirectUrl = $"{host}Plugins/SmartStore.FacebookAuth/logincallback/"; - return View(model); + return View(model); } - [SaveSetting, HttpPost, AdminAuthorize, ChildActionOnly] + [SaveSetting, HttpPost, AdminAuthorize, ChildActionOnly] + [ValidateAntiForgeryToken] [Permission(Permissions.Configuration.Authentication.Update)] - public ActionResult Configure(FacebookExternalAuthSettings settings, ConfigurationModel model) + public ActionResult Configure(FacebookExternalAuthSettings settings, ConfigurationModel model) { if (!ModelState.IsValid) { return Configure(settings); } - MiniMapper.Map(model, settings); - settings.ClientKeyIdentifier = model.ClientKeyIdentifier.TrimSafe(); - settings.ClientSecret = model.ClientSecret.TrimSafe(); + MiniMapper.Map(model, settings); + settings.ClientKeyIdentifier = model.ClientKeyIdentifier.TrimSafe(); + settings.ClientSecret = model.ClientSecret.TrimSafe(); - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - return RedirectToConfiguration(FacebookExternalAuthMethod.SystemName, true); + return RedirectToConfiguration(FacebookExternalAuthMethod.SystemName, true); } [ChildActionOnly] @@ -72,69 +73,69 @@ public ActionResult PublicInfo() return new EmptyResult(); } - public ActionResult Login(string returnUrl) - { - return LoginInternal(returnUrl, false); - } - - public ActionResult LoginCallback(string returnUrl) - { - return LoginInternal(returnUrl, true); - } - - [NonAction] - private ActionResult LoginInternal(string returnUrl, bool verifyResponse) - { - var processor = _openAuthenticationService.LoadExternalAuthenticationMethodBySystemName(FacebookExternalAuthMethod.SystemName, Services.StoreContext.CurrentStore.Id); - if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings)) - { + public ActionResult Login(string returnUrl) + { + return LoginInternal(returnUrl, false); + } + + public ActionResult LoginCallback(string returnUrl) + { + return LoginInternal(returnUrl, true); + } + + [NonAction] + private ActionResult LoginInternal(string returnUrl, bool verifyResponse) + { + var processor = _openAuthenticationService.LoadExternalAuthenticationMethodBySystemName(FacebookExternalAuthMethod.SystemName, Services.StoreContext.CurrentStore.Id); + if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings)) + { NotifyError(T("Plugins.CannotLoadModule", T("Plugins.FriendlyName.SmartStore.FacebookAuth"))); return new RedirectResult(Url.LogOn(returnUrl)); - } - - var viewModel = new LoginModel(); - TryUpdateModel(viewModel); - - var result = _oAuthProviderFacebookAuthorizer.Authorize(returnUrl, verifyResponse); - switch (result.AuthenticationStatus) - { - case OpenAuthenticationStatus.Error: - { - if (!result.Success) - { - result.Errors.Each(x => NotifyError(x)); - } - - return new RedirectResult(Url.LogOn(returnUrl)); - } - case OpenAuthenticationStatus.AssociateOnLogon: - { - return new RedirectResult(Url.LogOn(returnUrl)); - } - case OpenAuthenticationStatus.AutoRegisteredEmailValidation: - { - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl }); - } - case OpenAuthenticationStatus.AutoRegisteredAdminApproval: - { - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl }); - } - case OpenAuthenticationStatus.AutoRegisteredStandard: - { - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard, returnUrl }); - } - default: - break; - } + } + + var viewModel = new LoginModel(); + TryUpdateModel(viewModel); + + var result = _oAuthProviderFacebookAuthorizer.Authorize(returnUrl, verifyResponse); + switch (result.AuthenticationStatus) + { + case OpenAuthenticationStatus.Error: + { + if (!result.Success) + { + result.Errors.Each(x => NotifyError(x)); + } + + return new RedirectResult(Url.LogOn(returnUrl)); + } + case OpenAuthenticationStatus.AssociateOnLogon: + { + return new RedirectResult(Url.LogOn(returnUrl)); + } + case OpenAuthenticationStatus.AutoRegisteredEmailValidation: + { + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl }); + } + case OpenAuthenticationStatus.AutoRegisteredAdminApproval: + { + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl }); + } + case OpenAuthenticationStatus.AutoRegisteredStandard: + { + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard, returnUrl }); + } + default: + break; + } if (result.Result != null) { return result.Result; } - return HttpContext.Request.IsAuthenticated ? - RedirectToReferrer(returnUrl, "~/") : - new RedirectResult(Url.LogOn(returnUrl)); - } - } + return HttpContext.Request.IsAuthenticated ? + RedirectToReferrer(returnUrl, "~/") : + new RedirectResult(Url.LogOn(returnUrl)); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs index e11239ef4f..398f771ff5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs @@ -14,220 +14,220 @@ namespace SmartStore.FacebookAuth.Core { - /// - /// - internal class FacebookOAuth2Client : OAuth2Client - { - /// - /// The authorization endpoint. - /// - private const string AuthorizationEndpoint = "https://www.facebook.com/v2.8/dialog/oauth"; - /// - /// The token endpoint. - /// - private const string TokenEndpoint = "https://graph.facebook.com/v2.8/oauth/access_token"; - /// - /// The user info endpoint. - /// - private const string UserInfoEndpoint = "https://graph.facebook.com/v2.8/me"; - /// - /// The app id. - /// - private readonly string _appId; - /// - /// The app secret. - /// - private readonly string _appSecret; - - /// - /// The requested scopes. - /// - private readonly string[] _requestedScopes; - - - /// - /// Creates a new Facebook OAuth2 client, requesting the default "email" scope. - /// - /// The Facebook App Id - /// The Facebook App Secret - public FacebookOAuth2Client(string appId, string appSecret) - : this(appId, appSecret, new[] { "email" }) { } - - /// - /// Creates a new Facebook OAuth2 client. - /// - /// The Facebook App Id - /// The Facebook App Secret - /// One or more requested scopes, passed without the base URI. - public FacebookOAuth2Client(string appId, string appSecret, params string[] requestedScopes) - : base("facebook") + /// + /// + internal class FacebookOAuth2Client : OAuth2Client + { + /// + /// The authorization endpoint. + /// + private const string AuthorizationEndpoint = "https://www.facebook.com/v2.8/dialog/oauth"; + /// + /// The token endpoint. + /// + private const string TokenEndpoint = "https://graph.facebook.com/v2.8/oauth/access_token"; + /// + /// The user info endpoint. + /// + private const string UserInfoEndpoint = "https://graph.facebook.com/v2.8/me"; + /// + /// The app id. + /// + private readonly string _appId; + /// + /// The app secret. + /// + private readonly string _appSecret; + + /// + /// The requested scopes. + /// + private readonly string[] _requestedScopes; + + + /// + /// Creates a new Facebook OAuth2 client, requesting the default "email" scope. + /// + /// The Facebook App Id + /// The Facebook App Secret + public FacebookOAuth2Client(string appId, string appSecret) + : this(appId, appSecret, new[] { "email" }) { } + + /// + /// Creates a new Facebook OAuth2 client. + /// + /// The Facebook App Id + /// The Facebook App Secret + /// One or more requested scopes, passed without the base URI. + public FacebookOAuth2Client(string appId, string appSecret, params string[] requestedScopes) + : base("facebook") { - if (string.IsNullOrWhiteSpace(appId)) - throw new ArgumentNullException("appId"); - - if (string.IsNullOrWhiteSpace(appSecret)) - throw new ArgumentNullException("appSecret"); - - if (requestedScopes == null) - throw new ArgumentNullException("requestedScopes"); - - if (requestedScopes.Length == 0) - throw new ArgumentException("One or more scopes must be requested.", "requestedScopes"); - - _appId = appId; - _appSecret = appSecret; - _requestedScopes = requestedScopes; - } - - public override void RequestAuthentication(HttpContextBase context, Uri returnUrl) - { - string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; - context.Response.Redirect(redirectUrl, endResponse: true); - } - - public new AuthenticationResult VerifyAuthentication(HttpContextBase context) - { - throw new NoNullAllowedException(); - } - - public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl) - { - string code = context.Request.QueryString["code"]; - if (string.IsNullOrEmpty(code)) - { - return AuthenticationResult.Failed; - } - - string accessToken = this.QueryAccessToken(returnPageUrl, code); - if (accessToken == null) - { - return AuthenticationResult.Failed; - } - - IDictionary userData = this.GetUserData(accessToken); - if (userData == null) - { - return AuthenticationResult.Failed; - } - - string id = userData["id"]; - string name; - - // Some oAuth providers do not return value for the 'username' attribute. - // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' - if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) - { - name = id; - } - - // add the access token to the user data dictionary just in case page developers want to use it - userData["accesstoken"] = accessToken; - - return new AuthenticationResult(isSuccessful: true, provider: ProviderName, providerUserId: id, userName: name, extraData: userData); - } - - protected override Uri GetServiceLoginUrl(Uri returnUrl) - { - var state = string.IsNullOrEmpty(returnUrl.Query) ? string.Empty : returnUrl.Query.Substring(1); - - return BuildUri(AuthorizationEndpoint, new NameValueCollection - { - { "client_id", _appId }, - { "scope", string.Join(" ", _requestedScopes) }, - { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, - { "state", state }, - }); - } - - protected override IDictionary GetUserData(string accessToken) - { - var uri = BuildUri(UserInfoEndpoint, new NameValueCollection { { "access_token", accessToken } }); - - var webRequest = (HttpWebRequest)WebRequest.Create(uri); - - using (var webResponse = webRequest.GetResponse()) - using (var stream = webResponse.GetResponseStream()) - { - if (stream == null) - return null; - - using (var textReader = new StreamReader(stream)) - { - var json = textReader.ReadToEnd(); - var extraData = JsonConvert.DeserializeObject>(json); - var data = extraData.ToDictionary(x => x.Key, x => x.Value.ToString()); - - data.Add("picture", string.Format("https://graph.facebook.com/{0}/picture", data["id"])); - - return data; - } - } - } - - public string QueryAccessTokenByCode(Uri returnUrl, string authorizationCode) - { - return this.QueryAccessToken(returnUrl, authorizationCode); - } - - protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) - { - var uri = BuildUri(TokenEndpoint, new NameValueCollection - { - { "code", authorizationCode }, - { "client_id", _appId }, - { "client_secret", _appSecret }, - { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, - }); - - var webRequest = (HttpWebRequest)WebRequest.Create(uri); - string accessToken = null; - - using (var response = (HttpWebResponse)webRequest.GetResponse()) - { - // handle response from FB - // this will not be a url with params like the first request to get the 'code' - Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); - - using (var sr = new StreamReader(response.GetResponseStream(), rEncoding)) - { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); - var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); - - Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); - accessToken = desirializedJsonObject["access_token"].ToString(); - } - } - - return accessToken; - } - - private static Uri BuildUri(string baseUri, NameValueCollection queryParameters) - { - var keyValuePairs = queryParameters.AllKeys.Select(k => HttpUtility.UrlEncode(k) + "=" + HttpUtility.UrlEncode(queryParameters[k])); - var qs = String.Join("&", keyValuePairs); - - var builder = new UriBuilder(baseUri) { Query = qs }; - return builder.Uri; - } - - /// - /// Facebook works best when return data be packed into a "state" parameter. - /// This should be called before verifying the request, so that the url is rewritten to support this. - /// - public static void RewriteRequest() - { - var ctx = HttpContext.Current; - - var stateString = HttpUtility.UrlDecode(ctx.Request.QueryString["state"]); - if (stateString == null || !stateString.Contains("__provider__=facebook")) - return; - - var q = HttpUtility.ParseQueryString(stateString); - q.Add(ctx.Request.QueryString); - q.Remove("state"); - - ctx.RewritePath(ctx.Request.Path + "?" + q); - } - } + if (string.IsNullOrWhiteSpace(appId)) + throw new ArgumentNullException("appId"); + + if (string.IsNullOrWhiteSpace(appSecret)) + throw new ArgumentNullException("appSecret"); + + if (requestedScopes == null) + throw new ArgumentNullException("requestedScopes"); + + if (requestedScopes.Length == 0) + throw new ArgumentException("One or more scopes must be requested.", "requestedScopes"); + + _appId = appId; + _appSecret = appSecret; + _requestedScopes = requestedScopes; + } + + public override void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; + context.Response.Redirect(redirectUrl, endResponse: true); + } + + public new AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + throw new NoNullAllowedException(); + } + + public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl) + { + string code = context.Request.QueryString["code"]; + if (string.IsNullOrEmpty(code)) + { + return AuthenticationResult.Failed; + } + + string accessToken = this.QueryAccessToken(returnPageUrl, code); + if (accessToken == null) + { + return AuthenticationResult.Failed; + } + + IDictionary userData = this.GetUserData(accessToken); + if (userData == null) + { + return AuthenticationResult.Failed; + } + + string id = userData["id"]; + string name; + + // Some oAuth providers do not return value for the 'username' attribute. + // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' + if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) + { + name = id; + } + + // add the access token to the user data dictionary just in case page developers want to use it + userData["accesstoken"] = accessToken; + + return new AuthenticationResult(isSuccessful: true, provider: ProviderName, providerUserId: id, userName: name, extraData: userData); + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + var state = string.IsNullOrEmpty(returnUrl.Query) ? string.Empty : returnUrl.Query.Substring(1); + + return BuildUri(AuthorizationEndpoint, new NameValueCollection + { + { "client_id", _appId }, + { "scope", string.Join(" ", _requestedScopes) }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + { "state", state }, + }); + } + + protected override IDictionary GetUserData(string accessToken) + { + var uri = BuildUri(UserInfoEndpoint, new NameValueCollection { { "access_token", accessToken } }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + { + if (stream == null) + return null; + + using (var textReader = new StreamReader(stream)) + { + var json = textReader.ReadToEnd(); + var extraData = JsonConvert.DeserializeObject>(json); + var data = extraData.ToDictionary(x => x.Key, x => x.Value.ToString()); + + data.Add("picture", string.Format("https://graph.facebook.com/{0}/picture", data["id"])); + + return data; + } + } + } + + public string QueryAccessTokenByCode(Uri returnUrl, string authorizationCode) + { + return this.QueryAccessToken(returnUrl, authorizationCode); + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + var uri = BuildUri(TokenEndpoint, new NameValueCollection + { + { "code", authorizationCode }, + { "client_id", _appId }, + { "client_secret", _appSecret }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + string accessToken = null; + + using (var response = (HttpWebResponse)webRequest.GetResponse()) + { + // handle response from FB + // this will not be a url with params like the first request to get the 'code' + Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); + + using (var sr = new StreamReader(response.GetResponseStream(), rEncoding)) + { + var serializer = new JavaScriptSerializer(); + var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); + var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); + + Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); + accessToken = desirializedJsonObject["access_token"].ToString(); + } + } + + return accessToken; + } + + private static Uri BuildUri(string baseUri, NameValueCollection queryParameters) + { + var keyValuePairs = queryParameters.AllKeys.Select(k => HttpUtility.UrlEncode(k) + "=" + HttpUtility.UrlEncode(queryParameters[k])); + var qs = String.Join("&", keyValuePairs); + + var builder = new UriBuilder(baseUri) { Query = qs }; + return builder.Uri; + } + + /// + /// Facebook works best when return data be packed into a "state" parameter. + /// This should be called before verifying the request, so that the url is rewritten to support this. + /// + public static void RewriteRequest() + { + var ctx = HttpContext.Current; + + var stateString = HttpUtility.UrlDecode(ctx.Request.QueryString["state"]); + if (stateString == null || !stateString.Contains("__provider__=facebook")) + return; + + var q = HttpUtility.ParseQueryString(stateString); + q.Add(ctx.Request.QueryString); + q.Remove("state"); + + ctx.RewritePath(ctx.Request.Path + "?" + q); + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs index 6f987af313..41594bc23e 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs @@ -17,279 +17,279 @@ namespace SmartStore.FacebookAuth.Core { - public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer - { - #region Fields - - private readonly IExternalAuthorizer _authorizer; + public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer + { + #region Fields + + private readonly IExternalAuthorizer _authorizer; private readonly IOpenAuthenticationService _openAuthenticationService; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; private readonly HttpContextBase _httpContext; - private readonly ICommonServices _services; + private readonly ICommonServices _services; - private FacebookOAuth2Client _facebookApplication; + private FacebookOAuth2Client _facebookApplication; - #endregion + #endregion - #region Ctor + #region Ctor public FacebookProviderAuthorizer(IExternalAuthorizer authorizer, IOpenAuthenticationService openAuthenticationService, ExternalAuthenticationSettings externalAuthenticationSettings, HttpContextBase httpContext, - ICommonServices services) + ICommonServices services) { _authorizer = authorizer; _openAuthenticationService = openAuthenticationService; _externalAuthenticationSettings = externalAuthenticationSettings; _httpContext = httpContext; - _services = services; + _services = services; - Logger = NullLogger.Instance; - } + Logger = NullLogger.Instance; + } - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } - #endregion + #endregion - #region Utilities + #region Utilities - private FacebookOAuth2Client FacebookApplication + private FacebookOAuth2Client FacebookApplication { - get - { - if (_facebookApplication == null) - { - var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + get + { + if (_facebookApplication == null) + { + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - _facebookApplication = new FacebookOAuth2Client(settings.ClientKeyIdentifier, settings.ClientSecret); - } + _facebookApplication = new FacebookOAuth2Client(settings.ClientKeyIdentifier, settings.ClientSecret); + } - return _facebookApplication; - } + return _facebookApplication; + } } - private AuthorizeState VerifyAuthentication(string returnUrl) + private AuthorizeState VerifyAuthentication(string returnUrl) { - string error = null; - AuthenticationResult authResult = null; - - try - { - authResult = this.FacebookApplication.VerifyAuthentication(_httpContext, GenerateLocalCallbackUri()); - } - catch (WebException wexc) - { - using (var response = wexc.Response as HttpWebResponse) - { - error = response.StatusDescription; - - var enc = Encoding.GetEncoding(response.CharacterSet); - using (var reader = new StreamReader(response.GetResponseStream(), enc)) - { - var rawResponse = reader.ReadToEnd(); - Logger.Log(LogLevel.Error, new Exception(rawResponse), response.StatusDescription, null); - } - } - } - catch (Exception exception) - { - error = exception.ToString(); - Logger.Log(LogLevel.Error, exception, null, null); - } - - if (authResult != null && authResult.IsSuccessful) - { - if (!authResult.ExtraData.ContainsKey("id")) - throw new Exception("Authentication result does not contain id data"); - - if (!authResult.ExtraData.ContainsKey("accesstoken")) - throw new Exception("Authentication result does not contain accesstoken data"); - - var parameters = new OAuthAuthenticationParameters(FacebookExternalAuthMethod.SystemName) - { - ExternalIdentifier = authResult.ProviderUserId, - OAuthToken = authResult.ExtraData["accesstoken"], - OAuthAccessToken = authResult.ProviderUserId, - }; - - if (_externalAuthenticationSettings.AutoRegisterEnabled) - ParseClaims(authResult, parameters); - - var result = _authorizer.Authorize(parameters); - - return new AuthorizeState(returnUrl, result); - } - - if (error.IsEmpty() && authResult != null && authResult.Error != null) - { - error = authResult.Error.Message; - } - if (error.IsEmpty()) - { - error = _services.Localization.GetResource("Admin.Common.UnknownError"); - } - - var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); - state.AddError(error); + string error = null; + AuthenticationResult authResult = null; + + try + { + authResult = this.FacebookApplication.VerifyAuthentication(_httpContext, GenerateLocalCallbackUri()); + } + catch (WebException wexc) + { + using (var response = wexc.Response as HttpWebResponse) + { + error = response.StatusDescription; + + var enc = Encoding.GetEncoding(response.CharacterSet); + using (var reader = new StreamReader(response.GetResponseStream(), enc)) + { + var rawResponse = reader.ReadToEnd(); + Logger.Log(LogLevel.Error, new Exception(rawResponse), response.StatusDescription, null); + } + } + } + catch (Exception exception) + { + error = exception.ToString(); + Logger.Log(LogLevel.Error, exception, null, null); + } + + if (authResult != null && authResult.IsSuccessful) + { + if (!authResult.ExtraData.ContainsKey("id")) + throw new Exception("Authentication result does not contain id data"); + + if (!authResult.ExtraData.ContainsKey("accesstoken")) + throw new Exception("Authentication result does not contain accesstoken data"); + + var parameters = new OAuthAuthenticationParameters(FacebookExternalAuthMethod.SystemName) + { + ExternalIdentifier = authResult.ProviderUserId, + OAuthToken = authResult.ExtraData["accesstoken"], + OAuthAccessToken = authResult.ProviderUserId, + }; + + if (_externalAuthenticationSettings.AutoRegisterEnabled) + ParseClaims(authResult, parameters); + + var result = _authorizer.Authorize(parameters); + + return new AuthorizeState(returnUrl, result); + } + + if (error.IsEmpty() && authResult != null && authResult.Error != null) + { + error = authResult.Error.Message; + } + if (error.IsEmpty()) + { + error = _services.Localization.GetResource("Admin.Common.UnknownError"); + } + + var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); + state.AddError(error); return state; } - private string GetEmailFromFacebook(string accessToken) - { - var result = ""; - var webRequest = WebRequest.Create("https://graph.facebook.com/me?fields=email&access_token=" + EscapeUriDataStringRfc3986(accessToken)); - - using (var webResponse = webRequest.GetResponse()) - using (var stream = webResponse.GetResponseStream()) - using (var reader = new StreamReader(stream)) - { - var strResponse = reader.ReadToEnd(); - var info = JObject.Parse(strResponse); - - if (info["email"] != null) - { - result = info["email"].ToString(); - } - } - return result; - } - - private void ParseClaims(AuthenticationResult authenticationResult, OAuthAuthenticationParameters parameters) + private string GetEmailFromFacebook(string accessToken) + { + var result = ""; + var webRequest = WebRequest.Create("https://graph.facebook.com/me?fields=email&access_token=" + EscapeUriDataStringRfc3986(accessToken)); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + using (var reader = new StreamReader(stream)) + { + var strResponse = reader.ReadToEnd(); + var info = JObject.Parse(strResponse); + + if (info["email"] != null) + { + result = info["email"].ToString(); + } + } + return result; + } + + private void ParseClaims(AuthenticationResult authenticationResult, OAuthAuthenticationParameters parameters) { - var claims = new UserClaims(); - claims.Contact = new ContactClaims(); - - if (authenticationResult.ExtraData.ContainsKey("username")) - { - claims.Contact.Email = authenticationResult.ExtraData["username"]; - } - else - { - claims.Contact.Email = GetEmailFromFacebook(authenticationResult.ExtraData["accesstoken"]); - } - - claims.Name = new NameClaims(); - - if (authenticationResult.ExtraData.ContainsKey("name")) - { - var name = authenticationResult.ExtraData["name"]; - if (!String.IsNullOrEmpty(name)) - { - var nameSplit = name.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - if (nameSplit.Length >= 2) - { - claims.Name.First = nameSplit[0]; - claims.Name.Last = nameSplit[1]; - } - else - { - claims.Name.Last = nameSplit[0]; - } - } - } + var claims = new UserClaims(); + claims.Contact = new ContactClaims(); + + if (authenticationResult.ExtraData.ContainsKey("username")) + { + claims.Contact.Email = authenticationResult.ExtraData["username"]; + } + else + { + claims.Contact.Email = GetEmailFromFacebook(authenticationResult.ExtraData["accesstoken"]); + } + + claims.Name = new NameClaims(); + + if (authenticationResult.ExtraData.ContainsKey("name")) + { + var name = authenticationResult.ExtraData["name"]; + if (!String.IsNullOrEmpty(name)) + { + var nameSplit = name.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (nameSplit.Length >= 2) + { + claims.Name.First = nameSplit[0]; + claims.Name.Last = nameSplit[1]; + } + else + { + claims.Name.Last = nameSplit[0]; + } + } + } parameters.AddClaim(claims); } - private AuthorizeState RequestAuthentication(string returnUrl) + private AuthorizeState RequestAuthentication(string returnUrl) + { + var authUrl = GenerateServiceLoginUrl().AbsoluteUri; + return new AuthorizeState("", OpenAuthenticationStatus.RequiresRedirect) { Result = new RedirectResult(authUrl) }; + } + + private Uri GenerateLocalCallbackUri() + { + string url = string.Format("{0}Plugins/SmartStore.FacebookAuth/logincallback/", _services.WebHelper.GetStoreLocation()); + return new Uri(url); + } + + private Uri GenerateServiceLoginUrl() + { + //code copied from DotNetOpenAuth.AspNet.Clients.FacebookClient file + var builder = new UriBuilder("https://www.facebook.com/dialog/oauth"); + var args = new Dictionary(); + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + + args.Add("client_id", settings.ClientKeyIdentifier); + args.Add("redirect_uri", GenerateLocalCallbackUri().AbsoluteUri); + args.Add("scope", "email"); + + AppendQueryArgs(builder, args); + + return builder.Uri; + } + + private void AppendQueryArgs(UriBuilder builder, IEnumerable> args) + { + if ((args != null) && (args.Count>() > 0)) + { + StringBuilder builder2 = new StringBuilder(50 + (args.Count>() * 10)); + if (!string.IsNullOrEmpty(builder.Query)) + { + builder2.Append(builder.Query.Substring(1)); + builder2.Append('&'); + } + builder2.Append(CreateQueryString(args)); + builder.Query = builder2.ToString(); + } + } + + private string CreateQueryString(IEnumerable> args) + { + if (!args.Any>()) + { + return string.Empty; + } + StringBuilder builder = new StringBuilder(args.Count>() * 10); + foreach (KeyValuePair pair in args) + { + builder.Append(EscapeUriDataStringRfc3986(pair.Key)); + builder.Append('='); + builder.Append(EscapeUriDataStringRfc3986(pair.Value)); + builder.Append('&'); + } + builder.Length--; + return builder.ToString(); + } + + private readonly string[] UriRfc3986CharsToEscape = new string[] { "!", "*", "'", "(", ")" }; + + private string EscapeUriDataStringRfc3986(string value) + { + StringBuilder builder = new StringBuilder(Uri.EscapeDataString(value)); + for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++) + { + builder.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0])); + } + return builder.ToString(); + } + + #endregion + + #region Methods + + /// + /// Authorize response + /// + /// Return URL + /// true - Verify response;false - request authentication;null - determine automatically + /// Authorize state + public AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null) { - var authUrl = GenerateServiceLoginUrl().AbsoluteUri; - return new AuthorizeState("", OpenAuthenticationStatus.RequiresRedirect) { Result = new RedirectResult(authUrl) }; + if (!verifyResponse.HasValue) + throw new ArgumentException("Facebook plugin cannot automatically determine verifyResponse property"); + + if (verifyResponse.Value) + { + return VerifyAuthentication(returnUrl); + } + else + { + return RequestAuthentication(returnUrl); + } } - private Uri GenerateLocalCallbackUri() - { - string url = string.Format("{0}Plugins/SmartStore.FacebookAuth/logincallback/", _services.WebHelper.GetStoreLocation()); - return new Uri(url); - } - - private Uri GenerateServiceLoginUrl() - { - //code copied from DotNetOpenAuth.AspNet.Clients.FacebookClient file - var builder = new UriBuilder("https://www.facebook.com/dialog/oauth"); - var args = new Dictionary(); - var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - - args.Add("client_id", settings.ClientKeyIdentifier); - args.Add("redirect_uri", GenerateLocalCallbackUri().AbsoluteUri); - args.Add("scope", "email"); - - AppendQueryArgs(builder, args); - - return builder.Uri; - } - - private void AppendQueryArgs(UriBuilder builder, IEnumerable> args) - { - if ((args != null) && (args.Count>() > 0)) - { - StringBuilder builder2 = new StringBuilder(50 + (args.Count>() * 10)); - if (!string.IsNullOrEmpty(builder.Query)) - { - builder2.Append(builder.Query.Substring(1)); - builder2.Append('&'); - } - builder2.Append(CreateQueryString(args)); - builder.Query = builder2.ToString(); - } - } - - private string CreateQueryString(IEnumerable> args) - { - if (!args.Any>()) - { - return string.Empty; - } - StringBuilder builder = new StringBuilder(args.Count>() * 10); - foreach (KeyValuePair pair in args) - { - builder.Append(EscapeUriDataStringRfc3986(pair.Key)); - builder.Append('='); - builder.Append(EscapeUriDataStringRfc3986(pair.Value)); - builder.Append('&'); - } - builder.Length--; - return builder.ToString(); - } - - private readonly string[] UriRfc3986CharsToEscape = new string[] { "!", "*", "'", "(", ")" }; - - private string EscapeUriDataStringRfc3986(string value) - { - StringBuilder builder = new StringBuilder(Uri.EscapeDataString(value)); - for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++) - { - builder.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0])); - } - return builder.ToString(); - } - - #endregion - - #region Methods - - /// - /// Authorize response - /// - /// Return URL - /// true - Verify response;false - request authentication;null - determine automatically - /// Authorize state - public AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null) - { - if (!verifyResponse.HasValue) - throw new ArgumentException("Facebook plugin cannot automatically determine verifyResponse property"); - - if (verifyResponse.Value) - { - return VerifyAuthentication(returnUrl); - } - else - { - return RequestAuthentication(returnUrl); - } - } - - #endregion + #endregion } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/OAuthAuthenticationParameters.cs b/src/Plugins/SmartStore.FacebookAuth/Core/OAuthAuthenticationParameters.cs index 722482aa13..9ef3f5bbc5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/OAuthAuthenticationParameters.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/OAuthAuthenticationParameters.cs @@ -15,13 +15,7 @@ public OAuthAuthenticationParameters(string providerSystemName) this._providerSystemName = providerSystemName; } - public override IList UserClaims - { - get - { - return _claims; - } - } + public override IList UserClaims => _claims; public void AddClaim(UserClaims claim) { @@ -31,9 +25,6 @@ public void AddClaim(UserClaims claim) _claims.Add(claim); } - public override string ProviderSystemName - { - get { return _providerSystemName; } - } + public override string ProviderSystemName => _providerSystemName; } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs b/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs index ef4c32cf34..08c9c95ad5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs @@ -7,14 +7,11 @@ namespace SmartStore.FacebookAuth { public class DependencyRegistrar : IDependencyRegistrar { - public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) + public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { builder.RegisterType().As().InstancePerRequest(); } - public int Order - { - get { return 1; } - } + public int Order => 1; } } diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index 447d4bf86d..ed03b2381a 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Description.txt +++ b/src/Plugins/SmartStore.FacebookAuth/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Facebook Login SystemName: SmartStore.FacebookAuth Group: Security -Version: 3.2.2 -MinAppVersion: 3.2.2 +Version: 4.2.0 +MinAppVersion: 4.2.0 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll ResourceRootKey: Plugins.ExternalAuth.Facebook \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs b/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs index db68ea5d5c..0c7b986ed9 100644 --- a/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs +++ b/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs @@ -17,19 +17,19 @@ public FacebookExternalAuthMethod(ILocalizationService localizationService) _localizationService = localizationService; } - public static string SystemName => "SmartStore.FacebookAuth"; + public static string SystemName => "SmartStore.FacebookAuth"; - /// - /// Gets a route for provider configuration - /// - /// Action name - /// Controller name - /// Route values - public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + /// + /// Gets a route for provider configuration + /// + /// Action name + /// Controller name + /// Route values + public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { - actionName = "Configure"; - controllerName = "ExternalAuthFacebook"; - routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = SystemName }); + actionName = "Configure"; + controllerName = "ExternalAuthFacebook"; + routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = SystemName }); } /// @@ -42,7 +42,7 @@ public void GetPublicInfoRoute(out string actionName, out string controllerName, { actionName = "PublicInfo"; controllerName = "ExternalAuthFacebook"; - routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = SystemName }); + routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = SystemName }); } /// @@ -60,6 +60,6 @@ public override void Uninstall() _localizationService.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); base.Uninstall(); - } + } } } diff --git a/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs index dc0a098627..a437ae548d 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs @@ -11,7 +11,7 @@ public class ConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.ClientSecret")] public string ClientSecret { get; set; } - [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.RedirectUri")] - public string RedirectUrl { get; set; } - } + [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.RedirectUri")] + public string RedirectUrl { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs index fdb9911da7..1d9b8cf532 100644 --- a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs +++ b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs @@ -4,23 +4,17 @@ namespace SmartStore.FacebookAuth { - public partial class RouteProvider : IRouteProvider + public partial class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { - routes.MapRoute("SmartStore.FacebookAuth", - "Plugins/SmartStore.FacebookAuth/{action}", - new { controller = "ExternalAuthFacebook" }, - new[] { "SmartStore.FacebookAuth.Controllers" } - ) - .DataTokens["area"] = FacebookExternalAuthMethod.SystemName; - } - public int Priority - { - get - { - return 0; - } + routes.MapRoute("SmartStore.FacebookAuth", + "Plugins/SmartStore.FacebookAuth/{action}", + new { controller = "ExternalAuthFacebook" }, + new[] { "SmartStore.FacebookAuth.Controllers" } + ) + .DataTokens["area"] = FacebookExternalAuthMethod.SystemName; } + public int Priority => 0; } } diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 5a2774492f..7696d4a5f8 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -20,7 +20,7 @@ Properties SmartStore.FacebookAuth SmartStore.FacebookAuth - v4.6.1 + v4.7.2 512 @@ -85,12 +85,12 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll False - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll False @@ -123,18 +123,22 @@ ..\..\packages\DotNetOpenAuth.OpenId.RelyingParty.4.3.4.13329\lib\net45-full\DotNetOpenAuth.OpenId.RelyingParty.dll True - - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + False ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - False + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + @@ -147,13 +151,19 @@ False + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + - + ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll False diff --git a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml index 1658e3b419..a51e4a6d52 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml +++ b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml @@ -15,6 +15,8 @@ @using (Html.BeginForm()) { + @Html.AntiForgeryToken() +
@using (Html.BeginForm()) -{ +{ + @Html.AntiForgeryToken() + - - - + + + +
@@ -41,7 +43,7 @@ @Html.SmartLabelFor(model => model.TrackingScript) + @Html.SettingEditorFor(model => model.TrackingScript, Html.TextAreaFor(model => model.TrackingScript, new { style = "height: 300px;" })) @Html.ValidationMessageFor(model => model.TrackingScript) @@ -51,7 +53,7 @@ @Html.SmartLabelFor(model => model.EcommerceScript) + @Html.SettingEditorFor(model => model.EcommerceScript, Html.TextAreaFor(model => model.EcommerceScript, new { style = "height: 300px;" })) @Html.ValidationMessageFor(model => model.EcommerceScript) @@ -61,11 +63,23 @@ @Html.SmartLabelFor(model => model.EcommerceDetailScript) + @Html.SettingEditorFor(model => model.EcommerceDetailScript, Html.TextAreaFor(model => model.EcommerceDetailScript, new { style = "height: 200px;" })) @Html.ValidationMessageFor(model => model.EcommerceDetailScript)
+ + +
} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/packages.config b/src/Plugins/SmartStore.GoogleAnalytics/packages.config index d1411e05a4..bba317dcf9 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/packages.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/packages.config @@ -3,6 +3,6 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/web.config b/src/Plugins/SmartStore.GoogleAnalytics/web.config index 63451742ac..c2fd9be3a5 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/web.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/web.config @@ -1,9 +1,9 @@ - + - - - + + + - + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + - + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs index 6a62863d9a..14571d6a02 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs @@ -5,13 +5,13 @@ namespace SmartStore.GoogleMerchantCenter { public class AdminMenu : AdminMenuProvider { - protected override void BuildMenuCore(TreeNode pluginsNode) + protected override void BuildMenuCore(TreeNode pluginsNode) { - var menuItem = new MenuItem().ToBuilder() + var menuItem = new MenuItem().ToBuilder() .Text("Google Merchant Center") .Icon("fab fa-google") .ResKey("Plugins.FriendlyName.SmartStore.GoogleMerchantCenter") - .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) + .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) .ToItem(); pluginsNode.Prepend(menuItem); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs index ed8533af16..1b20158c12 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs @@ -15,177 +15,177 @@ namespace SmartStore.GoogleMerchantCenter.Controllers { - [AdminAuthorize] - public class FeedGoogleMerchantCenterController : PluginControllerBase - { - private readonly IGoogleFeedService _googleFeedService; - private readonly Lazy _adminAreaSettings; - private readonly Lazy _exportService; - - public FeedGoogleMerchantCenterController( - IGoogleFeedService googleFeedService, - Lazy adminAreaSettings, - Lazy exportService) - { - _googleFeedService = googleFeedService; - _adminAreaSettings = adminAreaSettings; - _exportService = exportService; - } - - public ActionResult ProductEditTab(int productId) - { - var culture = CultureInfo.InvariantCulture; - var model = new GoogleProductModel { ProductId = productId }; - var entity = _googleFeedService.GetGoogleProductRecord(productId); - string notSpecified = T("Common.Unspecified"); - - if (entity != null) - { - model.Taxonomy = entity.Taxonomy; - model.Gender = entity.Gender; - model.AgeGroup = entity.AgeGroup; - model.IsAdult = entity.IsAdult; - model.Color = entity.Color; - model.Size = entity.Size; - model.Material = entity.Material; - model.Pattern = entity.Pattern; - model.Export2 = entity.Export; - model.Multipack2 = entity.Multipack; - model.IsBundle = entity.IsBundle; - model.EnergyEfficiencyClass = entity.EnergyEfficiencyClass; - model.CustomLabel0 = entity.CustomLabel0; - model.CustomLabel1 = entity.CustomLabel1; - model.CustomLabel2 = entity.CustomLabel2; - model.CustomLabel3 = entity.CustomLabel3; - model.CustomLabel4 = entity.CustomLabel4; - } - else - { - model.Export2 = true; - } - - ViewBag.DefaultCategory = ""; - ViewBag.DefaultColor = ""; - ViewBag.DefaultSize = ""; - ViewBag.DefaultMaterial = ""; - ViewBag.DefaultPattern = ""; - ViewBag.DefaultGender = notSpecified; - ViewBag.DefaultAgeGroup = notSpecified; - ViewBag.DefaultIsAdult = ""; - ViewBag.DefaultMultipack2 = ""; - ViewBag.DefaultIsBundle = ""; - ViewBag.DefaultEnergyEfficiencyClass = notSpecified; - ViewBag.DefaultCustomLabel = ""; - ViewBag.LanguageSeoCode = Services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); - - // We do not have export profile context here, so we simply use the first profile. - var profile = _exportService.Value.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName).FirstOrDefault(); - if (profile != null) - { - var config = XmlHelper.Deserialize(profile.ProviderConfigData, typeof(ProfileConfigurationModel)) as ProfileConfigurationModel; - if (config != null) - { - ViewBag.DefaultCategory = config.DefaultGoogleCategory; - ViewBag.DefaultColor = config.Color; - ViewBag.DefaultSize = config.Size; - ViewBag.DefaultMaterial = config.Material; - ViewBag.DefaultPattern = config.Pattern; - - if (config.Gender.HasValue() && config.Gender != GmcXmlExportProvider.Unspecified) - { - ViewBag.DefaultGender = T("Plugins.Feed.Froogle.Gender" + culture.TextInfo.ToTitleCase(config.Gender)); - } - - if (config.AgeGroup.HasValue() && config.AgeGroup != GmcXmlExportProvider.Unspecified) - { - ViewBag.DefaultAgeGroup = T("Plugins.Feed.Froogle.AgeGroup" + culture.TextInfo.ToTitleCase(config.AgeGroup)); - } - } - } - - ViewBag.AvailableCategories = model.Taxonomy.HasValue() - ? new List { new SelectListItem { Text = model.Taxonomy, Value = model.Taxonomy, Selected = true } } - : new List(); - - ViewBag.AvailableGenders = new List - { - new SelectListItem { Value = "male", Text = T("Plugins.Feed.Froogle.GenderMale") }, - new SelectListItem { Value = "female", Text = T("Plugins.Feed.Froogle.GenderFemale") }, - new SelectListItem { Value = "unisex", Text = T("Plugins.Feed.Froogle.GenderUnisex") } - }; - - ViewBag.AvailableAgeGroups = new List - { - new SelectListItem { Value = "adult", Text = T("Plugins.Feed.Froogle.AgeGroupAdult") }, - new SelectListItem { Value = "kids", Text = T("Plugins.Feed.Froogle.AgeGroupKids") }, - }; - - ViewBag.AvailableEnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text - .SplitSafe(",") - .Select(x => new SelectListItem { Value = x, Text = x }) - .ToList(); - - var result = PartialView(model); - result.ViewData.TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "CustomProperties[GMC]" }; - return result; - } - - public ActionResult Configure() - { - var model = new FeedGoogleMerchantCenterModel(); - - model.GridPageSize = _adminAreaSettings.Value.GridPageSize; - model.LanguageSeoCode = Services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); - model.EnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text.SplitSafe(","); - - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult GoogleProductList(GridCommand command, string searchProductName, string touched) - { - return new JsonResult - { - Data = _googleFeedService.GetGridModel(command, searchProductName, touched) - }; - } - - [HttpPost] - public ActionResult GoogleProductEdit(int pk, string name, string value) - { - _googleFeedService.Upsert(pk, name, value); - - return new EmptyResult(); - } - - public JsonResult GetGoogleCategories(string search, int? page) - { - page = page ?? 1; - const int take = 100; - var skip = (page.Value - 1) * take; - var categories = _googleFeedService.GetTaxonomyList(search); - var hasMoreItems = (page.Value * take) < categories.Count; - //$"{skip}\\{categories.Length} {hasMoreItems}: {search.NaIfEmpty()}".Dump(); - - var items = categories.Select(x => new - { - id = x, - text = x - }) - .Skip(skip) - .Take(take) - .ToList(); - - return new JsonResult - { - Data = new - { - hasMoreItems, - results = items - }, - MaxJsonLength = int.MaxValue, - JsonRequestBehavior = JsonRequestBehavior.AllowGet - }; - } - } + [AdminAuthorize] + public class FeedGoogleMerchantCenterController : PluginControllerBase + { + private readonly IGoogleFeedService _googleFeedService; + private readonly Lazy _adminAreaSettings; + private readonly Lazy _exportService; + + public FeedGoogleMerchantCenterController( + IGoogleFeedService googleFeedService, + Lazy adminAreaSettings, + Lazy exportService) + { + _googleFeedService = googleFeedService; + _adminAreaSettings = adminAreaSettings; + _exportService = exportService; + } + + public ActionResult ProductEditTab(int productId) + { + var culture = CultureInfo.InvariantCulture; + var model = new GoogleProductModel { ProductId = productId }; + var entity = _googleFeedService.GetGoogleProductRecord(productId); + string notSpecified = T("Common.Unspecified"); + + if (entity != null) + { + model.Taxonomy = entity.Taxonomy; + model.Gender = entity.Gender; + model.AgeGroup = entity.AgeGroup; + model.IsAdult = entity.IsAdult; + model.Color = entity.Color; + model.Size = entity.Size; + model.Material = entity.Material; + model.Pattern = entity.Pattern; + model.Export2 = entity.Export; + model.Multipack2 = entity.Multipack; + model.IsBundle = entity.IsBundle; + model.EnergyEfficiencyClass = entity.EnergyEfficiencyClass; + model.CustomLabel0 = entity.CustomLabel0; + model.CustomLabel1 = entity.CustomLabel1; + model.CustomLabel2 = entity.CustomLabel2; + model.CustomLabel3 = entity.CustomLabel3; + model.CustomLabel4 = entity.CustomLabel4; + } + else + { + model.Export2 = true; + } + + ViewBag.DefaultCategory = ""; + ViewBag.DefaultColor = ""; + ViewBag.DefaultSize = ""; + ViewBag.DefaultMaterial = ""; + ViewBag.DefaultPattern = ""; + ViewBag.DefaultGender = notSpecified; + ViewBag.DefaultAgeGroup = notSpecified; + ViewBag.DefaultIsAdult = ""; + ViewBag.DefaultMultipack2 = ""; + ViewBag.DefaultIsBundle = ""; + ViewBag.DefaultEnergyEfficiencyClass = notSpecified; + ViewBag.DefaultCustomLabel = ""; + ViewBag.LanguageSeoCode = Services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); + + // We do not have export profile context here, so we simply use the first profile. + var profile = _exportService.Value.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName).FirstOrDefault(); + if (profile != null) + { + var config = XmlHelper.Deserialize(profile.ProviderConfigData, typeof(ProfileConfigurationModel)) as ProfileConfigurationModel; + if (config != null) + { + ViewBag.DefaultCategory = config.DefaultGoogleCategory; + ViewBag.DefaultColor = config.Color; + ViewBag.DefaultSize = config.Size; + ViewBag.DefaultMaterial = config.Material; + ViewBag.DefaultPattern = config.Pattern; + + if (config.Gender.HasValue() && config.Gender != GmcXmlExportProvider.Unspecified) + { + ViewBag.DefaultGender = T("Plugins.Feed.Froogle.Gender" + culture.TextInfo.ToTitleCase(config.Gender)); + } + + if (config.AgeGroup.HasValue() && config.AgeGroup != GmcXmlExportProvider.Unspecified) + { + ViewBag.DefaultAgeGroup = T("Plugins.Feed.Froogle.AgeGroup" + culture.TextInfo.ToTitleCase(config.AgeGroup)); + } + } + } + + ViewBag.AvailableCategories = model.Taxonomy.HasValue() + ? new List { new SelectListItem { Text = model.Taxonomy, Value = model.Taxonomy, Selected = true } } + : new List(); + + ViewBag.AvailableGenders = new List + { + new SelectListItem { Value = "male", Text = T("Plugins.Feed.Froogle.GenderMale") }, + new SelectListItem { Value = "female", Text = T("Plugins.Feed.Froogle.GenderFemale") }, + new SelectListItem { Value = "unisex", Text = T("Plugins.Feed.Froogle.GenderUnisex") } + }; + + ViewBag.AvailableAgeGroups = new List + { + new SelectListItem { Value = "adult", Text = T("Plugins.Feed.Froogle.AgeGroupAdult") }, + new SelectListItem { Value = "kids", Text = T("Plugins.Feed.Froogle.AgeGroupKids") }, + }; + + ViewBag.AvailableEnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text + .SplitSafe(",") + .Select(x => new SelectListItem { Value = x, Text = x }) + .ToList(); + + var result = PartialView(model); + result.ViewData.TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "CustomProperties[GMC]" }; + return result; + } + + public ActionResult Configure() + { + var model = new FeedGoogleMerchantCenterModel(); + + model.GridPageSize = _adminAreaSettings.Value.GridPageSize; + model.LanguageSeoCode = Services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); + model.EnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text.SplitSafe(","); + + return View(model); + } + + [HttpPost, GridAction(EnableCustomBinding = true)] + public ActionResult GoogleProductList(GridCommand command, string searchProductName, string touched) + { + return new JsonResult + { + Data = _googleFeedService.GetGridModel(command, searchProductName, touched) + }; + } + + [HttpPost] + public ActionResult GoogleProductEdit(int pk, string name, string value) + { + _googleFeedService.Upsert(pk, name, value); + + return new EmptyResult(); + } + + public JsonResult GetGoogleCategories(string search, int? page) + { + page = page ?? 1; + const int take = 100; + var skip = (page.Value - 1) * take; + var categories = _googleFeedService.GetTaxonomyList(search); + var hasMoreItems = (page.Value * take) < categories.Count; + //$"{skip}\\{categories.Length} {hasMoreItems}: {search.NaIfEmpty()}".Dump(); + + var items = categories.Select(x => new + { + id = x, + text = x + }) + .Skip(skip) + .Take(take) + .ToList(); + + return new JsonResult + { + Data = new + { + hasMoreItems, + results = items + }, + MaxJsonLength = int.MaxValue, + JsonRequestBehavior = JsonRequestBehavior.AllowGet + }; + } + } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductObjectContext.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductObjectContext.cs index 2d822a76b9..e4700a9642 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductObjectContext.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductObjectContext.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data.Entity; -using System.Data.Entity.Infrastructure; -using System.Linq; -using SmartStore.Core; using SmartStore.Data; using SmartStore.Data.Setup; using SmartStore.GoogleMerchantCenter.Data.Migrations; @@ -11,38 +6,38 @@ namespace SmartStore.GoogleMerchantCenter.Data { - public class GoogleProductObjectContext : ObjectContextBase - { + public class GoogleProductObjectContext : ObjectContextBase + { public const string ALIASKEY = "sm_object_context_google_product"; - static GoogleProductObjectContext() - { - var initializer = new MigrateDatabaseInitializer - { - TablesToCheck = new[] { "GoogleProduct" } - }; - Database.SetInitializer(initializer); - } - - /// - /// For tooling support, e.g. EF Migrations - /// - public GoogleProductObjectContext() - : base() - { - } + static GoogleProductObjectContext() + { + var initializer = new MigrateDatabaseInitializer + { + TablesToCheck = new[] { "GoogleProduct" } + }; + Database.SetInitializer(initializer); + } + + /// + /// For tooling support, e.g. EF Migrations + /// + public GoogleProductObjectContext() + : base() + { + } public GoogleProductObjectContext(string nameOrConnectionString) - : base(nameOrConnectionString, ALIASKEY) - { - } + : base(nameOrConnectionString, ALIASKEY) + { + } - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - modelBuilder.Configurations.Add(new GoogleProductRecordMap()); - base.OnModelCreating(modelBuilder); - } + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + modelBuilder.Configurations.Add(new GoogleProductRecordMap()); + base.OnModelCreating(modelBuilder); + } - } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs index 98aaec1aa4..81c634f39c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs @@ -10,13 +10,13 @@ public GoogleProductRecordMap() this.ToTable("GoogleProduct"); this.HasKey(x => x.Id); - this.Property(x => x.EnergyEfficiencyClass).HasMaxLength(50); + this.Property(x => x.EnergyEfficiencyClass).HasMaxLength(50); - this.Property(x => x.CustomLabel0).HasMaxLength(100); - this.Property(x => x.CustomLabel1).HasMaxLength(100); - this.Property(x => x.CustomLabel2).HasMaxLength(100); - this.Property(x => x.CustomLabel3).HasMaxLength(100); - this.Property(x => x.CustomLabel4).HasMaxLength(100); - } + this.Property(x => x.CustomLabel0).HasMaxLength(100); + this.Property(x => x.CustomLabel1).HasMaxLength(100); + this.Property(x => x.CustomLabel2).HasMaxLength(100); + this.Property(x => x.CustomLabel3).HasMaxLength(100); + this.Property(x => x.CustomLabel4).HasMaxLength(100); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201403112356126_Initial.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201403112356126_Initial.cs index c13ee018ad..7365f0641d 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201403112356126_Initial.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201403112356126_Initial.cs @@ -1,38 +1,36 @@ namespace SmartStore.GoogleMerchantCenter.Data.Migrations { - using System; using System.Data.Entity.Migrations; - using SmartStore.Data.Setup; - + public partial class Initial : DbMigration { public override void Up() { - //if (DbMigrationContext.Current.SuppressInitialCreate()) - // return; - - CreateTable( + //if (DbMigrationContext.Current.SuppressInitialCreate()) + // return; + + CreateTable( "dbo.GoogleProduct", c => new - { - Id = c.Int(nullable: false, identity: true), - ProductId = c.Int(nullable: false), - Taxonomy = c.String(maxLength: 4000), - Gender = c.String(maxLength: 4000), - AgeGroup = c.String(maxLength: 4000), - Color = c.String(maxLength: 4000), - Size = c.String(maxLength: 4000), - Material = c.String(maxLength: 4000), - Pattern = c.String(maxLength: 4000), - ItemGroupId = c.String(maxLength: 4000), - }) + { + Id = c.Int(nullable: false, identity: true), + ProductId = c.Int(nullable: false), + Taxonomy = c.String(maxLength: 4000), + Gender = c.String(maxLength: 4000), + AgeGroup = c.String(maxLength: 4000), + Color = c.String(maxLength: 4000), + Size = c.String(maxLength: 4000), + Material = c.String(maxLength: 4000), + Pattern = c.String(maxLength: 4000), + ItemGroupId = c.String(maxLength: 4000), + }) .PrimaryKey(t => t.Id); - + } - + public override void Down() { - //DropTable("dbo.GoogleProduct"); + DropTable("dbo.GoogleProduct"); } } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201408020941244_IsTouched.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201408020941244_IsTouched.cs index c4af118dd7..ca926c3bbb 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201408020941244_IsTouched.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201408020941244_IsTouched.cs @@ -2,23 +2,23 @@ namespace SmartStore.GoogleMerchantCenter.Data.Migrations { using System; using System.Data.Entity.Migrations; - + public partial class IsTouched : DbMigration { public override void Up() { - var utcNow = DateTime.UtcNow; + var utcNow = DateTime.UtcNow; - AddColumn("dbo.GoogleProduct", "IsTouched", c => c.Boolean(nullable: false)); - AddColumn("dbo.GoogleProduct", "CreatedOnUtc", c => c.DateTime(nullable: false, defaultValue: utcNow)); - AddColumn("dbo.GoogleProduct", "UpdatedOnUtc", c => c.DateTime(nullable: false, defaultValue: utcNow)); + AddColumn("dbo.GoogleProduct", "IsTouched", c => c.Boolean(nullable: false)); + AddColumn("dbo.GoogleProduct", "CreatedOnUtc", c => c.DateTime(nullable: false, defaultValue: utcNow)); + AddColumn("dbo.GoogleProduct", "UpdatedOnUtc", c => c.DateTime(nullable: false, defaultValue: utcNow)); } - + public override void Down() { - DropColumn("dbo.GoogleProduct", "IsTouched"); - DropColumn("dbo.GoogleProduct", "CreatedOnUtc"); - DropColumn("dbo.GoogleProduct", "UpdatedOnUtc"); + DropColumn("dbo.GoogleProduct", "IsTouched"); + DropColumn("dbo.GoogleProduct", "CreatedOnUtc"); + DropColumn("dbo.GoogleProduct", "UpdatedOnUtc"); } } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201504211854125_IsActive.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201504211854125_IsActive.cs index 6d62461bb1..9e998ea211 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201504211854125_IsActive.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201504211854125_IsActive.cs @@ -1,15 +1,14 @@ namespace SmartStore.GoogleMerchantCenter.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class IsActive : DbMigration { public override void Up() { AddColumn("dbo.GoogleProduct", "Export", c => c.Boolean(nullable: false, defaultValue: true)); } - + public override void Down() { DropColumn("dbo.GoogleProduct", "Export"); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs index bc8af7c35e..3624eee2bc 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs @@ -1,8 +1,7 @@ namespace SmartStore.GoogleMerchantCenter.Data.Migrations { - using System; using System.Data.Entity.Migrations; - + public partial class IsBundle : DbMigration { public override void Up() @@ -17,7 +16,7 @@ public override void Up() AddColumn("dbo.GoogleProduct", "CustomLabel3", c => c.String(maxLength: 100)); AddColumn("dbo.GoogleProduct", "CustomLabel4", c => c.String(maxLength: 100)); } - + public override void Down() { DropColumn("dbo.GoogleProduct", "CustomLabel4"); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201705250945238_Indexes.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201705250945238_Indexes.cs index bd0adfd1dc..063459188e 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201705250945238_Indexes.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201705250945238_Indexes.cs @@ -1,7 +1,7 @@ namespace SmartStore.GoogleMerchantCenter.Data.Migrations { using System.Data.Entity.Migrations; - + public partial class Indexes : DbMigration { public override void Up() @@ -10,7 +10,7 @@ public override void Up() CreateIndex("dbo.GoogleProduct", "IsTouched"); CreateIndex("dbo.GoogleProduct", "Export"); } - + public override void Down() { DropIndex("dbo.GoogleProduct", new[] { "Export" }); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/Configuration.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/Configuration.cs index 00c0104a40..fcb9afab52 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/Configuration.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/Configuration.cs @@ -1,21 +1,18 @@ namespace SmartStore.GoogleMerchantCenter.Data.Migrations { - using System; - using System.Data.Entity; - using System.Data.Entity.Migrations; - using System.Linq; + using System.Data.Entity.Migrations; - internal sealed class Configuration : DbMigrationsConfiguration - { - public Configuration() - { - AutomaticMigrationsEnabled = false; - MigrationsDirectory = @"Data\Migrations"; - ContextKey = "SmartStore.FeedGoogle"; // DO NOT CHANGE! - } + internal sealed class Configuration : DbMigrationsConfiguration + { + public Configuration() + { + AutomaticMigrationsEnabled = false; + MigrationsDirectory = @"Data\Migrations"; + ContextKey = "SmartStore.FeedGoogle"; // DO NOT CHANGE! + } - protected override void Seed(GoogleProductObjectContext context) - { - } - } + protected override void Seed(GoogleProductObjectContext context) + { + } + } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs index ac6e3dd393..6ef3f46631 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs @@ -12,16 +12,16 @@ namespace SmartStore.GoogleMerchantCenter { public class DependencyRegistrar : IDependencyRegistrar { - public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) + public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { builder.RegisterType().As().InstancePerRequest(); //register named context - builder.Register(c => new GoogleProductObjectContext(DataSettings.Current.DataConnectionString)) + builder.Register(c => new GoogleProductObjectContext(DataSettings.Current.DataConnectionString)) .Named(GoogleProductObjectContext.ALIASKEY) .InstancePerRequest(); - builder.Register(c => new GoogleProductObjectContext(DataSettings.Current.DataConnectionString)) + builder.Register(c => new GoogleProductObjectContext(DataSettings.Current.DataConnectionString)) .InstancePerRequest(); //override required repository with our custom context @@ -31,9 +31,6 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b .InstancePerRequest(); } - public int Order - { - get { return 1; } - } + public int Order => 1; } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index 3b87cd8d04..828172a31c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Merchant Center (GMC) feed SystemName: SmartStore.GoogleMerchantCenter Group: Marketing -Version: 3.2.2 -MinAppVersion: 3.2.2 +Version: 4.2.0 +MinAppVersion: 4.2.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs index 1d177d7809..46fdc6a4f9 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs @@ -9,13 +9,13 @@ namespace SmartStore.GoogleMerchantCenter.Domain ///
public partial class GoogleProductRecord : BaseEntity { - public GoogleProductRecord() - { - Export = true; - } + public GoogleProductRecord() + { + Export = true; + } - [Index] - public int ProductId { get; set; } + [Index] + public int ProductId { get; set; } public string Taxonomy { get; set; } public string Gender { get; set; } @@ -26,23 +26,23 @@ public GoogleProductRecord() public string Pattern { get; set; } public string ItemGroupId { get; set; } - [Index] - public bool IsTouched { get; set; } - public DateTime CreatedOnUtc { get; set; } - public DateTime UpdatedOnUtc { get; set; } + [Index] + public bool IsTouched { get; set; } + public DateTime CreatedOnUtc { get; set; } + public DateTime UpdatedOnUtc { get; set; } - [Index] - public bool Export { get; set; } + [Index] + public bool Export { get; set; } - public int Multipack { get; set; } - public bool? IsBundle { get; set; } - public bool? IsAdult { get; set; } - public string EnergyEfficiencyClass { get; set; } + public int Multipack { get; set; } + public bool? IsBundle { get; set; } + public bool? IsAdult { get; set; } + public string EnergyEfficiencyClass { get; set; } - public string CustomLabel0 { get; set; } - public string CustomLabel1 { get; set; } - public string CustomLabel2 { get; set; } - public string CustomLabel3 { get; set; } - public string CustomLabel4 { get; set; } - } + public string CustomLabel0 { get; set; } + public string CustomLabel1 { get; set; } + public string CustomLabel2 { get; set; } + public string CustomLabel3 { get; set; } + public string CustomLabel4 { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs index 211c45083d..ae1b9f9da4 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs @@ -8,20 +8,20 @@ namespace SmartStore.GoogleMerchantCenter { - public class Events : IConsumer - { - private readonly IGoogleFeedService _googleService; + public class Events : IConsumer + { + private readonly IGoogleFeedService _googleService; - public Events(IGoogleFeedService googleService) - { - _googleService = googleService; - } + public Events(IGoogleFeedService googleService) + { + _googleService = googleService; + } - public void HandleEvent(TabStripCreated eventMessage) - { - if (eventMessage.TabStripName == "product-edit") - { - var productId = ((TabbableModel)eventMessage.Model).Id; + public void HandleEvent(TabStripCreated eventMessage) + { + if (eventMessage.TabStripName == "product-edit") + { + var productId = ((TabbableModel)eventMessage.Model).Id; eventMessage.ItemFactory.Add().Text("GMC") .Name("tab-gmc") @@ -30,65 +30,65 @@ public void HandleEvent(TabStripCreated eventMessage) .Route("SmartStore.GoogleMerchantCenter", new { action = "ProductEditTab", productId = productId }) .Ajax(); } - } + } - public void HandleEvent(ModelBoundEvent eventMessage) - { - if (!eventMessage.BoundModel.CustomProperties.ContainsKey("GMC")) - return; - - var model = eventMessage.BoundModel.CustomProperties["GMC"] as GoogleProductModel; - if (model == null) - return; + public void HandleEvent(ModelBoundEvent eventMessage) + { + if (!eventMessage.BoundModel.CustomProperties.ContainsKey("GMC")) + return; - var utcNow = DateTime.UtcNow; - var entity = _googleService.GetGoogleProductRecord(model.ProductId); - var insert = (entity == null); + var model = eventMessage.BoundModel.CustomProperties["GMC"] as GoogleProductModel; + if (model == null) + return; - if (entity == null) - { - entity = new GoogleProductRecord - { - ProductId = model.ProductId, - CreatedOnUtc = utcNow - }; - } + var utcNow = DateTime.UtcNow; + var entity = _googleService.GetGoogleProductRecord(model.ProductId); + var insert = (entity == null); - entity.AgeGroup = model.AgeGroup; - entity.Color = model.Color; - entity.Gender = model.Gender; - entity.Size = model.Size; - entity.Taxonomy = model.Taxonomy; - entity.Material = model.Material; - entity.Pattern = model.Pattern; - entity.Export = model.Export2; - entity.UpdatedOnUtc = utcNow; - entity.Multipack = model.Multipack2 ?? 0; - entity.IsBundle = model.IsBundle; - entity.IsAdult = model.IsAdult; - entity.EnergyEfficiencyClass = model.EnergyEfficiencyClass; - entity.CustomLabel0 = model.CustomLabel0; - entity.CustomLabel1 = model.CustomLabel1; - entity.CustomLabel2 = model.CustomLabel2; - entity.CustomLabel3 = model.CustomLabel3; - entity.CustomLabel4 = model.CustomLabel4; + if (entity == null) + { + entity = new GoogleProductRecord + { + ProductId = model.ProductId, + CreatedOnUtc = utcNow + }; + } + + entity.AgeGroup = model.AgeGroup; + entity.Color = model.Color; + entity.Gender = model.Gender; + entity.Size = model.Size; + entity.Taxonomy = model.Taxonomy; + entity.Material = model.Material; + entity.Pattern = model.Pattern; + entity.Export = model.Export2; + entity.UpdatedOnUtc = utcNow; + entity.Multipack = model.Multipack2 ?? 0; + entity.IsBundle = model.IsBundle; + entity.IsAdult = model.IsAdult; + entity.EnergyEfficiencyClass = model.EnergyEfficiencyClass; + entity.CustomLabel0 = model.CustomLabel0; + entity.CustomLabel1 = model.CustomLabel1; + entity.CustomLabel2 = model.CustomLabel2; + entity.CustomLabel3 = model.CustomLabel3; + entity.CustomLabel4 = model.CustomLabel4; - entity.IsTouched = entity.IsTouched(); + entity.IsTouched = entity.IsTouched(); - if (!insert && !entity.IsTouched) - { - _googleService.DeleteGoogleProductRecord(entity); - return; - } + if (!insert && !entity.IsTouched) + { + _googleService.DeleteGoogleProductRecord(entity); + return; + } - if (insert) - { - _googleService.InsertGoogleProductRecord(entity); - } - else - { - _googleService.UpdateGoogleProductRecord(entity); - } - } - } + if (insert) + { + _googleService.InsertGoogleProductRecord(entity); + } + else + { + _googleService.UpdateGoogleProductRecord(entity); + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs index e3a3702bf1..bdb65a1e55 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs @@ -2,20 +2,20 @@ namespace SmartStore.GoogleMerchantCenter { - public static class MiscExtensions - { - public static bool IsTouched(this GoogleProductRecord p) - { - if (p != null) - { - return - p.Taxonomy.HasValue() || p.Gender.HasValue() || p.AgeGroup.HasValue() || p.Color.HasValue() || - p.Size.HasValue() || p.Material.HasValue() || p.Pattern.HasValue() || p.ItemGroupId.HasValue() || - !p.Export || p.Multipack != 0 || p.IsBundle.HasValue || p.IsAdult.HasValue || p.EnergyEfficiencyClass.HasValue() || - p.CustomLabel0.HasValue() || p.CustomLabel1.HasValue() || p.CustomLabel2.HasValue() || p.CustomLabel3.HasValue() || p.CustomLabel4.HasValue(); - } + public static class MiscExtensions + { + public static bool IsTouched(this GoogleProductRecord p) + { + if (p != null) + { + return + p.Taxonomy.HasValue() || p.Gender.HasValue() || p.AgeGroup.HasValue() || p.Color.HasValue() || + p.Size.HasValue() || p.Material.HasValue() || p.Pattern.HasValue() || p.ItemGroupId.HasValue() || + !p.Export || p.Multipack != 0 || p.IsBundle.HasValue || p.IsAdult.HasValue || p.EnergyEfficiencyClass.HasValue() || + p.CustomLabel0.HasValue() || p.CustomLabel1.HasValue() || p.CustomLabel2.HasValue() || p.CustomLabel3.HasValue() || p.CustomLabel4.HasValue(); + } - return false; - } - } + return false; + } + } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs index ddf77b093f..19cea6a178 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs @@ -3,7 +3,6 @@ using SmartStore.Core.Plugins; using SmartStore.GoogleMerchantCenter.Data.Migrations; using SmartStore.GoogleMerchantCenter.Providers; -using SmartStore.GoogleMerchantCenter.Services; using SmartStore.Services; using SmartStore.Services.DataExchange.Export; @@ -11,19 +10,16 @@ namespace SmartStore.GoogleMerchantCenter { public class GoogleMerchantCenterFeedPlugin : BasePlugin, IConfigurable { - private readonly ICommonServices _services; + private readonly ICommonServices _services; private readonly IExportProfileService _exportProfileService; public GoogleMerchantCenterFeedPlugin(ICommonServices services, IExportProfileService exportProfileService) { - _services = services; + _services = services; _exportProfileService = exportProfileService; } - public static string SystemName - { - get { return "SmartStore.GoogleMerchantCenter"; } - } + public static string SystemName => "SmartStore.GoogleMerchantCenter"; /// /// Gets a route for provider configuration @@ -34,8 +30,8 @@ public static string SystemName public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "Configure"; - controllerName = "FeedGoogleMerchantCenter"; - routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.GoogleMerchantCenter.Controllers" }, { "area", SystemName } }; + controllerName = "FeedGoogleMerchantCenter"; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.GoogleMerchantCenter.Controllers" }, { "area", SystemName } }; } /// @@ -43,7 +39,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa /// public override void Install() { - _services.Localization.ImportPluginResourcesFromXml(this.PluginDescriptor); + _services.Localization.ImportPluginResourcesFromXml(this.PluginDescriptor); base.Install(); } @@ -53,14 +49,15 @@ public override void Install() /// public override void Uninstall() { - _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); + _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); // Delete existing export profiles. var profiles = _exportProfileService.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName); profiles.Each(x => _exportProfileService.DeleteExportProfile(x, true)); - var migrator = new DbMigrator(new Configuration()); - migrator.Update(DbMigrator.InitialDatabase); + // Keep Google product table. + //var migrator = new DbMigrator(new Configuration()); + //migrator.Update(DbMigrator.InitialDatabase); base.Uninstall(); } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs index 973d44f295..392f33a555 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs @@ -6,123 +6,124 @@ namespace SmartStore.GoogleMerchantCenter.Models { - public class FeedGoogleMerchantCenterModel - { - public int GridPageSize { get; set; } - public string LanguageSeoCode { get; set; } - public string[] EnergyEfficiencyClasses { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] - public string SearchProductName { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchIsTouched")] - public string SearchIsTouched { get; set; } - } - - public class GoogleProductModel : ModelBase - { - public int TotalCount { get; set; } - - // This attribute is required to disable editing. - [ScaffoldColumn(false)] - public int ProductId - { - get { return Id; } - set { Id = value; } - } - public int Id { get; set; } - - // This attribute is required to disable editing. - [ReadOnly(true)] - [ScaffoldColumn(false)] - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.ProductName")] - public string Name { get; set; } - - public string SKU { get; set; } - public int ProductTypeId { get; set; } - public ProductType ProductType { get { return (ProductType)ProductTypeId; } } - public string ProductTypeName { get; set; } - public string ProductTypeLabelHint - { - get - { - switch (ProductType) - { - case ProductType.SimpleProduct: - return "secondary d-none"; - case ProductType.GroupedProduct: - return "success"; - case ProductType.BundledProduct: - return "info"; - default: - return ""; - } - } - } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.GoogleCategory")] - public string Taxonomy { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] - public string Gender { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] - public string AgeGroup { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] - public string Color { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] - public string Size { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] - public string Material { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] - public string Pattern { get; set; } - - [SmartResourceDisplayName("Common.Export")] - public int Export { get; set; } - [SmartResourceDisplayName("Common.Export")] - public bool Export2 - { - get { return Export != 0; } - set { Export = (value ? 1 : 0); } - } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] - public int Multipack { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] - public int? Multipack2 - { - get { return Multipack > 0 ? Multipack : (int?)null; } - set { Multipack = (value ?? 0); } - } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.IsBundle")] - public bool? IsBundle { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.IsAdult")] - public bool? IsAdult { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.EnergyEfficiencyClass")] - public string EnergyEfficiencyClass { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel0")] - public string CustomLabel0 { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel1")] - public string CustomLabel1 { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel2")] - public string CustomLabel2 { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel3")] - public string CustomLabel3 { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel4")] - public string CustomLabel4 { get; set; } - - public string GenderLocalize { get; set; } - public string AgeGroupLocalize { get; set; } - public string Export2Localize { get; set; } - public string IsBundleLocalize { get; set; } - public string IsAdultLocalize { get; set; } - } + public class FeedGoogleMerchantCenterModel + { + public int GridPageSize { get; set; } + public string LanguageSeoCode { get; set; } + public string[] EnergyEfficiencyClasses { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] + public string SearchProductName { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchIsTouched")] + public string SearchIsTouched { get; set; } + } + + [CustomModelPart] + public class GoogleProductModel : ModelBase + { + public int TotalCount { get; set; } + + // This attribute is required to disable editing. + [ScaffoldColumn(false)] + public int ProductId + { + get => Id; + set => Id = value; + } + public int Id { get; set; } + + // This attribute is required to disable editing. + [ReadOnly(true)] + [ScaffoldColumn(false)] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.ProductName")] + public string Name { get; set; } + + public string SKU { get; set; } + public int ProductTypeId { get; set; } + public ProductType ProductType => (ProductType)ProductTypeId; + public string ProductTypeName { get; set; } + public string ProductTypeLabelHint + { + get + { + switch (ProductType) + { + case ProductType.SimpleProduct: + return "secondary d-none"; + case ProductType.GroupedProduct: + return "success"; + case ProductType.BundledProduct: + return "info"; + default: + return ""; + } + } + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.GoogleCategory")] + public string Taxonomy { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] + public string Gender { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] + public string AgeGroup { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] + public string Color { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] + public string Size { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] + public string Material { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] + public string Pattern { get; set; } + + [SmartResourceDisplayName("Common.Export")] + public int Export { get; set; } + [SmartResourceDisplayName("Common.Export")] + public bool Export2 + { + get => Export != 0; + set => Export = (value ? 1 : 0); + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] + public int Multipack { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] + public int? Multipack2 + { + get => Multipack > 0 ? Multipack : (int?)null; + set => Multipack = (value ?? 0); + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.IsBundle")] + public bool? IsBundle { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.IsAdult")] + public bool? IsAdult { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.EnergyEfficiencyClass")] + public string EnergyEfficiencyClass { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel0")] + public string CustomLabel0 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel1")] + public string CustomLabel1 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel2")] + public string CustomLabel2 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel3")] + public string CustomLabel3 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel4")] + public string CustomLabel4 { get; set; } + + public string GenderLocalize { get; set; } + public string AgeGroupLocalize { get; set; } + public string Export2Localize { get; set; } + public string IsBundleLocalize { get; set; } + public string IsAdultLocalize { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs index f3b29bd7a9..3d5f89eda8 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs @@ -5,64 +5,60 @@ using FluentValidation; using FluentValidation.Attributes; using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.GoogleMerchantCenter.Models { + [CustomModelPart] [Serializable] - [Validator(typeof(ProfileConfigurationValidator))] - public class ProfileConfigurationModel - { - public ProfileConfigurationModel() - { - AdditionalImages = true; - SpecialPrice = true; - } - - [XmlIgnore] - public string LanguageSeoCode { get; set; } + [Validator(typeof(ProfileConfigurationValidator))] + public class ProfileConfigurationModel + { + [XmlIgnore] + public string LanguageSeoCode { get; set; } - [XmlIgnore] - public List AvailableCategories { get; set; } + [XmlIgnore] + public List AvailableCategories { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.DefaultGoogleCategory")] - public string DefaultGoogleCategory { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.DefaultGoogleCategory")] + public string DefaultGoogleCategory { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.AdditionalImages")] - public bool AdditionalImages { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.AdditionalImages")] + public bool AdditionalImages { get; set; } = true; - [SmartResourceDisplayName("Plugins.Feed.Froogle.Availability")] - public string Availability { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Availability")] + public string Availability { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.SpecialPrice")] - public bool SpecialPrice { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.SpecialPrice")] + public bool SpecialPrice { get; set; } = true; - [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] - public string Gender { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] + public string Gender { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] - public string AgeGroup { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] + public string AgeGroup { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] - public string Color { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] + public string Color { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] - public string Size { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] + public string Size { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] - public string Material { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] + public string Material { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] - public string Pattern { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] + public string Pattern { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExpirationDays")] - public int ExpirationDays { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.ExpirationDays")] + public int ExpirationDays { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportShipping")] - public bool ExportShipping { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportShipping")] + public bool ExportShipping { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportBasePrice")] - public bool ExportBasePrice { get; set; } - } + [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportBasePrice")] + public bool ExportBasePrice { get; set; } + } public class ProfileConfigurationValidator : AbstractValidator { diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index 369252cf62..6220b658da 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -20,361 +20,355 @@ namespace SmartStore.GoogleMerchantCenter.Providers { - [SystemName("Feeds.GoogleMerchantCenterProductXml")] - [FriendlyName("Google Merchant Center XML product feed")] - [DisplayOrder(1)] - [ExportFeatures(Features = - ExportFeatures.CreatesInitialPublicDeployment | - ExportFeatures.CanOmitGroupedProducts | - ExportFeatures.CanProjectAttributeCombinations | - ExportFeatures.CanProjectDescription | - ExportFeatures.UsesSkuAsMpnFallback | - ExportFeatures.OffersBrandFallback | - ExportFeatures.UsesSpecialPrice | - ExportFeatures.UsesAttributeCombination)] - public class GmcXmlExportProvider : ExportProviderBase - { - private const string _googleNamespace = "http://base.google.com/ns/1.0"; - - private readonly IGoogleFeedService _googleFeedService; - private readonly IMeasureService _measureService; - private readonly ICommonServices _services; - private readonly IProductAttributeService _productAttributeService; - private readonly MeasureSettings _measureSettings; - private Multimap _attributeMappings; - - public GmcXmlExportProvider( - IGoogleFeedService googleFeedService, - IMeasureService measureService, - ICommonServices services, - IProductAttributeService productAttributeService, - MeasureSettings measureSettings) - { - _googleFeedService = googleFeedService; - _measureService = measureService; - _services = services; - _productAttributeService = productAttributeService; - _measureSettings = measureSettings; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - private Multimap AttributeMappings - { - get - { - if (_attributeMappings == null) - { - _attributeMappings = _productAttributeService.GetExportFieldMappings("gmc"); - } - - return _attributeMappings; - } - } - - private string BasePriceUnits(string value) - { - const string defaultValue = "kg"; - - if (value.IsEmpty()) - return defaultValue; - - // TODO: Product.BasePriceMeasureUnit should be localized - switch (value.ToLowerInvariant()) - { - case "mg": - case "milligramm": - case "milligram": - return "mg"; - case "g": - case "gramm": - case "gram": - return "g"; - case "kg": - case "kilogramm": - case "kilogram": - return "kg"; - - case "ml": - case "milliliter": - case "millilitre": - return "ml"; - case "cl": - case "zentiliter": - case "centilitre": - return "cl"; - case "l": - case "liter": - case "litre": - return "l"; - case "cbm": - case "kubikmeter": - case "cubic metre": - return "cbm"; - - case "cm": - case "zentimeter": - case "centimetre": - return "cm"; - case "m": - case "meter": - return "m"; - - case "qm²": - case "quadratmeter": - case "square metre": - return "sqm"; - - default: - return defaultValue; - } - } - - private bool BasePriceSupported(int baseAmount, string unit) - { - if (baseAmount == 1 || baseAmount == 10 || baseAmount == 100) - return true; - - if (baseAmount == 75 && unit == "cl") - return true; - - if ((baseAmount == 50 || baseAmount == 1000) && unit == "kg") - return true; - - return false; - } - - private void WriteString(XmlWriter writer, string fieldName, string value) - { - if (value != null) - { - writer.WriteElementString("g", fieldName, _googleNamespace, value); - } - } - - private string GetAttributeValue( - Multimap attributeValues, - string fieldName, - int languageId, - string productEditTabValue, - string defaultValue) - { - // 1. attribute export mapping. - if (attributeValues != null && AttributeMappings.ContainsKey(fieldName)) - { - foreach (var attributeId in AttributeMappings[fieldName]) - { - if (attributeValues.ContainsKey(attributeId)) - { - var attributeValue = attributeValues[attributeId].FirstOrDefault(x => x.ProductVariantAttribute.ProductAttributeId == attributeId); - if (attributeValue != null) - { - return attributeValue.GetLocalized(x => x.Name, languageId, true, false).Value.EmptyNull(); - } - } - } - } - - // 2. explicit set to unspecified. - if (defaultValue.IsCaseInsensitiveEqual(Unspecified)) - { - return string.Empty; - } - - // 3. product edit tab value. - if (productEditTabValue.HasValue()) - { - return productEditTabValue; - } - - return defaultValue.EmptyNull(); - } - - private string GetBaseMeasureWeight() - { - var measureWeightEntity = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); - var measureWeight = measureWeightEntity != null - ? measureWeightEntity.SystemKeyword.EmptyNull().ToLower() - : string.Empty; - - switch (measureWeight) - { - case "gram": - case "gramme": - return "g"; - case "mg": - case "milligramme": - case "milligram": - return "mg"; - case "lb": - return "lb"; - case "ounce": - case "oz": - return "oz"; - default: - return "kg"; - } - } - - public static string SystemName => "Feeds.GoogleMerchantCenterProductXml"; - - public static string Unspecified => "__nospec__"; - - public override ExportConfigurationInfo ConfigurationInfo - { - get - { - return new ExportConfigurationInfo - { - PartialViewName = "~/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml", - ModelType = typeof(ProfileConfigurationModel), - Initialize = obj => - { - var model = (obj as ProfileConfigurationModel); - - model.LanguageSeoCode = _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); - - model.AvailableCategories = model.DefaultGoogleCategory.HasValue() - ? new List { new SelectListItem { Text = model.DefaultGoogleCategory, Value = model.DefaultGoogleCategory, Selected = true } } - : new List(); - } - }; - } - } - - public override string FileExtension => "XML"; - - protected override void Export(ExportExecuteContext context) - { - Currency currency = context.Currency.Entity; - var languageId = context.Projection.LanguageId ?? 0; - var dateFormat = "yyyy-MM-ddTHH:mmZ"; - var defaultAvailability = "in stock"; - var measureWeight = GetBaseMeasureWeight(); - - var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel(); - - if (config.Availability.IsCaseInsensitiveEqual(Unspecified)) - { - defaultAvailability = string.Empty; - } - else if (config.Availability.HasValue()) - { - defaultAvailability = config.Availability; - } - - - using (var writer = XmlWriter.Create(context.DataStream, ExportXmlHelper.DefaultSettings)) - { - writer.WriteStartDocument(); - writer.WriteStartElement("rss"); - writer.WriteAttributeString("version", "2.0"); - writer.WriteAttributeString("xmlns", "g", null, _googleNamespace); - writer.WriteStartElement("channel"); - writer.WriteElementString("title", "{0} - Feed for Google Merchant Center".FormatInvariant((string)context.Store.Name)); - writer.WriteElementString("link", "http://base.google.com/base/"); - writer.WriteElementString("description", "Information about products"); - - while (context.Abort == DataExchangeAbortion.None && context.DataSegmenter.ReadNextSegment()) - { - var segment = context.DataSegmenter.CurrentSegment; - - int[] productIds = segment.Select(x => (int)((dynamic)x).Id).ToArray(); - var googleProducts = _googleFeedService.GetGoogleProductRecords(productIds).ToDictionarySafe(x => x.ProductId); - - foreach (dynamic product in segment) - { - if (context.Abort != DataExchangeAbortion.None) - break; - - Product entity = product.Entity; - var gmc = googleProducts.Get(entity.Id); - - if (gmc != null && !gmc.Export) - continue; - - writer.WriteStartElement("item"); - - try - { - string category = gmc == null ? null : gmc.Taxonomy; - string productType = product._CategoryPath; - var price = (decimal)product.Price; - var uniqueId = (string)product._UniqueId; - var isParent = (bool)product._IsParent; - string brand = product._Brand; - string gtin = product.Gtin; - string mpn = product.ManufacturerPartNumber; - var availability = defaultAvailability; - List productPictures = product.ProductPictures; - var pictureUrls = productPictures - .Select(x => (string)x.Picture._FullSizeImageUrl) - .Where(x => x.HasValue()) - .ToList(); - - var attributeValues = !isParent && product._AttributeCombinationValues != null - ? ((ICollection)product._AttributeCombinationValues).ToMultimap(x => x.ProductVariantAttribute.ProductAttributeId, x => x) - : new Multimap(); - - var specialPrice = product._FutureSpecialPrice as decimal?; - if (!specialPrice.HasValue) - specialPrice = product._SpecialPrice; - - if (category.IsEmpty()) - category = config.DefaultGoogleCategory; - - if (category.IsEmpty()) - context.Log.Error(T("Plugins.Feed.Froogle.MissingDefaultCategory")); - - if (entity.ManageInventoryMethod == ManageInventoryMethod.ManageStock && entity.StockQuantity <= 0) - { - if (entity.BackorderMode == BackorderMode.NoBackorders) - availability = "out of stock"; - else if (entity.BackorderMode == BackorderMode.AllowQtyBelow0 || entity.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) - availability = entity.AvailableForPreOrder ? "preorder" : "out of stock"; - } - - WriteString(writer, "id", uniqueId); - - writer.WriteStartElement("title"); - writer.WriteCData(((string)product.Name).Truncate(70).RemoveInvalidXmlChars()); - writer.WriteEndElement(); - - writer.WriteStartElement("description"); - writer.WriteCData(((string)product.FullDescription).RemoveInvalidXmlChars()); - writer.WriteEndElement(); - - writer.WriteStartElement("g", "google_product_category", _googleNamespace); - writer.WriteCData(category.RemoveInvalidXmlChars()); - writer.WriteFullEndElement(); - - if (productType.HasValue()) - { - writer.WriteStartElement("g", "product_type", _googleNamespace); - writer.WriteCData(productType.RemoveInvalidXmlChars()); - writer.WriteFullEndElement(); - } - - writer.WriteElementString("link", (string)product._DetailUrl); - - if (pictureUrls.Any()) - { - WriteString(writer, "image_link", pictureUrls.First()); - - if (config.AdditionalImages) - { - var imageCount = 0; - foreach (var url in pictureUrls.Skip(1)) - { - if (++imageCount <= 10) - { - WriteString(writer, "additional_image_link", url); - } - } - } - } + [SystemName("Feeds.GoogleMerchantCenterProductXml")] + [FriendlyName("Google Merchant Center XML product feed")] + [DisplayOrder(1)] + [ExportFeatures(Features = + ExportFeatures.CreatesInitialPublicDeployment | + ExportFeatures.CanOmitGroupedProducts | + ExportFeatures.CanProjectAttributeCombinations | + ExportFeatures.CanProjectDescription | + ExportFeatures.UsesSkuAsMpnFallback | + ExportFeatures.OffersBrandFallback | + ExportFeatures.UsesSpecialPrice | + ExportFeatures.UsesAttributeCombination)] + public class GmcXmlExportProvider : ExportProviderBase + { + private const string _googleNamespace = "http://base.google.com/ns/1.0"; + + private readonly IGoogleFeedService _googleFeedService; + private readonly IMeasureService _measureService; + private readonly ICommonServices _services; + private readonly IProductAttributeService _productAttributeService; + private readonly MeasureSettings _measureSettings; + private Multimap _attributeMappings; + + public GmcXmlExportProvider( + IGoogleFeedService googleFeedService, + IMeasureService measureService, + ICommonServices services, + IProductAttributeService productAttributeService, + MeasureSettings measureSettings) + { + _googleFeedService = googleFeedService; + _measureService = measureService; + _services = services; + _productAttributeService = productAttributeService; + _measureSettings = measureSettings; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + private Multimap AttributeMappings + { + get + { + if (_attributeMappings == null) + { + _attributeMappings = _productAttributeService.GetExportFieldMappings("gmc"); + } + + return _attributeMappings; + } + } + + private string BasePriceUnits(string value) + { + const string defaultValue = "kg"; + + if (value.IsEmpty()) + return defaultValue; + + // TODO: Product.BasePriceMeasureUnit should be localized + switch (value.ToLowerInvariant()) + { + case "mg": + case "milligramm": + case "milligram": + return "mg"; + case "g": + case "gramm": + case "gram": + return "g"; + case "kg": + case "kilogramm": + case "kilogram": + return "kg"; + + case "ml": + case "milliliter": + case "millilitre": + return "ml"; + case "cl": + case "zentiliter": + case "centilitre": + return "cl"; + case "l": + case "liter": + case "litre": + return "l"; + case "cbm": + case "kubikmeter": + case "cubic metre": + return "cbm"; + + case "cm": + case "zentimeter": + case "centimetre": + return "cm"; + case "m": + case "meter": + return "m"; + + case "qm²": + case "quadratmeter": + case "square metre": + return "sqm"; + + default: + return defaultValue; + } + } + + private bool BasePriceSupported(int baseAmount, string unit) + { + if (baseAmount == 1 || baseAmount == 10 || baseAmount == 100) + return true; + + if (baseAmount == 75 && unit == "cl") + return true; + + if ((baseAmount == 50 || baseAmount == 1000) && unit == "kg") + return true; + + return false; + } + + private void WriteString(XmlWriter writer, string fieldName, string value) + { + if (value != null) + { + writer.WriteElementString("g", fieldName, _googleNamespace, value); + } + } + + private string GetAttributeValue( + Multimap attributeValues, + string fieldName, + int languageId, + string productEditTabValue, + string defaultValue) + { + // 1. attribute export mapping. + if (attributeValues != null && AttributeMappings.ContainsKey(fieldName)) + { + foreach (var attributeId in AttributeMappings[fieldName]) + { + if (attributeValues.ContainsKey(attributeId)) + { + var attributeValue = attributeValues[attributeId].FirstOrDefault(x => x.ProductVariantAttribute.ProductAttributeId == attributeId); + if (attributeValue != null) + { + return attributeValue.GetLocalized(x => x.Name, languageId, true, false).Value.EmptyNull(); + } + } + } + } + + // 2. explicit set to unspecified. + if (defaultValue.IsCaseInsensitiveEqual(Unspecified)) + { + return string.Empty; + } + + // 3. product edit tab value. + if (productEditTabValue.HasValue()) + { + return productEditTabValue; + } + + return defaultValue.EmptyNull(); + } + + private string GetBaseMeasureWeight() + { + var measureWeightEntity = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); + var measureWeight = measureWeightEntity != null + ? measureWeightEntity.SystemKeyword.EmptyNull().ToLower() + : string.Empty; + + switch (measureWeight) + { + case "gram": + case "gramme": + return "g"; + case "mg": + case "milligramme": + case "milligram": + return "mg"; + case "lb": + return "lb"; + case "ounce": + case "oz": + return "oz"; + default: + return "kg"; + } + } + + public static string SystemName => "Feeds.GoogleMerchantCenterProductXml"; + + public static string Unspecified => "__nospec__"; + + public override ExportConfigurationInfo ConfigurationInfo => new ExportConfigurationInfo + { + PartialViewName = "~/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml", + ModelType = typeof(ProfileConfigurationModel), + Initialize = obj => + { + var model = (obj as ProfileConfigurationModel); + + model.LanguageSeoCode = _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); + + model.AvailableCategories = model.DefaultGoogleCategory.HasValue() + ? new List { new SelectListItem { Text = model.DefaultGoogleCategory, Value = model.DefaultGoogleCategory, Selected = true } } + : new List(); + } + }; + + public override string FileExtension => "XML"; + + protected override void Export(ExportExecuteContext context) + { + Currency currency = context.Currency.Entity; + var languageId = context.Projection.LanguageId ?? 0; + var dateFormat = "yyyy-MM-ddTHH:mmZ"; + var defaultAvailability = "in stock"; + var measureWeight = GetBaseMeasureWeight(); + + var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel(); + + if (config.Availability.IsCaseInsensitiveEqual(Unspecified)) + { + defaultAvailability = string.Empty; + } + else if (config.Availability.HasValue()) + { + defaultAvailability = config.Availability; + } + + + using (var writer = XmlWriter.Create(context.DataStream, ExportXmlHelper.DefaultSettings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement("rss"); + writer.WriteAttributeString("version", "2.0"); + writer.WriteAttributeString("xmlns", "g", null, _googleNamespace); + writer.WriteStartElement("channel"); + writer.WriteElementString("title", "{0} - Feed for Google Merchant Center".FormatInvariant((string)context.Store.Name)); + writer.WriteElementString("link", "http://base.google.com/base/"); + writer.WriteElementString("description", "Information about products"); + + while (context.Abort == DataExchangeAbortion.None && context.DataSegmenter.ReadNextSegment()) + { + var segment = context.DataSegmenter.CurrentSegment; + + int[] productIds = segment.Select(x => (int)((dynamic)x).Id).ToArray(); + var googleProducts = _googleFeedService.GetGoogleProductRecords(productIds).ToDictionarySafe(x => x.ProductId); + + foreach (dynamic product in segment) + { + if (context.Abort != DataExchangeAbortion.None) + break; + + Product entity = product.Entity; + var gmc = googleProducts.Get(entity.Id); + + if (gmc != null && !gmc.Export) + continue; + + writer.WriteStartElement("item"); + + try + { + string category = gmc == null ? null : gmc.Taxonomy; + string productType = product._CategoryPath; + var price = (decimal)product.Price; + var uniqueId = (string)product._UniqueId; + var isParent = (bool)product._IsParent; + string brand = product._Brand; + string gtin = product.Gtin; + string mpn = product.ManufacturerPartNumber; + var availability = defaultAvailability; + List productPictures = product.ProductPictures; + var pictureUrls = productPictures + .Select(x => (string)x.Picture._FullSizeImageUrl) + .Where(x => x.HasValue()) + .ToList(); + + var attributeValues = !isParent && product._AttributeCombinationValues != null + ? ((ICollection)product._AttributeCombinationValues).ToMultimap(x => x.ProductVariantAttribute.ProductAttributeId, x => x) + : new Multimap(); + + var specialPrice = product._FutureSpecialPrice as decimal?; + if (!specialPrice.HasValue) + specialPrice = product._SpecialPrice; + + if (category.IsEmpty()) + category = config.DefaultGoogleCategory; + + if (category.IsEmpty()) + context.Log.Error(T("Plugins.Feed.Froogle.MissingDefaultCategory")); + + if (entity.ManageInventoryMethod == ManageInventoryMethod.ManageStock && entity.StockQuantity <= 0) + { + if (entity.BackorderMode == BackorderMode.NoBackorders) + availability = "out of stock"; + else if (entity.BackorderMode == BackorderMode.AllowQtyBelow0 || entity.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) + availability = entity.AvailableForPreOrder ? "preorder" : "out of stock"; + } + + WriteString(writer, "id", uniqueId); + + writer.WriteStartElement("title"); + writer.WriteCData(((string)product.Name).Truncate(70).RemoveInvalidXmlChars()); + writer.WriteEndElement(); + + writer.WriteStartElement("description"); + writer.WriteCData(((string)product.FullDescription).RemoveInvalidXmlChars()); + writer.WriteEndElement(); + + writer.WriteStartElement("g", "google_product_category", _googleNamespace); + writer.WriteCData(category.RemoveInvalidXmlChars()); + writer.WriteFullEndElement(); + + if (productType.HasValue()) + { + writer.WriteStartElement("g", "product_type", _googleNamespace); + writer.WriteCData(productType.RemoveInvalidXmlChars()); + writer.WriteFullEndElement(); + } + + writer.WriteElementString("link", (string)product._DetailUrl); + + if (pictureUrls.Any()) + { + WriteString(writer, "image_link", pictureUrls.First()); + + if (config.AdditionalImages) + { + var imageCount = 0; + foreach (var url in pictureUrls.Skip(1)) + { + if (++imageCount <= 10) + { + WriteString(writer, "additional_image_link", url); + } + } + } + } switch (entity.Condition) { @@ -391,97 +385,97 @@ protected override void Export(ExportExecuteContext context) break; } - WriteString(writer, "availability", availability); + WriteString(writer, "availability", availability); - if (availability == "preorder" && entity.AvailableStartDateTimeUtc.HasValue && entity.AvailableStartDateTimeUtc.Value > DateTime.UtcNow) - { - var availabilityDate = entity.AvailableStartDateTimeUtc.Value.ToString(dateFormat); + if (availability == "preorder" && entity.AvailableStartDateTimeUtc.HasValue && entity.AvailableStartDateTimeUtc.Value > DateTime.UtcNow) + { + var availabilityDate = entity.AvailableStartDateTimeUtc.Value.ToString(dateFormat); - WriteString(writer, "availability_date", availabilityDate); - } + WriteString(writer, "availability_date", availabilityDate); + } - if (config.SpecialPrice && specialPrice.HasValue) - { - WriteString(writer, "sale_price", string.Concat(specialPrice.Value.FormatInvariant(), " ", currency.CurrencyCode)); + if (config.SpecialPrice && specialPrice.HasValue) + { + WriteString(writer, "sale_price", string.Concat(specialPrice.Value.FormatInvariant(), " ", currency.CurrencyCode)); - if (entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) - { - var specialPriceDate = "{0}/{1}".FormatInvariant( - entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); + if (entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) + { + var specialPriceDate = "{0}/{1}".FormatInvariant( + entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); - WriteString(writer, "sale_price_effective_date", specialPriceDate); - } + WriteString(writer, "sale_price_effective_date", specialPriceDate); + } - price = (product._RegularPrice as decimal?) ?? price; - } + price = (product._RegularPrice as decimal?) ?? price; + } - WriteString(writer, "price", string.Concat(price.FormatInvariant(), " ", currency.CurrencyCode)); + WriteString(writer, "price", string.Concat(price.FormatInvariant(), " ", currency.CurrencyCode)); - WriteString(writer, "gtin", gtin); - WriteString(writer, "brand", brand); - WriteString(writer, "mpn", mpn); + WriteString(writer, "gtin", gtin); + WriteString(writer, "brand", brand); + WriteString(writer, "mpn", mpn); - var identifierExists = brand.HasValue() && (gtin.HasValue() || mpn.HasValue()); - WriteString(writer, "identifier_exists", identifierExists ? "yes" : "no"); + var identifierExists = brand.HasValue() && (gtin.HasValue() || mpn.HasValue()); + WriteString(writer, "identifier_exists", identifierExists ? "yes" : "no"); - var gender = GetAttributeValue(attributeValues, "gender", languageId, gmc?.Gender, config.Gender); - WriteString(writer, "gender", gender); + var gender = GetAttributeValue(attributeValues, "gender", languageId, gmc?.Gender, config.Gender); + WriteString(writer, "gender", gender); - var ageGroup = GetAttributeValue(attributeValues, "age_group", languageId, gmc?.AgeGroup, config.AgeGroup); - WriteString(writer, "age_group", ageGroup); + var ageGroup = GetAttributeValue(attributeValues, "age_group", languageId, gmc?.AgeGroup, config.AgeGroup); + WriteString(writer, "age_group", ageGroup); - var color = GetAttributeValue(attributeValues, "color", languageId, gmc?.Color, config.Color); - WriteString(writer, "color", color); + var color = GetAttributeValue(attributeValues, "color", languageId, gmc?.Color, config.Color); + WriteString(writer, "color", color); - var size = GetAttributeValue(attributeValues, "size", languageId, gmc?.Size, config.Size); - WriteString(writer, "size", size); + var size = GetAttributeValue(attributeValues, "size", languageId, gmc?.Size, config.Size); + WriteString(writer, "size", size); - var material = GetAttributeValue(attributeValues, "material", languageId, gmc?.Material, config.Material); - WriteString(writer, "material", material); + var material = GetAttributeValue(attributeValues, "material", languageId, gmc?.Material, config.Material); + WriteString(writer, "material", material); - var pattern = GetAttributeValue(attributeValues, "pattern", languageId, gmc?.Pattern, config.Pattern); - WriteString(writer, "pattern", pattern); + var pattern = GetAttributeValue(attributeValues, "pattern", languageId, gmc?.Pattern, config.Pattern); + WriteString(writer, "pattern", pattern); - var itemGroupId = gmc != null && gmc.ItemGroupId.HasValue() ? gmc.ItemGroupId : string.Empty; - if (itemGroupId.HasValue()) - { - WriteString(writer, "item_group_id", itemGroupId); - } + var itemGroupId = gmc != null && gmc.ItemGroupId.HasValue() ? gmc.ItemGroupId : string.Empty; + if (itemGroupId.HasValue()) + { + WriteString(writer, "item_group_id", itemGroupId); + } - if (config.ExpirationDays > 0) - { - WriteString(writer, "expiration_date", DateTime.UtcNow.AddDays(config.ExpirationDays).ToString("yyyy-MM-dd")); - } + if (config.ExpirationDays > 0) + { + WriteString(writer, "expiration_date", DateTime.UtcNow.AddDays(config.ExpirationDays).ToString("yyyy-MM-dd")); + } - if (config.ExportShipping) - { - var weight = string.Concat(((decimal)product.Weight).FormatInvariant(), " ", measureWeight); - WriteString(writer, "shipping_weight", weight); - } + if (config.ExportShipping) + { + var weight = string.Concat(((decimal)product.Weight).FormatInvariant(), " ", measureWeight); + WriteString(writer, "shipping_weight", weight); + } - if (config.ExportBasePrice && entity.BasePriceHasValue) - { - var measureUnit = BasePriceUnits((string)product.BasePriceMeasureUnit); + if (config.ExportBasePrice && entity.BasePriceHasValue) + { + var measureUnit = BasePriceUnits((string)product.BasePriceMeasureUnit); - if (BasePriceSupported(entity.BasePriceBaseAmount ?? 0, measureUnit)) - { - var basePriceMeasure = "{0} {1}".FormatInvariant((entity.BasePriceAmount ?? decimal.Zero).FormatInvariant(), measureUnit); - var basePriceBaseMeasure = "{0} {1}".FormatInvariant(entity.BasePriceBaseAmount ?? 1, measureUnit); + if (BasePriceSupported(entity.BasePriceBaseAmount ?? 0, measureUnit)) + { + var basePriceMeasure = "{0} {1}".FormatInvariant((entity.BasePriceAmount ?? decimal.Zero).FormatInvariant(), measureUnit); + var basePriceBaseMeasure = "{0} {1}".FormatInvariant(entity.BasePriceBaseAmount ?? 1, measureUnit); - WriteString(writer, "unit_pricing_measure", basePriceMeasure); - WriteString(writer, "unit_pricing_base_measure", basePriceBaseMeasure); - } - } + WriteString(writer, "unit_pricing_measure", basePriceMeasure); + WriteString(writer, "unit_pricing_base_measure", basePriceBaseMeasure); + } + } - if (gmc != null) - { - WriteString(writer, "multipack", gmc.Multipack > 1 ? gmc.Multipack.ToString() : null); - WriteString(writer, "is_bundle", gmc.IsBundle.HasValue ? (gmc.IsBundle.Value ? "yes" : "no") : null); - WriteString(writer, "adult", gmc.IsAdult.HasValue ? (gmc.IsAdult.Value ? "yes" : "no") : null); - WriteString(writer, "energy_efficiency_class", gmc.EnergyEfficiencyClass.HasValue() ? gmc.EnergyEfficiencyClass : null); - } + if (gmc != null) + { + WriteString(writer, "multipack", gmc.Multipack > 1 ? gmc.Multipack.ToString() : null); + WriteString(writer, "is_bundle", gmc.IsBundle.HasValue ? (gmc.IsBundle.Value ? "yes" : "no") : null); + WriteString(writer, "adult", gmc.IsAdult.HasValue ? (gmc.IsAdult.Value ? "yes" : "no") : null); + WriteString(writer, "energy_efficiency_class", gmc.EnergyEfficiencyClass.HasValue() ? gmc.EnergyEfficiencyClass : null); + } - var customLabel0 = GetAttributeValue(attributeValues, "custom_label_0", languageId, gmc?.CustomLabel0, null); + var customLabel0 = GetAttributeValue(attributeValues, "custom_label_0", languageId, gmc?.CustomLabel0, null); WriteString(writer, "custom_label_0", gmc?.CustomLabel0.NullEmpty()); var customLabel1 = GetAttributeValue(attributeValues, "custom_label_1", languageId, gmc?.CustomLabel1, null); @@ -497,7 +491,7 @@ protected override void Export(ExportExecuteContext context) WriteString(writer, "custom_label_4", gmc?.CustomLabel4.NullEmpty()); ++context.RecordsSucceeded; - } + } catch (OutOfMemoryException ex) { context.RecordOutOfMemoryException(ex, entity.Id, T); @@ -505,18 +499,18 @@ protected override void Export(ExportExecuteContext context) throw; } catch (Exception ex) - { - context.RecordException(ex, entity.Id); - } - - writer.WriteEndElement(); // item - } - } - - writer.WriteEndElement(); // channel - writer.WriteEndElement(); // rss - writer.WriteEndDocument(); - } - } - } + { + context.RecordException(ex, entity.Id); + } + + writer.WriteEndElement(); // item + } + } + + writer.WriteEndElement(); // channel + writer.WriteEndElement(); // rss + writer.WriteEndDocument(); + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs index d0e3b85340..32b09e96ef 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs @@ -8,20 +8,14 @@ public partial class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { - routes.MapRoute("SmartStore.GoogleMerchantCenter", - "Plugins/SmartStore.GoogleMerchantCenter/{action}", - new { controller = "FeedGoogleMerchantCenter", action = "Configure" }, - new[] { "SmartStore.GoogleMerchantCenter.Controllers" } + routes.MapRoute("SmartStore.GoogleMerchantCenter", + "Plugins/SmartStore.GoogleMerchantCenter/{action}", + new { controller = "FeedGoogleMerchantCenter", action = "Configure" }, + new[] { "SmartStore.GoogleMerchantCenter.Controllers" } ) - .DataTokens["area"] = GoogleMerchantCenterFeedPlugin.SystemName; + .DataTokens["area"] = GoogleMerchantCenterFeedPlugin.SystemName; } - public int Priority - { - get - { - return 0; - } - } + public int Priority => 0; } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs index c440ea04aa..d8ea715973 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs @@ -16,53 +16,53 @@ namespace SmartStore.GoogleMerchantCenter.Services { - public partial class GoogleFeedService : IGoogleFeedService + public partial class GoogleFeedService : IGoogleFeedService { - private const string _googleNamespace = "http://base.google.com/ns/1.0"; + private const string _googleNamespace = "http://base.google.com/ns/1.0"; private readonly IRepository _gpRepository; - private readonly ICommonServices _services; - private readonly IPluginFinder _pluginFinder; - private readonly ILogger _logger; - - public GoogleFeedService( - IRepository gpRepository, - ICommonServices services, - IPluginFinder pluginFinder, - ILogger logger) + private readonly ICommonServices _services; + private readonly IPluginFinder _pluginFinder; + private readonly ILogger _logger; + + public GoogleFeedService( + IRepository gpRepository, + ICommonServices services, + IPluginFinder pluginFinder, + ILogger logger) { _gpRepository = gpRepository; - _services = services; - _pluginFinder = pluginFinder; - _logger = logger; + _services = services; + _pluginFinder = pluginFinder; + _logger = logger; - T = NullLocalizer.Instance; + T = NullLocalizer.Instance; } - public Localizer T { get; set; } + public Localizer T { get; set; } public GoogleProductRecord GetGoogleProductRecord(int productId) { if (productId == 0) return null; - var query = - from gp in _gpRepository.Table - where gp.ProductId == productId - select gp; + var query = + from gp in _gpRepository.Table + where gp.ProductId == productId + select gp; var record = query.FirstOrDefault(); return record; } - public List GetGoogleProductRecords(int[] productIds) - { - if (productIds == null || productIds.Length == 0) - return new List(); + public List GetGoogleProductRecords(int[] productIds) + { + if (productIds == null || productIds.Length == 0) + return new List(); - var lst = _gpRepository.TableUntracked.Where(x => productIds.Contains(x.ProductId)).ToList(); - return lst; - } + var lst = _gpRepository.TableUntracked.Where(x => productIds.Contains(x.ProductId)).ToList(); + return lst; + } public void InsertGoogleProductRecord(GoogleProductRecord record) { @@ -72,271 +72,271 @@ public void InsertGoogleProductRecord(GoogleProductRecord record) _gpRepository.Insert(record); } - public void UpdateGoogleProductRecord(GoogleProductRecord record) + public void UpdateGoogleProductRecord(GoogleProductRecord record) + { + if (record == null) + throw new ArgumentNullException("record"); + + _gpRepository.Update(record); + } + + public void DeleteGoogleProductRecord(GoogleProductRecord record) { - if (record == null) - throw new ArgumentNullException("record"); + if (record == null) + throw new ArgumentNullException("record"); - _gpRepository.Update(record); + _gpRepository.Delete(record); } - public void DeleteGoogleProductRecord(GoogleProductRecord record) - { - if (record == null) - throw new ArgumentNullException("record"); - - _gpRepository.Delete(record); - } - - public void Upsert(int pk, string name, string value) - { - if (pk == 0 || name.IsEmpty()) - return; - - var product = GetGoogleProductRecord(pk); - var insert = (product == null); - var utcNow = DateTime.UtcNow; - - if (product == null) - { - product = new GoogleProductRecord - { - ProductId = pk, - CreatedOnUtc = utcNow - }; - } - - switch (name) - { - case "Taxonomy": - product.Taxonomy = value; - break; - case "Gender": - product.Gender = value; - break; - case "AgeGroup": - product.AgeGroup = value; - break; - case "Color": - product.Color = value; - break; - case "Size": - product.Size = value; - break; - case "Material": - product.Material = value; - break; - case "Pattern": - product.Pattern = value; - break; - case "Export2": - product.Export = value.ToBool(true); - break; - case "Multipack2": - product.Multipack = value.ToInt(); - break; - case "IsBundle": - product.IsBundle = (value.IsEmpty() ? (bool?)null : value.ToBool()); - break; - case "IsAdult": - product.IsAdult = (value.IsEmpty() ? (bool?)null : value.ToBool()); - break; - case "EnergyEfficiencyClass": - product.EnergyEfficiencyClass = value; - break; - case "CustomLabel0": - product.CustomLabel0 = value; - break; - case "CustomLabel1": - product.CustomLabel1 = value; - break; - case "CustomLabel2": - product.CustomLabel2 = value; - break; - case "CustomLabel3": - product.CustomLabel3 = value; - break; - case "CustomLabel4": - product.CustomLabel4 = value; - break; - } - - product.UpdatedOnUtc = utcNow; - product.IsTouched = product.IsTouched(); - - if (!insert && !product.IsTouched) - { - _gpRepository.Delete(product); - return; - } - - if (insert) - { - _gpRepository.Insert(product); - } - else - { - _gpRepository.Update(product); - } - } - - public GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null) - { - var model = new GridModel(); - var textInfo = CultureInfo.InvariantCulture.TextInfo; - string yes = T("Admin.Common.Yes"); - string no = T("Admin.Common.No"); + public void Upsert(int pk, string name, string value) + { + if (pk == 0 || name.IsEmpty()) + return; + + var product = GetGoogleProductRecord(pk); + var insert = (product == null); + var utcNow = DateTime.UtcNow; + + if (product == null) + { + product = new GoogleProductRecord + { + ProductId = pk, + CreatedOnUtc = utcNow + }; + } + + switch (name) + { + case "Taxonomy": + product.Taxonomy = value; + break; + case "Gender": + product.Gender = value; + break; + case "AgeGroup": + product.AgeGroup = value; + break; + case "Color": + product.Color = value; + break; + case "Size": + product.Size = value; + break; + case "Material": + product.Material = value; + break; + case "Pattern": + product.Pattern = value; + break; + case "Export2": + product.Export = value.ToBool(true); + break; + case "Multipack2": + product.Multipack = value.ToInt(); + break; + case "IsBundle": + product.IsBundle = (value.IsEmpty() ? (bool?)null : value.ToBool()); + break; + case "IsAdult": + product.IsAdult = (value.IsEmpty() ? (bool?)null : value.ToBool()); + break; + case "EnergyEfficiencyClass": + product.EnergyEfficiencyClass = value; + break; + case "CustomLabel0": + product.CustomLabel0 = value; + break; + case "CustomLabel1": + product.CustomLabel1 = value; + break; + case "CustomLabel2": + product.CustomLabel2 = value; + break; + case "CustomLabel3": + product.CustomLabel3 = value; + break; + case "CustomLabel4": + product.CustomLabel4 = value; + break; + } + + product.UpdatedOnUtc = utcNow; + product.IsTouched = product.IsTouched(); + + if (!insert && !product.IsTouched) + { + _gpRepository.Delete(product); + return; + } + + if (insert) + { + _gpRepository.Insert(product); + } + else + { + _gpRepository.Update(product); + } + } + + public GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null) + { + var model = new GridModel(); + var textInfo = CultureInfo.InvariantCulture.TextInfo; + string yes = T("Admin.Common.Yes"); + string no = T("Admin.Common.No"); // there's no way to share a context instance across repositories in EF. // so we have to fallback to pure SQL here to get the data paged and filtered. var hidden = (int)ProductVisibility.Hidden; - var whereClause = new StringBuilder($"(NOT ([t2].[Deleted] = 1)) AND ([t2].[Visibility] <> {hidden})"); - - if (searchProductName.HasValue()) - { - whereClause.AppendFormat(" AND ([t2].[Name] LIKE '%{0}%')", searchProductName.Replace("'", "''")); - } - - if (touched.HasValue()) - { - if (touched.IsCaseInsensitiveEqual("touched")) - whereClause.Append(" AND ([t2].[IsTouched] = 1)"); - else - whereClause.Append(" AND ([t2].[IsTouched] = 0 OR [t2].[IsTouched] IS NULL)"); - } - - string sql = null; - string sqlCount = null; - var isSqlServer = DataSettings.Current.IsSqlServer; - - if (isSqlServer) - { - // fastest possible paged data query - sql = - "SELECT [TotalCount], [t3].[Id], [t3].[Name], [t3].[SKU], [t3].[ProductTypeId], [t3].[value] AS [Taxonomy], [t3].[value2] AS [Gender], [t3].[value3] AS [AgeGroup], [t3].[value4] AS [Color], [t3].[value5] AS [Size], [t3].[value6] AS [Material], [t3].[value7] AS [Pattern], [t3].[value8] AS [Export], [t3].[value9] AS [Multipack], [t3].[value10] AS [IsBundle], [t3].[value11] AS [IsAdult], [t3].[value12] AS [EnergyEfficiencyClass], [t3].[value13] AS [CustomLabel0], [t3].[value14] AS [CustomLabel1], [t3].[value15] AS [CustomLabel2], [t3].[value16] AS [CustomLabel3], [t3].[value17] AS [CustomLabel4]" + - " FROM (" + - " SELECT COUNT(id) OVER() [TotalCount], ROW_NUMBER() OVER (ORDER BY [t2].[Name]) AS [ROW_NUMBER], [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value], [t2].[value2], [t2].[value3], [t2].[value4], [t2].[value5], [t2].[value6], [t2].[value7], [t2].[value8], [t2].[value9], [t2].[value10], [t2].[value11], [t2].[value12], [t2].[value13], [t2].[value14], [t2].[value15], [t2].[value16], [t2].[value17]" + - " FROM (" + + var whereClause = new StringBuilder($"(NOT ([t2].[Deleted] = 1)) AND ([t2].[Visibility] <> {hidden})"); + + if (searchProductName.HasValue()) + { + whereClause.AppendFormat(" AND ([t2].[Name] LIKE '%{0}%')", searchProductName.Replace("'", "''")); + } + + if (touched.HasValue()) + { + if (touched.IsCaseInsensitiveEqual("touched")) + whereClause.Append(" AND ([t2].[IsTouched] = 1)"); + else + whereClause.Append(" AND ([t2].[IsTouched] = 0 OR [t2].[IsTouched] IS NULL)"); + } + + string sql = null; + string sqlCount = null; + var isSqlServer = DataSettings.Current.IsSqlServer; + + if (isSqlServer) + { + // fastest possible paged data query + sql = + "SELECT [TotalCount], [t3].[Id], [t3].[Name], [t3].[SKU], [t3].[ProductTypeId], [t3].[value] AS [Taxonomy], [t3].[value2] AS [Gender], [t3].[value3] AS [AgeGroup], [t3].[value4] AS [Color], [t3].[value5] AS [Size], [t3].[value6] AS [Material], [t3].[value7] AS [Pattern], [t3].[value8] AS [Export], [t3].[value9] AS [Multipack], [t3].[value10] AS [IsBundle], [t3].[value11] AS [IsAdult], [t3].[value12] AS [EnergyEfficiencyClass], [t3].[value13] AS [CustomLabel0], [t3].[value14] AS [CustomLabel1], [t3].[value15] AS [CustomLabel2], [t3].[value16] AS [CustomLabel3], [t3].[value17] AS [CustomLabel4]" + + " FROM (" + + " SELECT COUNT(id) OVER() [TotalCount], ROW_NUMBER() OVER (ORDER BY [t2].[Name]) AS [ROW_NUMBER], [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value], [t2].[value2], [t2].[value3], [t2].[value4], [t2].[value5], [t2].[value6], [t2].[value7], [t2].[value8], [t2].[value9], [t2].[value10], [t2].[value11], [t2].[value12], [t2].[value13], [t2].[value14], [t2].[value15], [t2].[value16], [t2].[value17]" + + " FROM (" + " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], COALESCE([t1].[Multipack],0) AS [value9], [t1].[IsBundle] AS [value10], [t1].[IsAdult] AS [value11], [t1].[EnergyEfficiencyClass] AS [value12], [t1].[CustomLabel0] AS [value13], [t1].[CustomLabel1] AS [value14], [t1].[CustomLabel2] AS [value15], [t1].[CustomLabel3] AS [value16], [t1].[CustomLabel4] AS [value17], [t0].[Deleted], [t0].[Visibility], [t1].[IsTouched]" + - " FROM [Product] AS [t0]" + - " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + - " ) AS [t2]" + - " WHERE " + whereClause.ToString() + - " ) AS [t3]" + - " WHERE [t3].[ROW_NUMBER] BETWEEN {0} + 1 AND {0} + {1}" + - " ORDER BY [t3].[ROW_NUMBER]"; - } - else - { - // OFFSET... FETCH NEXT requires SQL Server 2012 or SQL CE 4 - sql = - "SELECT [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value] AS [Taxonomy], [t2].[value2] AS [Gender], [t2].[value3] AS [AgeGroup], [t2].[value4] AS [Color], [t2].[value5] AS [Size], [t2].[value6] AS [Material], [t2].[value7] AS [Pattern], [t2].[value8] AS [Export], [t2].[value9] AS [Multipack], [t2].[value10] AS [IsBundle], [t2].[value11] AS [IsAdult], [t2].[value12] AS [EnergyEfficiencyClass], [t2].[value13] AS [CustomLabel0], [t2].[value14] AS [CustomLabel1], [t2].[value15] AS [CustomLabel2], [t2].[value16] AS [CustomLabel3], [t2].[value17] AS [CustomLabel4]" + - " FROM (" + + " FROM [Product] AS [t0]" + + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + + " ) AS [t2]" + + " WHERE " + whereClause.ToString() + + " ) AS [t3]" + + " WHERE [t3].[ROW_NUMBER] BETWEEN {0} + 1 AND {0} + {1}" + + " ORDER BY [t3].[ROW_NUMBER]"; + } + else + { + // OFFSET... FETCH NEXT requires SQL Server 2012 or SQL CE 4 + sql = + "SELECT [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value] AS [Taxonomy], [t2].[value2] AS [Gender], [t2].[value3] AS [AgeGroup], [t2].[value4] AS [Color], [t2].[value5] AS [Size], [t2].[value6] AS [Material], [t2].[value7] AS [Pattern], [t2].[value8] AS [Export], [t2].[value9] AS [Multipack], [t2].[value10] AS [IsBundle], [t2].[value11] AS [IsAdult], [t2].[value12] AS [EnergyEfficiencyClass], [t2].[value13] AS [CustomLabel0], [t2].[value14] AS [CustomLabel1], [t2].[value15] AS [CustomLabel2], [t2].[value16] AS [CustomLabel3], [t2].[value17] AS [CustomLabel4]" + + " FROM (" + " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], COALESCE([t1].[Multipack],0) AS [value9], [t1].[IsBundle] AS [value10], [t1].[IsAdult] AS [value11], [t1].[EnergyEfficiencyClass] AS [value12], [t1].[CustomLabel0] AS [value13], [t1].[CustomLabel1] AS [value14], [t1].[CustomLabel2] AS [value15], [t1].[CustomLabel3] AS [value16], [t1].[CustomLabel4] AS [value17], [t0].[Deleted], [t0].[Visibility], [t1].[IsTouched] AS [IsTouched]" + - " FROM [Product] AS [t0]" + - " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + - " ) AS [t2]" + - " WHERE " + whereClause.ToString() + - " ORDER BY [t2].[Name]" + - " OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY"; - - sqlCount = - "SELECT COUNT(*)" + - " FROM (" + + " FROM [Product] AS [t0]" + + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + + " ) AS [t2]" + + " WHERE " + whereClause.ToString() + + " ORDER BY [t2].[Name]" + + " OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY"; + + sqlCount = + "SELECT COUNT(*)" + + " FROM (" + " SELECT [t0].[Id], [t0].[Name], [t0].[Deleted], [t0].[Visibility], [t1].[IsTouched] AS [IsTouched]" + - " FROM [Product] AS [t0]" + - " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + - " ) AS [t2]" + - " WHERE " + whereClause.ToString(); - } - - - var data = _gpRepository.Context.SqlQuery(sql, (command.Page - 1) * command.PageSize, command.PageSize).ToList(); - - data.ForEach(x => - { - if (x.ProductType != ProductType.SimpleProduct) - x.ProductTypeName = T("Admin.Catalog.Products.ProductType.{0}.Label".FormatInvariant(x.ProductType.ToString())); - - if (x.Gender.HasValue()) - x.GenderLocalize = T("Plugins.Feed.Froogle.Gender" + textInfo.ToTitleCase(x.Gender)); - - if (x.AgeGroup.HasValue()) - x.AgeGroupLocalize = T("Plugins.Feed.Froogle.AgeGroup" + textInfo.ToTitleCase(x.AgeGroup)); - - x.Export2Localize = (x.Export == 0 ? no : yes); - - if (x.IsBundle.HasValue) - x.IsBundleLocalize = (x.IsBundle.Value ? yes : no); - else - x.IsBundleLocalize = null; - - if (x.IsAdult.HasValue) - x.IsAdultLocalize = (x.IsAdult.Value ? yes : no); - else - x.IsAdultLocalize = null; - }); - - model.Data = data; - model.Total = (data.Count > 0 ? data.First().TotalCount : 0); - - if (data.Count > 0) - { - if (isSqlServer) - model.Total = data.First().TotalCount; - else - model.Total = _gpRepository.Context.SqlQuery(sqlCount).FirstOrDefault(); - } - else - { - model.Total = 0; - } - - return model; - } - - public List GetTaxonomyList(string searchTerm) - { - var result = new List(); - - try - { - var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(GoogleMerchantCenterFeedPlugin.SystemName); - var fileDir = Path.Combine(descriptor.Assembly.OriginalFile.Directory.FullName, "Files"); - var fileName = "taxonomy.{0}.txt".FormatInvariant(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); - var path = Path.Combine(fileDir, fileName); - var filter = searchTerm.HasValue(); - string line; - - if (!File.Exists(path)) - { - path = Path.Combine(fileDir, "taxonomy.en-US.txt"); - } - - using (var file = new StreamReader(path, Encoding.UTF8)) - { - while ((line = file.ReadLine()) != null) - { - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - if (filter && line.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) < 0) - { - continue; - } - - result.Add(line); - } - } - } - catch (Exception exc) - { - _logger.Error(exc); - } - - return result; - } + " FROM [Product] AS [t0]" + + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + + " ) AS [t2]" + + " WHERE " + whereClause.ToString(); + } + + + var data = _gpRepository.Context.SqlQuery(sql, (command.Page - 1) * command.PageSize, command.PageSize).ToList(); + + data.ForEach(x => + { + if (x.ProductType != ProductType.SimpleProduct) + x.ProductTypeName = T("Admin.Catalog.Products.ProductType.{0}.Label".FormatInvariant(x.ProductType.ToString())); + + if (x.Gender.HasValue()) + x.GenderLocalize = T("Plugins.Feed.Froogle.Gender" + textInfo.ToTitleCase(x.Gender)); + + if (x.AgeGroup.HasValue()) + x.AgeGroupLocalize = T("Plugins.Feed.Froogle.AgeGroup" + textInfo.ToTitleCase(x.AgeGroup)); + + x.Export2Localize = (x.Export == 0 ? no : yes); + + if (x.IsBundle.HasValue) + x.IsBundleLocalize = (x.IsBundle.Value ? yes : no); + else + x.IsBundleLocalize = null; + + if (x.IsAdult.HasValue) + x.IsAdultLocalize = (x.IsAdult.Value ? yes : no); + else + x.IsAdultLocalize = null; + }); + + model.Data = data; + model.Total = (data.Count > 0 ? data.First().TotalCount : 0); + + if (data.Count > 0) + { + if (isSqlServer) + model.Total = data.First().TotalCount; + else + model.Total = _gpRepository.Context.SqlQuery(sqlCount).FirstOrDefault(); + } + else + { + model.Total = 0; + } + + return model; + } + + public List GetTaxonomyList(string searchTerm) + { + var result = new List(); + + try + { + var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(GoogleMerchantCenterFeedPlugin.SystemName); + var fileDir = Path.Combine(descriptor.Assembly.OriginalFile.Directory.FullName, "Files"); + var fileName = "taxonomy.{0}.txt".FormatInvariant(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); + var path = Path.Combine(fileDir, fileName); + var filter = searchTerm.HasValue(); + string line; + + if (!File.Exists(path)) + { + path = Path.Combine(fileDir, "taxonomy.en-US.txt"); + } + + using (var file = new StreamReader(path, Encoding.UTF8)) + { + while ((line = file.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + if (filter && line.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + result.Add(line); + } + } + } + catch (Exception exc) + { + _logger.Error(exc); + } + + return result; + } } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs index cfd2bc5f06..6ea687c13b 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs @@ -5,22 +5,22 @@ namespace SmartStore.GoogleMerchantCenter.Services { - public partial interface IGoogleFeedService + public partial interface IGoogleFeedService { - GoogleProductRecord GetGoogleProductRecord(int productId); + GoogleProductRecord GetGoogleProductRecord(int productId); - List GetGoogleProductRecords(int[] productIds); + List GetGoogleProductRecords(int[] productIds); - void InsertGoogleProductRecord(GoogleProductRecord record); + void InsertGoogleProductRecord(GoogleProductRecord record); - void UpdateGoogleProductRecord(GoogleProductRecord record); - - void DeleteGoogleProductRecord(GoogleProductRecord record); + void UpdateGoogleProductRecord(GoogleProductRecord record); - void Upsert(int pk, string name, string value); + void DeleteGoogleProductRecord(GoogleProductRecord record); - GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null); + void Upsert(int pk, string name, string value); - List GetTaxonomyList(string searchTerm); + GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null); + + List GetTaxonomyList(string searchTerm); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index b8bfd5df32..b6ee05227a 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -20,7 +20,7 @@ Properties SmartStore.GoogleMerchantCenter SmartStore.GoogleMerchantCenter - v4.6.1 + v4.7.2 512 @@ -85,47 +85,57 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll False - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll False - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll False ..\..\packages\FluentValidation.7.4.0\lib\net45\FluentValidation.dll False - - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + False ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - False + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + - + ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll False diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml index 6594f41d59..a3e42dcc2e 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml @@ -164,9 +164,8 @@
- diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml index 25d91e3bbb..16aab3b239 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml @@ -164,9 +164,8 @@
- diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config index 1b8cbc1400..2000cd5d23 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config @@ -1,13 +1,16 @@  - - - + + + - + + - + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config index 81f0867c5c..b15e514586 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config @@ -1,155 +1,163 @@ - + - - - + + + - - + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + - + diff --git a/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs b/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs index 515865d519..e7af8235c0 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs @@ -20,99 +20,99 @@ namespace SmartStore.OfflinePayment.Controllers { public class OfflinePaymentController : PaymentControllerBase { - private readonly IComponentContext _ctx; - private readonly HttpContextBase _httpContext; + private readonly IComponentContext _ctx; + private readonly HttpContextBase _httpContext; private readonly IMediaService _mediaService; public OfflinePaymentController( - HttpContextBase httpContext, - IComponentContext ctx, + HttpContextBase httpContext, + IComponentContext ctx, IMediaService mediaService) { - _httpContext = httpContext; - _ctx = ctx; + _httpContext = httpContext; + _ctx = ctx; _mediaService = mediaService; - } - - #region Global - - private List GetTransactModes() - { - var list = new List - { - new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Pending"), Value = ((int)TransactMode.Pending).ToString() }, - new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Authorized"), Value = ((int)TransactMode.Authorize).ToString() }, - new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Paid"), Value = ((int)TransactMode.Paid).ToString() } - }; - - return list; - } - - [NonAction] - private TModel ConfigureGet(Action fn = null) - where TModel : ConfigurationModelBase, new() - where TSetting : PaymentSettingsBase, new() - { - var model = new TModel(); - - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - var store = storeScope == 0 - ? Services.StoreContext.CurrentStore - : Services.StoreService.GetStoreById(storeScope); - - model.PrimaryStoreCurrencyCode = store.PrimaryStoreCurrency.CurrencyCode; - model.DescriptionText = settings.DescriptionText; - model.PaymentMethodLogo = settings.ThumbnailPictureId; - model.AdditionalFee = settings.AdditionalFee; - model.AdditionalFeePercentage = settings.AdditionalFeePercentage; - - if (fn != null) - { - fn(model, settings); - } - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); - - return model; - } - - [NonAction] - private void ConfigurePost(TModel model, FormCollection form, Action fn = null) - where TModel : ConfigurationModelBase, new() - where TSetting : PaymentSettingsBase, new() - { - ModelState.Clear(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - - settings.DescriptionText = model.DescriptionText; - settings.ThumbnailPictureId = model.PaymentMethodLogo; - settings.AdditionalFee = model.AdditionalFee; - settings.AdditionalFeePercentage = model.AdditionalFeePercentage; - - if (fn != null) - { - fn(settings); - } - - using (Services.Settings.BeginScope()) - { - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); - } - - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - } - - [NonAction] - private TModel PaymentInfoGet(Action fn = null) - where TModel : PaymentInfoModelBase, new() - where TSetting : PaymentSettingsBase, new() - { - var settings = _ctx.Resolve(); + } + + #region Global + + private List GetTransactModes() + { + var list = new List + { + new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Pending"), Value = ((int)TransactMode.Pending).ToString() }, + new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Authorized"), Value = ((int)TransactMode.Authorize).ToString() }, + new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Paid"), Value = ((int)TransactMode.Paid).ToString() } + }; + + return list; + } + + [NonAction] + private TModel ConfigureGet(Action fn = null) + where TModel : ConfigurationModelBase, new() + where TSetting : PaymentSettingsBase, new() + { + var model = new TModel(); + + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + var store = storeScope == 0 + ? Services.StoreContext.CurrentStore + : Services.StoreService.GetStoreById(storeScope); + + model.PrimaryStoreCurrencyCode = store.PrimaryStoreCurrency.CurrencyCode; + model.DescriptionText = settings.DescriptionText; + model.PaymentMethodLogo = settings.ThumbnailPictureId; + model.AdditionalFee = settings.AdditionalFee; + model.AdditionalFeePercentage = settings.AdditionalFeePercentage; + + if (fn != null) + { + fn(model, settings); + } + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + + return model; + } + + [NonAction] + private void ConfigurePost(TModel model, FormCollection form, Action fn = null) + where TModel : ConfigurationModelBase, new() + where TSetting : PaymentSettingsBase, new() + { + ModelState.Clear(); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + settings.DescriptionText = model.DescriptionText; + settings.ThumbnailPictureId = model.PaymentMethodLogo; + settings.AdditionalFee = model.AdditionalFee; + settings.AdditionalFeePercentage = model.AdditionalFeePercentage; + + if (fn != null) + { + fn(settings); + } + + using (Services.Settings.BeginScope()) + { + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } + + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + } + + [NonAction] + private TModel PaymentInfoGet(Action fn = null) + where TModel : PaymentInfoModelBase, new() + where TSetting : PaymentSettingsBase, new() + { + var settings = _ctx.Resolve(); var model = new TModel { DescriptionText = GetLocalizedText(settings.DescriptionText), @@ -120,34 +120,34 @@ private TModel PaymentInfoGet(Action fn = nu }; if (fn != null) - { - fn(model, settings); - } - - return model; - } - - private string GetLocalizedText(string text) - { - if (text.EmptyNull().StartsWith("@")) - { - return T(text.Substring(1)); - } - - return text; - } - - [NonAction] - public override IList ValidatePaymentForm(FormCollection form) - { - var warnings = new List(); - IValidator validator; - ValidationResult validationResult = null; - - string type = form["OfflinePaymentMethodType"].NullEmpty(); - - if (type.HasValue()) - { + { + fn(model, settings); + } + + return model; + } + + private string GetLocalizedText(string text) + { + if (text.EmptyNull().StartsWith("@")) + { + return T(text.Substring(1)); + } + + return text; + } + + [NonAction] + public override IList ValidatePaymentForm(FormCollection form) + { + var warnings = new List(); + IValidator validator; + ValidationResult validationResult = null; + + string type = form["OfflinePaymentMethodType"].NullEmpty(); + + if (type.HasValue()) + { if (type == "Manual") { validator = new ManualPaymentInfoValidator(T); @@ -185,348 +185,354 @@ public override IList ValidatePaymentForm(FormCollection form) validationResult = validator.Validate(model); } - if (validationResult != null && !validationResult.IsValid) - { - validationResult.Errors.Each(x => warnings.Add(x.ErrorMessage)); - } - } - - return warnings; - } - - [NonAction] - public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) - { - var paymentInfo = new ProcessPaymentRequest(); - - string type = form["OfflinePaymentMethodType"].NullEmpty(); - - if (type.HasValue()) - { - if (type == "Manual") - { - paymentInfo.CreditCardType = form["CreditCardType"]; - paymentInfo.CreditCardName = form["CardholderName"]; - paymentInfo.CreditCardNumber = form["CardNumber"]; - paymentInfo.CreditCardExpireMonth = int.Parse(form["ExpireMonth"].SplitSafe(",")[0]); - paymentInfo.CreditCardExpireYear = int.Parse(form["ExpireYear"].SplitSafe(",")[0]); - paymentInfo.CreditCardCvv2 = form["CardCode"]; - } - else if (type == "DirectDebit") - { - paymentInfo.DirectDebitAccountHolder = form["DirectDebitAccountHolder"]; - paymentInfo.DirectDebitAccountNumber = form["DirectDebitAccountNumber"]; - paymentInfo.DirectDebitBankCode = form["DirectDebitBankCode"]; - paymentInfo.DirectDebitBankName = form["DirectDebitBankName"]; - paymentInfo.DirectDebitBic = form["DirectDebitBic"]; - paymentInfo.DirectDebitCountry = form["DirectDebitCountry"]; - paymentInfo.DirectDebitIban = form["DirectDebitIban"]; - } + if (validationResult != null && !validationResult.IsValid) + { + validationResult.Errors.Each(x => warnings.Add(x.ErrorMessage)); + } + } + + return warnings; + } + + [NonAction] + public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) + { + var paymentInfo = new ProcessPaymentRequest(); + + string type = form["OfflinePaymentMethodType"].NullEmpty(); + + if (type.HasValue()) + { + if (type == "Manual") + { + paymentInfo.CreditCardType = form["CreditCardType"]; + paymentInfo.CreditCardName = form["CardholderName"]; + paymentInfo.CreditCardNumber = form["CardNumber"]; + paymentInfo.CreditCardExpireMonth = int.Parse(form["ExpireMonth"].SplitSafe(",")[0]); + paymentInfo.CreditCardExpireYear = int.Parse(form["ExpireYear"].SplitSafe(",")[0]); + paymentInfo.CreditCardCvv2 = form["CardCode"]; + } + else if (type == "DirectDebit") + { + paymentInfo.DirectDebitAccountHolder = form["DirectDebitAccountHolder"]; + paymentInfo.DirectDebitAccountNumber = form["DirectDebitAccountNumber"]; + paymentInfo.DirectDebitBankCode = form["DirectDebitBankCode"]; + paymentInfo.DirectDebitBankName = form["DirectDebitBankName"]; + paymentInfo.DirectDebitBic = form["DirectDebitBic"]; + paymentInfo.DirectDebitCountry = form["DirectDebitCountry"]; + paymentInfo.DirectDebitIban = form["DirectDebitIban"]; + } else if (type == "PurchaseOrderNumber") { paymentInfo.PurchaseOrderNumber = form["PurchaseOrderNumber"]; } - } - - return paymentInfo; - } - - [NonAction] - public override string GetPaymentSummary(FormCollection form) - { - string type = form["OfflinePaymentMethodType"].NullEmpty(); - - if (type.HasValue()) - { - if (type == "Manual") - { - var number = form["CardNumber"]; - return "{0}, {1}, {2}".FormatCurrent( - form["CreditCardType"], - form["CardholderName"], - number.Mask(4) - ); - } - else if (type == "DirectDebit") - { - if (form["DirectDebitAccountNumber"].HasValue() && (form["DirectDebitBankCode"].HasValue()) && form["DirectDebitAccountHolder"].HasValue()) - { - var number = form["DirectDebitAccountNumber"]; - return "{0}, {1}, {2}".FormatCurrent( - form["DirectDebitAccountHolder"], - form["DirectDebitBankName"].NullEmpty() ?? form["DirectDebitBankCode"], - number.Mask(4) - ); - } - else if (form["DirectDebitIban"].HasValue()) - { - var number = form["DirectDebitIban"]; - return number.Mask(8); - } - } + } + + return paymentInfo; + } + + [NonAction] + public override string GetPaymentSummary(FormCollection form) + { + string type = form["OfflinePaymentMethodType"].NullEmpty(); + + if (type.HasValue()) + { + if (type == "Manual") + { + var number = form["CardNumber"]; + return "{0}, {1}, {2}".FormatCurrent( + form["CreditCardType"], + form["CardholderName"], + number.Mask(4) + ); + } + else if (type == "DirectDebit") + { + if (form["DirectDebitAccountNumber"].HasValue() && (form["DirectDebitBankCode"].HasValue()) && form["DirectDebitAccountHolder"].HasValue()) + { + var number = form["DirectDebitAccountNumber"]; + return "{0}, {1}, {2}".FormatCurrent( + form["DirectDebitAccountHolder"], + form["DirectDebitBankName"].NullEmpty() ?? form["DirectDebitBankCode"], + number.Mask(4) + ); + } + else if (form["DirectDebitIban"].HasValue()) + { + var number = form["DirectDebitIban"]; + return number.Mask(8); + } + } else if (type == "PurchaseOrderNumber") { return form["PurchaseOrderNumber"]; } - } + } + + return null; + } + + #endregion + + #region CashOnDelivery + + [AdminAuthorize, AdminThemed, ChildActionOnly] + public ActionResult CashOnDeliveryConfigure() + { + var model = ConfigureGet(); + + return View("GenericConfigure", model); + } - return null; - } + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] + public ActionResult CashOnDeliveryConfigure(CashOnDeliveryConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return CashOnDeliveryConfigure(); - #endregion - - #region CashOnDelivery - - [AdminAuthorize, AdminThemed, ChildActionOnly] - public ActionResult CashOnDeliveryConfigure() - { - var model = ConfigureGet(); + ConfigurePost(model, form); - return View("GenericConfigure", model); - } + return CashOnDeliveryConfigure(); + } - [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] - public ActionResult CashOnDeliveryConfigure(CashOnDeliveryConfigurationModel model, FormCollection form) - { - if (!ModelState.IsValid) - return CashOnDeliveryConfigure(); + public ActionResult CashOnDeliveryPaymentInfo() + { + var model = PaymentInfoGet(); + return PartialView("GenericPaymentInfo", model); + } - ConfigurePost(model, form); + #endregion - return CashOnDeliveryConfigure(); - } - - public ActionResult CashOnDeliveryPaymentInfo() - { - var model = PaymentInfoGet(); - return PartialView("GenericPaymentInfo", model); - } + #region Invoice - #endregion - - #region Invoice - - [ChildActionOnly, AdminThemed, AdminAuthorize] - public ActionResult InvoiceConfigure() - { - var model = ConfigureGet(); + [ChildActionOnly, AdminThemed, AdminAuthorize] + public ActionResult InvoiceConfigure() + { + var model = ConfigureGet(); - return View("GenericConfigure", model); - } + return View("GenericConfigure", model); + } - [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] - public ActionResult InvoiceConfigure(InvoiceConfigurationModel model, FormCollection form) - { - if (!ModelState.IsValid) - return InvoiceConfigure(); - - ConfigurePost(model, form); - - return InvoiceConfigure(); - } - - public ActionResult InvoicePaymentInfo() - { - var model = PaymentInfoGet(); - return PartialView("GenericPaymentInfo", model); - } - - #endregion - - #region PayInStore - - [ChildActionOnly, AdminThemed, AdminAuthorize] - public ActionResult PayInStoreConfigure() - { - var model = ConfigureGet(); - - return View("GenericConfigure", model); - } - - [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] - public ActionResult PayInStoreConfigure(PayInStoreConfigurationModel model, FormCollection form) - { - if (!ModelState.IsValid) - return PayInStoreConfigure(); - - ConfigurePost(model, form); - - return PayInStoreConfigure(); - } - - public ActionResult PayInStorePaymentInfo() - { - var model = PaymentInfoGet(); - return PartialView("GenericPaymentInfo", model); - } - - #endregion - - #region Prepayment - - [AdminAuthorize, AdminThemed, ChildActionOnly] - public ActionResult PrepaymentConfigure() - { - var model = ConfigureGet(); - - return View("GenericConfigure", model); - } - - [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] - public ActionResult PrepaymentConfigure(PrepaymentConfigurationModel model, FormCollection form) - { - if (!ModelState.IsValid) - return PrepaymentConfigure(); - - ConfigurePost(model, form); - - return PrepaymentConfigure(); - } - - public ActionResult PrepaymentPaymentInfo() - { - var model = PaymentInfoGet(); - return PartialView("GenericPaymentInfo", model); - } - - #endregion - - #region DirectDebit - - [AdminAuthorize, AdminThemed, ChildActionOnly] - public ActionResult DirectDebitConfigure() - { - var model = ConfigureGet(); - - return View("GenericConfigure", model); - } - - [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] - public ActionResult DirectDebitConfigure(DirectDebitConfigurationModel model, FormCollection form) - { - if (!ModelState.IsValid) - return DirectDebitConfigure(); - - ConfigurePost(model, form); - - return DirectDebitConfigure(); - } - - public ActionResult DirectDebitPaymentInfo() - { - var model = PaymentInfoGet(); - var paymentData = _httpContext.GetCheckoutState().PaymentData; - - model.DirectDebitAccountHolder = (string)paymentData.Get("DirectDebitAccountHolder"); - model.DirectDebitAccountNumber = (string)paymentData.Get("DirectDebitAccountNumber"); - model.DirectDebitBankCode = (string)paymentData.Get("DirectDebitBankCode"); - model.DirectDebitBankName = (string)paymentData.Get("DirectDebitBankName"); - model.DirectDebitBic = (string)paymentData.Get("DirectDebitBic"); - model.DirectDebitCountry = (string)paymentData.Get("DirectDebitCountry"); - model.DirectDebitIban = (string)paymentData.Get("DirectDebitIban"); - - return PartialView(model); - } - - #endregion - - #region Manual - - [AdminAuthorize, AdminThemed, ChildActionOnly] - public ActionResult ManualConfigure() - { - var model = ConfigureGet((m, s) => - { - m.TransactMode = s.TransactMode; - m.TransactModeValues = GetTransactModes(); - m.ExcludedCreditCards = s.ExcludedCreditCards.SplitSafe(","); - - m.AvailableCreditCards = ManualProvider.CreditCardTypes - .Select(x => new SelectListItem - { - Text = x.Text, - Value = x.Value, - Selected = m.ExcludedCreditCards.Contains(x.Value) - }) - .ToList(); - }); - - return View(model); - } - - [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] - public ActionResult ManualConfigure(ManualConfigurationModel model, FormCollection form) - { - if (!ModelState.IsValid) - return ManualConfigure(); - - ConfigurePost(model, form, s => - { - s.TransactMode = model.TransactMode; - s.ExcludedCreditCards = string.Join(",", model.ExcludedCreditCards ?? new string[0]); - }); - - return ManualConfigure(); - } - - public ActionResult ManualPaymentInfo() - { - var model = PaymentInfoGet((m, s) => - { - var excludedCreditCards = s.ExcludedCreditCards.SplitSafe(","); - - foreach (var creditCard in ManualProvider.CreditCardTypes) - { - if (!excludedCreditCards.Any(x => x.IsCaseInsensitiveEqual(creditCard.Value))) - { - m.CreditCardTypes.Add(new SelectListItem - { - Text = creditCard.Text, - Value = creditCard.Value - }); - } - } - }); - - // years - for (int i = 0; i < 15; i++) - { - string year = Convert.ToString(DateTime.Now.Year + i); - model.ExpireYears.Add(new SelectListItem { Text = year, Value = year }); - } - - // months - for (int i = 1; i <= 12; i++) - { - string text = (i < 10) ? "0" + i.ToString() : i.ToString(); - model.ExpireMonths.Add(new SelectListItem { Text = text, Value = i.ToString() }); - } - - // set postback values - var paymentData = _httpContext.GetCheckoutState().PaymentData; - model.CardholderName = (string)paymentData.Get("CardholderName"); - model.CardNumber = (string)paymentData.Get("CardNumber"); - model.CardCode = (string)paymentData.Get("CardCode"); - - var creditCardType = (string)paymentData.Get("CreditCardType"); - var selectedCcType = model.CreditCardTypes.Where(x => x.Value.Equals(creditCardType, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (selectedCcType != null) - selectedCcType.Selected = true; - - var expireMonth = (string)paymentData.Get("ExpireMonth"); - var selectedMonth = model.ExpireMonths.Where(x => x.Value.Equals(expireMonth, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (selectedMonth != null) - selectedMonth.Selected = true; - - var expireYear = (string)paymentData.Get("ExpireYear"); - var selectedYear = model.ExpireYears.Where(x => x.Value.Equals(expireYear, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (selectedYear != null) - selectedYear.Selected = true; - - return PartialView(model); - } - - #endregion + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] + public ActionResult InvoiceConfigure(InvoiceConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return InvoiceConfigure(); + + ConfigurePost(model, form); + + return InvoiceConfigure(); + } + + public ActionResult InvoicePaymentInfo() + { + var model = PaymentInfoGet(); + return PartialView("GenericPaymentInfo", model); + } + + #endregion + + #region PayInStore + + [ChildActionOnly, AdminThemed, AdminAuthorize] + public ActionResult PayInStoreConfigure() + { + var model = ConfigureGet(); + + return View("GenericConfigure", model); + } + + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] + public ActionResult PayInStoreConfigure(PayInStoreConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return PayInStoreConfigure(); + + ConfigurePost(model, form); + + return PayInStoreConfigure(); + } + + public ActionResult PayInStorePaymentInfo() + { + var model = PaymentInfoGet(); + return PartialView("GenericPaymentInfo", model); + } + + #endregion + + #region Prepayment + + [AdminAuthorize, AdminThemed, ChildActionOnly] + public ActionResult PrepaymentConfigure() + { + var model = ConfigureGet(); + + return View("GenericConfigure", model); + } + + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] + public ActionResult PrepaymentConfigure(PrepaymentConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return PrepaymentConfigure(); + + ConfigurePost(model, form); + + return PrepaymentConfigure(); + } + + public ActionResult PrepaymentPaymentInfo() + { + var model = PaymentInfoGet(); + return PartialView("GenericPaymentInfo", model); + } + + #endregion + + #region DirectDebit + + [AdminAuthorize, AdminThemed, ChildActionOnly] + public ActionResult DirectDebitConfigure() + { + var model = ConfigureGet(); + + return View("GenericConfigure", model); + } + + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] + public ActionResult DirectDebitConfigure(DirectDebitConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return DirectDebitConfigure(); + + ConfigurePost(model, form); + + return DirectDebitConfigure(); + } + + public ActionResult DirectDebitPaymentInfo() + { + var model = PaymentInfoGet(); + var paymentData = _httpContext.GetCheckoutState().PaymentData; + + model.DirectDebitAccountHolder = (string)paymentData.Get("DirectDebitAccountHolder"); + model.DirectDebitAccountNumber = (string)paymentData.Get("DirectDebitAccountNumber"); + model.DirectDebitBankCode = (string)paymentData.Get("DirectDebitBankCode"); + model.DirectDebitBankName = (string)paymentData.Get("DirectDebitBankName"); + model.DirectDebitBic = (string)paymentData.Get("DirectDebitBic"); + model.DirectDebitCountry = (string)paymentData.Get("DirectDebitCountry"); + model.DirectDebitIban = (string)paymentData.Get("DirectDebitIban"); + + return PartialView(model); + } + + #endregion + + #region Manual + + [AdminAuthorize, AdminThemed, ChildActionOnly] + public ActionResult ManualConfigure() + { + var model = ConfigureGet((m, s) => + { + m.TransactMode = s.TransactMode; + m.TransactModeValues = GetTransactModes(); + m.ExcludedCreditCards = s.ExcludedCreditCards.SplitSafe(","); + + m.AvailableCreditCards = ManualProvider.CreditCardTypes + .Select(x => new SelectListItem + { + Text = x.Text, + Value = x.Value, + Selected = m.ExcludedCreditCards.Contains(x.Value) + }) + .ToList(); + }); + + return View(model); + } + + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] + public ActionResult ManualConfigure(ManualConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return ManualConfigure(); + + ConfigurePost(model, form, s => + { + s.TransactMode = model.TransactMode; + s.ExcludedCreditCards = string.Join(",", model.ExcludedCreditCards ?? new string[0]); + }); + + return ManualConfigure(); + } + + public ActionResult ManualPaymentInfo() + { + var model = PaymentInfoGet((m, s) => + { + var excludedCreditCards = s.ExcludedCreditCards.SplitSafe(","); + + foreach (var creditCard in ManualProvider.CreditCardTypes) + { + if (!excludedCreditCards.Any(x => x.IsCaseInsensitiveEqual(creditCard.Value))) + { + m.CreditCardTypes.Add(new SelectListItem + { + Text = creditCard.Text, + Value = creditCard.Value + }); + } + } + }); + + // years + for (int i = 0; i < 15; i++) + { + string year = Convert.ToString(DateTime.Now.Year + i); + model.ExpireYears.Add(new SelectListItem { Text = year, Value = year }); + } + + // months + for (int i = 1; i <= 12; i++) + { + string text = (i < 10) ? "0" + i.ToString() : i.ToString(); + model.ExpireMonths.Add(new SelectListItem { Text = text, Value = i.ToString() }); + } + + // set postback values + var paymentData = _httpContext.GetCheckoutState().PaymentData; + model.CardholderName = (string)paymentData.Get("CardholderName"); + model.CardNumber = (string)paymentData.Get("CardNumber"); + model.CardCode = (string)paymentData.Get("CardCode"); + + var creditCardType = (string)paymentData.Get("CreditCardType"); + var selectedCcType = model.CreditCardTypes.Where(x => x.Value.Equals(creditCardType, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (selectedCcType != null) + selectedCcType.Selected = true; + + var expireMonth = (string)paymentData.Get("ExpireMonth"); + var selectedMonth = model.ExpireMonths.Where(x => x.Value.Equals(expireMonth, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (selectedMonth != null) + selectedMonth.Selected = true; + + var expireYear = (string)paymentData.Get("ExpireYear"); + var selectedYear = model.ExpireYears.Where(x => x.Value.Equals(expireYear, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (selectedYear != null) + selectedYear.Selected = true; + + return PartialView(model); + } + + #endregion #region PurchaseOrderNumber @@ -539,12 +545,13 @@ public ActionResult PurchaseOrderNumberConfigure() } [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] + [ValidateAntiForgeryToken] public ActionResult PurchaseOrderNumberConfigure(PurchaseOrderNumberConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) - return PurchaseOrderNumberConfigure(); + return PurchaseOrderNumberConfigure(); - ConfigurePost(model, form); + ConfigurePost(model, form); return PurchaseOrderNumberConfigure(); } @@ -552,7 +559,7 @@ public ActionResult PurchaseOrderNumberConfigure(PurchaseOrderNumberConfiguratio public ActionResult PurchaseOrderNumberPaymentInfo() { var model = PaymentInfoGet(); - var paymentData = _httpContext.GetCheckoutState().PaymentData; + var paymentData = _httpContext.GetCheckoutState().PaymentData; model.PurchaseOrderNumber = (string)paymentData.Get("PurchaseOrderNumber"); diff --git a/src/Plugins/SmartStore.OfflinePayment/Description.txt b/src/Plugins/SmartStore.OfflinePayment/Description.txt index d2ed286f27..680ab03804 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Description.txt +++ b/src/Plugins/SmartStore.OfflinePayment/Description.txt @@ -2,8 +2,8 @@ Description: Contains common offline payment methods like Direct Debit, Invoice, Prepayment etc. Group: Payment SystemName: SmartStore.OfflinePayment -Version: 3.2.2 -MinAppVersion: 3.2.2 +Version: 4.2.0 +MinAppVersion: 4.2.0 DisplayOrder: 0 FileName: SmartStore.OfflinePayment.dll ResourceRootKey: Plugins.SmartStore.OfflinePayment diff --git a/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs index 11c1f5578e..9bab769070 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs @@ -1,61 +1,61 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using SmartStore.OfflinePayment.Settings; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; -using System.ComponentModel.DataAnnotations; namespace SmartStore.OfflinePayment.Models -{ - public abstract class ConfigurationModelBase : ModelBase +{ + public abstract class ConfigurationModelBase : ModelBase { - public string PrimaryStoreCurrencyCode { get; set; } + public string PrimaryStoreCurrencyCode { get; set; } - [AllowHtml] - [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.DescriptionText")] - public string DescriptionText { get; set; } + [AllowHtml] + [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.DescriptionText")] + public string DescriptionText { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] - public decimal AdditionalFee { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] + public decimal AdditionalFee { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] - public bool AdditionalFeePercentage { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] + public bool AdditionalFeePercentage { get; set; } - [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.PaymentMethodLogo")] - [UIHint("Media")] - public int PaymentMethodLogo { get; set; } - } + [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.PaymentMethodLogo")] + [UIHint("Media"), AdditionalMetadata("album", "content")] + public int PaymentMethodLogo { get; set; } + } - public class CashOnDeliveryConfigurationModel : ConfigurationModelBase - { - } + public class CashOnDeliveryConfigurationModel : ConfigurationModelBase + { + } - public class DirectDebitConfigurationModel : ConfigurationModelBase - { - } + public class DirectDebitConfigurationModel : ConfigurationModelBase + { + } - public class InvoiceConfigurationModel : ConfigurationModelBase - { - } + public class InvoiceConfigurationModel : ConfigurationModelBase + { + } - public class ManualConfigurationModel : ConfigurationModelBase - { - [SmartResourceDisplayName("Plugins.Payments.Manual.Fields.TransactMode")] - public TransactMode TransactMode { get; set; } - public List TransactModeValues { get; set; } + public class ManualConfigurationModel : ConfigurationModelBase + { + [SmartResourceDisplayName("Plugins.Payments.Manual.Fields.TransactMode")] + public TransactMode TransactMode { get; set; } + public List TransactModeValues { get; set; } - [SmartResourceDisplayName("Plugins.Payments.Manual.ExcludedCreditCards")] - public string[] ExcludedCreditCards { get; set; } - public List AvailableCreditCards { get; set; } - } + [SmartResourceDisplayName("Plugins.Payments.Manual.ExcludedCreditCards")] + public string[] ExcludedCreditCards { get; set; } + public List AvailableCreditCards { get; set; } + } - public class PayInStoreConfigurationModel : ConfigurationModelBase - { - } + public class PayInStoreConfigurationModel : ConfigurationModelBase + { + } - public class PrepaymentConfigurationModel : ConfigurationModelBase - { - } + public class PrepaymentConfigurationModel : ConfigurationModelBase + { + } public class PurchaseOrderNumberConfigurationModel : ConfigurationModelBase { diff --git a/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs b/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs index 965d4adaf6..b32748e19d 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs @@ -10,103 +10,103 @@ namespace SmartStore.OfflinePayment.Models { public abstract class PaymentInfoModelBase : ModelBase { - public string DescriptionText { get; set; } - public string ThumbnailUrl { get; set; } - } + public string DescriptionText { get; set; } + public string ThumbnailUrl { get; set; } + } + + public class CashOnDeliveryPaymentInfoModel : PaymentInfoModelBase + { + } - public class CashOnDeliveryPaymentInfoModel : PaymentInfoModelBase - { - } + public class DirectDebitPaymentInfoModel : PaymentInfoModelBase + { + public DirectDebitPaymentInfoModel() + { + EnterIBAN = "iban"; + } - public class DirectDebitPaymentInfoModel : PaymentInfoModelBase - { - public DirectDebitPaymentInfoModel() - { - EnterIBAN = "iban"; - } - - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.EnterIBAN")] - public string EnterIBAN { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.EnterIBAN")] + public string EnterIBAN { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitAccountHolder")] - public string DirectDebitAccountHolder { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitAccountHolder")] + public string DirectDebitAccountHolder { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitAccountNumber")] - public string DirectDebitAccountNumber { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitAccountNumber")] + public string DirectDebitAccountNumber { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitBankCode")] - public string DirectDebitBankCode { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitBankCode")] + public string DirectDebitBankCode { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitCountry")] - public string DirectDebitCountry { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitCountry")] + public string DirectDebitCountry { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitBankName")] - public string DirectDebitBankName { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitBankName")] + public string DirectDebitBankName { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitIban")] - public string DirectDebitIban { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitIban")] + public string DirectDebitIban { get; set; } - [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitBic")] - public string DirectDebitBic { get; set; } + [SmartResourceDisplayName("Plugins.Payments.DirectDebit.DirectDebitBic")] + public string DirectDebitBic { get; set; } - public List Countries { get; set; } - } + public List Countries { get; set; } + } - public class InvoicePaymentInfoModel : PaymentInfoModelBase - { - } + public class InvoicePaymentInfoModel : PaymentInfoModelBase + { + } - public class ManualPaymentInfoModel : PaymentInfoModelBase - { - public ManualPaymentInfoModel() + public class ManualPaymentInfoModel : PaymentInfoModelBase + { + public ManualPaymentInfoModel() { CreditCardTypes = new List(); ExpireMonths = new List(); ExpireYears = new List(); } - - [SmartResourceDisplayName("Payment.SelectCreditCard")] - [AllowHtml] - public string CreditCardType { get; set; } - [SmartResourceDisplayName("Payment.SelectCreditCard")] - public IList CreditCardTypes { get; set; } - - [SmartResourceDisplayName("Payment.CardholderName")] - [AllowHtml] - public string CardholderName { get; set; } - - [SmartResourceDisplayName("Payment.CardNumber")] - [AllowHtml] - public string CardNumber { get; set; } - - [SmartResourceDisplayName("Payment.ExpirationDate")] - [AllowHtml] - public string ExpireMonth { get; set; } - [SmartResourceDisplayName("Payment.ExpirationDate")] - [AllowHtml] - public string ExpireYear { get; set; } - public IList ExpireMonths { get; set; } - public IList ExpireYears { get; set; } - - [SmartResourceDisplayName("Payment.CardCode")] - [AllowHtml] - public string CardCode { get; set; } - } - - public class PayInStorePaymentInfoModel : PaymentInfoModelBase - { - } - - public class PrepaymentPaymentInfoModel : PaymentInfoModelBase - { - } + + [SmartResourceDisplayName("Payment.SelectCreditCard")] + [AllowHtml] + public string CreditCardType { get; set; } + [SmartResourceDisplayName("Payment.SelectCreditCard")] + public IList CreditCardTypes { get; set; } + + [SmartResourceDisplayName("Payment.CardholderName")] + [AllowHtml] + public string CardholderName { get; set; } + + [SmartResourceDisplayName("Payment.CardNumber")] + [AllowHtml] + public string CardNumber { get; set; } + + [SmartResourceDisplayName("Payment.ExpirationDate")] + [AllowHtml] + public string ExpireMonth { get; set; } + [SmartResourceDisplayName("Payment.ExpirationDate")] + [AllowHtml] + public string ExpireYear { get; set; } + public IList ExpireMonths { get; set; } + public IList ExpireYears { get; set; } + + [SmartResourceDisplayName("Payment.CardCode")] + [AllowHtml] + public string CardCode { get; set; } + } + + public class PayInStorePaymentInfoModel : PaymentInfoModelBase + { + } + + public class PrepaymentPaymentInfoModel : PaymentInfoModelBase + { + } public class PurchaseOrderNumberPaymentInfoModel : PaymentInfoModelBase - { + { [SmartResourceDisplayName("Plugins.Payments.PurchaseOrder.PurchaseOrderNumber")] [AllowHtml] public string PurchaseOrderNumber { get; set; } - } + } #region Validators diff --git a/src/Plugins/SmartStore.OfflinePayment/Plugin.cs b/src/Plugins/SmartStore.OfflinePayment/Plugin.cs index f496f22698..2006006001 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Plugin.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Plugin.cs @@ -1,86 +1,83 @@ -using System; using SmartStore.Core.Plugins; -using SmartStore.Services.Localization; -using SmartStore.Services.Configuration; using SmartStore.OfflinePayment.Settings; using SmartStore.Services; namespace SmartStore.OfflinePayment { - public partial class Plugin : BasePlugin + public partial class Plugin : BasePlugin { - private readonly ICommonServices _services; + private readonly ICommonServices _services; - public Plugin(ICommonServices services) + public Plugin(ICommonServices services) { - this._services = services; + this._services = services; } public override void Install() { - var settings = _services.Settings; - var loc = _services.Localization; - - // add settings - settings.SaveSetting(new CashOnDeliveryPaymentSettings - { - DescriptionText = "@Plugins.Payment.CashOnDelivery.PaymentInfoDescription" - }); - settings.SaveSetting(new InvoicePaymentSettings - { - DescriptionText = "@Plugins.Payment.Invoice.PaymentInfoDescription" - }); - settings.SaveSetting(new PayInStorePaymentSettings - { - DescriptionText = "@Plugins.Payment.PayInStore.PaymentInfoDescription" - }); - settings.SaveSetting(new PrepaymentPaymentSettings - { - DescriptionText = "@Plugins.Payment.Prepayment.PaymentInfoDescription" - }); - settings.SaveSetting(new ManualPaymentSettings - { + var settings = _services.Settings; + var loc = _services.Localization; + + // add settings + settings.SaveSetting(new CashOnDeliveryPaymentSettings + { + DescriptionText = "@Plugins.Payment.CashOnDelivery.PaymentInfoDescription" + }); + settings.SaveSetting(new InvoicePaymentSettings + { + DescriptionText = "@Plugins.Payment.Invoice.PaymentInfoDescription" + }); + settings.SaveSetting(new PayInStorePaymentSettings + { + DescriptionText = "@Plugins.Payment.PayInStore.PaymentInfoDescription" + }); + settings.SaveSetting(new PrepaymentPaymentSettings + { + DescriptionText = "@Plugins.Payment.Prepayment.PaymentInfoDescription" + }); + settings.SaveSetting(new ManualPaymentSettings + { DescriptionText = "@Plugins.Payments.Manual.PaymentInfoDescription", - TransactMode = TransactMode.Pending - }); - settings.SaveSetting(new DirectDebitPaymentSettings - { - DescriptionText = "@Plugins.Payments.DirectDebit.PaymentInfoDescription" - }); + TransactMode = TransactMode.Pending + }); + settings.SaveSetting(new DirectDebitPaymentSettings + { + DescriptionText = "@Plugins.Payments.DirectDebit.PaymentInfoDescription" + }); settings.SaveSetting(new PurchaseOrderNumberPaymentSettings { DescriptionText = "@Plugins.Payments.PurchaseOrderNumber.PaymentInfoDescription", TransactMode = TransactMode.Pending }); - // add resources - loc.ImportPluginResourcesFromXml(this.PluginDescriptor); - + // add resources + loc.ImportPluginResourcesFromXml(this.PluginDescriptor); + base.Install(); } public override void Uninstall() { - var settings = _services.Settings; - var loc = _services.Localization; + var settings = _services.Settings; + var loc = _services.Localization; - // delete settings - settings.DeleteSetting(); - settings.DeleteSetting(); - settings.DeleteSetting(); - settings.DeleteSetting(); - settings.DeleteSetting(); - settings.DeleteSetting(); + // delete settings + settings.DeleteSetting(); + settings.DeleteSetting(); + settings.DeleteSetting(); + settings.DeleteSetting(); + settings.DeleteSetting(); + settings.DeleteSetting(); - // delete resources - loc.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); - loc.DeleteLocaleStringResources("Plugins.Payment.CashOnDelivery"); - loc.DeleteLocaleStringResources("Plugins.Payment.Invoice"); - loc.DeleteLocaleStringResources("Plugins.Payment.PayInStore"); - loc.DeleteLocaleStringResources("Plugins.Payment.Prepayment"); - loc.DeleteLocaleStringResources("Plugins.Payments.Manual"); - loc.DeleteLocaleStringResources("Plugins.Payments.DirectDebit"); + // delete resources + loc.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); + loc.DeleteLocaleStringResources("Plugins.Payment.CashOnDelivery"); + loc.DeleteLocaleStringResources("Plugins.Payment.Invoice"); + loc.DeleteLocaleStringResources("Plugins.Payment.PayInStore"); + loc.DeleteLocaleStringResources("Plugins.Payment.Prepayment"); + loc.DeleteLocaleStringResources("Plugins.Payments.Manual"); + loc.DeleteLocaleStringResources("Plugins.Payments.DirectDebit"); base.Uninstall(); } diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/CashOnDeliveryProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/CashOnDeliveryProvider.cs index 696f1cbabd..624c71a941 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/CashOnDeliveryProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/CashOnDeliveryProvider.cs @@ -3,14 +3,14 @@ namespace SmartStore.OfflinePayment { - [SystemName("Payments.CashOnDelivery")] - [FriendlyName("Cash On Delivery (COD)")] - [DisplayOrder(1)] - public class CashOnDeliveryPaymentProcessor : OfflinePaymentProviderBase, IConfigurable - { - protected override string GetActionPrefix() - { - return "CashOnDelivery"; - } - } + [SystemName("Payments.CashOnDelivery")] + [FriendlyName("Cash On Delivery (COD)")] + [DisplayOrder(1)] + public class CashOnDeliveryPaymentProcessor : OfflinePaymentProviderBase, IConfigurable + { + protected override string GetActionPrefix() + { + return "CashOnDelivery"; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/DirectDebitProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/DirectDebitProvider.cs index 88cc051a90..b2b40cd31b 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/DirectDebitProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/DirectDebitProvider.cs @@ -5,30 +5,24 @@ namespace SmartStore.OfflinePayment { - [SystemName("Payments.DirectDebit")] - [FriendlyName("Direct Debit")] - [DisplayOrder(1)] - public class DirectDebitProvider : OfflinePaymentProviderBase, IConfigurable - { - public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = new ProcessPaymentResult(); - result.AllowStoringDirectDebit = true; - result.NewPaymentStatus = PaymentStatus.Pending; - return result; - } + [SystemName("Payments.DirectDebit")] + [FriendlyName("Direct Debit")] + [DisplayOrder(1)] + public class DirectDebitProvider : OfflinePaymentProviderBase, IConfigurable + { + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult(); + result.AllowStoringDirectDebit = true; + result.NewPaymentStatus = PaymentStatus.Pending; + return result; + } - protected override string GetActionPrefix() - { - return "DirectDebit"; - } + protected override string GetActionPrefix() + { + return "DirectDebit"; + } - public override bool RequiresInteraction - { - get - { - return true; - } - } - } + public override bool RequiresInteraction => true; + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/InvoiceProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/InvoiceProvider.cs index 3608124f58..79d2d44e62 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/InvoiceProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/InvoiceProvider.cs @@ -3,14 +3,14 @@ namespace SmartStore.OfflinePayment { - [SystemName("Payments.Invoice")] - [FriendlyName("Invoice")] - [DisplayOrder(1)] - public class InvoiceProvider : OfflinePaymentProviderBase, IConfigurable - { - protected override string GetActionPrefix() - { - return "Invoice"; - } - } + [SystemName("Payments.Invoice")] + [FriendlyName("Invoice")] + [DisplayOrder(1)] + public class InvoiceProvider : OfflinePaymentProviderBase, IConfigurable + { + protected override string GetActionPrefix() + { + return "Invoice"; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs index 4f4331c2eb..bfcb3744d3 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs @@ -7,103 +7,91 @@ namespace SmartStore.OfflinePayment { - [SystemName("Payments.Manual")] - [FriendlyName("Credit Card (manual)")] - [DisplayOrder(1)] - public class ManualProvider : OfflinePaymentProviderBase, IConfigurable - { - public static List CreditCardTypes - { - get - { - var creditCardTypes = new List - { - new SelectListItem { Text = "Visa", Value = "Visa" }, - new SelectListItem { Text = "Master Card", Value = "MasterCard" }, - new SelectListItem { Text = "Discover", Value = "Discover" }, - new SelectListItem { Text = "Amex", Value = "Amex" } - }; - return creditCardTypes; - } - } + [SystemName("Payments.Manual")] + [FriendlyName("Credit Card (manual)")] + [DisplayOrder(1)] + public class ManualProvider : OfflinePaymentProviderBase, IConfigurable + { + public static List CreditCardTypes + { + get + { + var creditCardTypes = new List + { + new SelectListItem { Text = "Visa", Value = "Visa" }, + new SelectListItem { Text = "Master Card", Value = "MasterCard" }, + new SelectListItem { Text = "Discover", Value = "Discover" }, + new SelectListItem { Text = "Amex", Value = "Amex" } + }; + return creditCardTypes; + } + } - public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = new ProcessPaymentResult(); - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult(); + var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); - result.AllowStoringCreditCardNumber = true; + result.AllowStoringCreditCardNumber = true; - switch (settings.TransactMode) - { - case TransactMode.Pending: - result.NewPaymentStatus = PaymentStatus.Pending; - break; - case TransactMode.Authorize: - result.NewPaymentStatus = PaymentStatus.Authorized; - break; - case TransactMode.Paid: - result.NewPaymentStatus = PaymentStatus.Paid; - break; - default: - result.AddError(T("Common.Payment.TranactionTypeNotSupported")); - return result; - } + switch (settings.TransactMode) + { + case TransactMode.Pending: + result.NewPaymentStatus = PaymentStatus.Pending; + break; + case TransactMode.Authorize: + result.NewPaymentStatus = PaymentStatus.Authorized; + break; + case TransactMode.Paid: + result.NewPaymentStatus = PaymentStatus.Paid; + break; + default: + result.AddError(T("Common.Payment.TranactionTypeNotSupported")); + return result; + } - return result; - } + return result; + } - public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = new ProcessPaymentResult(); - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult(); + var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); - result.AllowStoringCreditCardNumber = true; - switch (settings.TransactMode) - { - case TransactMode.Pending: - result.NewPaymentStatus = PaymentStatus.Pending; - break; - case TransactMode.Authorize: - result.NewPaymentStatus = PaymentStatus.Authorized; - break; - case TransactMode.Paid: - result.NewPaymentStatus = PaymentStatus.Paid; - break; - default: - { - result.AddError(T("Common.Payment.TranactionTypeNotSupported")); - return result; - } - } + result.AllowStoringCreditCardNumber = true; + switch (settings.TransactMode) + { + case TransactMode.Pending: + result.NewPaymentStatus = PaymentStatus.Pending; + break; + case TransactMode.Authorize: + result.NewPaymentStatus = PaymentStatus.Authorized; + break; + case TransactMode.Paid: + result.NewPaymentStatus = PaymentStatus.Paid; + break; + default: + { + result.AddError(T("Common.Payment.TranactionTypeNotSupported")); + return result; + } + } - return result; - } + return result; + } - public override CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurringPaymentRequest cancelPaymentRequest) - { - return new CancelRecurringPaymentResult(); - } + public override CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurringPaymentRequest cancelPaymentRequest) + { + return new CancelRecurringPaymentResult(); + } - public override RecurringPaymentType RecurringPaymentType - { - get - { - return RecurringPaymentType.Manual; - } - } + public override RecurringPaymentType RecurringPaymentType => RecurringPaymentType.Manual; - protected override string GetActionPrefix() - { - return "Manual"; - } + protected override string GetActionPrefix() + { + return "Manual"; + } - public override bool RequiresInteraction - { - get - { - return true; - } - } - } + public override bool RequiresInteraction => true; + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/OfflinePaymentProviderBase.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/OfflinePaymentProviderBase.cs index abe6b73027..d5ed1a3a59 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/OfflinePaymentProviderBase.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/OfflinePaymentProviderBase.cs @@ -12,61 +12,55 @@ namespace SmartStore.OfflinePayment { - public abstract class OfflinePaymentProviderBase : PaymentMethodBase - where TSetting : PaymentSettingsBase, ISettings, new() + public abstract class OfflinePaymentProviderBase : PaymentMethodBase + where TSetting : PaymentSettingsBase, ISettings, new() { - public ICommonServices CommonServices { get; set; } - public IOrderTotalCalculationService OrderTotalCalculationService { get; set; } + public ICommonServices CommonServices { get; set; } + public IOrderTotalCalculationService OrderTotalCalculationService { get; set; } - public override Type GetControllerType() - { - return typeof(OfflinePaymentController); - } + public override Type GetControllerType() + { + return typeof(OfflinePaymentController); + } - public override PaymentMethodType PaymentMethodType - { - get - { - return PaymentMethodType.Standard; - } - } + public override PaymentMethodType PaymentMethodType => PaymentMethodType.Standard; - protected abstract string GetActionPrefix(); + protected abstract string GetActionPrefix(); - public override decimal GetAdditionalHandlingFee(IList cart) - { - var result = decimal.Zero; - try - { - var settings = CommonServices.Settings.LoadSetting(CommonServices.StoreContext.CurrentStore.Id); + public override decimal GetAdditionalHandlingFee(IList cart) + { + var result = decimal.Zero; + try + { + var settings = CommonServices.Settings.LoadSetting(CommonServices.StoreContext.CurrentStore.Id); - result = this.CalculateAdditionalFee(OrderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); - } - catch (Exception) - { - } - return result; - } + result = this.CalculateAdditionalFee(OrderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); + } + catch (Exception) + { + } + return result; + } - public override void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) - { - actionName = "{0}Configure".FormatInvariant(GetActionPrefix()); - controllerName = "OfflinePayment"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.OfflinePayment" } }; - } + public override void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "{0}Configure".FormatInvariant(GetActionPrefix()); + controllerName = "OfflinePayment"; + routeValues = new RouteValueDictionary() { { "area", "SmartStore.OfflinePayment" } }; + } - public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) - { - actionName = "{0}PaymentInfo".FormatInvariant(GetActionPrefix()); - controllerName = "OfflinePayment"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.OfflinePayment" } }; - } + public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "{0}PaymentInfo".FormatInvariant(GetActionPrefix()); + controllerName = "OfflinePayment"; + routeValues = new RouteValueDictionary() { { "area", "SmartStore.OfflinePayment" } }; + } - public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = new ProcessPaymentResult(); - result.NewPaymentStatus = PaymentStatus.Pending; - return result; - } - } + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult(); + result.NewPaymentStatus = PaymentStatus.Pending; + return result; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/PayInStoreProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/PayInStoreProvider.cs index 6d61b37828..b955337637 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/PayInStoreProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/PayInStoreProvider.cs @@ -3,14 +3,14 @@ namespace SmartStore.OfflinePayment { - [SystemName("Payments.PayInStore")] - [FriendlyName("Pay In Store")] - [DisplayOrder(1)] - public class PayInStoreProvider : OfflinePaymentProviderBase, IConfigurable - { - protected override string GetActionPrefix() - { - return "PayInStore"; - } - } + [SystemName("Payments.PayInStore")] + [FriendlyName("Pay In Store")] + [DisplayOrder(1)] + public class PayInStoreProvider : OfflinePaymentProviderBase, IConfigurable + { + protected override string GetActionPrefix() + { + return "PayInStore"; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/PrepaymentStoreProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/PrepaymentStoreProvider.cs index 48b36849d4..9851200186 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/PrepaymentStoreProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/PrepaymentStoreProvider.cs @@ -3,14 +3,14 @@ namespace SmartStore.OfflinePayment { - [SystemName("Payments.Prepayment")] - [FriendlyName("Prepayment")] - [DisplayOrder(1)] - public class PrepaymentProvider : OfflinePaymentProviderBase, IConfigurable - { - protected override string GetActionPrefix() - { - return "Prepayment"; - } - } + [SystemName("Payments.Prepayment")] + [FriendlyName("Prepayment")] + [DisplayOrder(1)] + public class PrepaymentProvider : OfflinePaymentProviderBase, IConfigurable + { + protected override string GetActionPrefix() + { + return "Prepayment"; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs index fbbefb2044..0da5c3ec02 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs @@ -35,13 +35,7 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces return result; } - public override bool RequiresInteraction - { - get - { - return true; - } - } + public override bool RequiresInteraction => true; protected override string GetActionPrefix() { diff --git a/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs b/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs index 70a8e63318..2960e936a4 100644 --- a/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs @@ -8,19 +8,13 @@ public partial class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { - routes.MapRoute("SmartStore.OfflinePayment", - "Plugins/SmartStore.OfflinePayment/{action}", - new { controller = "OfflinePayment", action = "Index" }, - new[] { "SmartStore.OfflinePayment.Controllers" } + routes.MapRoute("SmartStore.OfflinePayment", + "Plugins/SmartStore.OfflinePayment/{action}", + new { controller = "OfflinePayment", action = "Index" }, + new[] { "SmartStore.OfflinePayment.Controllers" } ) - .DataTokens["area"] = "SmartStore.OfflinePayment"; - } - public int Priority - { - get - { - return 0; - } + .DataTokens["area"] = "SmartStore.OfflinePayment"; } + public int Priority => 0; } } diff --git a/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs b/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs index 89c409a2b8..34210ce565 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs @@ -1,65 +1,64 @@ -using System.Collections.Generic; -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.OfflinePayment.Settings { - public abstract class PaymentSettingsBase : ISettings - { - public string DescriptionText { get; set; } - public int ThumbnailPictureId { get; set; } - public decimal AdditionalFee { get; set; } - public bool AdditionalFeePercentage { get; set; } - } + public abstract class PaymentSettingsBase : ISettings + { + public string DescriptionText { get; set; } + public int ThumbnailPictureId { get; set; } + public decimal AdditionalFee { get; set; } + public bool AdditionalFeePercentage { get; set; } + } - public class CashOnDeliveryPaymentSettings : PaymentSettingsBase, ISettings - { - } + public class CashOnDeliveryPaymentSettings : PaymentSettingsBase, ISettings + { + } - public class DirectDebitPaymentSettings : PaymentSettingsBase, ISettings - { - } + public class DirectDebitPaymentSettings : PaymentSettingsBase, ISettings + { + } - public class InvoicePaymentSettings : PaymentSettingsBase, ISettings - { - } + public class InvoicePaymentSettings : PaymentSettingsBase, ISettings + { + } - public class ManualPaymentSettings : PaymentSettingsBase, ISettings - { - public TransactMode TransactMode { get; set; } - public string ExcludedCreditCards { get; set; } - } + public class ManualPaymentSettings : PaymentSettingsBase, ISettings + { + public TransactMode TransactMode { get; set; } + public string ExcludedCreditCards { get; set; } + } public class PurchaseOrderNumberPaymentSettings : PaymentSettingsBase, ISettings { public TransactMode TransactMode { get; set; } } - public class PayInStorePaymentSettings : PaymentSettingsBase, ISettings - { - } + public class PayInStorePaymentSettings : PaymentSettingsBase, ISettings + { + } - public class PrepaymentPaymentSettings : PaymentSettingsBase, ISettings - { - } + public class PrepaymentPaymentSettings : PaymentSettingsBase, ISettings + { + } - /// - /// Represents manual payment processor transaction mode - /// - public enum TransactMode - { - /// - /// Pending - /// - Pending = 0, + /// + /// Represents manual payment processor transaction mode + /// + public enum TransactMode + { + /// + /// Pending + /// + Pending = 0, - /// - /// Authorize - /// - Authorize = 1, + /// + /// Authorize + /// + Authorize = 1, - /// - /// Paid - /// - Paid = 2 - } + /// + /// Paid + /// + Paid = 2 + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj index a84ef07ca4..b392fe891f 100644 --- a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj +++ b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj @@ -20,7 +20,7 @@ Properties SmartStore.OfflinePayment SmartStore.OfflinePayment - v4.6.1 + v4.7.2 512 @@ -85,35 +85,45 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll False ..\..\packages\FluentValidation.7.4.0\lib\net45\FluentValidation.dll False - - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + False ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll - False + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + - + ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll False diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml index 685ad834f0..85d6a332b2 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml @@ -55,8 +55,8 @@ else @Html.ControlGroupFor(model => model.DirectDebitBankName, required: true, breakpoint: "md") -\n"; - } + var script = string.Empty; + if (buttonsSelector.HasValue()) + { + script = "\n"; + } - helper.SmartStore().Window().Name(modalId) - .Title(EngineContext.Current.Resolve().GetResource("Admin.Common.AreYouSure")) - .Content(helper.Partial("Delete", deleteConfirmationModel).ToHtmlString()) - .Show(false) - .Render(); + helper.SmartStore().Window().Name(modalId) + .Title(EngineContext.Current.Resolve().GetResource("Admin.Common.AreYouSure")) + .AddCssClass("modal-confirm-delete") + .BodyHtmlAttributes(new { @class = "pt-0" }) + .Content(helper.Partial("Delete", deleteConfirmationModel).ToHtmlString()) + .Show(false) + .Render(); return new MvcHtmlString(script); } - public static MvcHtmlString SmartLabel(this HtmlHelper helper, string expression, string labelText, string hint = null, object htmlAttributes = null) - { - var result = PooledStringBuilder.Rent(); + public static MvcHtmlString SmartLabel(this HtmlHelper helper, string expression, string labelText, string hint = null, object htmlAttributes = null) + { + var result = PooledStringBuilder.Rent(); - result.Append("
"); + result.Append("
"); if (expression.IsEmpty() && labelText.IsEmpty()) { @@ -177,95 +181,95 @@ public static MvcHtmlString SmartLabel(this HtmlHelper helper, string expression result.Append("
"); - return MvcHtmlString.Create(result.ToStringAndReturn()); - } + return MvcHtmlString.Create(result.ToStringAndReturn()); + } + + public static MvcHtmlString SmartLabelFor( + this HtmlHelper helper, + Expression> expression, + bool displayHint = true, + object htmlAttributes = null) + { + var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData); + metadata.AdditionalValues.TryGetValue("SmartResourceDisplayName", out object resourceDisplayName); + + return SmartLabelFor(helper, expression, resourceDisplayName as SmartResourceDisplayName, metadata, displayHint, htmlAttributes); + } public static MvcHtmlString SmartLabelFor( - this HtmlHelper helper, - Expression> expression, - bool displayHint = true, - object htmlAttributes = null) - { - var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData); - metadata.AdditionalValues.TryGetValue("SmartResourceDisplayName", out object resourceDisplayName); - - return SmartLabelFor(helper, expression, resourceDisplayName as SmartResourceDisplayName, metadata, displayHint, htmlAttributes); - } - - public static MvcHtmlString SmartLabelFor( - this HtmlHelper helper, - Expression> expression, - string resourceKey, - bool displayHint = true, - object htmlAttributes = null) - { - Guard.NotEmpty(resourceKey, nameof(resourceKey)); - - var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData); - var resourceDisplayName = new SmartResourceDisplayName(resourceKey, metadata.PropertyName); - - return SmartLabelFor(helper, expression, resourceDisplayName, metadata, displayHint, htmlAttributes); - } - - private static MvcHtmlString SmartLabelFor( - this HtmlHelper helper, - Expression> expression, - SmartResourceDisplayName resourceDisplayName, - ModelMetadata metadata, - bool displayHint = true, - object htmlAttributes = null) - { - var result = PooledStringBuilder.Rent(); - string labelText = null; - string hint = null; - - if (resourceDisplayName != null) - { - // resolve label display name - labelText = resourceDisplayName.DisplayName.NullEmpty(); - if (labelText == null) - { - // take reskey as absolute fallback - labelText = resourceDisplayName.ResourceKey; - } - - // resolve hint - if (displayHint) - { - var langId = EngineContext.Current.Resolve().WorkingLanguage.Id; - hint = EngineContext.Current.Resolve().GetResource(resourceDisplayName.ResourceKey + ".Hint", langId, false, "", true); - } - } - - if (labelText == null) - { - labelText = metadata.PropertyName.SplitPascalCase(); - } - - var labelAttrs = new RouteValueDictionary(htmlAttributes); - //labelAttrs.AppendCssClass("col-form-label"); - - var label = helper.LabelFor(expression, labelText, labelAttrs); - - if (displayHint) - { - result.Append("
"); - { - result.Append(label); - if (hint.HasValue()) - { - result.Append(helper.Hint(hint).ToHtmlString()); - } - } - result.Append("
"); - } - else - { - result.Append(label); - } - - return MvcHtmlString.Create(result.ToStringAndReturn()); - } + this HtmlHelper helper, + Expression> expression, + string resourceKey, + bool displayHint = true, + object htmlAttributes = null) + { + Guard.NotEmpty(resourceKey, nameof(resourceKey)); + + var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData); + var resourceDisplayName = new SmartResourceDisplayName(resourceKey, metadata.PropertyName); + + return SmartLabelFor(helper, expression, resourceDisplayName, metadata, displayHint, htmlAttributes); + } + + private static MvcHtmlString SmartLabelFor( + this HtmlHelper helper, + Expression> expression, + SmartResourceDisplayName resourceDisplayName, + ModelMetadata metadata, + bool displayHint = true, + object htmlAttributes = null) + { + var result = PooledStringBuilder.Rent(); + string labelText = null; + string hint = null; + + if (resourceDisplayName != null) + { + // resolve label display name + labelText = resourceDisplayName.DisplayName.NullEmpty(); + if (labelText == null) + { + // take reskey as absolute fallback + labelText = resourceDisplayName.ResourceKey; + } + + // resolve hint + if (displayHint) + { + var langId = EngineContext.Current.Resolve().WorkingLanguage.Id; + hint = EngineContext.Current.Resolve().GetResource(resourceDisplayName.ResourceKey + ".Hint", langId, false, "", true); + } + } + + if (labelText == null) + { + labelText = metadata.PropertyName.SplitPascalCase(); + } + + var labelAttrs = new RouteValueDictionary(htmlAttributes); + //labelAttrs.AppendCssClass("col-form-label"); + + var label = helper.LabelFor(expression, labelText, labelAttrs); + + if (displayHint) + { + result.Append("
"); + { + result.Append(label); + if (hint.HasValue()) + { + result.Append(helper.Hint(hint).ToHtmlString()); + } + } + result.Append("
"); + } + else + { + result.Append(label); + } + + return MvcHtmlString.Create(result.ToStringAndReturn()); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FieldNameFor(this HtmlHelper html, Expression> expression) @@ -300,19 +304,19 @@ public static MvcHtmlString DatePickerDropDowns(this HtmlHelper html, int? beginYear = null, int? endYear = null, int? selectedDay = null, int? selectedMonth = null, int? selectedYear = null, bool localizeLabels = true, bool disabled = false) { - var row = new TagBuilder("div"); - row.AddCssClass("row xs-gutters"); + var row = new TagBuilder("div"); + row.AddCssClass("row xs-gutters"); - var daysCol = new TagBuilder("div"); - daysCol.AddCssClass("col"); + var daysCol = new TagBuilder("div"); + daysCol.AddCssClass("col"); - var monthsCol = new TagBuilder("div"); - monthsCol.AddCssClass("col"); + var monthsCol = new TagBuilder("div"); + monthsCol.AddCssClass("col"); - var yearsCol = new TagBuilder("div"); - yearsCol.AddCssClass("col"); + var yearsCol = new TagBuilder("div"); + yearsCol.AddCssClass("col"); - var daysList = new TagBuilder("select"); + var daysList = new TagBuilder("select"); var monthsList = new TagBuilder("select"); var yearsList = new TagBuilder("select"); @@ -323,21 +327,25 @@ public static MvcHtmlString DatePickerDropDowns(this HtmlHelper html, daysList.Attributes.Add("name", dayName); monthsList.Attributes.Add("name", monthName); yearsList.Attributes.Add("name", yearName); - - daysList.Attributes.Add("class", "date-part form-control noskin"); - monthsList.Attributes.Add("class", "date-part form-control noskin"); - yearsList.Attributes.Add("class", "date-part form-control noskin"); - - daysList.Attributes.Add("data-minimum-results-for-search", "100"); - monthsList.Attributes.Add("data-minimum-results-for-search", "100"); - //yearsList.Attributes.Add("data-minimum-results-for-search", "100"); - - if (disabled) - { - daysList.Attributes.Add("disabled", "disabled"); - monthsList.Attributes.Add("disabled", "disabled"); - yearsList.Attributes.Add("disabled", "disabled"); - } + + daysList.Attributes.Add("id", dayName); + monthsList.Attributes.Add("id", monthName); + yearsList.Attributes.Add("id", yearName); + + daysList.Attributes.Add("class", "date-part form-control noskin remember"); + monthsList.Attributes.Add("class", "date-part form-control noskin remember"); + yearsList.Attributes.Add("class", "date-part form-control noskin remember"); + + daysList.Attributes.Add("data-minimum-results-for-search", "100"); + monthsList.Attributes.Add("data-minimum-results-for-search", "100"); + //yearsList.Attributes.Add("data-minimum-results-for-search", "100"); + + if (disabled) + { + daysList.Attributes.Add("disabled", "disabled"); + monthsList.Attributes.Add("disabled", "disabled"); + yearsList.Attributes.Add("disabled", "disabled"); + } var days = new StringBuilder(); var months = new StringBuilder(); @@ -368,7 +376,7 @@ public static MvcHtmlString DatePickerDropDowns(this HtmlHelper html, for (int i = 1; i <= 12; i++) { months.AppendFormat("", - i, + i, (selectedMonth.HasValue && selectedMonth.Value == i) ? " selected=\"selected\"" : null, CultureInfo.CurrentUICulture.DateTimeFormat.GetMonthName(i)); } @@ -389,13 +397,13 @@ public static MvcHtmlString DatePickerDropDowns(this HtmlHelper html, monthsList.InnerHtml = months.ToString(); yearsList.InnerHtml = years.ToString(); - daysCol.InnerHtml = daysList.ToString(); - monthsCol.InnerHtml = monthsList.ToString(); - yearsCol.InnerHtml = yearsList.ToString(); + daysCol.InnerHtml = daysList.ToString(); + monthsCol.InnerHtml = monthsList.ToString(); + yearsCol.InnerHtml = yearsList.ToString(); - row.InnerHtml = string.Concat(daysCol, monthsCol, yearsCol); + row.InnerHtml = string.Concat(daysCol, monthsCol, yearsCol); - return MvcHtmlString.Create(row.ToString()); + return MvcHtmlString.Create(row.ToString()); } #region DropDownList Extensions @@ -407,7 +415,6 @@ public static MvcHtmlString DropDownListForEnum( Expression> expression, string optionLabel = null) where TEnum : struct { - return htmlHelper.DropDownListForEnum(expression, null, optionLabel); } @@ -427,7 +434,7 @@ public static MvcHtmlString DropDownListForEnum( } public static MvcHtmlString DropDownListForEnum( - this HtmlHelper htmlHelper, + this HtmlHelper htmlHelper, Expression> expression, IDictionary htmlAttributes, string optionLabel = null) where TEnum : struct @@ -441,7 +448,7 @@ public static MvcHtmlString DropDownListForEnum( ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); Type enumType = metadata.ModelType.GetNonNullableType(); IEnumerable values = Enum.GetValues(enumType).Cast(); - + IEnumerable items = values.Select(value => new SelectListItem { @@ -463,119 +470,120 @@ public static MvcHtmlString DropDownListForEnum( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone) { - return helper.Widget(widgetZone, null); + return helper.Widget(widgetZone, null); } - public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone, object model) - { - var routeValues = GetWidgetsByZoneRouteValues(helper, widgetZone, model); - if (routeValues != null) - { - return helper.Action("WidgetsByZone", "Widget", routeValues); - } + public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone, object model) + { + var routeValues = GetWidgetsByZoneRouteValues(helper, widgetZone, model); + if (routeValues != null) + { + return helper.Action("WidgetsByZone", "Widget", routeValues); + } - return MvcHtmlString.Empty; - } + return MvcHtmlString.Empty; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void RenderWidget(this HtmlHelper helper, string widgetZone) - { - helper.RenderWidget(widgetZone, null); - } - - public static void RenderWidget(this HtmlHelper helper, string widgetZone, object model) - { - var routeValues = GetWidgetsByZoneRouteValues(helper, widgetZone, model); - if (routeValues != null) - { - helper.RenderAction("WidgetsByZone", "Widget", routeValues); - } - } - - private static object GetWidgetsByZoneRouteValues(HtmlHelper helper, string widgetZone, object model) - { - if (widgetZone.HasValue()) - { - model = model ?? helper.ViewData.Model; - var widgetSelector = EngineContext.Current.Resolve(); - var widgets = widgetSelector.GetWidgets(widgetZone, model).ToList(); - if (widgets.Any()) - { - var zoneModel = new WidgetZoneModel { Widgets = widgets, WidgetZone = widgetZone, Model = model }; - return new { zoneModel = zoneModel, model = model, area = "" }; - } - } - - return null; - } - - public static IHtmlString MetaAcceptLanguage(this HtmlHelper html) + { + helper.RenderWidget(widgetZone, null); + } + + public static void RenderWidget(this HtmlHelper helper, string widgetZone, object model) + { + var routeValues = GetWidgetsByZoneRouteValues(helper, widgetZone, model); + if (routeValues != null) + { + helper.RenderAction("WidgetsByZone", "Widget", routeValues); + } + } + + private static object GetWidgetsByZoneRouteValues(HtmlHelper helper, string widgetZone, object model) + { + if (widgetZone.HasValue()) + { + model = model ?? helper.ViewData.Model; + var widgetSelector = EngineContext.Current.Resolve(); + var widgets = widgetSelector.GetWidgets(widgetZone, model).ToList(); + if (widgets.Any()) + { + var zoneModel = new WidgetZoneModel { Widgets = widgets, WidgetZone = widgetZone, Model = model }; + return new { zoneModel = zoneModel, model = model, area = "" }; + } + } + + return null; + } + + public static IHtmlString MetaAcceptLanguage(this HtmlHelper html) { var acceptLang = HttpUtility.HtmlAttributeEncode(Thread.CurrentThread.CurrentUICulture.ToString()); return new MvcHtmlString(string.Format("", acceptLang)); } - public static IHtmlString LanguageAttributes(this HtmlHelper html, bool omitLTR = false) - { - return LanguageAttributes(html, EngineContext.Current.Resolve().WorkingLanguage, omitLTR); - } + public static IHtmlString LanguageAttributes(this HtmlHelper html, bool omitLTR = false) + { + return LanguageAttributes(html, EngineContext.Current.Resolve().WorkingLanguage, omitLTR); + } - public static IHtmlString LanguageAttributes(this HtmlHelper html, LocalizedValue localizedValue) - { - if (localizedValue == null || !localizedValue.BidiOverride) - { - return MvcHtmlString.Empty; - } + public static IHtmlString LanguageAttributes(this HtmlHelper html, LocalizedValue localizedValue) + { + if (localizedValue == null || !localizedValue.BidiOverride) + { + return MvcHtmlString.Empty; + } - return LanguageAttributes(html, localizedValue.CurrentLanguage, false); - } + return LanguageAttributes(html, localizedValue.CurrentLanguage, false); + } - public static IHtmlString LanguageAttributes(this HtmlHelper html, Language currentLanguage, bool omitLTR = false) - { - Guard.NotNull(currentLanguage, nameof(currentLanguage)); + public static IHtmlString LanguageAttributes(this HtmlHelper html, Language currentLanguage, bool omitLTR = false) + { + Guard.NotNull(currentLanguage, nameof(currentLanguage)); - var code = currentLanguage.GetTwoLetterISOLanguageName(); - var rtl = currentLanguage.Rtl; + var code = currentLanguage.GetTwoLetterISOLanguageName(); + var rtl = currentLanguage.Rtl; - var result = "lang=\"" + code + "\""; - if (rtl || !omitLTR) - { - result += " dir=\"" + (rtl ? "rtl" : "ltr") + "\""; - } + var result = "lang=\"" + code + "\""; + if (rtl || !omitLTR) + { + result += " dir=\"" + (rtl ? "rtl" : "ltr") + "\""; + } - return new MvcHtmlString(result); - } + return new MvcHtmlString(result); + } - public static IHtmlString LanguageAttributes(this HtmlHelper html, bool currentRtl, Language pageLanguage) - { - Guard.NotNull(pageLanguage, nameof(pageLanguage)); + public static IHtmlString LanguageAttributes(this HtmlHelper html, bool currentRtl, Language pageLanguage) + { + Guard.NotNull(pageLanguage, nameof(pageLanguage)); - if (currentRtl == pageLanguage.Rtl) - { - return MvcHtmlString.Empty; - } + if (currentRtl == pageLanguage.Rtl) + { + return MvcHtmlString.Empty; + } - var result = "dir=\"" + (currentRtl ? "rtl" : "ltr") + "\""; - return new MvcHtmlString(result); - } + var result = "dir=\"" + (currentRtl ? "rtl" : "ltr") + "\""; + return new MvcHtmlString(result); + } - public static MvcHtmlString ControlGroupFor( - this HtmlHelper html, - Expression> expression, + public static MvcHtmlString ControlGroupFor( + this HtmlHelper html, + Expression> expression, InputEditorType editorType = InputEditorType.TextBox, bool required = false, string helpHint = null, - string breakpoint = "md") + string breakpoint = "md", + string additionalCtrlClasses = "") { if (editorType == InputEditorType.Hidden) { return html.HiddenFor(expression); } - string inputHtml = ""; - var htmlAttributes = new RouteValueDictionary(); - var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); - var dataTypeName = metadata.DataTypeName.EmptyNull(); + string inputHtml = ""; + var htmlAttributes = new RouteValueDictionary(); + var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); + var dataTypeName = metadata.DataTypeName.EmptyNull(); var groupClass = "form-group row"; var labelClass = "col-{0}-3 col-form-label".FormatInvariant(breakpoint.NullEmpty() ?? "md"); var controlsClass = "col-{0}-9".FormatInvariant(breakpoint.NullEmpty() ?? "md"); @@ -592,29 +600,29 @@ public static MvcHtmlString ControlGroupFor( if (!required && (editorType == InputEditorType.TextBox || editorType == InputEditorType.Password)) { - htmlAttributes.Add("placeholder", EngineContext.Current.Resolve().GetResource("Common.Optional")); - } - - switch (dataTypeName) - { - case "EmailAddress": - htmlAttributes.Add("type", "email"); - break; - case "PhoneNumber": - htmlAttributes.Add("type", "tel"); - break; - } - - htmlAttributes.Add("class", "form-control"); - + htmlAttributes.Add("placeholder", EngineContext.Current.Resolve().GetResource("Common.Optional")); + } + + switch (dataTypeName) + { + case "EmailAddress": + htmlAttributes.Add("type", "email"); + break; + case "PhoneNumber": + htmlAttributes.Add("type", "tel"); + break; + } + + htmlAttributes.Add("class", "form-control" + (additionalCtrlClasses.HasValue() ? " " + additionalCtrlClasses : "")); + switch (editorType) { case InputEditorType.Checkbox: - CommonHelper.TryConvert(metadata.Model, out var isChecked); + CommonHelper.TryConvert(metadata.Model, out var isChecked); inputHtml = string.Format("
{0}
", html.CheckBox(ExpressionHelper.GetExpressionText(expression), isChecked, new { @class = "form-check-input" }).ToString(), - html.IdFor(expression), - metadata.DisplayName); + html.IdFor(expression), + metadata.DisplayName); break; case InputEditorType.Password: inputHtml = html.PasswordFor(expression, htmlAttributes).ToString(); @@ -623,7 +631,7 @@ public static MvcHtmlString ControlGroupFor( inputHtml = html.TextBoxFor(expression, htmlAttributes).ToString(); break; } - + sb.AppendLine(inputHtml); sb.AppendLine(html.ValidationMessageFor(expression).ToString()); if (helpHint.HasValue()) @@ -639,16 +647,16 @@ public static MvcHtmlString ControlGroupFor( [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MvcHtmlString ColorBox(this HtmlHelper html, string name, string color) - { - return ColorBox(html, name, color, null); - } + { + return ColorBox(html, name, color, null); + } public static MvcHtmlString ColorBox(this HtmlHelper html, string name, string color, string defaultColor) { - var sb = PooledStringBuilder.Rent(); + var sb = PooledStringBuilder.Rent(); - defaultColor = defaultColor.EmptyNull(); - var isDefault = color.IsCaseInsensitiveEqual(defaultColor); + defaultColor = defaultColor.EmptyNull(); + var isDefault = color.IsCaseInsensitiveEqual(defaultColor); sb.Append("
".FormatInvariant(defaultColor)); @@ -660,268 +668,350 @@ public static MvcHtmlString ColorBox(this HtmlHelper html, string name, string c return MvcHtmlString.Create(sb.ToStringAndReturn()); } - public static MvcHtmlString TableFormattedVariantAttributes(this HtmlHelper helper, string formattedVariantAttributes, string separatorLines = "
", string separatorValues = ": ") { - var sb = new StringBuilder(); - string name, value; - string[] lines = formattedVariantAttributes.SplitSafe(separatorLines); - - if (lines.Length <= 0) - return MvcHtmlString.Empty; - - sb.Append(""); - - foreach (string line in lines) { - sb.Append(""); - if (line.SplitToPair(out name, out value, separatorValues)) { - sb.AppendFormat("", name); - sb.AppendFormat("", value); - } - else { - sb.AppendFormat("", line); - } - sb.Append(""); - } - - sb.Append("
{0}:{0}{0}
"); - return MvcHtmlString.Create(sb.ToString()); - } - - public static MvcHtmlString SettingEditorFor( - this HtmlHelper helper, - Expression> expression, - string parentSelector = null, - object additionalViewData = null) - { - return SettingEditorFor( - helper, - expression, - helper.EditorFor(expression, additionalViewData), - parentSelector); - } - - public static MvcHtmlString SettingEditorFor( - this HtmlHelper helper, - Expression> expression, - Func editor, - string parentSelector = null) - { - return SettingEditorFor( - helper, - expression, - new MvcHtmlString(editor(helper.ViewData.Model).ToHtmlString()), - parentSelector); - } - - public static MvcHtmlString EnumSettingEditorFor( - this HtmlHelper helper, - Expression> expression, - string parentSelector = null, - object htmlAttributes = null, - string optionLabel = null) where TValue : struct - { - return SettingEditorFor( - helper, - expression, - helper.DropDownListForEnum(expression, htmlAttributes, optionLabel), - parentSelector); - } - - public static MvcHtmlString SettingEditorFor( - this HtmlHelper helper, - Expression> expression, - MvcHtmlString editor, - string parentSelector = null) - { - Guard.NotNull(expression, nameof(expression)); - Guard.NotNull(editor, nameof(editor)); - - var data = helper.ViewData[StoreDependingSettingHelper.ViewDataKey] as StoreDependingSettingData; - if (data == null || data.ActiveStoreScopeConfiguration <= 0) - return editor; // CONTROL - - var sb = PooledStringBuilder.Rent("
"); - sb.Append("
"); - sb.Append(helper.SettingOverrideCheckboxInternal(expression, data, parentSelector)); // CHECK - sb.Append("
"); - sb.Append("
"); - sb.Append(editor.ToHtmlString()); // CONTROL - sb.Append("
"); - - return MvcHtmlString.Create(sb.ToStringAndReturn()); - } - - private static MvcHtmlString SettingOverrideCheckboxInternal( - this HtmlHelper helper, - Expression> expression, - StoreDependingSettingData data, - string parentSelector = null) - { - var fieldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; - var settingKey = ExpressionHelper.GetExpressionText(expression); - var localizeService = EngineContext.Current.Resolve(); - - if (fieldPrefix.HasValue()) - settingKey = string.Concat(fieldPrefix, ".", settingKey); - else if (!settingKey.Contains(".")) - settingKey = string.Concat(data.RootSettingClass, ".", settingKey); - - var overrideForStore = (data.OverrideSettingKeys.FirstOrDefault(x => x.IsCaseInsensitiveEqual(settingKey)) != null); - var fieldId = settingKey + (settingKey.EndsWith("_OverrideForStore") ? "" : "_OverrideForStore"); - - var sb = PooledStringBuilder.Rent(); - sb.Append(""); - // Controls are not floating, so line-break prevents different distances between them. - sb.Append("\r\n"); - - return MvcHtmlString.Create(sb.ToStringAndReturn()); - } - - public static MvcHtmlString CollapsedText(this HtmlHelper helper, string text) - { - if (text.IsEmpty()) - return MvcHtmlString.Empty; - - var catalogSettings = EngineContext.Current.Resolve(); - - if (!catalogSettings.EnableHtmlTextCollapser) - return MvcHtmlString.Create(text); - - string result = "
{1}
".FormatWith( - catalogSettings.HtmlTextCollapsedHeight, text - ); - - return MvcHtmlString.Create(result); - } + public static MvcHtmlString TableFormattedVariantAttributes(this HtmlHelper helper, string formattedVariantAttributes, string separatorLines = "
", string separatorValues = ": ") + { + var sb = new StringBuilder(); + string name, value; + string[] lines = formattedVariantAttributes.SplitSafe(separatorLines); + + if (lines.Length <= 0) + return MvcHtmlString.Empty; + + sb.Append(""); + + foreach (string line in lines) + { + sb.Append(""); + if (line.SplitToPair(out name, out value, separatorValues)) + { + sb.AppendFormat("", name); + sb.AppendFormat("", value); + } + else + { + sb.AppendFormat("", line); + } + sb.Append(""); + } + + sb.Append("
{0}:{0}{0}
"); + return MvcHtmlString.Create(sb.ToString()); + } + + public static MvcHtmlString SettingEditorFor( + this HtmlHelper helper, + Expression> expression, + string parentSelector = null, + object additionalViewData = null) + { + return SettingEditorFor( + helper, + expression, + helper.EditorFor(expression, additionalViewData), + parentSelector); + } + + public static MvcHtmlString SettingEditorFor( + this HtmlHelper helper, + Expression> expression, + Func editor, + string parentSelector = null) + { + return SettingEditorFor( + helper, + expression, + new MvcHtmlString(editor(helper.ViewData.Model).ToHtmlString()), + parentSelector); + } + + public static MvcHtmlString EnumSettingEditorFor( + this HtmlHelper helper, + Expression> expression, + string parentSelector = null, + object htmlAttributes = null, + string optionLabel = null) where TValue : struct + { + return SettingEditorFor( + helper, + expression, + helper.DropDownListForEnum(expression, htmlAttributes, optionLabel), + parentSelector); + } + + public static MvcHtmlString SettingEditorFor( + this HtmlHelper helper, + Expression> expression, + MvcHtmlString editor, + string parentSelector = null) + { + Guard.NotNull(expression, nameof(expression)); + Guard.NotNull(editor, nameof(editor)); + + var data = helper.ViewData[StoreDependingSettingHelper.ViewDataKey] as StoreDependingSettingData; + if (data == null || data.ActiveStoreScopeConfiguration <= 0) + return editor; // CONTROL + + var sb = PooledStringBuilder.Rent("
"); + sb.Append("
"); + sb.Append(SettingOverrideCheckboxInternal(helper, expression, data, parentSelector)); // CHECK + sb.Append("
"); + sb.Append("
"); + sb.Append(editor.ToHtmlString()); // CONTROL + sb.Append("
"); + + return MvcHtmlString.Create(sb.ToStringAndReturn()); + } + + private static MvcHtmlString SettingOverrideCheckboxInternal( + HtmlHelper helper, + Expression> expression, + StoreDependingSettingData data, + string parentSelector = null) + { + var fieldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; + var settingKey = ExpressionHelper.GetExpressionText(expression); + var localizeService = EngineContext.Current.Resolve(); + + if (fieldPrefix.HasValue()) + { + settingKey = fieldPrefix + "." + settingKey; + } + else if (data.RootSettingClass.HasValue() && !settingKey.StartsWith(data.RootSettingClass + ".", StringComparison.OrdinalIgnoreCase)) + { + settingKey = data.RootSettingClass + "." + settingKey; + } + + var overrideForStore = data.OverrideSettingKeys.Contains(settingKey); + var fieldId = settingKey.EnsureEndsWith("_OverrideForStore"); + + var sb = PooledStringBuilder.Rent(); + sb.Append(""); + // Controls are not floating, so line-break prevents different distances between them. + sb.Append("\r\n"); + + return MvcHtmlString.Create(sb.ToStringAndReturn()); + } + + public static MvcHtmlString CollapsedText(this HtmlHelper helper, string text) + { + if (text.IsEmpty()) + return MvcHtmlString.Empty; + + var catalogSettings = EngineContext.Current.Resolve(); + + if (!catalogSettings.EnableHtmlTextCollapser) + return MvcHtmlString.Create(text); + + string result = "
{1}
".FormatWith( + catalogSettings.HtmlTextCollapsedHeight, text + ); + + return MvcHtmlString.Create(result); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string fileExtension, bool renderLabel = false) - { - return IconForFileExtension(helper, fileExtension, null, renderLabel); - } - - public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string fileExtension, string extraCssClasses = null, bool renderLabel = false) - { - Guard.NotNull(helper, nameof(helper)); - Guard.NotEmpty(fileExtension, nameof(fileExtension)); - - var icon = "far fa-file"; - var ext = fileExtension; - - if (ext != null && ext.StartsWith(".")) - { - ext = ext.Substring(1); - } - - if (ext.HasValue()) - { - switch (ext.ToLowerInvariant()) - { - case "pdf": - icon = "far fa-file-pdf"; - break; - case "doc": - case "docx": - case "docm": - case "odt": - case "dot": - case "dotx": - case "dotm": - icon = "far fa-file-word"; - break; - case "xls": - case "xlsx": - case "xlsm": - case "xlsb": - case "ods": - icon = "far fa-file-excel"; - break; - case "csv": - case "tab": - icon = "fa fa-file-csv"; - break; - case "ppt": - case "pptx": - case "pptm": - case "ppsx": - case "odp": - case "potx": - case "pot": - case "potm": - case "pps": - case "ppsm": - icon = "far fa-file-powerpoint"; - break; - case "zip": - case "rar": - case "7z": - icon = "far fa-file-archive"; - break; - case "png": - case "jpg": - case "jpeg": - case "bmp": - case "psd": - icon = "far fa-file-image"; - break; - case "mp3": - case "wav": - case "ogg": - case "wma": - icon = "far fa-file-audio"; - break; - case "mp4": - case "mkv": - case "wmv": - case "avi": - case "asf": - case "mpg": - case "mpeg": - icon = "far fa-file-video"; - break; - case "txt": - icon = "far fa-file-alt"; - break; - case "exe": - icon = "fa fa-cog"; - break; - case "xml": - case "html": - case "htm": - icon = "far fa-file-code"; - break; - } - } - - var label = ext.NaIfEmpty().ToUpper(); - - var result = "".FormatInvariant( - icon, - extraCssClasses.HasValue() ? " " + extraCssClasses : "", - label); - - if (renderLabel) - { - if (ext.IsEmpty()) - { - result = "{0}".FormatInvariant("".NaIfEmpty()); - } - else - { - result = result + "{0}".FormatInvariant(label); - } - } - - return MvcHtmlString.Create(result); - } - } + { + return IconForFileExtension(helper, fileExtension, null, renderLabel); + } + + public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string fileExtension, string extraCssClasses = null, bool renderLabel = false) + { + Guard.NotNull(helper, nameof(helper)); + Guard.NotEmpty(fileExtension, nameof(fileExtension)); + + var icon = "far fa-file"; + var ext = fileExtension; + + if (ext != null && ext.StartsWith(".")) + { + ext = ext.Substring(1); + } + + if (ext.HasValue()) + { + switch (ext.ToLowerInvariant()) + { + case "pdf": + icon = "far fa-file-pdf"; + break; + case "doc": + case "docx": + case "docm": + case "odt": + case "dot": + case "dotx": + case "dotm": + icon = "far fa-file-word"; + break; + case "xls": + case "xlsx": + case "xlsm": + case "xlsb": + case "ods": + icon = "far fa-file-excel"; + break; + case "csv": + case "tab": + icon = "fa fa-file-csv"; + break; + case "ppt": + case "pptx": + case "pptm": + case "ppsx": + case "odp": + case "potx": + case "pot": + case "potm": + case "pps": + case "ppsm": + icon = "far fa-file-powerpoint"; + break; + case "zip": + case "rar": + case "7z": + icon = "far fa-file-archive"; + break; + case "png": + case "jpg": + case "jpeg": + case "bmp": + case "psd": + icon = "far fa-file-image"; + break; + case "mp3": + case "wav": + case "ogg": + case "wma": + icon = "far fa-file-audio"; + break; + case "mp4": + case "mkv": + case "wmv": + case "avi": + case "asf": + case "mpg": + case "mpeg": + icon = "far fa-file-video"; + break; + case "txt": + icon = "far fa-file-alt"; + break; + case "exe": + icon = "fa fa-cog"; + break; + case "xml": + case "html": + case "htm": + icon = "far fa-file-code"; + break; + } + } + + var label = ext.NaIfEmpty().ToUpper(); + + var result = "".FormatInvariant( + icon, + extraCssClasses.HasValue() ? " " + extraCssClasses : "", + label); + + if (renderLabel) + { + if (ext.IsEmpty()) + { + result = "{0}".FormatInvariant("".NaIfEmpty()); + } + else + { + result = result + "{0}".FormatInvariant(label); + } + } + + return MvcHtmlString.Create(result); + } + + #region Media + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MvcHtmlString MediaViewer(this HtmlHelper helper, MediaFileInfo file, object htmlAttributes = null) + { + return MediaInternal(helper, file, true, 0, CommonHelper.ObjectToDictionary(htmlAttributes)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MvcHtmlString MediaViewer(this HtmlHelper helper, MediaFileInfo file, IDictionary htmlAttributes) + { + return MediaInternal(helper, file, true, 0, htmlAttributes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MvcHtmlString MediaThumbnail(this HtmlHelper helper, MediaFileInfo file, int size, object htmlAttributes = null) + { + return MediaInternal(helper, file, false, size, CommonHelper.ObjectToDictionary(htmlAttributes)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MvcHtmlString MediaThumbnail(this HtmlHelper helper, MediaFileInfo file, int size, IDictionary htmlAttributes) + { + return MediaInternal(helper, file, false, size, htmlAttributes); + } + + private static MvcHtmlString MediaInternal( + HtmlHelper helper, + MediaFileInfo file, + bool renderViewer, + int size, + IDictionary htmlAttributes) + { + if (file?.File == null) + { + return MvcHtmlString.Empty; + } + + var mediaType = file.MediaType.NullEmpty() ?? "image"; + + // Validate size parameter. + if (mediaType != "image" && !renderViewer) + { + Guard.IsPositive(size, nameof(size), $"The size must be greater than 0 to get a thumbnail for type '{mediaType.NaIfEmpty()}'."); + } + + var f = file?.File; + + var model = new MediaTemplateModel(file, renderViewer) + { + ThumbSize = size, + HtmlAttributes = htmlAttributes + }; + + if (htmlAttributes.TryGetValue("alt", out var alt)) + { + htmlAttributes.Remove("alt"); + } + + if (htmlAttributes.TryGetValue("title", out var title)) + { + htmlAttributes.Remove("title"); + } + + //alt != null ? (string)alt : f?.GetLocalized(x => x.Alt).Value + + model.Alt = (string)(alt ?? f?.GetLocalized(x => x.Alt).Value); + model.Title = (string)(title ?? f?.GetLocalized(x => x.Title).Value); + + return helper.Partial("MediaTemplates/" + mediaType, model); + } + + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlPrefixScopeExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlPrefixScopeExtensions.cs index 30f54829bf..86ddff19a9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlPrefixScopeExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlPrefixScopeExtensions.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Web.Mvc; using System.Web; -using System.Collections.Generic; +using System.Web.Mvc; using SmartStore.Utilities; namespace SmartStore.Web.Framework @@ -52,7 +52,7 @@ private static Queue GetIdsToReuse(HttpContextBase httpContext, string c foreach (var previouslyUsedId in previouslyUsedIds.Split(',')) { queue.Enqueue(previouslyUsedId); - } + } } } return queue; diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs index 6644ac91bc..6d042beca7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs @@ -13,143 +13,143 @@ namespace SmartStore.Web.Framework { - public static class HtmlSelectListExtensions - { - public static SelectList ToSelectList(this TEnum enumObj, bool markCurrentAsSelected = true) where TEnum : struct - { - if (!typeof(TEnum).IsEnum) - throw new ArgumentException("An Enumeration type is required.", "enumObj"); - - var localizationService = EngineContext.Current.Resolve(); - var workContext = EngineContext.Current.Resolve(); - - var values = from TEnum enumValue in Enum.GetValues(typeof(TEnum)) - select new { ID = Convert.ToInt32(enumValue), Name = enumValue.GetLocalizedEnum(localizationService, workContext) }; - - object selectedValue = null; - if (markCurrentAsSelected) - selectedValue = Convert.ToInt32(enumObj); - - return new SelectList(values, "ID", "Name", selectedValue); - } - - /// - /// Get a select list of all stores - /// - public static IList ToSelectListItems(this IEnumerable stores, params int[] selectedStoreIds) - { - var list = new List(); - - foreach (var store in stores) - { - list.Add(new SelectListItem - { - Text = store.Name, - Value = store.Id.ToString(), - Selected = selectedStoreIds != null && selectedStoreIds.Contains(store.Id) - }); - } - - return list; - } - - /// - /// Get a select list of all customer roles - /// - public static IList ToSelectListItems(this IEnumerable roles, params int[] selectedCustomerRoleIds) - { - var list = new List(); - - foreach (var role in roles) - { - list.Add(new SelectListItem - { - Text = role.Name, - Value = role.Id.ToString(), - Selected = selectedCustomerRoleIds != null && selectedCustomerRoleIds.Contains(role.Id) - }); - } - - return list; - } - - /// - /// Get a select list of all payment methods. - /// - /// Payment providers. - /// Plugin mediator. - /// System name of selected methods. - /// List of select items - public static IList ToSelectListItems( - this IEnumerable> paymentProviders, - PluginMediator pluginMediator, - params string[] selectedMethods) - { - var list = new List(); - - foreach (var provider in paymentProviders) - { - var systemName = provider.Metadata.SystemName; - var name = pluginMediator.GetLocalizedFriendlyName(provider.Metadata); - - if (name.IsEmpty()) - { - name = provider.Metadata.FriendlyName; - } - - if (name.IsEmpty()) - { - name = provider.Metadata.SystemName; - } - - var item = new ExtendedSelectListItem - { - Text = name.NaIfEmpty(), - Value = systemName, - Selected = selectedMethods != null && selectedMethods.Contains(systemName) - }; - - item.CustomProperties.Add("hint", systemName.Replace("SmartStore.", "").Replace("Payments.", "")); - - list.Add(item); - } - - return list.OrderBy(x => x.Text).ToList(); - } - - public static void SelectValue(this List lst, string value, string defaultValue = null) - { - if (lst != null) - { - var itm = lst.FirstOrDefault(i => i.Value.IsCaseInsensitiveEqual(value)); - - if (itm == null && defaultValue != null) - itm = lst.FirstOrDefault(i => i.Value.IsCaseInsensitiveEqual(defaultValue)); - - if (itm != null) - itm.Selected = true; - } - } - } - - - public partial class ExtendedSelectListItem : SelectListItem - { - public ExtendedSelectListItem() - { - CustomProperties = new Dictionary(); - } - - public Dictionary CustomProperties { get; set; } - - public TProperty Get(string key, TProperty defaultValue = default(TProperty)) - { - if (CustomProperties.TryGetValue(key, out object value)) - { - return (TProperty)value; - } - - return defaultValue; - } - } + public static class HtmlSelectListExtensions + { + public static SelectList ToSelectList(this TEnum enumObj, bool markCurrentAsSelected = true) where TEnum : struct + { + if (!typeof(TEnum).IsEnum) + throw new ArgumentException("An Enumeration type is required.", "enumObj"); + + var localizationService = EngineContext.Current.Resolve(); + var workContext = EngineContext.Current.Resolve(); + + var values = from TEnum enumValue in Enum.GetValues(typeof(TEnum)) + select new { ID = Convert.ToInt32(enumValue), Name = enumValue.GetLocalizedEnum(localizationService, workContext) }; + + object selectedValue = null; + if (markCurrentAsSelected) + selectedValue = Convert.ToInt32(enumObj); + + return new SelectList(values, "ID", "Name", selectedValue); + } + + /// + /// Get a select list of all stores + /// + public static IList ToSelectListItems(this IEnumerable stores, params int[] selectedStoreIds) + { + var list = new List(); + + foreach (var store in stores) + { + list.Add(new SelectListItem + { + Text = store.Name, + Value = store.Id.ToString(), + Selected = selectedStoreIds != null && selectedStoreIds.Contains(store.Id) + }); + } + + return list; + } + + /// + /// Get a select list of all customer roles + /// + public static IList ToSelectListItems(this IEnumerable roles, params int[] selectedCustomerRoleIds) + { + var list = new List(); + + foreach (var role in roles) + { + list.Add(new SelectListItem + { + Text = role.Name, + Value = role.Id.ToString(), + Selected = selectedCustomerRoleIds != null && selectedCustomerRoleIds.Contains(role.Id) + }); + } + + return list; + } + + /// + /// Get a select list of all payment methods. + /// + /// Payment providers. + /// Plugin mediator. + /// System name of selected methods. + /// List of select items + public static IList ToSelectListItems( + this IEnumerable> paymentProviders, + PluginMediator pluginMediator, + params string[] selectedMethods) + { + var list = new List(); + + foreach (var provider in paymentProviders) + { + var systemName = provider.Metadata.SystemName; + var name = pluginMediator.GetLocalizedFriendlyName(provider.Metadata); + + if (name.IsEmpty()) + { + name = provider.Metadata.FriendlyName; + } + + if (name.IsEmpty()) + { + name = provider.Metadata.SystemName; + } + + var item = new ExtendedSelectListItem + { + Text = name.NaIfEmpty(), + Value = systemName, + Selected = selectedMethods != null && selectedMethods.Contains(systemName) + }; + + item.CustomProperties.Add("hint", systemName.Replace("SmartStore.", "").Replace("Payments.", "")); + + list.Add(item); + } + + return list.OrderBy(x => x.Text).ToList(); + } + + public static void SelectValue(this List lst, string value, string defaultValue = null) + { + if (lst != null) + { + var itm = lst.FirstOrDefault(i => i.Value.IsCaseInsensitiveEqual(value)); + + if (itm == null && defaultValue != null) + itm = lst.FirstOrDefault(i => i.Value.IsCaseInsensitiveEqual(defaultValue)); + + if (itm != null) + itm.Selected = true; + } + } + } + + + public partial class ExtendedSelectListItem : SelectListItem + { + public ExtendedSelectListItem() + { + CustomProperties = new Dictionary(); + } + + public Dictionary CustomProperties { get; set; } + + public TProperty Get(string key, TProperty defaultValue = default(TProperty)) + { + if (CustomProperties.TryGetValue(key, out object value)) + { + return (TProperty)value; + } + + return defaultValue; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs index fd9b6f3717..9677aa41a9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs @@ -1,147 +1,139 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Web; using System.Web.Mvc; using SmartStore.Collections; -using SmartStore.Utilities; namespace SmartStore.Web.Framework { - public enum ZoneInjectMode - { - Append, - Prepend, - Replace - } - - public static class HtmlZoneExtensions - { - private class DocumentZone : IDisposable - { - internal const string ItemsKey = "DocumentTail.Snippets"; - private const string UniqueKeysKey = "DocumentTail.UniqueKeys"; - - private readonly ViewContext _viewContext; - private readonly TextWriter _originalViewContextWriter; - private readonly WebViewPage _page; - private readonly string _targetZone; - private readonly ZoneInjectMode _injectMode; - private readonly bool _isVoid; - - public DocumentZone(HtmlHelper html, string targetZone, ZoneInjectMode injectMode, string key) - { - Guard.NotEmpty(targetZone, nameof(targetZone)); - - _viewContext = html.ViewContext; - _originalViewContextWriter = _viewContext.Writer; - _page = (WebViewPage)html.ViewDataContainer; - - var writer = new StringWriter(); - _page.OutputStack.Push(writer); - _viewContext.Writer = writer; - - _targetZone = targetZone; - _injectMode = injectMode; - - if (key.HasValue()) - { - if (HasUniqueKey(key)) - { - _isVoid = true; - } - else - { - UniqueKeys.Add(key); - } - } - - if (_page.Request.IsAjaxRequest()) - { - _isVoid = true; - } - } - - public static IEnumerable GetZoneContent(string zone) - { - Guard.NotEmpty(zone, nameof(zone)); - - var storage = HttpContext.Current?.GetItem>(ItemsKey, forceCreation: false); - return storage?[zone]; - } - - public static bool HasUniqueKey(string key) - { - Guard.NotEmpty(key, nameof(key)); - - var uniqueKeys = HttpContext.Current?.GetItem>(UniqueKeysKey, forceCreation: false); - return uniqueKeys != null && uniqueKeys.Contains(key); - } - - private static HashSet UniqueKeys - { - get - { - return HttpContext.Current?.GetItem>(UniqueKeysKey, () => new HashSet(StringComparer.OrdinalIgnoreCase), true); - } - } - - private Multimap GetStorage() - { - return _page.ViewContext?.HttpContext?.GetItem>(ItemsKey, () => new Multimap(StringComparer.OrdinalIgnoreCase)); - } - - public void Dispose() - { - var storage = GetStorage(); - if (storage == null) - return; - - var writer = ((StringWriter)_page.OutputStack.Pop()); - var content = _isVoid ? string.Empty : writer.ToString(); - - _viewContext.Writer = _originalViewContextWriter; - - if (_injectMode == ZoneInjectMode.Append) - { - storage[_targetZone].Add(content); - } - else if (_injectMode == ZoneInjectMode.Prepend) - { - var list = new List(storage[_targetZone]); - list.Insert(0, content); - storage.RemoveAll(_targetZone); - storage[_targetZone].AddRange(list); - } - else // Replace - { - storage.RemoveAll(_targetZone); - storage[_targetZone].Add(content); - } - } - } - - public static IDisposable BeginZoneContent(this HtmlHelper helper, - string targetZone, - ZoneInjectMode injectMode = ZoneInjectMode.Append, - string key = null) - { - return new DocumentZone(helper, targetZone, injectMode, key); - } - - public static void RenderZone(this HtmlHelper helper, string zone) - { - var snippets = DocumentZone.GetZoneContent(zone); - if (snippets == null) - { - return; - } - - foreach (var snippet in snippets) - { - helper.ViewContext.Writer.WriteLine(snippet); - } - } - } + public enum ZoneInjectMode + { + Append, + Prepend, + Replace + } + + public static class HtmlZoneExtensions + { + private class DocumentZone : IDisposable + { + internal const string ItemsKey = "DocumentTail.Snippets"; + private const string UniqueKeysKey = "DocumentTail.UniqueKeys"; + + private readonly ViewContext _viewContext; + private readonly TextWriter _originalViewContextWriter; + private readonly WebViewPage _page; + private readonly string _targetZone; + private readonly ZoneInjectMode _injectMode; + private readonly bool _isVoid; + + public DocumentZone(HtmlHelper html, string targetZone, ZoneInjectMode injectMode, string key) + { + Guard.NotEmpty(targetZone, nameof(targetZone)); + + _viewContext = html.ViewContext; + _originalViewContextWriter = _viewContext.Writer; + _page = (WebViewPage)html.ViewDataContainer; + + var writer = new StringWriter(); + _page.OutputStack.Push(writer); + _viewContext.Writer = writer; + + _targetZone = targetZone; + _injectMode = injectMode; + + if (key.HasValue()) + { + if (HasUniqueKey(key)) + { + _isVoid = true; + } + else + { + UniqueKeys.Add(key); + } + } + + if (_page.Request.IsAjaxRequest()) + { + _isVoid = true; + } + } + + public static IEnumerable GetZoneContent(string zone) + { + Guard.NotEmpty(zone, nameof(zone)); + + var storage = HttpContext.Current?.GetItem>(ItemsKey, forceCreation: false); + return storage?[zone]; + } + + public static bool HasUniqueKey(string key) + { + Guard.NotEmpty(key, nameof(key)); + + var uniqueKeys = HttpContext.Current?.GetItem>(UniqueKeysKey, forceCreation: false); + return uniqueKeys != null && uniqueKeys.Contains(key); + } + + private static HashSet UniqueKeys => HttpContext.Current?.GetItem>(UniqueKeysKey, () => new HashSet(StringComparer.OrdinalIgnoreCase), true); + + private Multimap GetStorage() + { + return _page.ViewContext?.HttpContext?.GetItem>(ItemsKey, () => new Multimap(StringComparer.OrdinalIgnoreCase)); + } + + public void Dispose() + { + var storage = GetStorage(); + if (storage == null) + return; + + var writer = ((StringWriter)_page.OutputStack.Pop()); + var content = _isVoid ? string.Empty : writer.ToString(); + + _viewContext.Writer = _originalViewContextWriter; + + if (_injectMode == ZoneInjectMode.Append) + { + storage[_targetZone].Add(content); + } + else if (_injectMode == ZoneInjectMode.Prepend) + { + var list = new List(storage[_targetZone]); + list.Insert(0, content); + storage.RemoveAll(_targetZone); + storage[_targetZone].AddRange(list); + } + else // Replace + { + storage.RemoveAll(_targetZone); + storage[_targetZone].Add(content); + } + } + } + + public static IDisposable BeginZoneContent(this HtmlHelper helper, + string targetZone, + ZoneInjectMode injectMode = ZoneInjectMode.Append, + string key = null) + { + return new DocumentZone(helper, targetZone, injectMode, key); + } + + public static void RenderZone(this HtmlHelper helper, string zone) + { + var snippets = DocumentZone.GetZoneContent(zone); + if (snippets == null) + { + return; + } + + foreach (var snippet in snippets) + { + helper.ViewContext.Writer.WriteLine(snippet); + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs index 56dba19bee..19d0128b72 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs @@ -4,127 +4,129 @@ using System.Web; using System.Web.Mvc; using System.Web.Routing; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Infrastructure; using SmartStore.Services.Orders; using SmartStore.Utilities; namespace SmartStore { - public static class HttpExtensions - { - public static bool IsAdminArea(this HttpRequest request) - { - if (request != null) - { - return IsAdminArea(new HttpRequestWrapper(request)); - } - - return false; - } - - public static bool IsAdminArea(this HttpRequestBase request) - { - try - { - // TODO: not really reliable. Change this. - var area = request?.RequestContext?.RouteData?.GetAreaName(); - if (area != null) - { - return area.IsCaseInsensitiveEqual("admin"); - } - else - { - var paths = new[] { "~/administration/", "~/admin/" }; - return paths.Any(x => request?.AppRelativeCurrentExecutionFilePath?.StartsWith(x, StringComparison.OrdinalIgnoreCase) == true); - } - } - catch - { - return false; - } - } - - public static bool IsPublicArea(this HttpRequest request) - { - if (request != null) - { - return IsPublicArea(new HttpRequestWrapper(request)); - } - - return false; - } - - public static bool IsPublicArea(this HttpRequestBase request) - { - try - { - // TODO: not really reliable. Change this. - var area = request?.RequestContext?.RouteData?.GetAreaName(); - return area.IsEmpty(); - } - catch - { - return false; - } - } - - public static PostedFileResult ToPostedFileResult(this HttpRequestBase httpRequest) - { - if (httpRequest != null && httpRequest.Files.Count > 0) - { - return httpRequest.Files[0].ToPostedFileResult(); - } - - return null; - } - - public static PostedFileResult ToPostedFileResult(this HttpPostedFile httpFile) - { - if (httpFile != null && httpFile.ContentLength > 0) - { - return new PostedFileResult(new HttpPostedFileWrapper(httpFile)); - } - - return null; - } - - public static PostedFileResult ToPostedFileResult(this HttpPostedFileBase httpFile) - { - if (httpFile != null && httpFile.ContentLength > 0) - { - return new PostedFileResult(httpFile); - } - - return null; - } - - public static IEnumerable ToPostedFileResults(this HttpFileCollection httpFileCollection) - { - if (httpFileCollection != null && httpFileCollection.Count > 0) - { - return new HttpFileCollectionWrapper(httpFileCollection).ToPostedFileResults(); - } - - return Enumerable.Empty(); - } - - public static IEnumerable ToPostedFileResults(this HttpFileCollectionBase httpFileCollection) - { - if (httpFileCollection == null) - yield break; - - var batchId = Guid.NewGuid(); - - for (var i = 0; i < httpFileCollection.Count; i++) - { - var httpFile = httpFileCollection[i]; - var result = httpFile.ToPostedFileResult(); - if (result != null) - { - result.BatchId = batchId; - yield return result; - } - } - } + public static class HttpExtensions + { + public static bool IsAdminArea(this HttpRequest request) + { + if (request != null) + { + return IsAdminArea(new HttpRequestWrapper(request)); + } + + return false; + } + + public static bool IsAdminArea(this HttpRequestBase request) + { + try + { + // TODO: not really reliable. Change this. + var area = request?.RequestContext?.RouteData?.GetAreaName(); + if (area != null) + { + return area.IsCaseInsensitiveEqual("admin"); + } + else + { + var paths = new[] { "~/administration/", "~/admin/" }; + return paths.Any(x => request?.AppRelativeCurrentExecutionFilePath?.StartsWith(x, StringComparison.OrdinalIgnoreCase) == true); + } + } + catch + { + return false; + } + } + + public static bool IsPublicArea(this HttpRequest request) + { + if (request != null) + { + return IsPublicArea(new HttpRequestWrapper(request)); + } + + return false; + } + + public static bool IsPublicArea(this HttpRequestBase request) + { + try + { + // TODO: not really reliable. Change this. + var area = request?.RequestContext?.RouteData?.GetAreaName(); + return area.IsEmpty(); + } + catch + { + return false; + } + } + + public static PostedFileResult ToPostedFileResult(this HttpRequestBase httpRequest) + { + if (httpRequest != null && httpRequest.Files.Count > 0) + { + return httpRequest.Files[0].ToPostedFileResult(); + } + + return null; + } + + public static PostedFileResult ToPostedFileResult(this HttpPostedFile httpFile) + { + if (httpFile != null && httpFile.ContentLength > 0) + { + return new PostedFileResult(new HttpPostedFileWrapper(httpFile)); + } + + return null; + } + + public static PostedFileResult ToPostedFileResult(this HttpPostedFileBase httpFile) + { + if (httpFile != null && httpFile.ContentLength > 0) + { + return new PostedFileResult(httpFile); + } + + return null; + } + + public static IEnumerable ToPostedFileResults(this HttpFileCollection httpFileCollection) + { + if (httpFileCollection != null && httpFileCollection.Count > 0) + { + return new HttpFileCollectionWrapper(httpFileCollection).ToPostedFileResults(); + } + + return Enumerable.Empty(); + } + + public static IEnumerable ToPostedFileResults(this HttpFileCollectionBase httpFileCollection) + { + if (httpFileCollection == null) + yield break; + + var batchId = Guid.NewGuid(); + + for (var i = 0; i < httpFileCollection.Count; i++) + { + var httpFile = httpFileCollection[i]; + var result = httpFile.ToPostedFileResult(); + if (result != null) + { + result.BatchId = batchId; + yield return result; + } + } + } public static RouteData GetRouteData(this HttpContextBase httpContext) { @@ -144,152 +146,150 @@ public static bool TryGetRouteData(this HttpContextBase httpContext, out RouteDa return routeData != null; } - public static CheckoutState GetCheckoutState(this HttpContextBase httpContext) - { - Guard.NotNull(httpContext, nameof(httpContext)); - - var state = httpContext.Session.SafeGetValue(CheckoutState.CheckoutStateSessionKey); - - if (state != null) - return state; - - state = new CheckoutState(); - httpContext.Session.SafeSet(CheckoutState.CheckoutStateSessionKey, state); - - return state; - } - - public static void RemoveCheckoutState(this HttpContextBase httpContext) - { - Guard.NotNull(httpContext, nameof(httpContext)); - - httpContext.Session.SafeRemove(CheckoutState.CheckoutStateSessionKey); - } - - internal static string GetUserThemeChoiceFromCookie(this HttpContextBase context) - { - if (context == null) - return null; - - var cookie = context.Request.Cookies.Get("sm.UserThemeChoice"); - if (cookie != null) - { - return cookie.Value.NullEmpty(); - } - - return null; - } - - internal static void SetUserThemeChoiceInCookie(this HttpContextBase context, string value) - { - if (context == null) - return; - - var cookie = context.Request.Cookies.Get("sm.UserThemeChoice"); - - if (value.HasValue() && cookie == null) - { - cookie = new HttpCookie("sm.UserThemeChoice") - { - HttpOnly = true, - Expires = DateTime.UtcNow.AddYears(1) - }; - } - - if (value.HasValue()) - { - cookie.Value = value; - context.Request.Cookies.Set(cookie); - } - - if (value.IsEmpty() && cookie != null) - { - cookie.Expires = DateTime.UtcNow.AddYears(-10); - } - - if (cookie != null) - { - context.Response.SetCookie(cookie); - } - } - - internal static HttpCookie GetPreviewModeCookie(this HttpContextBase context, bool createIfMissing) - { - var httpRequest = context.SafeGetHttpRequest(); - var cookie = httpRequest?.Cookies?.Get("sm.PreviewModeOverrides"); - - if (cookie == null && createIfMissing && httpRequest != null) - { - cookie = new HttpCookie("sm.PreviewModeOverrides") { HttpOnly = true }; - httpRequest.Cookies.Set(cookie); - } - - if (cookie != null) - { - // when cookie gets created or touched, extend its lifetime - cookie.Expires = DateTime.UtcNow.AddMinutes(20); - } - - return cookie; - } - - internal static void SetPreviewModeValue(this HttpContextBase context, string key, string value) - { - if (context == null) - return; - - var cookie = context.GetPreviewModeCookie(value.HasValue()); - if (cookie != null) - { - if (value.HasValue()) - { - cookie.Values[key] = value; - } - else - { - cookie.Values.Remove(key); - } - } - } - - public static IDisposable PreviewModeCookie(this HttpContextBase context) - { - var disposable = new ActionDisposable(() => - { - var cookie = GetPreviewModeCookie(context, false); - if (cookie != null) - { - if (!cookie.HasKeys) - { - cookie.Expires = DateTime.UtcNow.AddYears(-10); - } - else - { - cookie.Expires = DateTime.UtcNow.AddMinutes(20); - } - - context.Response.SetCookie(cookie); - } - }); - - return disposable; - } - - public static string GetContentUrl(this HttpContextBase context, string path) - { - if (path.HasValue()) - { - if (!path.StartsWith("~")) - { - if (!path.StartsWith("/")) - path = "/" + path; - path = "~" + path; - } - - return UrlHelper.GenerateContentUrl(path, context); - } - - return path; - } - } + public static CheckoutState GetCheckoutState(this HttpContextBase httpContext) + { + Guard.NotNull(httpContext, nameof(httpContext)); + + var state = httpContext.Session.SafeGetValue(CheckoutState.CheckoutStateSessionKey); + + if (state != null) + return state; + + state = new CheckoutState(); + httpContext.Session.SafeSet(CheckoutState.CheckoutStateSessionKey, state); + + return state; + } + + public static void RemoveCheckoutState(this HttpContextBase httpContext) + { + Guard.NotNull(httpContext, nameof(httpContext)); + + httpContext.Session.SafeRemove(CheckoutState.CheckoutStateSessionKey); + } + + internal static string GetUserThemeChoiceFromCookie(this HttpContextBase context) + { + if (context == null) + return null; + + var cookie = context.Request.Cookies.Get("sm.UserThemeChoice"); + if (cookie != null) + { + return cookie.Value.NullEmpty(); + } + + return null; + } + + internal static void SetUserThemeChoiceInCookie(this HttpContextBase context, string value) + { + if (context?.Request == null) + { + return; + } + + if (value.IsEmpty()) + { + context.Request.Cookies.Remove("sm.UserThemeChoice"); + return; + } + + var settings = EngineContext.Current.Resolve(); + var cookie = context.Request.Cookies.Get("sm.UserThemeChoice") ?? new HttpCookie("sm.UserThemeChoice"); + cookie.Value = value; + cookie.Expires = DateTime.UtcNow.AddYears(1); + cookie.HttpOnly = true; + cookie.Secure = context.Request.IsHttps(); + cookie.SameSite = cookie.Secure ? (SameSiteMode)settings.SameSiteMode : SameSiteMode.Lax; + + context.Request.Cookies.Set(cookie); + } + + internal static HttpCookie GetPreviewModeCookie(this HttpContextBase context, bool createIfMissing) + { + var httpRequest = context.SafeGetHttpRequest(); + var cookie = httpRequest?.Cookies?.Get("sm.PreviewModeOverrides"); + + if (cookie == null && createIfMissing && httpRequest != null) + { + var settings = EngineContext.Current.Resolve(); + var secure = context.Request.IsHttps(); + cookie = new HttpCookie("sm.PreviewModeOverrides") + { + HttpOnly = true, + Secure = secure, + SameSite = secure ? (SameSiteMode)settings.SameSiteMode : SameSiteMode.Lax + }; + httpRequest.Cookies.Set(cookie); + } + + if (cookie != null) + { + // when cookie gets created or touched, extend its lifetime + cookie.Expires = DateTime.UtcNow.AddMinutes(20); + } + + return cookie; + } + + internal static void SetPreviewModeValue(this HttpContextBase context, string key, string value) + { + if (context == null) + return; + + var cookie = context.GetPreviewModeCookie(value.HasValue()); + if (cookie != null) + { + if (value.HasValue()) + { + cookie.Values[key] = value; + } + else + { + cookie.Values.Remove(key); + } + } + } + + public static IDisposable PreviewModeCookie(this HttpContextBase context) + { + var disposable = new ActionDisposable(() => + { + var cookie = GetPreviewModeCookie(context, false); + if (cookie != null) + { + if (!cookie.HasKeys) + { + cookie.Expires = DateTime.UtcNow.AddYears(-10); + } + else + { + cookie.Expires = DateTime.UtcNow.AddMinutes(20); + } + + context.Response.SetCookie(cookie); + } + }); + + return disposable; + } + + public static string GetContentUrl(this HttpContextBase context, string path) + { + if (path.HasValue()) + { + if (!path.StartsWith("~")) + { + if (!path.StartsWith("/")) + path = "/" + path; + path = "~" + path; + } + + return UrlHelper.GenerateContentUrl(path, context); + } + + return path; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpSessionStateExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpSessionStateExtensions.cs index 5f07d27c58..2f77e252da 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpSessionStateExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpSessionStateExtensions.cs @@ -3,52 +3,52 @@ namespace SmartStore { - public static class HttpSessionStateExtensions - { - public static void SafeRemove(this HttpSessionStateBase session, string key) - { - try - { - if (session != null && session[key] != null) - session.Remove(key); - } - catch (Exception exc) - { - exc.Dump(); - } - } + public static class HttpSessionStateExtensions + { + public static void SafeRemove(this HttpSessionStateBase session, string key) + { + try + { + if (session != null && session[key] != null) + session.Remove(key); + } + catch (Exception exc) + { + exc.Dump(); + } + } - public static T SafeGetValue(this HttpSessionStateBase session, string key, T defaultValue = default(T)) - { - try - { - if (session != null && session[key] != null) - return (T)session[key]; - } - catch (Exception exc) - { - exc.Dump(); - } + public static T SafeGetValue(this HttpSessionStateBase session, string key, T defaultValue = default(T)) + { + try + { + if (session != null && session[key] != null) + return (T)session[key]; + } + catch (Exception exc) + { + exc.Dump(); + } - return defaultValue; - } + return defaultValue; + } - public static void SafeSet(this HttpSessionStateBase session, string key, object value) - { - try - { - if (session != null) - { - if (value == null) - session.SafeRemove(key); - else - session[key] = value; - } - } - catch (Exception exc) - { - exc.Dump(); - } - } - } + public static void SafeSet(this HttpSessionStateBase session, string key, object value) + { + try + { + if (session != null) + { + if (value == null) + session.SafeRemove(key); + else + session[key] = value; + } + } + catch (Exception exc) + { + exc.Dump(); + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/LocalizedValueExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/LocalizedValueExtensions.cs index ffc474ff87..97be49e86f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/LocalizedValueExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/LocalizedValueExtensions.cs @@ -1,27 +1,26 @@ -using System; -using SmartStore.Services.Localization; +using SmartStore.Services.Localization; namespace SmartStore.Web.Framework { - /// - /// Wrapper for the most common string extension helpers used in views. - /// Just here to avoid runtime exceptions in views after refactoring GetLocalized() helper. - /// - public static class LocalizedValueExtensions - { - public static bool HasValue(this LocalizedValue value) - { - return value?.Value?.HasValue() == true; - } + /// + /// Wrapper for the most common string extension helpers used in views. + /// Just here to avoid runtime exceptions in views after refactoring GetLocalized() helper. + /// + public static class LocalizedValueExtensions + { + public static bool HasValue(this LocalizedValue value) + { + return value?.Value?.HasValue() == true; + } - public static bool IsEmpty(this LocalizedValue value) - { - return value?.Value?.IsEmpty() == false; - } + public static bool IsEmpty(this LocalizedValue value) + { + return value?.Value?.IsEmpty() == false; + } - public static string Truncate(this LocalizedValue value, int maxLength, string suffix = "") - { - return value?.Value?.Truncate(maxLength, suffix); - } - } + public static string Truncate(this LocalizedValue value, int maxLength, string suffix = "") + { + return value?.Value?.Truncate(maxLength, suffix); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/NameValueCollectionExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/NameValueCollectionExtensions.cs index 87ba980283..e1c7dce05f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/NameValueCollectionExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/NameValueCollectionExtensions.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Collections.Specialized; +using System.Linq; namespace SmartStore { - public static class NameValueCollectionExtension - { + public static class NameValueCollectionExtension + { public static IDictionary ToDictionary(this NameValueCollection collection) { Guard.NotNull(collection, nameof(collection)); - + var query = from key in collection.AllKeys where key != null select key; @@ -19,5 +19,5 @@ public static IDictionary ToDictionary(this NameValueCollection return query.ToDictionary(key => key, elementSelector); } - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/PostedFileResult.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/PostedFileResult.cs index 366d07a05e..cf55ba8490 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/PostedFileResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/PostedFileResult.cs @@ -6,138 +6,129 @@ namespace SmartStore { - public class PostedFileResult - { - private static readonly Regex s_ImageTypes = new Regex(@"(.*?)\.(gif|jpg|jpeg|jpe|jfif|pjpeg|pjp|png|tiff|tif|bmp|ico|svg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private readonly HttpPostedFileBase _httpFile; - - private string _contentType; - private string _fileName; - private string _fileTitle; - private string _fileExt; - private bool? _isImage; - private byte[] _buffer; - - public PostedFileResult(HttpPostedFileBase httpFile) - { - Guard.NotNull(httpFile, nameof(httpFile)); - - this._httpFile = httpFile; - - this.TimeStamp = DateTime.UtcNow; - this.BatchId = Guid.NewGuid(); - } - - public HttpPostedFileBase File - { - get { return _httpFile; } - } - - public DateTime TimeStamp - { - get; - private set; - } - - public Guid BatchId - { - get; - internal set; - } - - public string FileName - { - get - { - if (_fileName == null) - { - _fileName = Path.GetFileName(_httpFile.FileName); - } - return _fileName; - } - } - - public string FileTitle - { - get - { - if (_fileTitle == null) - { - _fileTitle = Path.GetFileNameWithoutExtension(this.FileName); - } - return _fileTitle; - } - } - - public string FileExtension - { - get - { - if (_fileExt == null) - { - _fileExt = Path.GetExtension(this.FileName).EmptyNull(); - } - return _fileExt; - } - } - - public string ContentType - { - get - { - if (_contentType == null) - { - var contentType = _httpFile.ContentType; - - if (contentType == null && this.FileExtension.HasValue()) - { - contentType = MimeTypes.MapNameToMimeType(this.FileExtension); - } - - _contentType = contentType.EmptyNull(); - } - return _contentType; - } - } - - public bool IsImage - { - get - { - if (!_isImage.HasValue) - { - _isImage = s_ImageTypes.IsMatch(FileExtension); - } - return _isImage.Value; - } - } - - public int Size - { - get { return _httpFile.ContentLength; } - } - - public Stream Stream - { - get { return _httpFile.InputStream; } - } - - public byte[] Buffer - { - get - { - if (_buffer == null) - { - _buffer = _httpFile.InputStream.ToByteArray(); - } - - return _buffer; - } - } - - public bool FileNameMatches(string pattern) - { - return Regex.IsMatch(_httpFile.FileName, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); - } - } + public class PostedFileResult + { + private static readonly Regex s_ImageTypes = new Regex(@"(.*?)\.(gif|jpg|jpeg|jpe|jfif|pjpeg|pjp|png|tiff|tif|bmp|ico|svg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly HttpPostedFileBase _httpFile; + + private string _contentType; + private string _fileName; + private string _fileTitle; + private string _fileExt; + private bool? _isImage; + private byte[] _buffer; + + public PostedFileResult(HttpPostedFileBase httpFile) + { + Guard.NotNull(httpFile, nameof(httpFile)); + + this._httpFile = httpFile; + + this.TimeStamp = DateTime.UtcNow; + this.BatchId = Guid.NewGuid(); + } + + public HttpPostedFileBase File => _httpFile; + + public DateTime TimeStamp + { + get; + private set; + } + + public Guid BatchId + { + get; + internal set; + } + + public string FileName + { + get + { + if (_fileName == null) + { + _fileName = Path.GetFileName(_httpFile.FileName); + } + return _fileName; + } + } + + public string FileTitle + { + get + { + if (_fileTitle == null) + { + _fileTitle = Path.GetFileNameWithoutExtension(this.FileName); + } + return _fileTitle; + } + } + + public string FileExtension + { + get + { + if (_fileExt == null) + { + _fileExt = Path.GetExtension(this.FileName).EmptyNull(); + } + return _fileExt; + } + } + + public string ContentType + { + get + { + if (_contentType == null) + { + var contentType = _httpFile.ContentType; + + if (contentType == null && this.FileExtension.HasValue()) + { + contentType = MimeTypes.MapNameToMimeType(this.FileExtension); + } + + _contentType = contentType.EmptyNull(); + } + return _contentType; + } + } + + public bool IsImage + { + get + { + if (!_isImage.HasValue) + { + _isImage = s_ImageTypes.IsMatch(FileExtension); + } + return _isImage.Value; + } + } + + public int Size => _httpFile.ContentLength; + + public Stream Stream => _httpFile.InputStream; + + public byte[] Buffer + { + get + { + if (_buffer == null) + { + _buffer = _httpFile.InputStream.ToByteArray(); + } + + return _buffer; + } + } + + public bool FileNameMatches(string pattern) + { + return Regex.IsMatch(_httpFile.FileName, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/TagBuilderExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/TagBuilderExtensions.cs index e21d61de5e..17834cb11b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/TagBuilderExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/TagBuilderExtensions.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Globalization; using System.Web.Mvc; diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs index 898e7a2923..dbcebdbc32 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs @@ -10,233 +10,233 @@ namespace SmartStore.Web.Framework { - [Serializable] - public class GridStateInfo - { - public GridStateInfo.GridState State { get; set; } - public string Path { get; set; } - - [Serializable] - public class GridState - { - public string Filter { get; set; } - public string GroupBy { get; set; } - public string OrderBy { get; set; } - public int Page { get; set; } - public int Size { get; set; } - } - } - - public static class TelerikExtensions - { - [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] - public static GridBuilder PreserveGridState(this GridBuilder builder) where T : class - { - var grid = builder.ToComponent(); - - if (!grid.DataBinding.Ajax.Enabled) - return builder; - - if (grid.Id.IsEmpty()) - throw new SmartException("A grid with preservable state must have a valid Id or Name"); - - var urlHelper = new UrlHelper(grid.ViewContext.RequestContext); - - var gridId = "GridState." + grid.Id + "__" + grid.ViewContext.RouteData.GenerateRouteIdentifier(); - - grid.AppendCssClass("grid-preservestate"); - grid.HtmlAttributes.Add("data-statepreserver-href", urlHelper.Action("SetGridState", "Common", new { area = "admin" })); - grid.HtmlAttributes.Add("data-statepreserver-key", gridId); - - // Try restore state from a previous request - var info = (GridStateInfo)grid.ViewContext.TempData[gridId]; - - if (info == null) - return builder; - - var state = info.State; - var command = GridCommand.Parse(state.Page, state.Size, state.OrderBy, state.GroupBy, state.Filter); - - if (grid.Paging.Enabled) - { - var pathChanged = !info.Path.Equals(grid.ViewContext.HttpContext.Request.RawUrl, StringComparison.OrdinalIgnoreCase); - if (!pathChanged) - { - if (command.PageSize > 0) - grid.Paging.PageSize = command.PageSize; - if (command.Page > 0) - grid.Paging.CurrentPage = command.Page; - } - } - - if (grid.Sorting.Enabled) - { - grid.Sorting.OrderBy.Clear(); - - foreach (var sort in command.SortDescriptors) - { - var existingSort = grid.Sorting.OrderBy.FirstOrDefault(x => x.Member.IsCaseInsensitiveEqual(sort.Member)); - if (existingSort != null) - { - grid.Sorting.OrderBy.Remove(existingSort); - } - grid.Sorting.OrderBy.Add(sort); - } - } - - if (grid.Grouping.Enabled) - { - grid.Grouping.Groups.Clear(); - - foreach (var group in command.GroupDescriptors) - { - var existingGroup = grid.Grouping.Groups.FirstOrDefault(x => x.Member.IsCaseInsensitiveEqual(group.Member)); - if (existingGroup != null) - { - grid.Grouping.Groups.Remove(existingGroup); - } - grid.Grouping.Groups.Add(group); - } - } - - if (grid.Filtering.Enabled) - { - grid.Filtering.Filters.Clear(); - - foreach (var filter in command.FilterDescriptors) - { - var compositeFilter = filter as CompositeFilterDescriptor; - if (compositeFilter == null) - { - compositeFilter = new CompositeFilterDescriptor { LogicalOperator = FilterCompositionLogicalOperator.And }; - compositeFilter.FilterDescriptors.Add(filter); - } - grid.Filtering.Filters.Add(compositeFilter); - } - } - - // persist again for the next request - grid.ViewContext.TempData[gridId] = info; - - return builder; - } - - public static IEnumerable ForCommand(this IEnumerable current, GridCommand command) - { - var query = current.AsQueryable() as IQueryable; - if (command.FilterDescriptors.Any()) - { - query = query.Where(command.FilterDescriptors.AsEnumerable()).AsQueryable() as IQueryable; - } - - IList temporarySortDescriptors = new List(); - - if (!command.SortDescriptors.Any() && query.Provider.IsEntityFrameworkProvider()) - { - // The Entity Framework provider demands OrderBy before calling Skip. - SortDescriptor sortDescriptor = new SortDescriptor - { - Member = GetFirstSortableProperty(query.ElementType) - }; - command.SortDescriptors.Add(sortDescriptor); - temporarySortDescriptors.Add(sortDescriptor); - } - - if (command.GroupDescriptors.Any()) - { - command.GroupDescriptors.Reverse().Each(groupDescriptor => - { - SortDescriptor sortDescriptor = new SortDescriptor - { - Member = groupDescriptor.Member, - SortDirection = groupDescriptor.SortDirection - }; - - command.SortDescriptors.Insert(0, sortDescriptor); - temporarySortDescriptors.Add(sortDescriptor); - }); - } - - if (command.SortDescriptors.Any()) - { - query = query.Sort(command.SortDescriptors); - } - - return query as IQueryable; - } - - public static IEnumerable PagedForCommand(this IEnumerable current, GridCommand command) - { - return current.Skip((command.Page - 1) * command.PageSize).Take(command.PageSize); - } - - - public static GridBoundColumnBuilder Centered(this GridBoundColumnBuilder columnBuilder) where T : class - { - return columnBuilder.HtmlAttributes(new { align = "center" }).HeaderHtmlAttributes(new { style = "text-align:center;" }); - } - - public static GridTemplateColumnBuilder Centered(this GridTemplateColumnBuilder columnBuilder) where T : class - { - return columnBuilder.HtmlAttributes(new { align = "center" }).HeaderHtmlAttributes(new { style = "text-align:center;" }); - } - - public static GridBoundColumnBuilder RightAlign(this GridBoundColumnBuilder columnBuilder) where T : class - { - return columnBuilder.HtmlAttributes(new { style = "text-align:right;" }).HeaderHtmlAttributes(new { style = "text-align:right;" }); - } - - public static GridTemplateColumnBuilder RightAlign(this GridTemplateColumnBuilder columnBuilder) where T : class - { - return columnBuilder.HtmlAttributes(new { style = "text-align:right;" }).HeaderHtmlAttributes(new { style = "text-align:right;" }); - } - - public static string GetValueFromAppliedFilter(this IFilterDescriptor filter, string valueName, FilterOperator? filterOperator = null) - { - if (filter is CompositeFilterDescriptor) - { - foreach (IFilterDescriptor childFilter in ((CompositeFilterDescriptor)filter).FilterDescriptors) - { - var val1 = GetValueFromAppliedFilter(childFilter, valueName, filterOperator); - if (!String.IsNullOrEmpty(val1)) - return val1; - } - } - else - { - var filterDescriptor = (FilterDescriptor)filter; - if (filterDescriptor != null && - filterDescriptor.Member.Equals(valueName, StringComparison.InvariantCultureIgnoreCase)) - { - if (!filterOperator.HasValue || filterDescriptor.Operator == filterOperator.Value) - return Convert.ToString(filterDescriptor.Value); - } - } - - return ""; - } - - public static string GetValueFromAppliedFilters(this IList filters, string valueName, FilterOperator? filterOperator = null) - { - foreach (var filter in filters) - { - var val1 = GetValueFromAppliedFilter(filter, valueName, filterOperator); - if (!String.IsNullOrEmpty(val1)) - return val1; - } - return ""; - } - - private static string GetFirstSortableProperty(Type type) - { - PropertyInfo firstSortableProperty = type.GetProperties().Where(property => property.PropertyType.IsPredefinedSimpleType()).FirstOrDefault(); - - if (firstSortableProperty == null) - { - throw new NotSupportedException("Cannot find property to sort by."); - } - - return firstSortableProperty.Name; - } - } + [Serializable] + public class GridStateInfo + { + public GridStateInfo.GridState State { get; set; } + public string Path { get; set; } + + [Serializable] + public class GridState + { + public string Filter { get; set; } + public string GroupBy { get; set; } + public string OrderBy { get; set; } + public int Page { get; set; } + public int Size { get; set; } + } + } + + public static class TelerikExtensions + { + [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] + public static GridBuilder PreserveGridState(this GridBuilder builder) where T : class + { + var grid = builder.ToComponent(); + + if (!grid.DataBinding.Ajax.Enabled) + return builder; + + if (grid.Id.IsEmpty()) + throw new SmartException("A grid with preservable state must have a valid Id or Name"); + + var urlHelper = new UrlHelper(grid.ViewContext.RequestContext); + + var gridId = "GridState." + grid.Id + "__" + grid.ViewContext.RouteData.GenerateRouteIdentifier(); + + grid.AppendCssClass("grid-preservestate"); + grid.HtmlAttributes.Add("data-statepreserver-href", urlHelper.Action("SetGridState", "Common", new { area = "admin" })); + grid.HtmlAttributes.Add("data-statepreserver-key", gridId); + + // Try restore state from a previous request + var info = (GridStateInfo)grid.ViewContext.TempData[gridId]; + + if (info == null) + return builder; + + var state = info.State; + var command = GridCommand.Parse(state.Page, state.Size, state.OrderBy, state.GroupBy, state.Filter); + + if (grid.Paging.Enabled) + { + var pathChanged = !info.Path.Equals(grid.ViewContext.HttpContext.Request.RawUrl, StringComparison.OrdinalIgnoreCase); + if (!pathChanged) + { + if (command.PageSize > 0) + grid.Paging.PageSize = command.PageSize; + if (command.Page > 0) + grid.Paging.CurrentPage = command.Page; + } + } + + if (grid.Sorting.Enabled) + { + grid.Sorting.OrderBy.Clear(); + + foreach (var sort in command.SortDescriptors) + { + var existingSort = grid.Sorting.OrderBy.FirstOrDefault(x => x.Member.IsCaseInsensitiveEqual(sort.Member)); + if (existingSort != null) + { + grid.Sorting.OrderBy.Remove(existingSort); + } + grid.Sorting.OrderBy.Add(sort); + } + } + + if (grid.Grouping.Enabled) + { + grid.Grouping.Groups.Clear(); + + foreach (var group in command.GroupDescriptors) + { + var existingGroup = grid.Grouping.Groups.FirstOrDefault(x => x.Member.IsCaseInsensitiveEqual(group.Member)); + if (existingGroup != null) + { + grid.Grouping.Groups.Remove(existingGroup); + } + grid.Grouping.Groups.Add(group); + } + } + + if (grid.Filtering.Enabled) + { + grid.Filtering.Filters.Clear(); + + foreach (var filter in command.FilterDescriptors) + { + var compositeFilter = filter as CompositeFilterDescriptor; + if (compositeFilter == null) + { + compositeFilter = new CompositeFilterDescriptor { LogicalOperator = FilterCompositionLogicalOperator.And }; + compositeFilter.FilterDescriptors.Add(filter); + } + grid.Filtering.Filters.Add(compositeFilter); + } + } + + // persist again for the next request + grid.ViewContext.TempData[gridId] = info; + + return builder; + } + + public static IEnumerable ForCommand(this IEnumerable current, GridCommand command) + { + var query = current.AsQueryable() as IQueryable; + if (command.FilterDescriptors.Any()) + { + query = query.Where(command.FilterDescriptors.AsEnumerable()).AsQueryable() as IQueryable; + } + + IList temporarySortDescriptors = new List(); + + if (!command.SortDescriptors.Any() && query.Provider.IsEntityFrameworkProvider()) + { + // The Entity Framework provider demands OrderBy before calling Skip. + SortDescriptor sortDescriptor = new SortDescriptor + { + Member = GetFirstSortableProperty(query.ElementType) + }; + command.SortDescriptors.Add(sortDescriptor); + temporarySortDescriptors.Add(sortDescriptor); + } + + if (command.GroupDescriptors.Any()) + { + command.GroupDescriptors.Reverse().Each(groupDescriptor => + { + SortDescriptor sortDescriptor = new SortDescriptor + { + Member = groupDescriptor.Member, + SortDirection = groupDescriptor.SortDirection + }; + + command.SortDescriptors.Insert(0, sortDescriptor); + temporarySortDescriptors.Add(sortDescriptor); + }); + } + + if (command.SortDescriptors.Any()) + { + query = query.Sort(command.SortDescriptors); + } + + return query as IQueryable; + } + + public static IEnumerable PagedForCommand(this IEnumerable current, GridCommand command) + { + return current.Skip((command.Page - 1) * command.PageSize).Take(command.PageSize); + } + + + public static GridBoundColumnBuilder Centered(this GridBoundColumnBuilder columnBuilder) where T : class + { + return columnBuilder.HtmlAttributes(new { align = "center" }).HeaderHtmlAttributes(new { style = "text-align:center;" }); + } + + public static GridTemplateColumnBuilder Centered(this GridTemplateColumnBuilder columnBuilder) where T : class + { + return columnBuilder.HtmlAttributes(new { align = "center" }).HeaderHtmlAttributes(new { style = "text-align:center;" }); + } + + public static GridBoundColumnBuilder RightAlign(this GridBoundColumnBuilder columnBuilder) where T : class + { + return columnBuilder.HtmlAttributes(new { style = "text-align:right;" }).HeaderHtmlAttributes(new { style = "text-align:right;" }); + } + + public static GridTemplateColumnBuilder RightAlign(this GridTemplateColumnBuilder columnBuilder) where T : class + { + return columnBuilder.HtmlAttributes(new { style = "text-align:right;" }).HeaderHtmlAttributes(new { style = "text-align:right;" }); + } + + public static string GetValueFromAppliedFilter(this IFilterDescriptor filter, string valueName, FilterOperator? filterOperator = null) + { + if (filter is CompositeFilterDescriptor) + { + foreach (IFilterDescriptor childFilter in ((CompositeFilterDescriptor)filter).FilterDescriptors) + { + var val1 = GetValueFromAppliedFilter(childFilter, valueName, filterOperator); + if (!String.IsNullOrEmpty(val1)) + return val1; + } + } + else + { + var filterDescriptor = (FilterDescriptor)filter; + if (filterDescriptor != null && + filterDescriptor.Member.Equals(valueName, StringComparison.InvariantCultureIgnoreCase)) + { + if (!filterOperator.HasValue || filterDescriptor.Operator == filterOperator.Value) + return Convert.ToString(filterDescriptor.Value); + } + } + + return ""; + } + + public static string GetValueFromAppliedFilters(this IList filters, string valueName, FilterOperator? filterOperator = null) + { + foreach (var filter in filters) + { + var val1 = GetValueFromAppliedFilter(filter, valueName, filterOperator); + if (!String.IsNullOrEmpty(val1)) + return val1; + } + return ""; + } + + private static string GetFirstSortableProperty(Type type) + { + PropertyInfo firstSortableProperty = type.GetProperties().Where(property => property.PropertyType.IsPredefinedSimpleType()).FirstOrDefault(); + + if (firstSortableProperty == null) + { + throw new NotSupportedException("Cannot find property to sort by."); + } + + return firstSortableProperty.Name; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs index f42ad0b854..e88283e4f3 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs @@ -1,7 +1,9 @@ -using System.Web.Mvc; +using System.Runtime.CompilerServices; +using System.Web.Mvc; using SmartStore.Core.Infrastructure; using SmartStore.Services.Cms; using SmartStore.Services.Media; +using SmartStore.Services.Media.Imaging; namespace SmartStore.Web.Framework { @@ -14,7 +16,7 @@ public static string LogOn(this UrlHelper urlHelper, string returnUrl) return urlHelper.Action("Login", "Customer", new { returnUrl, area = "" }); } - return urlHelper.Action("Login", "Customer", new { area = "" }); + return urlHelper.Action("Login", "Customer", new { area = "" }); } public static string LogOff(this UrlHelper urlHelper, string returnUrl) @@ -24,134 +26,134 @@ public static string LogOff(this UrlHelper urlHelper, string returnUrl) return urlHelper.Action("Logout", "Customer", new { returnUrl, area = "" }); } - return urlHelper.Action("Logout", "Customer", new { area = "" }); + return urlHelper.Action("Logout", "Customer", new { area = "" }); } - public static string Referrer(this UrlHelper urlHelper, string fallbackUrl = "") - { - var request = urlHelper.RequestContext.HttpContext.Request; - if (request.UrlReferrer != null && request.UrlReferrer.ToString().HasValue()) - { - return request.UrlReferrer.ToString(); - } - - return fallbackUrl; - } - - /// - /// Resolves a link to a topic page. - /// - /// The system name of the topic. - /// Link - /// - /// This method returns an empty string in following cases: - /// - the requested page does not exist. - /// - the current user has no permission to acces the page. - /// - public static string Topic(this UrlHelper urlHelper, string systemName, bool popup = false) - { - Guard.NotEmpty(systemName, nameof(systemName)); - - var expression = "topic:" + systemName; - if (popup) - { - expression += "?popup=true"; - } - - return Entity(urlHelper, expression); - } - - /// - /// Resolves a link label for a topic page. - /// The label is either the page short title or title. - /// - /// The system name of the topic. - /// Label - /// - /// This method returns an empty string if the requested page does not exist. - /// - public static string TopicLabel(this UrlHelper urlHelper, string systemName) - { - Guard.NotEmpty(systemName, nameof(systemName)); - - return EntityLabel(urlHelper, "topic:" + systemName); - } - - /// - /// Resolves a link to a system internal entity like product, topic, category or manufacturer. - /// - /// A link expression as supported by the - /// Link - /// - /// This method returns an empty string in following cases: - /// - the requested entity does not exist. - /// - the current user has no permission to acces the entity. - /// - public static string Entity(this UrlHelper urlHelper, string expression) - { - Guard.NotEmpty(expression, nameof(expression)); - - var linkResolver = EngineContext.Current.Resolve(); - var link = linkResolver.Resolve(expression); - - if (link.Status == LinkStatus.Ok) - { - return link.Link.EmptyNull(); - } - - return string.Empty; - } - - /// - /// Resolves a link label for a system internal entity like product, topic, category or manufacturer. - /// The label is either the entity short title, title or name, whichever is applicable. - /// - /// A link expression as supported by the - /// Label - /// - /// This method returns an empty string if the requested entity does not exist. - /// - public static string EntityLabel(this UrlHelper urlHelper, string expression) - { - Guard.NotEmpty(expression, nameof(expression)); - - var linkResolver = EngineContext.Current.Resolve(); - var link = linkResolver.Resolve(expression); - - if (link.Status == LinkStatus.Ok || link.Status == LinkStatus.Forbidden) - { - return link.Label; - } - - return string.Empty; - } - - #region Media - - public static string Media(this UrlHelper urlHelper, int? fileId, int thumbnailSize = 0, string host = null, bool doFallback = true) - { - var mediaService = EngineContext.Current.Resolve(); - return mediaService.GetUrl(fileId, thumbnailSize, host, doFallback); - } - - public static string Media(this UrlHelper urlHelper, MediaFileInfo file, int thumbnailSize = 0, string host = null, bool doFallback = true) - { - var mediaService = EngineContext.Current.Resolve(); - return mediaService.GetUrl(file, thumbnailSize, host, doFallback); - } - - public static string Media(this UrlHelper urlHelper, int? fileId, ProcessImageQuery query, string host = null, bool doFallback = true) - { - var mediaService = EngineContext.Current.Resolve(); - return mediaService.GetUrl(fileId, query, host, doFallback); - } - - public static string Media(this UrlHelper urlHelper, MediaFileInfo file, ProcessImageQuery query, string host = null, bool doFallback = true) - { - var mediaService = EngineContext.Current.Resolve(); - return mediaService.GetUrl(file, query, host, doFallback); - } - - #endregion - } + public static string Referrer(this UrlHelper urlHelper, string fallbackUrl = "") + { + var request = urlHelper.RequestContext.HttpContext.Request; + if (request.UrlReferrer != null && request.UrlReferrer.ToString().HasValue()) + { + return request.UrlReferrer.ToString(); + } + + return fallbackUrl; + } + + /// + /// Resolves a link to a topic page. + /// + /// The system name of the topic. + /// Link + /// + /// This method returns an empty string in following cases: + /// - the requested page does not exist. + /// - the current user has no permission to acces the page. + /// + public static string Topic(this UrlHelper urlHelper, string systemName, bool popup = false) + { + Guard.NotEmpty(systemName, nameof(systemName)); + + var expression = "topic:" + systemName; + if (popup) + { + expression += "?popup=true"; + } + + return Entity(urlHelper, expression); + } + + /// + /// Resolves a link label for a topic page. + /// The label is either the page short title or title. + /// + /// The system name of the topic. + /// Label + /// + /// This method returns an empty string if the requested page does not exist. + /// + public static string TopicLabel(this UrlHelper urlHelper, string systemName) + { + Guard.NotEmpty(systemName, nameof(systemName)); + + return EntityLabel(urlHelper, "topic:" + systemName); + } + + /// + /// Resolves a link to a system internal entity like product, topic, category or manufacturer. + /// + /// A link expression as supported by the + /// Link + /// + /// This method returns an empty string in following cases: + /// - the requested entity does not exist. + /// - the current user has no permission to acces the entity. + /// + public static string Entity(this UrlHelper urlHelper, string expression) + { + Guard.NotEmpty(expression, nameof(expression)); + + var linkResolver = EngineContext.Current.Resolve(); + var link = linkResolver.Resolve(expression); + + if (link.Status == LinkStatus.Ok) + { + return link.Link.EmptyNull(); + } + + return string.Empty; + } + + /// + /// Resolves a link label for a system internal entity like product, topic, category or manufacturer. + /// The label is either the entity short title, title or name, whichever is applicable. + /// + /// A link expression as supported by the + /// Label + /// + /// This method returns an empty string if the requested entity does not exist. + /// + public static string EntityLabel(this UrlHelper urlHelper, string expression) + { + Guard.NotEmpty(expression, nameof(expression)); + + var linkResolver = EngineContext.Current.Resolve(); + var link = linkResolver.Resolve(expression); + + if (link.Status == LinkStatus.Ok || link.Status == LinkStatus.Forbidden) + { + return link.Label; + } + + return string.Empty; + } + + #region Media + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Media(this UrlHelper urlHelper, int? fileId, int thumbnailSize = 0, string host = null, bool doFallback = true) + { + return EngineContext.Current.Resolve().GetUrl(fileId, thumbnailSize, host, doFallback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Media(this UrlHelper urlHelper, MediaFileInfo file, int thumbnailSize = 0, string host = null, bool doFallback = true) + { + return EngineContext.Current.Resolve().GetUrl(file, thumbnailSize, host, doFallback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Media(this UrlHelper urlHelper, int? fileId, ProcessImageQuery query, string host = null, bool doFallback = true) + { + return EngineContext.Current.Resolve().GetUrl(fileId, query, host, doFallback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Media(this UrlHelper urlHelper, MediaFileInfo file, ProcessImageQuery query, string host = null, bool doFallback = true) + { + return EngineContext.Current.Resolve().GetUrl(file, query, host, doFallback); + } + + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CheckAffiliateAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CheckAffiliateAttribute.cs index 34d50eb997..ed1663b5be 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CheckAffiliateAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CheckAffiliateAttribute.cs @@ -1,5 +1,4 @@ using System; -using System.Web; using System.Web.Mvc; using SmartStore.Core; using SmartStore.Services.Affiliates; @@ -9,11 +8,11 @@ namespace SmartStore.Web.Framework.Filters { public class CheckAffiliateAttribute : FilterAttribute, IActionFilter { - public Lazy AffiliateService { get; set; } - public Lazy WorkContext { get; set; } - public Lazy CustomerService { get; set; } - - public virtual void OnActionExecuting(ActionExecutingContext filterContext) + public Lazy AffiliateService { get; set; } + public Lazy WorkContext { get; set; } + public Lazy CustomerService { get; set; } + + public virtual void OnActionExecuting(ActionExecutingContext filterContext) { if (filterContext?.HttpContext?.Request == null) return; @@ -35,16 +34,16 @@ public virtual void OnActionExecuting(ActionExecutingContext filterContext) var customer = WorkContext.Value.CurrentCustomer; if (customer != null && !customer.IsSystemAccount && customer.AffiliateId != affiliate.Id) { - customer.AffiliateId = affiliate.Id; - CustomerService.Value.UpdateCustomer(customer); + customer.AffiliateId = affiliate.Id; + CustomerService.Value.UpdateCustomer(customer); } } } } } - public virtual void OnActionExecuted(ActionExecutedContext filterContext) - { - } - } + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs index 9f568db703..6ce3ae8ce2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs @@ -1,32 +1,31 @@ using System.IO.Compression; -using System.Web; using System.Web.Mvc; namespace SmartStore.Web.Framework.Filters { - public class CompressAttribute : ActionFilterAttribute - { - public bool PreferDeflate { get; set; } + public class CompressAttribute : ActionFilterAttribute + { + public bool PreferDeflate { get; set; } - public override void OnActionExecuting(ActionExecutingContext filterContext) - { - var response = filterContext.HttpContext.Response; - var acceptEncoding = filterContext.HttpContext.Request.Headers["Accept-Encoding"].EmptyNull().ToLower(); + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + var response = filterContext.HttpContext.Response; + var acceptEncoding = filterContext.HttpContext.Request.Headers["Accept-Encoding"].EmptyNull().ToLower(); - if (acceptEncoding.Contains("gzip") && !(PreferDeflate && acceptEncoding.Contains("deflate"))) - { - response.Filter = new GZipStream(response.Filter, CompressionMode.Compress); - response.Headers.Remove("Content-Encoding"); - response.AppendHeader("Content-Encoding", "gzip"); - } - else if (acceptEncoding.Contains("deflate")) - { - response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress); - response.Headers.Remove("Content-Encoding"); - response.AppendHeader("Content-Encoding", "deflate"); - } + if (acceptEncoding.Contains("gzip") && !(PreferDeflate && acceptEncoding.Contains("deflate"))) + { + response.Filter = new GZipStream(response.Filter, CompressionMode.Compress); + response.Headers.Remove("Content-Encoding"); + response.AppendHeader("Content-Encoding", "gzip"); + } + else if (acceptEncoding.Contains("deflate")) + { + response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress); + response.Headers.Remove("Content-Encoding"); + response.AppendHeader("Content-Encoding", "deflate"); + } - response.AppendHeader("Vary", "Content-Encoding"); - } - } + response.AppendHeader("Vary", "Content-Encoding"); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CookieConsentFilter.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CookieConsentFilter.cs index 46f7ef511f..ca99c83119 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CookieConsentFilter.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CookieConsentFilter.cs @@ -1,5 +1,4 @@ using System; -using System.Web; using System.Web.Mvc; using Newtonsoft.Json; using SmartStore.Core.Domain.Customers; @@ -10,160 +9,160 @@ namespace SmartStore.Web.Framework.Filters { - public class CookieConsentFilter : IActionFilter, IResultFilter - { - private readonly IUserAgent _userAgent; - private readonly ICommonServices _services; - private readonly Lazy _widgetProvider; - private readonly PrivacySettings _privacySettings; - private readonly ICookieManager _cookieManager; - - private bool _isProcessableRequest; - - public CookieConsentFilter( - IUserAgent userAgent, - ICommonServices services, - Lazy widgetProvider, - PrivacySettings privacySettings, - ICookieManager cookieManager) - { - _userAgent = userAgent; - _services = services; - _widgetProvider = widgetProvider; - _privacySettings = privacySettings; - _cookieManager = cookieManager; - } - - private bool IsProcessableRequest(ControllerContext controllerContext) - { - if (!_privacySettings.EnableCookieConsent) - return false; - - if (controllerContext.IsChildAction) - return false; - - var request = controllerContext.HttpContext?.Request; - - if (request == null) - return false; - - if (request.IsAjaxRequest()) - return false; - - if (!String.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) - return false; - - return true; - } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - _isProcessableRequest = IsProcessableRequest(filterContext); - - if (!_isProcessableRequest) - return; - - var isLegacy = false; - var request = filterContext.HttpContext.Request; - var response = filterContext.HttpContext.Response; - ConsentCookie cookieData = null; - - // Check if the user has a consent cookie. - var consentCookie = request.Cookies["CookieConsent"]; - if (consentCookie == null) - { - // No consent cookie. We first check the Do Not Track header value, this can have the value "0" or "1" - var doNotTrack = request.Headers.Get("DNT"); - - // If we receive a DNT header, we accept its value (0 = give consent, 1 = deny) and do not ask the user anymore. - if (doNotTrack.HasValue()) - { - if (doNotTrack.Equals("0")) - { - // Tracking consented. - _cookieManager.SetConsentCookie(response, true, true); - } - else - { - // Tracking denied. - _cookieManager.SetConsentCookie(response, false, false); - } - } - else - { - if (_userAgent.IsBot) - { - // Don't ask consent from search engines, also don't set cookies. - _cookieManager.SetConsentCookie(response, true, true); - } - else - { - // First request on the site and no DNT header (we could use session cookie, which is allowed by EU cookie law). - // Don't set cookie! - } - } - } - else - { - // We received a consent cookie - try - { - cookieData = JsonConvert.DeserializeObject(consentCookie.Value); - } - catch { } - - if (cookieData == null) - { - // Cookie was found but could not be converted thus it's a legacy cookie. - isLegacy = true; - var str = consentCookie.Value; - - // 'asked' means customer has not consented. - // '2' was the Value of legacy enum CookieConsentStatus.Denied - if (str.Equals("asked") || str.Equals("2")) - { - // Remove legacy Cookie & thus show CookieManager. - request.Cookies.Remove("CookieConsent"); - } - // 'true' means consented to all cookies. - // '1' was the Value of legacy enum CookieConsentStatus.Consented - else if (str.Equals("true") || str.Equals("1")) - { - // Set Cookie with all types allowed. - _cookieManager.SetConsentCookie(response, true, true); - } - } - } - - if (!isLegacy) - { - filterContext.HttpContext.Items["CookieConsent"] = cookieData; - } - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - } - - public void OnResultExecuting(ResultExecutingContext filterContext) - { - if (!_isProcessableRequest) - return; - - // Should only run on a full view rendering result or HTML ContentResult. - if (!filterContext.Result.IsHtmlViewResult()) - return; - - // Always render the child action because of output caching. - _widgetProvider.Value.RegisterAction( - new[] { "body_end_html_tag_before" }, - "CookieManager", - "Common", - new { area = "" }); - } - - public void OnResultExecuted(ResultExecutedContext filterContext) - { - } - } + public class CookieConsentFilter : IActionFilter, IResultFilter + { + private readonly IUserAgent _userAgent; + private readonly ICommonServices _services; + private readonly Lazy _widgetProvider; + private readonly PrivacySettings _privacySettings; + private readonly ICookieManager _cookieManager; + + private bool _isProcessableRequest; + + public CookieConsentFilter( + IUserAgent userAgent, + ICommonServices services, + Lazy widgetProvider, + PrivacySettings privacySettings, + ICookieManager cookieManager) + { + _userAgent = userAgent; + _services = services; + _widgetProvider = widgetProvider; + _privacySettings = privacySettings; + _cookieManager = cookieManager; + } + + private bool IsProcessableRequest(ControllerContext controllerContext) + { + if (!_privacySettings.EnableCookieConsent) + return false; + + if (controllerContext.IsChildAction) + return false; + + var request = controllerContext.HttpContext?.Request; + + if (request == null) + return false; + + if (request.IsAjaxRequest()) + return false; + + if (!String.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + return false; + + return true; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + _isProcessableRequest = IsProcessableRequest(filterContext); + + if (!_isProcessableRequest) + return; + + var isLegacy = false; + var request = filterContext.HttpContext.Request; + var response = filterContext.HttpContext.Response; + ConsentCookie cookieData = null; + + // Check if the user has a consent cookie. + var consentCookie = request.Cookies["CookieConsent"]; + if (consentCookie == null) + { + // No consent cookie. We first check the Do Not Track header value, this can have the value "0" or "1" + var doNotTrack = request.Headers.Get("DNT"); + + // If we receive a DNT header, we accept its value (0 = give consent, 1 = deny) and do not ask the user anymore. + if (doNotTrack.HasValue()) + { + if (doNotTrack.Equals("0")) + { + // Tracking consented. + _cookieManager.SetConsentCookie(response, true, true); + } + else + { + // Tracking denied. + _cookieManager.SetConsentCookie(response, false, false); + } + } + else + { + if (_userAgent.IsBot) + { + // Don't ask consent from search engines, also don't set cookies. + _cookieManager.SetConsentCookie(response, true, true); + } + else + { + // First request on the site and no DNT header (we could use session cookie, which is allowed by EU cookie law). + // Don't set cookie! + } + } + } + else + { + // We received a consent cookie + try + { + cookieData = JsonConvert.DeserializeObject(consentCookie.Value); + } + catch { } + + if (cookieData == null) + { + // Cookie was found but could not be converted thus it's a legacy cookie. + isLegacy = true; + var str = consentCookie.Value; + + // 'asked' means customer has not consented. + // '2' was the Value of legacy enum CookieConsentStatus.Denied + if (str.Equals("asked") || str.Equals("2")) + { + // Remove legacy Cookie & thus show CookieManager. + request.Cookies.Remove("CookieConsent"); + } + // 'true' means consented to all cookies. + // '1' was the Value of legacy enum CookieConsentStatus.Consented + else if (str.Equals("true") || str.Equals("1")) + { + // Set Cookie with all types allowed. + _cookieManager.SetConsentCookie(response, true, true); + } + } + } + + if (!isLegacy) + { + filterContext.HttpContext.Items["CookieConsent"] = cookieData; + } + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (!_isProcessableRequest) + return; + + // Should only run on a full view rendering result or HTML ContentResult. + if (!filterContext.Result.IsHtmlViewResult()) + return; + + // Always render the child action because of output caching. + _widgetProvider.Value.RegisterAction( + new[] { "body_end_html_tag_before" }, + "CookieManager", + "Common", + new { area = "" }); + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CustomerLastActivityAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CustomerLastActivityAttribute.cs index 60ed2a7f90..d0ecd40b0c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CustomerLastActivityAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CustomerLastActivityAttribute.cs @@ -8,10 +8,10 @@ namespace SmartStore.Web.Framework.Filters { public class CustomerLastActivityAttribute : FilterAttribute, IActionFilter { - public Lazy WorkContext { get; set; } - public Lazy CustomerService { get; set; } - - public virtual void OnActionExecuting(ActionExecutingContext filterContext) + public Lazy WorkContext { get; set; } + public Lazy CustomerService { get; set; } + + public virtual void OnActionExecuting(ActionExecutingContext filterContext) { if (!DataSettings.DatabaseIsInstalled()) return; @@ -26,17 +26,28 @@ public virtual void OnActionExecuting(ActionExecutingContext filterContext) return; var customer = WorkContext.Value.CurrentCustomer; - - // update last activity date - if (!customer.IsSystemAccount && customer.LastActivityDateUtc.AddMinutes(1.0) < DateTime.UtcNow) + + // Update last activity date. + if (customer != null && !customer.Deleted && !customer.IsSystemAccount && customer.LastActivityDateUtc.AddMinutes(1.0) < DateTime.UtcNow) { - customer.LastActivityDateUtc = DateTime.UtcNow; - CustomerService.Value.UpdateCustomer(customer); + try + { + customer.LastActivityDateUtc = DateTime.UtcNow; + CustomerService.Value.UpdateCustomer(customer); + } + catch (InvalidOperationException ioe) + { + // The exception may occur on the first call after a migration. + if (!ioe.IsAlreadyAttachedEntityException()) + { + throw; + } + } } } - public virtual void OnActionExecuted(ActionExecutedContext filterContext) - { - } - } + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/FormValueRequiredAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/FormValueRequiredAttribute.cs index c0a759a5c5..e24540ab11 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/FormValueRequiredAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/FormValueRequiredAttribute.cs @@ -7,125 +7,125 @@ namespace SmartStore.Web.Framework.Filters { - - public class FormValueRequiredAttribute : ActionMethodSelectorAttribute + + public class FormValueRequiredAttribute : ActionMethodSelectorAttribute { private readonly string[] _submitButtonNames; private readonly FormValueRequirement _requirement; - private readonly FormValueRequirementRule _rule; - private readonly bool _inverse; + private readonly FormValueRequirementRule _rule; + private readonly bool _inverse; - public FormValueRequiredAttribute(params string[] submitButtonNames): + public FormValueRequiredAttribute(params string[] submitButtonNames) : this(FormValueRequirement.Equal, FormValueRequirementRule.MatchAny, false, submitButtonNames) { } public FormValueRequiredAttribute(FormValueRequirement requirement, params string[] submitButtonNames) - : this(requirement, FormValueRequirementRule.MatchAny, false, submitButtonNames) + : this(requirement, FormValueRequirementRule.MatchAny, false, submitButtonNames) + { + } + + public FormValueRequiredAttribute(FormValueRequirementRule rule, params string[] submitButtonNames) + : this(FormValueRequirement.Equal, rule, false, submitButtonNames) + { + } + + public FormValueRequiredAttribute(FormValueRequirement requirement, FormValueRequirementRule rule, params string[] submitButtonNames) + : this(requirement, rule, false, submitButtonNames) { } - public FormValueRequiredAttribute(FormValueRequirementRule rule, params string[] submitButtonNames) - : this(FormValueRequirement.Equal, rule, false, submitButtonNames) - { - } - - public FormValueRequiredAttribute(FormValueRequirement requirement, FormValueRequirementRule rule, params string[] submitButtonNames) - : this(requirement, rule, false, submitButtonNames) - { - } - - protected internal FormValueRequiredAttribute( - FormValueRequirement requirement, - FormValueRequirementRule rule, - bool inverse, - params string[] submitButtonNames) - { - // at least one submit button should be found (or being absent if 'inverse') - this._submitButtonNames = submitButtonNames; - this._requirement = requirement; - this._rule = rule; - this._inverse = inverse; - } + protected internal FormValueRequiredAttribute( + FormValueRequirement requirement, + FormValueRequirementRule rule, + bool inverse, + params string[] submitButtonNames) + { + // at least one submit button should be found (or being absent if 'inverse') + this._submitButtonNames = submitButtonNames; + this._requirement = requirement; + this._rule = rule; + this._inverse = inverse; + } public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { - return IsValidForRequest(controllerContext.HttpContext.Request.Form); - } - - protected internal virtual bool IsValidForRequest(NameValueCollection form) - { - try - { - bool isMatch = false; - if (_rule == FormValueRequirementRule.MatchAny) - { - isMatch = _submitButtonNames.Any(x => IsMatch(form, x)); - } - else - { - isMatch = _submitButtonNames.All(x => IsMatch(form, x)); - } - return isMatch; - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } - - return false; - } - - private bool IsMatch(NameValueCollection form, string key) - { - string value = ""; - - if (_requirement == FormValueRequirement.Equal) - { - // do not iterate because "Invalid request" exception can be thrown - value = form[key]; - } - else - { - var firstMatch = form.AllKeys.FirstOrDefault(x => x.StartsWith(key, StringComparison.InvariantCultureIgnoreCase)); - if (firstMatch != null) - { - value = form[firstMatch]; - } - } - - if (_inverse) - { - return value.IsEmpty(); - } - - return value.HasValue(); - } + return IsValidForRequest(controllerContext.HttpContext.Request.Form); + } + + protected internal virtual bool IsValidForRequest(NameValueCollection form) + { + try + { + bool isMatch = false; + if (_rule == FormValueRequirementRule.MatchAny) + { + isMatch = _submitButtonNames.Any(x => IsMatch(form, x)); + } + else + { + isMatch = _submitButtonNames.All(x => IsMatch(form, x)); + } + return isMatch; + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + + return false; + } + + private bool IsMatch(NameValueCollection form, string key) + { + string value = ""; + + if (_requirement == FormValueRequirement.Equal) + { + // do not iterate because "Invalid request" exception can be thrown + value = form[key]; + } + else + { + var firstMatch = form.AllKeys.FirstOrDefault(x => x.StartsWith(key, StringComparison.InvariantCultureIgnoreCase)); + if (firstMatch != null) + { + value = form[firstMatch]; + } + } + + if (_inverse) + { + return value.IsEmpty(); + } + + return value.HasValue(); + } } - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public class FormValueAbsentAttribute : FormValueRequiredAttribute - { - public FormValueAbsentAttribute(params string[] submitButtonNames): + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class FormValueAbsentAttribute : FormValueRequiredAttribute + { + public FormValueAbsentAttribute(params string[] submitButtonNames) : base(FormValueRequirement.Equal, FormValueRequirementRule.MatchAny, true, submitButtonNames) { } - public FormValueAbsentAttribute(FormValueRequirement requirement, params string[] submitButtonNames) - : base(requirement, FormValueRequirementRule.MatchAny, true, submitButtonNames) + public FormValueAbsentAttribute(FormValueRequirement requirement, params string[] submitButtonNames) + : base(requirement, FormValueRequirementRule.MatchAny, true, submitButtonNames) { } - public FormValueAbsentAttribute(FormValueRequirementRule rule, params string[] submitButtonNames) - : base(FormValueRequirement.Equal, rule, true, submitButtonNames) - { - } + public FormValueAbsentAttribute(FormValueRequirementRule rule, params string[] submitButtonNames) + : base(FormValueRequirement.Equal, rule, true, submitButtonNames) + { + } - public FormValueAbsentAttribute(FormValueRequirement requirement, FormValueRequirementRule rule, params string[] submitButtonNames) - : base(requirement, rule, true, submitButtonNames) - { - } - } + public FormValueAbsentAttribute(FormValueRequirement requirement, FormValueRequirementRule rule, params string[] submitButtonNames) + : base(requirement, rule, true, submitButtonNames) + { + } + } public enum FormValueRequirement { @@ -133,9 +133,9 @@ public enum FormValueRequirement StartsWith } - public enum FormValueRequirementRule - { - MatchAny, - MatchAll - } + public enum FormValueRequirementRule + { + MatchAny, + MatchAll + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/GdprConsentAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/GdprConsentAttribute.cs index b76bf74857..a15a40a478 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/GdprConsentAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/GdprConsentAttribute.cs @@ -1,102 +1,100 @@ -using SmartStore.Core; +using System; +using System.Web.Mvc; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Logging; using SmartStore.Services; using SmartStore.Services.Common; using SmartStore.Web.Framework.UI; -using System; -using System.Web.Mvc; -using System.Web.Routing; namespace SmartStore.Web.Framework.Filters { - public class GdprConsentAttribute : FilterAttribute, IActionFilter, IResultFilter - { - public Lazy Services { get; set; } - public Lazy PrivacySettings { get; set; } - public Lazy GenericAttributeService { get; set; } - public Lazy WidgetProvider { get; set; } - public Lazy Notifier { get; set; } - - public void OnActionExecuting(ActionExecutingContext filterContext) - { - if (!PrivacySettings.Value.DisplayGdprConsentOnForms) - return; - - if (filterContext?.ActionDescriptor == null || filterContext?.HttpContext?.Request == null) - return; - - if (filterContext.HttpContext.Request.HttpMethod.Equals("GET")) - return; - - var customer = Services.Value.WorkContext.CurrentCustomer; - var hasConsentedToGdpr = filterContext.HttpContext.Request.Form["GdprConsent"]; - - if (filterContext.HttpContext.Request.HttpMethod.Equals("POST") && hasConsentedToGdpr.HasValue()) - { - // set flag which can be accessed in corresponding action - filterContext.Controller.ViewData.Add("GdprConsent", hasConsentedToGdpr.Contains("true")); - - if (hasConsentedToGdpr.Contains("true")) - { - GenericAttributeService.Value.SaveAttribute(customer, SystemCustomerAttributeNames.HasConsentedToGdpr, true); - } - else - { - if (!filterContext.HttpContext.Request.IsAjaxRequest()) - { - // add a validation message - filterContext.Controller.ViewData.ModelState.AddModelError("", Services.Value.Localization.GetResource("Gdpr.Consent.ValidationMessage")); - } - else - { - // notify - Notifier.Value.Error(Services.Value.Localization.GetResource("Gdpr.Consent.ValidationMessage")); - } - - return; - } - } - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - } - - public void OnResultExecuting(ResultExecutingContext filterContext) - { - if (!PrivacySettings.Value.DisplayGdprConsentOnForms) - return; - - if (filterContext.HttpContext.Items.Contains("GdprConsentRendered")) - return; - - var result = filterContext.Result; - - // should only run on a full view rendering result or HTML ContentResult - if (!result.IsHtmlViewResult()) - return; - - if (!filterContext.IsChildAction) - { - WidgetProvider.Value.RegisterAction( - new[] { "gdpr_consent" }, - "GdprConsent", - "Common", - new { area = "", isSmall = false }); - } - - WidgetProvider.Value.RegisterAction( - new[] { "gdpr_consent_small" }, - "GdprConsent", - "Common", - new { area = "", isSmall = true }); - - filterContext.HttpContext.Items["GdprConsentRendered"] = true; - } - - public void OnResultExecuted(ResultExecutedContext filterContext) - { - } - } + public class GdprConsentAttribute : FilterAttribute, IActionFilter, IResultFilter + { + public Lazy Services { get; set; } + public Lazy PrivacySettings { get; set; } + public Lazy GenericAttributeService { get; set; } + public Lazy WidgetProvider { get; set; } + public Lazy Notifier { get; set; } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (!PrivacySettings.Value.DisplayGdprConsentOnForms) + return; + + if (filterContext?.ActionDescriptor == null || filterContext?.HttpContext?.Request == null) + return; + + if (filterContext.HttpContext.Request.HttpMethod.Equals("GET")) + return; + + var customer = Services.Value.WorkContext.CurrentCustomer; + var hasConsentedToGdpr = filterContext.HttpContext.Request.Form["GdprConsent"]; + + if (filterContext.HttpContext.Request.HttpMethod.Equals("POST") && hasConsentedToGdpr.HasValue()) + { + // set flag which can be accessed in corresponding action + filterContext.Controller.ViewData.Add("GdprConsent", hasConsentedToGdpr.Contains("true")); + + if (hasConsentedToGdpr.Contains("true")) + { + GenericAttributeService.Value.SaveAttribute(customer, SystemCustomerAttributeNames.HasConsentedToGdpr, true); + } + else + { + if (!filterContext.HttpContext.Request.IsAjaxRequest()) + { + // add a validation message + filterContext.Controller.ViewData.ModelState.AddModelError("", Services.Value.Localization.GetResource("Gdpr.Consent.ValidationMessage")); + } + else + { + // notify + Notifier.Value.Error(Services.Value.Localization.GetResource("Gdpr.Consent.ValidationMessage")); + } + + return; + } + } + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (!PrivacySettings.Value.DisplayGdprConsentOnForms) + return; + + if (filterContext.HttpContext.Items.Contains("GdprConsentRendered")) + return; + + var result = filterContext.Result; + + // should only run on a full view rendering result or HTML ContentResult + if (!result.IsHtmlViewResult()) + return; + + if (!filterContext.IsChildAction) + { + WidgetProvider.Value.RegisterAction( + new[] { "gdpr_consent" }, + "GdprConsent", + "Common", + new { area = "", isSmall = false }); + } + + WidgetProvider.Value.RegisterAction( + new[] { "gdpr_consent_small" }, + "GdprConsent", + "Common", + new { area = "", isSmall = true }); + + filterContext.HttpContext.Items["GdprConsentRendered"] = true; + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs b/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs index 3b259123ad..09aeb115aa 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Runtime.InteropServices; -using System.Security; -using System.Threading; using System.Web; using System.Web.Mvc; using SmartStore.Core; @@ -13,29 +8,29 @@ namespace SmartStore.Web.Framework.Filters { - public class HandleExceptionFilter : IActionFilter, IExceptionFilter + public class HandleExceptionFilter : IActionFilter, IExceptionFilter { - private readonly ILoggerFactory _loggerFactory; - private readonly Lazy _workContext; + private readonly ILoggerFactory _loggerFactory; + private readonly Lazy _workContext; - public HandleExceptionFilter(ILoggerFactory loggerFactory, Lazy workContext) - { - _loggerFactory = loggerFactory; - _workContext = workContext; - } + public HandleExceptionFilter(ILoggerFactory loggerFactory, Lazy workContext) + { + _loggerFactory = loggerFactory; + _workContext = workContext; + } public virtual void OnActionExecuting(ActionExecutingContext filterContext) { } - + public virtual void OnActionExecuted(ActionExecutedContext filterContext) { if (filterContext.IsChildAction) return; - + if (filterContext.Result is HttpStatusCodeResult statusCodeResult && statusCodeResult.StatusCode == 404) - { - // Handle not found (404) from within the MVC pipeline (only called when HttpNotFoundResult is returned from actions) + { + // Handle not found (404) from within the MVC pipeline (only called when HttpNotFoundResult is returned from actions) filterContext.Result = Create404Result(filterContext); _workContext.Value.IsAdmin = false; @@ -54,7 +49,7 @@ public void OnException(ExceptionContext filterContext) { LogException(exception, filterContext); } - + if (!filterContext.IsChildAction) { var controllerName = filterContext.RouteData.GetRequiredString("controller"); @@ -94,6 +89,8 @@ public void OnException(ExceptionContext filterContext) response.Clear(); response.StatusCode = 500; + // Truncate message to avoid ArgumentOutOfRangeException. + response.StatusDescription = exception.Message?.Truncate(512); response.TrySkipIisCustomErrors = true; } } @@ -119,28 +116,28 @@ protected internal static ActionResult Create404Result(ControllerContext context } protected void LogException(Exception exception, ControllerContext context) - { - if (exception == null) - return; - - if (!DataSettings.DatabaseIsInstalled()) - return; - - //// ignore 404 HTTP errors - //var httpException = exception as HttpException; - //if (httpException != null && httpException.GetHttpCode() == 404) - // return; - - try - { - var logger = _loggerFactory.GetLogger(context.Controller.GetType()); - logger.Error(exception); - } - catch - { - // don't throw new exception - } - } + { + if (exception == null) + return; + + if (!DataSettings.DatabaseIsInstalled()) + return; + + //// ignore 404 HTTP errors + //var httpException = exception as HttpException; + //if (httpException != null && httpException.GetHttpCode() == 404) + // return; + + try + { + var logger = _loggerFactory.GetLogger(context.Controller.GetType()); + logger.Error(exception); + } + catch + { + // don't throw new exception + } + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/HandleInstallFilter.cs b/src/Presentation/SmartStore.Web.Framework/Filters/HandleInstallFilter.cs index af2e1fbaf5..d89f8c6eb7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/HandleInstallFilter.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/HandleInstallFilter.cs @@ -3,29 +3,29 @@ using SmartStore.Core.Data; namespace SmartStore.Web.Framework.Filters -{ +{ public class HandleInstallFilter : IAuthorizationFilter - { - public void OnAuthorization(AuthorizationContext filterContext) - { - var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName; - var actionName = filterContext.ActionDescriptor.ActionName; + { + public void OnAuthorization(AuthorizationContext filterContext) + { + var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName; + var actionName = filterContext.ActionDescriptor.ActionName; - if (controllerName == "Install" && actionName != "Index" && filterContext.HttpContext.Request.IsAjaxRequest()) - { - // probably "progress" or "finalize" call - return; - } + if (controllerName == "Install" && actionName != "Index" && filterContext.HttpContext.Request.IsAjaxRequest()) + { + // probably "progress" or "finalize" call + return; + } - if (!DataSettings.DatabaseIsInstalled() && controllerName != "Install") - { - filterContext.Result = new RedirectToRouteResult( - new RouteValueDictionary { - { "Controller", "Install" }, - { "Action", "Index" } - }); - } - } - } + if (!DataSettings.DatabaseIsInstalled() && controllerName != "Install") + { + filterContext.Result = new RedirectToRouteResult( + new RouteValueDictionary { + { "Controller", "Install" }, + { "Action", "Index" } + }); + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/JsonNetAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/JsonNetAttribute.cs index a1f5532806..ee45056709 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/JsonNetAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/JsonNetAttribute.cs @@ -6,9 +6,9 @@ namespace SmartStore.Web.Framework.Filters { - public class JsonNetAttribute : FilterAttribute, IActionFilter - { - public Lazy DateTimeHelper { get; set; } + public class JsonNetAttribute : FilterAttribute, IActionFilter + { + public Lazy DateTimeHelper { get; set; } public void OnActionExecuting(ActionExecutingContext filterContext) { diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/MaxMediaFileSizeAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/MaxMediaFileSizeAttribute.cs new file mode 100644 index 0000000000..6be3a52e41 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Filters/MaxMediaFileSizeAttribute.cs @@ -0,0 +1,44 @@ +using System; +using System.Web.Mvc; +using SmartStore.Core.Domain.Media; +using SmartStore.Services.Media; + +namespace SmartStore.Web.Framework.Filters +{ + /// + /// Filters a request by the size of uploaded files according to . + /// + public class MaxMediaFileSizeAttribute : FilterAttribute, IActionFilter + { + public Lazy MediaSettings { get; set; } + public Lazy ExceptionFactory { get; set; } + + public virtual void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext?.HttpContext?.Request == null || filterContext.IsChildAction) + { + return; + } + + var request = filterContext.HttpContext.Request; + if (request.Files.Count <= 0) + { + return; + } + + long maxBytes = 1024 * MediaSettings.Value.MaxUploadFileSize; + for (var i = 0; i < request.Files.Count; ++i) + { + var file = request.Files[i]; + if (file.ContentLength > maxBytes) + { + throw ExceptionFactory.Value.MaxFileSizeExceeded(file.FileName, file.ContentLength, maxBytes); + } + } + } + + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs index ec44b5bb1d..2cc6b8665f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; @@ -7,67 +6,67 @@ namespace SmartStore.Web.Framework.Filters { - public class NotifyAttribute : FilterAttribute, IResultFilter - { - public const string NotificationsKey = "sm.notifications.all"; - - public INotifier Notifier { get; set; } - - public virtual void OnResultExecuting(ResultExecutingContext filterContext) - { - if (Notifier == null || Notifier.Entries.Count == 0) - return; - - if (!filterContext.IsChildAction && filterContext.HttpContext.Request.IsAjaxRequest()) - { - HandleAjaxRequest(Notifier.Entries.FirstOrDefault(), filterContext.HttpContext.Response); - return; - } - - Persist(filterContext.Controller.ViewData, Notifier.Entries.Where(x => x.Durable == false)); - Persist(filterContext.Controller.TempData, Notifier.Entries.Where(x => x.Durable == true)); - - Notifier.Entries.Clear(); - } - - public virtual void OnResultExecuted(ResultExecutedContext filterContext) - { - } - - private void Persist(IDictionary bag, IEnumerable source) - { - if (!source.Any()) - return; - - var existing = (bag[NotificationsKey] ?? new HashSet()) as HashSet; - - source.Each(x => - { - if (x.Message.Text.HasValue()) - existing.Add(x); - }); - - bag[NotificationsKey] = TrimSet(existing); - } - - private void HandleAjaxRequest(NotifyEntry entry, HttpResponseBase response) - { - if (entry == null) - return; - - response.AddHeader("X-Message-Type", entry.Type.ToString().ToLower()); - response.AddHeader("X-Message", entry.Message.Text); - } - - private HashSet TrimSet(HashSet entries) - { - if (entries.Count <= 20) - { - return entries; - } - - return new HashSet(entries.Skip(entries.Count - 20)); - } - } + public class NotifyAttribute : FilterAttribute, IResultFilter + { + public const string NotificationsKey = "sm.notifications.all"; + + public INotifier Notifier { get; set; } + + public virtual void OnResultExecuting(ResultExecutingContext filterContext) + { + if (Notifier == null || Notifier.Entries.Count == 0) + return; + + if (!filterContext.IsChildAction && filterContext.HttpContext.Request.IsAjaxRequest()) + { + HandleAjaxRequest(Notifier.Entries.FirstOrDefault(), filterContext.HttpContext.Response); + return; + } + + Persist(filterContext.Controller.ViewData, Notifier.Entries.Where(x => x.Durable == false)); + Persist(filterContext.Controller.TempData, Notifier.Entries.Where(x => x.Durable == true)); + + Notifier.Entries.Clear(); + } + + public virtual void OnResultExecuted(ResultExecutedContext filterContext) + { + } + + private void Persist(IDictionary bag, IEnumerable source) + { + if (!source.Any()) + return; + + var existing = (bag[NotificationsKey] ?? new HashSet()) as HashSet; + + source.Each(x => + { + if (x.Message.Text.HasValue()) + existing.Add(x); + }); + + bag[NotificationsKey] = TrimSet(existing); + } + + private void HandleAjaxRequest(NotifyEntry entry, HttpResponseBase response) + { + if (entry == null) + return; + + response.AddHeader("X-Message-Type", entry.Type.ToString().ToLower()); + response.AddHeader("X-Message", entry.Message.Text); + } + + private HashSet TrimSet(HashSet entries) + { + if (entries.Count <= 20) + { + return entries; + } + + return new HashSet(entries.Skip(entries.Count - 20)); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs index e38d6f6bd0..e6d3dea060 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs @@ -9,21 +9,22 @@ namespace SmartStore.Web.Framework.Filters { public class PublicStoreAllowNavigationAttribute : FilterAttribute, IAuthorizationFilter { - private static readonly List> s_permittedRoutes = new List> - { - new Tuple("SmartStore.Web.Controllers.CustomerController", "Login"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "Logout"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "Register"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "PasswordRecovery"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "PasswordRecoveryConfirm"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "AccountActivation"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "CheckUsernameAvailability"), - new Tuple("SmartStore.Web.Controllers.MenuController", "OffCanvas") - }; + private static readonly List> s_permittedRoutes = new List> + { + new Tuple("SmartStore.Web.Controllers.CustomerController", "Login"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "Logout"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "Register"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "PasswordRecovery"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "PasswordRecoveryConfirm"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "AccountActivation"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "CheckUsernameAvailability"), + new Tuple("SmartStore.Web.Controllers.MenuController", "OffCanvas"), + new Tuple("SmartStore.Web.Controllers.ShoppingCartController", "CartSummary") + }; + + public Lazy PermissionService { get; set; } - public Lazy PermissionService { get; set; } - - public virtual void OnAuthorization(AuthorizationContext filterContext) + public virtual void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null || filterContext.HttpContext == null) return; @@ -36,13 +37,13 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) if (filterContext.IsChildAction) return; - string actionName = filterContext.ActionDescriptor.ActionName; - if (String.IsNullOrEmpty(actionName)) - return; + string actionName = filterContext.ActionDescriptor.ActionName; + if (String.IsNullOrEmpty(actionName)) + return; - string controllerName = filterContext.Controller.ToString(); - if (String.IsNullOrEmpty(controllerName)) - return; + string controllerName = filterContext.Controller.ToString(); + if (String.IsNullOrEmpty(controllerName)) + return; if (!DataSettings.DatabaseIsInstalled()) return; @@ -68,17 +69,17 @@ protected virtual bool HasStoreAccess() return false; } - private static bool IsPermittedRoute(string controllerName, string actionName) - { - foreach (var route in s_permittedRoutes) - { - if (controllerName.IsCaseInsensitiveEqual(route.Item1) && actionName.IsCaseInsensitiveEqual(route.Item2)) - { - return true; - } - } + private static bool IsPermittedRoute(string controllerName, string actionName) + { + foreach (var route in s_permittedRoutes) + { + if (controllerName.IsCaseInsensitiveEqual(route.Item1) && actionName.IsCaseInsensitiveEqual(route.Item2)) + { + return true; + } + } - return false; - } - } + return false; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/ResponseFilterStream.cs b/src/Presentation/SmartStore.Web.Framework/Filters/ResponseFilterStream.cs index d6e0948edb..3b7905ca67 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/ResponseFilterStream.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/ResponseFilterStream.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Web; namespace SmartStore.Web.Framework.Filters @@ -62,19 +58,13 @@ public ResponseFilterStream(Stream innerStream, HttpContextBase httpContext, int /// /// Determines whether the stream is captured /// - private bool IsCaptured - { - get => CaptureStream != null || CaptureString != null || TransformStream != null || TransformString != null; - } + private bool IsCaptured => CaptureStream != null || CaptureString != null || TransformStream != null || TransformString != null; /// /// Determines whether the Write method is outputting data immediately /// or delaying output until Flush() is fired. /// - private bool IsOutputDelayed - { - get => TransformStream != null || TransformString != null; - } + private bool IsOutputDelayed => TransformStream != null || TransformString != null; /// @@ -203,40 +193,40 @@ internal MemoryStream OnTransformCompleteStringInternal(MemoryStream ms) return ms; } - public override bool CanRead + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => true; + + public override long Length => 0; + + public override long Position { - get => true; + get => _position; + set => _position = value; } - public override bool CanSeek + public override long Seek(long offset, SeekOrigin direction) { - get => true; + return _innerStream.Seek(offset, direction); } - public override bool CanWrite + public override void SetLength(long length) { - get => true; + _innerStream.SetLength(length); } - public override long Length + public override void Close() { - get => 0; + _innerStream.Close(); } - public override long Position + public override int Read(byte[] buffer, int offset, int count) { - get { return _position; } - set { _position = value; } + return _innerStream.Read(buffer, offset, count); } - public override long Seek(long offset, SeekOrigin direction) => _innerStream.Seek(offset, direction); - - public override void SetLength(long length) => _innerStream.SetLength(length); - - public override void Close() => _innerStream.Close(); - - public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); - /// /// Overriden to capture output written by ASP.NET and captured /// into a cached stream that is written out later when Flush() is called. diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreClosedAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreClosedAttribute.cs index ac4c1c9f3d..43cbd1dce2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreClosedAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreClosedAttribute.cs @@ -13,84 +13,84 @@ namespace SmartStore.Web.Framework.Filters { public class StoreClosedAttribute : FilterAttribute, IAuthorizationFilter { - private static readonly List> s_permittedRoutes = new List> - { - new Tuple("SmartStore.Web.Controllers.CustomerController", "Login"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "Logout"), - new Tuple("SmartStore.Web.Controllers.HomeController", "StoreClosed"), - new Tuple("SmartStore.Web.Controllers.CommonController", "SetLanguage") - }; + private static readonly List> s_permittedRoutes = new List> + { + new Tuple("SmartStore.Web.Controllers.CustomerController", "Login"), + new Tuple("SmartStore.Web.Controllers.CustomerController", "Logout"), + new Tuple("SmartStore.Web.Controllers.HomeController", "StoreClosed"), + new Tuple("SmartStore.Web.Controllers.CommonController", "SetLanguage") + }; + + public Lazy Localizer { get; set; } + public Lazy Notifier { get; set; } + public Lazy WorkContext { get; set; } + public Lazy StoreInformationSettings { get; set; } - public Lazy Localizer { get; set; } - public Lazy Notifier { get; set; } - public Lazy WorkContext { get; set; } - public Lazy StoreInformationSettings { get; set; } - - public virtual void OnAuthorization(AuthorizationContext filterContext) + public virtual void OnAuthorization(AuthorizationContext filterContext) { - if (filterContext == null || filterContext.HttpContext == null) - return; + if (filterContext == null || filterContext.HttpContext == null) + return; - HttpRequestBase request = filterContext.HttpContext.Request; - if (request == null) - return; + HttpRequestBase request = filterContext.HttpContext.Request; + if (request == null) + return; - // Don't apply filter to child methods - if (filterContext.IsChildAction) - return; + // Don't apply filter to child methods + if (filterContext.IsChildAction) + return; - string actionName = filterContext.ActionDescriptor.ActionName; - if (String.IsNullOrEmpty(actionName)) - return; + string actionName = filterContext.ActionDescriptor.ActionName; + if (String.IsNullOrEmpty(actionName)) + return; - string controllerName = filterContext.Controller.ToString(); - if (String.IsNullOrEmpty(controllerName)) - return; + string controllerName = filterContext.Controller.ToString(); + if (String.IsNullOrEmpty(controllerName)) + return; - if (!DataSettings.DatabaseIsInstalled()) - return; + if (!DataSettings.DatabaseIsInstalled()) + return; - var storeInformationSettings = StoreInformationSettings.Value; - if (!storeInformationSettings.StoreClosed) - return; + var storeInformationSettings = StoreInformationSettings.Value; + if (!storeInformationSettings.StoreClosed) + return; - if (!IsPermittedRoute(controllerName, actionName)) - { - if (storeInformationSettings.StoreClosedAllowForAdmins && WorkContext.Value.CurrentCustomer.IsAdmin()) - { - //do nothing - allow admin access - } - else - { - if (request.IsAjaxRequest()) - { - var storeClosedMessage = "{0} {1}".FormatCurrentUI( - Localizer.Value.GetResource("StoreClosed", 0, false), - Localizer.Value.GetResource("StoreClosed.Hint", 0, false)); - Notifier.Value.Error(storeClosedMessage); + if (!IsPermittedRoute(controllerName, actionName)) + { + if (storeInformationSettings.StoreClosedAllowForAdmins && WorkContext.Value.CurrentCustomer.IsAdmin()) + { + //do nothing - allow admin access + } + else + { + if (request.IsAjaxRequest()) + { + var storeClosedMessage = "{0} {1}".FormatCurrentUI( + Localizer.Value.GetResource("StoreClosed", 0, false), + Localizer.Value.GetResource("StoreClosed.Hint", 0, false)); + Notifier.Value.Error(storeClosedMessage); - //filterContext.Result = new ContentResult { Content = "", ContentType = "text/html" }; - } - else - { - var storeClosedUrl = new UrlHelper(filterContext.RequestContext).RouteUrl("StoreClosed"); - filterContext.Result = new RedirectResult(storeClosedUrl); - } - } - } - } + //filterContext.Result = new ContentResult { Content = "", ContentType = "text/html" }; + } + else + { + var storeClosedUrl = new UrlHelper(filterContext.RequestContext).RouteUrl("StoreClosed"); + filterContext.Result = new RedirectResult(storeClosedUrl); + } + } + } + } - private static bool IsPermittedRoute(string controllerName, string actionName) - { - foreach (var route in s_permittedRoutes) - { - if (controllerName.IsCaseInsensitiveEqual(route.Item1) && actionName.IsCaseInsensitiveEqual(route.Item2)) - { - return true; - } - } + private static bool IsPermittedRoute(string controllerName, string actionName) + { + foreach (var route in s_permittedRoutes) + { + if (controllerName.IsCaseInsensitiveEqual(route.Item1) && actionName.IsCaseInsensitiveEqual(route.Item2)) + { + return true; + } + } - return false; - } - } + return false; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs index 8a65cd046a..aa57f5c33f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs @@ -9,12 +9,12 @@ namespace SmartStore.Web.Framework.Filters { public class StoreIpAddressAttribute : FilterAttribute, IActionFilter { - public Lazy WebHelper { get; set; } - public Lazy WorkContext { get; set; } - public Lazy CustomerService { get; set; } - public Lazy PrivacySettings { get; set; } + public Lazy WebHelper { get; set; } + public Lazy WorkContext { get; set; } + public Lazy CustomerService { get; set; } + public Lazy PrivacySettings { get; set; } - public virtual void OnActionExecuting(ActionExecutingContext filterContext) + public virtual void OnActionExecuting(ActionExecutingContext filterContext) { if (!DataSettings.DatabaseIsInstalled()) return; @@ -30,26 +30,38 @@ public virtual void OnActionExecuting(ActionExecutingContext filterContext) if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return; - if (!PrivacySettings.Value.StoreLastIpAddress) - return; + if (!PrivacySettings.Value.StoreLastIpAddress) + return; + + // Update IP address. + var currentIpAddress = WebHelper.Value.GetCurrentIpAddress(); - // Update IP address. - var webHelper = WebHelper.Value; - var currentIpAddress = webHelper.GetCurrentIpAddress(); - if (!String.IsNullOrEmpty(currentIpAddress)) + if (!string.IsNullOrEmpty(currentIpAddress)) { var customer = WorkContext.Value.CurrentCustomer; - if (customer != null && !currentIpAddress.Equals(customer.LastIpAddress, StringComparison.InvariantCultureIgnoreCase)) + + if (customer != null && !customer.Deleted && !customer.IsSystemAccount && + !currentIpAddress.Equals(customer.LastIpAddress, StringComparison.InvariantCultureIgnoreCase)) { - var customerService = CustomerService.Value; - customer.LastIpAddress = currentIpAddress; - customerService.UpdateCustomer(customer); + try + { + customer.LastIpAddress = currentIpAddress; + CustomerService.Value.UpdateCustomer(customer); + } + catch (InvalidOperationException ioe) + { + // The exception may occur on the first call after a migration. + if (!ioe.IsAlreadyAttachedEntityException()) + { + throw; + } + } } } } - public virtual void OnActionExecuted(ActionExecutedContext filterContext) - { - } - } + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs index fdc82d72c6..a9392a363c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs @@ -10,64 +10,73 @@ namespace SmartStore.Web.Framework.Filters { public class StoreLastVisitedPageAttribute : FilterAttribute, IActionFilter { - public Lazy WebHelper { get; set; } - public Lazy WorkContext { get; set; } - public Lazy CustomerSettings { get; set; } - public Lazy GenericAttributeService { get; set; } - public Lazy UserAgent { get; set; } + public Lazy WebHelper { get; set; } + public Lazy WorkContext { get; set; } + public Lazy CustomerSettings { get; set; } + public Lazy GenericAttributeService { get; set; } + public Lazy UserAgent { get; set; } public Lazy CustomerService { get; set; } public virtual void OnActionExecuting(ActionExecutingContext filterContext) { - if (!DataSettings.DatabaseIsInstalled()) + if (!DataSettings.DatabaseIsInstalled()) return; if (filterContext == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) return; - // don't apply filter to child methods + // Don't apply filter to child methods. if (filterContext.IsChildAction) return; - //only GET requests + // Only GET requests. if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return; if (!CustomerSettings.Value.StoreLastVisitedPage) return; - var customer = WorkContext.Value.CurrentCustomer; + var customer = WorkContext.Value.CurrentCustomer; - if (customer == null || customer.IsSystemAccount) - return; - - var pageUrl = WebHelper.Value.GetThisPageUrl(true); - var userAgent = UserAgent.Value.RawValue; + if (customer == null || customer.Deleted || customer.IsSystemAccount) + return; - var genericAttributeService = GenericAttributeService.Value; + var pageUrl = WebHelper.Value.GetThisPageUrl(true); + var userAgent = UserAgent.Value.RawValue; - if (pageUrl.HasValue()) + if (pageUrl.HasValue()) { - var previousPageUrl = customer.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage); - if (!pageUrl.IsCaseInsensitiveEqual(previousPageUrl)) - { - genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.LastVisitedPage, pageUrl); - } + var previousPageUrl = customer.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage); + if (!pageUrl.IsCaseInsensitiveEqual(previousPageUrl)) + { + GenericAttributeService.Value.SaveAttribute(customer, SystemCustomerAttributeNames.LastVisitedPage, pageUrl); + } } - if (userAgent.HasValue()) - { - var previousUserAgent = customer.LastUserAgent; - if (!userAgent.IsCaseInsensitiveEqual(previousUserAgent)) - { - customer.LastUserAgent = userAgent; - CustomerService.Value.UpdateCustomer(customer); - } - } - } + if (userAgent.HasValue()) + { + var previousUserAgent = customer.LastUserAgent; + if (!userAgent.IsCaseInsensitiveEqual(previousUserAgent)) + { + try + { + customer.LastUserAgent = userAgent; + CustomerService.Value.UpdateCustomer(customer); + } + catch (InvalidOperationException ioe) + { + // The exception may occur on the first call after a migration. + if (!ioe.IsAlreadyAttachedEntityException()) + { + throw; + } + } + } + } + } - public virtual void OnActionExecuted(ActionExecutedContext filterContext) - { - } - } + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs index 2721a44f65..93eddf7752 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs @@ -7,12 +7,12 @@ namespace SmartStore.Web.Framework.Filters [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public class UnitOfWorkAttribute : ActionFilterAttribute { - public UnitOfWorkAttribute() + public UnitOfWorkAttribute() : this(null, int.MaxValue) { } - public UnitOfWorkAttribute(int ordinal) + public UnitOfWorkAttribute(int ordinal) : this(null, ordinal) { } @@ -28,7 +28,7 @@ public UnitOfWorkAttribute(string alias, int ordinal) base.Order = ordinal; } - public Func DbContext { get; set; } + public Func DbContext { get; set; } public string Alias { get; set; } @@ -37,8 +37,8 @@ public override void OnActionExecuted(ActionExecutedContext filterContext) if (filterContext.Exception != null) return; - var context = DbContext(this.Alias); - + var context = DbContext(this.Alias); + if (context != null && context.HasChanges) { try diff --git a/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs b/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs index 5dee2d6c96..13a5991802 100644 --- a/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs +++ b/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs @@ -26,8 +26,8 @@ public partial class FrameworkCacheConsumer : DbSaveHook, IConsumer /// {1} : store identifier /// public const string THEMEVARS_KEY = "fw:themevars-{0}-{1}"; - public const string THEMEVARS_THEME_KEY = "fw:themevars-{0}"; - + public const string THEMEVARS_THEME_KEY = "fw:themevars-{0}"; + /// /// Key for tax display type caching /// @@ -38,75 +38,75 @@ public partial class FrameworkCacheConsumer : DbSaveHook, IConsumer public const string CUSTOMERROLES_TAX_DISPLAY_TYPES_KEY = "fw:customerroles:taxdisplaytypes-{0}-{1}"; public const string CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY = "fw:customerroles:taxdisplaytypes*"; - private readonly ICacheManager _cacheManager; - private readonly IAssetCache _assetCache; + private readonly ICacheManager _cacheManager; + private readonly IAssetCache _assetCache; + + // Item1 = ThemeName, Item2 = StoreId + private HashSet> _themeScopes; + + public FrameworkCacheConsumer(ICacheManager cacheManager, IAssetCache assetCache) + { + _cacheManager = cacheManager; + _assetCache = assetCache; + } + + public void HandleEvent(ThemeTouchedEvent eventMessage) + { + var cacheKey = BuildThemeVarsCacheKey(eventMessage.ThemeName, 0); + HttpRuntime.Cache.RemoveByPattern(cacheKey); + } + + public override void OnAfterSave(IHookedEntity entry) + { + if (entry.Entity is ThemeVariable) + { + var themeVar = entry.Entity as ThemeVariable; + AddEvictableThemeScope(themeVar.Theme, themeVar.StoreId); + } + else if (entry.Entity is CustomerRole) + { + _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); + } + else if (entry.Entity is Setting && entry.InitialState == EntityState.Modified) + { + var setting = entry.Entity as Setting; + if (setting.Name.IsCaseInsensitiveEqual(TypeHelper.NameOf(x => x.TaxDisplayType, true))) + { + _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); // depends on TaxSettings.TaxDisplayType + } + } + else + { + throw new NotImplementedException(); + } + } + + public override void OnAfterSaveCompleted() + { + FlushThemeVarsCacheEviction(); + } - // Item1 = ThemeName, Item2 = StoreId - private HashSet> _themeScopes; + #region Helpers - public FrameworkCacheConsumer(ICacheManager cacheManager, IAssetCache assetCache) + private void AddEvictableThemeScope(string themeName, int storeId) { - _cacheManager = cacheManager; - _assetCache = assetCache; + if (_themeScopes == null) + _themeScopes = new HashSet>(); + _themeScopes.Add(new Tuple(themeName, storeId)); } - public void HandleEvent(ThemeTouchedEvent eventMessage) - { - var cacheKey = BuildThemeVarsCacheKey(eventMessage.ThemeName, 0); - HttpRuntime.Cache.RemoveByPattern(cacheKey); - } - - public override void OnAfterSave(IHookedEntity entry) - { - if (entry.Entity is ThemeVariable) - { - var themeVar = entry.Entity as ThemeVariable; - AddEvictableThemeScope(themeVar.Theme, themeVar.StoreId); - } - else if (entry.Entity is CustomerRole) - { - _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); - } - else if (entry.Entity is Setting && entry.InitialState == EntityState.Modified) - { - var setting = entry.Entity as Setting; - if (setting.Name.IsCaseInsensitiveEqual(TypeHelper.NameOf(x => x.TaxDisplayType, true))) - { - _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); // depends on TaxSettings.TaxDisplayType - } - } - else - { - throw new NotImplementedException(); - } - } - - public override void OnAfterSaveCompleted() - { - FlushThemeVarsCacheEviction(); - } - - #region Helpers - - private void AddEvictableThemeScope(string themeName, int storeId) - { - if (_themeScopes == null) - _themeScopes = new HashSet>(); - _themeScopes.Add(new Tuple(themeName, storeId)); - } - - private void FlushThemeVarsCacheEviction() - { - if (_themeScopes == null || _themeScopes.Count == 0) - return; - - foreach (var scope in _themeScopes) - { - HttpRuntime.Cache.Remove(BuildThemeVarsCacheKey(scope.Item1 /* ThemeName */, scope.Item2 /* StoreId */)); - } - - _themeScopes.Clear(); - } + private void FlushThemeVarsCacheEviction() + { + if (_themeScopes == null || _themeScopes.Count == 0) + return; + + foreach (var scope in _themeScopes) + { + HttpRuntime.Cache.Remove(BuildThemeVarsCacheKey(scope.Item1 /* ThemeName */, scope.Item2 /* StoreId */)); + } + + _themeScopes.Clear(); + } private static string BuildThemeVarsCacheKey(ThemeVariable entity) { @@ -115,14 +115,14 @@ private static string BuildThemeVarsCacheKey(ThemeVariable entity) internal static string BuildThemeVarsCacheKey(string themeName, int storeId) { - if (storeId > 0) - { - return HttpRuntime.Cache.BuildScopedKey(THEMEVARS_KEY.FormatInvariant(themeName, storeId)); - } + if (storeId > 0) + { + return HttpRuntime.Cache.BuildScopedKey(THEMEVARS_KEY.FormatInvariant(themeName, storeId)); + } - return HttpRuntime.Cache.BuildScopedKey(THEMEVARS_THEME_KEY.FormatInvariant(themeName)); + return HttpRuntime.Cache.BuildScopedKey(THEMEVARS_THEME_KEY.FormatInvariant(themeName)); } #endregion - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/ErrorMessageProvider.cs b/src/Presentation/SmartStore.Web.Framework/Localization/ErrorMessageProvider.cs index c927fc2d38..972cb931f1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/ErrorMessageProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/ErrorMessageProvider.cs @@ -4,74 +4,74 @@ namespace SmartStore.Web.Framework.Localization { - public class ErrorMessageProvider - { - private static string _resourceClassKey; - - public static void SetResourceClassKey(string resourceClassKey) - { - if (resourceClassKey == null) - { - throw new ArgumentNullException("resourceClassKey"); - } + public class ErrorMessageProvider + { + private static string _resourceClassKey; - _resourceClassKey = resourceClassKey; + public static void SetResourceClassKey(string resourceClassKey) + { + if (resourceClassKey == null) + { + throw new ArgumentNullException("resourceClassKey"); + } - ModelBinderErrorMessageProviders.ValueRequiredErrorMessageProvider = ErrorMessageProvider.DefaultValueRequiredErrorMessageProvider; - ModelBinderErrorMessageProviders.TypeConversionErrorMessageProvider = ErrorMessageProvider.DefaultTypeConversionErrorMessageProvider; - } + _resourceClassKey = resourceClassKey; - private static string DefaultTypeConversionErrorMessageProvider(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue) - { - return GetResourceCommon(modelBindingExecutionContext, modelMetadata, incomingValue, new Func(GetValueInvalidResource)); - } - - private static string DefaultValueRequiredErrorMessageProvider(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue) - { - return GetResourceCommon(modelBindingExecutionContext, modelMetadata, incomingValue, new Func(GetValueRequiredResource)); - } + ModelBinderErrorMessageProviders.ValueRequiredErrorMessageProvider = ErrorMessageProvider.DefaultValueRequiredErrorMessageProvider; + ModelBinderErrorMessageProviders.TypeConversionErrorMessageProvider = ErrorMessageProvider.DefaultTypeConversionErrorMessageProvider; + } - private static string GetResourceCommon(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue, - Func resourceAccessor) - { - string displayName = modelMetadata.GetDisplayName(); - string str = resourceAccessor(modelBindingExecutionContext); - object[] objArray = new object[2]; + private static string DefaultTypeConversionErrorMessageProvider(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue) + { + return GetResourceCommon(modelBindingExecutionContext, modelMetadata, incomingValue, new Func(GetValueInvalidResource)); + } - objArray[0] = incomingValue; - objArray[1] = displayName; + private static string DefaultValueRequiredErrorMessageProvider(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue) + { + return GetResourceCommon(modelBindingExecutionContext, modelMetadata, incomingValue, new Func(GetValueRequiredResource)); + } - string str1 = string.Format(CultureInfo.CurrentCulture, str, objArray); - return str1; - } + private static string GetResourceCommon(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue, + Func resourceAccessor) + { + string displayName = modelMetadata.GetDisplayName(); + string str = resourceAccessor(modelBindingExecutionContext); + object[] objArray = new object[2]; - private static string GetUserResourceString(ModelBindingExecutionContext modelBindingExecutionContext, string resourceName, string resourceClassKey) - { - if (string.IsNullOrEmpty(resourceClassKey) || modelBindingExecutionContext == null || modelBindingExecutionContext.HttpContext == null) - { - return null; - } - else - { - return modelBindingExecutionContext.HttpContext.GetGlobalResourceObject(resourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string; - } - } - - private static string GetUserResourceString(ModelBindingExecutionContext modelBindingExecutionContext, string resourceName) - { - return GetUserResourceString(modelBindingExecutionContext, resourceName, _resourceClassKey); - } - - private static string GetValueInvalidResource(ModelBindingExecutionContext modelBindingExecutionContext) - { - string userResourceString = GetUserResourceString(modelBindingExecutionContext, "PropertyValueInvalid"); - return userResourceString; - } - - private static string GetValueRequiredResource(ModelBindingExecutionContext modelBindingExecutionContext) - { - string userResourceString = GetUserResourceString(modelBindingExecutionContext, "PropertyValueRequired"); - return userResourceString; - } - } + objArray[0] = incomingValue; + objArray[1] = displayName; + + string str1 = string.Format(CultureInfo.CurrentCulture, str, objArray); + return str1; + } + + private static string GetUserResourceString(ModelBindingExecutionContext modelBindingExecutionContext, string resourceName, string resourceClassKey) + { + if (string.IsNullOrEmpty(resourceClassKey) || modelBindingExecutionContext == null || modelBindingExecutionContext.HttpContext == null) + { + return null; + } + else + { + return modelBindingExecutionContext.HttpContext.GetGlobalResourceObject(resourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string; + } + } + + private static string GetUserResourceString(ModelBindingExecutionContext modelBindingExecutionContext, string resourceName) + { + return GetUserResourceString(modelBindingExecutionContext, resourceName, _resourceClassKey); + } + + private static string GetValueInvalidResource(ModelBindingExecutionContext modelBindingExecutionContext) + { + string userResourceString = GetUserResourceString(modelBindingExecutionContext, "PropertyValueInvalid"); + return userResourceString; + } + + private static string GetValueRequiredResource(ModelBindingExecutionContext modelBindingExecutionContext) + { + string userResourceString = GetUserResourceString(modelBindingExecutionContext, "PropertyValueRequired"); + return userResourceString; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs b/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs index d91f9f2ad8..e9524c34ea 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs @@ -6,13 +6,13 @@ public interface ILocalizedModel { } - public interface ILocalizedModel : ILocalizedModel where T : ILocalizedModelLocal + public interface ILocalizedModel : ILocalizedModel where T : ILocalizedModelLocal { IList Locales { get; set; } } - public interface ILocalizedModelLocal - { - int LanguageId { get; set; } - } -} + public interface ILocalizedModelLocal + { + int LanguageId { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs b/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs index ec648c3377..732cea82eb 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs @@ -1,11 +1,10 @@ -using System; -using SmartStore.Core.Localization; +using SmartStore.Core.Localization; namespace SmartStore.Web.Framework.Localization { - public interface IText - { - LocalizedString Get(string key, params object[] args); - LocalizedString GetEx(string key, int languageId, params object[] args); - } + public interface IText + { + LocalizedString Get(string key, params object[] args); + LocalizedString GetEx(string key, int languageId, params object[] args); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/LanguageSeoCodeAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/LanguageSeoCodeAttribute.cs index baadafd956..bdb4489842 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/LanguageSeoCodeAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/LanguageSeoCodeAttribute.cs @@ -1,6 +1,4 @@ using System; -using System.Net; -using System.Web; using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Data; @@ -15,9 +13,9 @@ namespace SmartStore.Web.Framework.Localization /// public class LanguageSeoCodeAttribute : FilterAttribute, IAuthorizationFilter { - public Lazy WorkContext { get; set; } - public Lazy LanguageService { get; set; } - public Lazy LocalizationSettings { get; set; } + public Lazy WorkContext { get; set; } + public Lazy LanguageService { get; set; } + public Lazy LocalizationSettings { get; set; } public virtual void OnAuthorization(AuthorizationContext filterContext) { @@ -37,30 +35,30 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) if (!(filterContext.RouteData?.Route is LocalizedRoute)) return; - if (!DataSettings.DatabaseIsInstalled()) + if (!DataSettings.DatabaseIsInstalled()) return; var localizationSettings = LocalizationSettings.Value; if (!localizationSettings.SeoFriendlyUrlsForLanguagesEnabled) return; - + // Process current URL - var workContext = WorkContext.Value; - var languageService = LanguageService.Value; + var workContext = WorkContext.Value; + var languageService = LanguageService.Value; var workingLanguage = workContext.WorkingLanguage; var helper = new LocalizedUrlHelper(request, true); - var defaultSeoCode = languageService.GetDefaultLanguageSeoCode(); - - if (helper.IsLocalizedUrl(out var seoCode)) + var defaultSeoCode = languageService.GetDefaultLanguageSeoCode(); + + if (helper.IsLocalizedUrl(out var seoCode)) { - if (!languageService.IsPublishedLanguage(seoCode)) + if (!languageService.IsPublishedLanguage(seoCode)) { - // Language is not defined in system or not assigned to store - if (localizationSettings.InvalidLanguageRedirectBehaviour == InvalidLanguageRedirectBehaviour.ReturnHttp404) + // Language is not defined in system or not assigned to store + if (localizationSettings.InvalidLanguageRedirectBehaviour == InvalidLanguageRedirectBehaviour.ReturnHttp404) { filterContext.Result = HandleExceptionFilter.Create404Result(filterContext); - var seoCodeReplacement = localizationSettings.DefaultLanguageRedirectBehaviour == DefaultLanguageRedirectBehaviour.PrependSeoCodeAndRedirect + var seoCodeReplacement = localizationSettings.DefaultLanguageRedirectBehaviour == DefaultLanguageRedirectBehaviour.PrependSeoCodeAndRedirect ? workingLanguage.GetTwoLetterISOLanguageName() : string.Empty; diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs index 12567ecccd..dba94ca1cc 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs @@ -18,7 +18,7 @@ public class LocalizedRoute : Route, ICloneable #region Fields private static bool? _seoFriendlyUrlsForLanguagesEnabled; - private string _leftPart; + private string _leftPart; #endregion @@ -96,16 +96,16 @@ public override RouteData GetRouteData(HttpContextBase httpContext) } } - if (_leftPart == null) - { - var url = this.Url; - int idx = url.IndexOf('{'); - _leftPart = "~/" + (idx >= 0 ? url.Substring(0, idx) : url).TrimEnd('/'); - } + if (_leftPart == null) + { + var url = this.Url; + int idx = url.IndexOf('{'); + _leftPart = "~/" + (idx >= 0 ? url.Substring(0, idx) : url).TrimEnd('/'); + } - // Perf - if (!httpContext.Request.AppRelativeCurrentExecutionFilePath.StartsWith(_leftPart, true, CultureInfo.InvariantCulture)) - return null; + // Perf + if (!httpContext.Request.AppRelativeCurrentExecutionFilePath.StartsWith(_leftPart, true, CultureInfo.InvariantCulture)) + return null; RouteData data = base.GetRouteData(httpContext); return data; @@ -126,8 +126,8 @@ public override VirtualPathData GetVirtualPath(RequestContext requestContext, Ro if (data != null && DataSettings.DatabaseIsInstalled() && SeoFriendlyUrlsForLanguagesEnabled) { var helper = new LocalizedUrlHelper(requestContext.HttpContext.Request, true); - if (helper.IsLocalizedUrl(out string cultureCode)) - { + if (helper.IsLocalizedUrl(out string cultureCode)) + { if (requestContext.RouteData.DataTokens.Get("SeoCodeReplacement") is string seoCodeReplacement) { // The LanguageSeoCode filter detected a localized URL, but the locale does not exist or is inactive. @@ -142,7 +142,7 @@ public override VirtualPathData GetVirtualPath(RequestContext requestContext, Ro data.VirtualPath = String.Concat(cultureCode, "/", data.VirtualPath).TrimEnd('/'); } } - } + } return data; } @@ -163,16 +163,16 @@ protected internal static bool SeoFriendlyUrlsForLanguagesEnabled get { if (_seoFriendlyUrlsForLanguagesEnabled == null && EngineContext.Current.IsFullyInitialized) - { - try - { - var enabled = EngineContext.Current.Resolve().SeoFriendlyUrlsForLanguagesEnabled; - _seoFriendlyUrlsForLanguagesEnabled = enabled; - } - catch { } - } - - // Assume is enabled on very first request to prevent IIS 404 with localized URLs + { + try + { + var enabled = EngineContext.Current.Resolve().SeoFriendlyUrlsForLanguagesEnabled; + _seoFriendlyUrlsForLanguagesEnabled = enabled; + } + catch { } + } + + // Assume is enabled on very first request to prevent IIS 404 with localized URLs return _seoFriendlyUrlsForLanguagesEnabled ?? true; } } @@ -183,16 +183,16 @@ protected internal static bool SeoFriendlyUrlsForLanguagesEnabled public LocalizedRoute Clone() { - var clone = new LocalizedRoute(this.Url, - new RouteValueDictionary(this.Defaults), - new RouteValueDictionary(this.Constraints), - new RouteValueDictionary(this.DataTokens), - new MvcRouteHandler()) - { - RouteExistingFiles = this.RouteExistingFiles, - IsClone = true - }; - return clone; + var clone = new LocalizedRoute(this.Url, + new RouteValueDictionary(this.Defaults), + new RouteValueDictionary(this.Constraints), + new RouteValueDictionary(this.DataTokens), + new MvcRouteHandler()) + { + RouteExistingFiles = this.RouteExistingFiles, + IsClone = true + }; + return clone; } object ICloneable.Clone() diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRouteExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRouteExtensions.cs index 1b039c9e98..aeb0c37107 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRouteExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRouteExtensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using System.Web.Mvc; +using System.Web.Mvc; using System.Web.Routing; namespace SmartStore.Web.Framework.Localization @@ -64,7 +62,7 @@ private static Route MapLocalizedRouteInternal(this RouteCollection routes, stri if (add) { routes.Add(name, route); - } + } return route; } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs index 6a4009863c..d7f61ca73d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Text; using System.Threading; -using System.Web; using System.Web.Mvc; using Newtonsoft.Json; using SmartStore.ComponentModel; using SmartStore.Core; using SmartStore.Core.Data; -using SmartStore.Core.Domain.Localization; using SmartStore.Utilities.ObjectPools; using SmartStore.Web.Framework.UI; @@ -20,152 +17,161 @@ namespace SmartStore.Web.Framework.Localization /// public class SetWorkingCultureAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter { - public Lazy WorkContext { get; set; } - public Lazy AssetBuilder { get; set; } + // DIN 5008. + private static string[] _deMonthAbbreviations = new[] { "Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez.", "" }; - public void OnAuthorization(AuthorizationContext filterContext) + public Lazy WorkContext { get; set; } + public Lazy AssetBuilder { get; set; } + + public void OnAuthorization(AuthorizationContext filterContext) { var request = filterContext?.HttpContext?.Request; if (request == null) return; - if (filterContext.IsChildAction) - return; + if (filterContext.IsChildAction) + return; - if (!DataSettings.DatabaseIsInstalled()) + if (!DataSettings.DatabaseIsInstalled()) return; var workContext = WorkContext.Value; + var language = workContext.WorkingLanguage; + + var culture = workContext.CurrentCustomer != null && language != null + ? new CultureInfo(language.LanguageCulture) + : new CultureInfo("en-US"); - CultureInfo culture = workContext.CurrentCustomer != null && workContext.WorkingLanguage != null - ? new CultureInfo(workContext.WorkingLanguage.LanguageCulture) - : new CultureInfo("en-US"); + if (language?.UniqueSeoCode?.IsCaseInsensitiveEqual("de") ?? false) + { + culture.DateTimeFormat.AbbreviatedMonthNames = culture.DateTimeFormat.AbbreviatedMonthGenitiveNames = _deMonthAbbreviations; + } Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; } - public void OnActionExecuting(ActionExecutingContext filterContext) - { - } - - public void OnActionExecuted(ActionExecutedContext filterContext) - { - if (filterContext.IsChildAction) - return; - - if (!DataSettings.DatabaseIsInstalled()) - return; - - if (!(filterContext.Result is ViewResult)) - return; - - var culture = Thread.CurrentThread.CurrentUICulture; - if (culture.Name == "en-US") - return; - - var builder = AssetBuilder.Value; - var json = CreateCultureJson(culture); - - var sb = PooledStringBuilder.Rent(); - sb.Append(""); - - var script = sb.ToStringAndReturn(); - - builder.AppendCustomHeadParts(script); - } - - private string CreateCultureJson(CultureInfo ci) - { - var nf = ci.NumberFormat; - var df = ci.DateTimeFormat; - - var dict = new Dictionary - { - { "name", ci.Name }, - { "englishName", ci.EnglishName }, - { "nativeName", ci.NativeName }, - { "isRTL", WorkContext.Value.WorkingLanguage?.Rtl ?? ci.TextInfo.IsRightToLeft }, // favor RTL property of Language + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext.IsChildAction) + return; + + if (!DataSettings.DatabaseIsInstalled()) + return; + + if (!(filterContext.Result is ViewResult)) + return; + + var culture = Thread.CurrentThread.CurrentUICulture; + if (culture.Name == "en-US") + return; + + var builder = AssetBuilder.Value; + var json = CreateCultureJson(culture); + + var sb = PooledStringBuilder.Rent(); + sb.Append(""); + + var script = sb.ToStringAndReturn(); + + builder.AppendCustomHeadParts(script); + } + + private string CreateCultureJson(CultureInfo ci) + { + var nf = ci.NumberFormat; + var df = ci.DateTimeFormat; + + var dict = new Dictionary + { + { "name", ci.Name }, + { "englishName", ci.EnglishName }, + { "nativeName", ci.NativeName }, + { "isRTL", WorkContext.Value.WorkingLanguage?.Rtl ?? ci.TextInfo.IsRightToLeft }, // favor RTL property of Language { "language", ci.TwoLetterISOLanguageName }, - { "numberFormat", new Dictionary - { - { ",", nf.NumberGroupSeparator }, - { ".", nf.NumberDecimalSeparator }, - { "pattern", new[] { nf.NumberNegativePattern } }, - { "decimals", nf.NumberDecimalDigits }, - { "groupSizes", nf.NumberGroupSizes }, - { "+", nf.PositiveSign }, - { "-", nf.NegativeSign }, - { "NaN", nf.NaNSymbol }, - { "negativeInfinity", nf.NegativeInfinitySymbol }, - { "positiveInfinity", nf.PositiveInfinitySymbol }, - { "percent", new Dictionary - { - { ",", nf.PercentGroupSeparator }, - { ".", nf.PercentDecimalSeparator }, - { "pattern", new[] { nf.PercentNegativePattern, nf.PercentPositivePattern } }, - { "decimals", nf.PercentDecimalDigits }, - { "groupSizes", nf.PercentGroupSizes }, - { "symbol", nf.PercentSymbol } - } }, - { "currency", new Dictionary - { - { ",", nf.CurrencyGroupSeparator }, - { ".", nf.CurrencyDecimalSeparator }, - { "pattern", new[] { nf.CurrencyNegativePattern, nf.CurrencyPositivePattern } }, - { "decimals", nf.CurrencyDecimalDigits }, - { "groupSizes", nf.CurrencyGroupSizes }, - { "symbol", nf.CurrencySymbol } - } }, - } }, - { "dateTimeFormat", new Dictionary - { - { "calendarName", df.NativeCalendarName }, - { "/", df.DateSeparator }, - { ":", df.TimeSeparator }, - { "firstDay", (int)df.FirstDayOfWeek }, - { "twoDigitYearMax", ci.Calendar.TwoDigitYearMax }, - { "AM", df.AMDesignator.IsEmpty() ? null : new[] { df.AMDesignator, df.AMDesignator.ToLower(), df.AMDesignator.ToUpper() } }, - { "PM", df.PMDesignator.IsEmpty() ? null : new[] { df.PMDesignator, df.PMDesignator.ToLower(), df.PMDesignator.ToUpper() } }, - { "days", new Dictionary - { - { "names", df.DayNames }, - { "namesAbbr", df.AbbreviatedDayNames }, - { "namesShort", df.ShortestDayNames }, - } }, - { "months", new Dictionary - { - { "names", df.MonthNames }, - { "namesAbbr", df.AbbreviatedMonthNames }, - } }, - { "patterns", new Dictionary - { - { "d", df.ShortDatePattern }, - { "D", df.LongDatePattern }, - { "t", df.ShortTimePattern }, - { "T", df.LongTimePattern }, - { "g", df.ShortDatePattern + " " + df.ShortTimePattern }, - { "G", df.ShortDatePattern + " " + df.LongTimePattern }, - { "f", df.FullDateTimePattern }, // TODO: (mc) find it actually + { "numberFormat", new Dictionary + { + { ",", nf.NumberGroupSeparator }, + { ".", nf.NumberDecimalSeparator }, + { "pattern", new[] { nf.NumberNegativePattern } }, + { "decimals", nf.NumberDecimalDigits }, + { "groupSizes", nf.NumberGroupSizes }, + { "+", nf.PositiveSign }, + { "-", nf.NegativeSign }, + { "NaN", nf.NaNSymbol }, + { "negativeInfinity", nf.NegativeInfinitySymbol }, + { "positiveInfinity", nf.PositiveInfinitySymbol }, + { "percent", new Dictionary + { + { ",", nf.PercentGroupSeparator }, + { ".", nf.PercentDecimalSeparator }, + { "pattern", new[] { nf.PercentNegativePattern, nf.PercentPositivePattern } }, + { "decimals", nf.PercentDecimalDigits }, + { "groupSizes", nf.PercentGroupSizes }, + { "symbol", nf.PercentSymbol } + } }, + { "currency", new Dictionary + { + { ",", nf.CurrencyGroupSeparator }, + { ".", nf.CurrencyDecimalSeparator }, + { "pattern", new[] { nf.CurrencyNegativePattern, nf.CurrencyPositivePattern } }, + { "decimals", nf.CurrencyDecimalDigits }, + { "groupSizes", nf.CurrencyGroupSizes }, + { "symbol", nf.CurrencySymbol } + } }, + } }, + { "dateTimeFormat", new Dictionary + { + { "calendarName", df.NativeCalendarName }, + { "/", df.DateSeparator }, + { ":", df.TimeSeparator }, + { "firstDay", (int)df.FirstDayOfWeek }, + { "twoDigitYearMax", ci.Calendar.TwoDigitYearMax }, + { "AM", df.AMDesignator.IsEmpty() ? null : new[] { df.AMDesignator, df.AMDesignator.ToLower(), df.AMDesignator.ToUpper() } }, + { "PM", df.PMDesignator.IsEmpty() ? null : new[] { df.PMDesignator, df.PMDesignator.ToLower(), df.PMDesignator.ToUpper() } }, + { "days", new Dictionary + { + { "names", df.DayNames }, + { "namesAbbr", df.AbbreviatedDayNames }, + { "namesShort", df.ShortestDayNames }, + } }, + { "months", new Dictionary + { + { "names", df.MonthNames }, + { "namesAbbr", df.AbbreviatedMonthNames }, + } }, + { "patterns", new Dictionary + { + { "d", df.ShortDatePattern }, + { "D", df.LongDatePattern }, + { "t", df.ShortTimePattern }, + { "T", df.LongTimePattern }, + { "g", df.ShortDatePattern + " " + df.ShortTimePattern }, + { "G", df.ShortDatePattern + " " + df.LongTimePattern }, + { "f", df.FullDateTimePattern }, // TODO: (mc) find it actually { "F", df.FullDateTimePattern }, - { "M", df.MonthDayPattern }, - { "Y", df.YearMonthPattern }, - { "u", df.UniversalSortableDateTimePattern }, - } } - } } - }; - - var json = JsonConvert.SerializeObject(dict, new JsonSerializerSettings - { + { "M", df.MonthDayPattern }, + { "Y", df.YearMonthPattern }, + { "u", df.UniversalSortableDateTimePattern }, + } } + } } + }; + + var json = JsonConvert.SerializeObject(dict, new JsonSerializerSettings + { ContractResolver = SmartContractResolver.Instance, Formatting = Formatting.None - }); + }); - return json; - } - } + return json; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/SmartResourceDisplayName.cs b/src/Presentation/SmartStore.Web.Framework/Localization/SmartResourceDisplayName.cs index e64ca7c164..b009eb95f2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/SmartResourceDisplayName.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/SmartResourceDisplayName.cs @@ -1,5 +1,4 @@ -using System; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using SmartStore.Core; using SmartStore.Core.Infrastructure; using SmartStore.Services.Localization; @@ -37,9 +36,6 @@ public override string DisplayName } } - public string Name - { - get { return "SmartResourceDisplayName"; } - } + public string Name => "SmartResourceDisplayName"; } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationService.cs b/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationService.cs index 273283616e..c32631747d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationService.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using SmartStore.Core.Caching; namespace SmartStore.Web.Framework.Localization @@ -9,43 +7,40 @@ public class TelerikLocalizationService : Telerik.Web.Mvc.Infrastructure.ILocali { private readonly string _componentName; private readonly SmartStore.Services.Localization.ILocalizationService _localizationService; - private readonly IRequestCache _requestCache; + private readonly IRequestCache _requestCache; private readonly int _currentLanguageId; public TelerikLocalizationService( - string componentName, - int currentLanguageId, - Services.Localization.ILocalizationService localizationService, - IRequestCache requestCache) + string componentName, + int currentLanguageId, + Services.Localization.ILocalizationService localizationService, + IRequestCache requestCache) { _componentName = componentName; _currentLanguageId = currentLanguageId; _localizationService = localizationService; - _requestCache = requestCache; + _requestCache = requestCache; } public IDictionary All() { - var scope = "Admin.Telerik." + _componentName; + var scope = "Admin.Telerik." + _componentName; - var cacheKey = scope + ".Resources"; + var cacheKey = scope + ".Resources"; - return _requestCache.Get(cacheKey, () => - { - var resources = _localizationService.GetResourcesByPattern(scope, _currentLanguageId); - var dict = resources.ToDictionarySafe( - x => x.ResourceName.Substring(scope.Length).ToLowerInvariant(), - x => x.ResourceValue); + return _requestCache.Get(cacheKey, () => + { + var resources = _localizationService.GetResourcesByPattern(scope, _currentLanguageId); + var dict = resources.ToDictionarySafe( + x => x.ResourceName.Substring(scope.Length).ToLowerInvariant(), + x => x.ResourceValue); - return dict; - }); - } - - public bool IsDefault - { - get { return true; } + return dict; + }); } + public bool IsDefault => true; + public string One(string key) { var resourceName = "Admin.Telerik." + _componentName + "." + key; diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationServiceFactory.cs b/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationServiceFactory.cs index 5d6855db9a..9bccaf1612 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationServiceFactory.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/TelerikLocalizationServiceFactory.cs @@ -8,16 +8,16 @@ public class TelerikLocalizationServiceFactory : ILocalizationServiceFactory { private readonly IWorkContext _workContext; private readonly Services.Localization.ILocalizationService _localizationService; - private readonly IRequestCache _requestCache; + private readonly IRequestCache _requestCache; - public TelerikLocalizationServiceFactory( - IWorkContext workContext, - Services.Localization.ILocalizationService localizationService, - IRequestCache requestCache) + public TelerikLocalizationServiceFactory( + IWorkContext workContext, + Services.Localization.ILocalizationService localizationService, + IRequestCache requestCache) { _workContext = workContext; _localizationService = localizationService; - _requestCache = requestCache; + _requestCache = requestCache; } public ILocalizationService Create(string componentName, System.Globalization.CultureInfo culture) diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs b/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs index e3ace14f69..a2296ac505 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs @@ -1,44 +1,43 @@ -using System; -using SmartStore.Core.Localization; +using SmartStore.Core.Localization; using SmartStore.Services.Localization; namespace SmartStore.Web.Framework.Localization { - public class Text : IText - { - private readonly ILocalizationService _localizationService; - - public Text(ILocalizationService localizationService) - { - _localizationService = localizationService; - } - - public LocalizedString Get(string key, params object[] args) - { - return GetEx(key, 0, args); - } - - public LocalizedString GetEx(string key, int languageId, params object[] args) - { - try - { - var value = _localizationService.GetResource(key, languageId); - - if (string.IsNullOrEmpty(value)) - { - return new LocalizedString(key); - } - - if (args == null || args.Length == 0) - { - return new LocalizedString(value); - } - - return new LocalizedString(string.Format(value, args), key, args); - } - catch { } - - return new LocalizedString(key); - } - } + public class Text : IText + { + private readonly ILocalizationService _localizationService; + + public Text(ILocalizationService localizationService) + { + _localizationService = localizationService; + } + + public LocalizedString Get(string key, params object[] args) + { + return GetEx(key, 0, args); + } + + public LocalizedString GetEx(string key, int languageId, params object[] args) + { + try + { + var value = _localizationService.GetResource(key, languageId); + + if (string.IsNullOrEmpty(value)) + { + return new LocalizedString(key); + } + + if (args == null || args.Length == 0) + { + return new LocalizedString(value); + } + + return new LocalizedString(string.Format(value, args), key, args); + } + catch { } + + return new LocalizedString(key); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/ChoiceListItem.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/ChoiceListItem.cs new file mode 100644 index 0000000000..6318ade7c2 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/ChoiceListItem.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json; + +namespace SmartStore.Web.Framework.Modelling +{ + /// + /// Represents a selectable list item. + /// It can be used together with smartstore.selectwrapper.js to create an extended select list. + /// + public class ChoiceListItem + { + /// + /// The ID value, e.g. an entity ID. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Displayed text. + /// + [JsonProperty("text")] + public string Text { get; set; } + + /// + /// A value indicating whether the item is selected. + /// + [JsonProperty("selected")] + public bool Selected { get; set; } + + /// + /// Optional description. + /// + [JsonProperty(PropertyName = "description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// + /// Optional item hint, e.g. the product SKU. + /// + [JsonProperty(PropertyName = "hint", NullValueHandling = NullValueHandling.Ignore)] + public string Hint { get; set; } + + /// + /// URL to add a link to the list item. + /// + [JsonProperty(PropertyName = "url", NullValueHandling = NullValueHandling.Ignore)] + public string Url { get; set; } + + /// + /// Optional title for the item link. + /// + [JsonProperty(PropertyName = "urlTitle", NullValueHandling = NullValueHandling.Ignore)] + public string UrlTitle { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/MediaTemplateModel.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/MediaTemplateModel.cs new file mode 100644 index 0000000000..9b2b6bd456 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/MediaTemplateModel.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using SmartStore.Services.Media; + +namespace SmartStore.Web.Framework.Modelling +{ + public partial class MediaTemplateModel : EntityModelBase + { + public MediaTemplateModel(MediaFileInfo file, bool renderViewer) + { + Guard.NotNull(file, nameof(file)); + + Id = file.Id; + File = file; + RenderViewer = renderViewer; + } + + public MediaFileInfo File { get; private set; } + public bool RenderViewer { get; private set; } + + public int ThumbSize { get; set; } = 256; + public IDictionary HtmlAttributes { get; set; } + + public string Title { get; set; } + public string Alt { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBase.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBase.cs index 68dfb6bdfb..9ae7515a84 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBase.cs @@ -6,108 +6,112 @@ namespace SmartStore.Web.Framework.Modelling { + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class CustomModelPartAttribute : Attribute + { + } + [Serializable] - public sealed class CustomPropertiesDictionary : Dictionary - { - } + public sealed class CustomPropertiesDictionary : Dictionary + { + } - [Serializable] - public abstract partial class ModelBase + [Serializable] + public abstract partial class ModelBase { - private readonly static ContextState>> _contextState; + private readonly static ContextState>> _contextState; - static ModelBase() - { - _contextState = new ContextState>>("ModelBase.CustomThreadProperties"); - } + static ModelBase() + { + _contextState = new ContextState>>("ModelBase.CustomThreadProperties"); + } - protected ModelBase() + protected ModelBase() { - CustomProperties = new CustomPropertiesDictionary(); + CustomProperties = new CustomPropertiesDictionary(); } - + public virtual void BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { } - /// - /// Gets a custom property value either from the thread local or the static storage (in this particular order) - /// - /// Type of property - /// Custom property key - /// The property value or null - public TProperty Get(string key) - { - Guard.NotEmpty(key, nameof(key)); + /// + /// Gets a custom property value either from the thread local or the static storage (in this particular order) + /// + /// Type of property + /// Custom property key + /// The property value or null + public TProperty Get(string key) + { + Guard.NotEmpty(key, nameof(key)); - IDictionary dict; - object value; + IDictionary dict; - if (TryGetCustomThreadProperties(false, out dict) && dict.TryGetValue(key, out value)) - { - return (TProperty)value; - } + if (TryGetCustomThreadProperties(false, out dict) && dict.TryGetValue(key, out var value)) + { + return (TProperty)value; + } - if (CustomProperties.TryGetValue(key, out value)) - { - return (TProperty)value; - } + if (CustomProperties.TryGetValue(key, out value)) + { + return (TProperty)value; + } - return default(TProperty); - } + return default(TProperty); + } /// /// Use this property to store any custom value for your models. /// public CustomPropertiesDictionary CustomProperties { get; set; } - /// - /// A data bag for custom model properties which only - /// lives during a thread/request lifecycle - /// - /// - /// Use thread properties whenever you need to persist request-scoped data, - /// but the model is potentially cached statically. - /// - [JsonIgnore] - public IDictionary CustomThreadProperties - { - get - { - IDictionary dict; - TryGetCustomThreadProperties(true, out dict); - return dict; - } - } - - private bool TryGetCustomThreadProperties(bool create, out IDictionary dict) - { - dict = null; - var state = _contextState.GetState(); - - if (state == null && create) - { - state = new Dictionary>(); - _contextState.SetState(state); - } - - if (state != null) - { - if (!state.TryGetValue(this, out dict)) - { - if (create) - { - dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - state[this] = dict; - } - } - - return dict != null; - } - - return false; - } - } + /// + /// A data bag for custom model properties which only + /// lives during a thread/request lifecycle + /// + /// + /// Use thread properties whenever you need to persist request-scoped data, + /// but the model is potentially cached statically. + /// + [JsonIgnore] + public IDictionary CustomThreadProperties + { + get + { + IDictionary dict; + TryGetCustomThreadProperties(true, out dict); + return dict; + } + } + + private bool TryGetCustomThreadProperties(bool create, out IDictionary dict) + { + dict = null; + var state = _contextState.GetState(); + + if (state == null && create) + { + state = new Dictionary>(); + _contextState.SetState(state); + } + + if (state != null) + { + if (!state.TryGetValue(this, out dict)) + { + if (create) + { + dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + state[this] = dict; + } + } + + return dict != null; + } + + return false; + } + } public abstract partial class EntityModelBase : ModelBase { @@ -124,8 +128,8 @@ public abstract partial class EntityModelBase : ModelBase } - public abstract partial class TabbableModel : EntityModelBase - { - public virtual string[] LoadedTabs { get; set; } - } + public abstract partial class TabbableModel : EntityModelBase + { + public virtual string[] LoadedTabs { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBoundEvent.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBoundEvent.cs index 756aceedde..976ac877b3 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBoundEvent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/ModelBoundEvent.cs @@ -2,17 +2,17 @@ namespace SmartStore.Web.Framework.Modelling { - public class ModelBoundEvent - { - public ModelBoundEvent(TabbableModel boundModel, object entityModel, FormCollection form) + public class ModelBoundEvent + { + public ModelBoundEvent(TabbableModel boundModel, object entityModel, FormCollection form) { - this.BoundModel = boundModel; - this.EntityModel = entityModel; - this.Form = form; + this.BoundModel = boundModel; + this.EntityModel = entityModel; + this.Form = form; } - public TabbableModel BoundModel { get; private set; } + public TabbableModel BoundModel { get; private set; } public object EntityModel { get; private set; } - public FormCollection Form { get; private set; } - } + public FormCollection Form { get; private set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs index 3055ea8726..ed232f2cf9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs @@ -1,5 +1,4 @@ using System; -using System.Drawing; using System.Globalization; using System.IO; using System.Web; @@ -10,235 +9,232 @@ namespace SmartStore.Web.Framework.Modelling { - public class CachedFileResult : ActionResult, IFileResponse - { - #region Ctor - - public CachedFileResult(string path, string contentType = null) - : this(GetFileInfo(path), contentType) - { - } - - public CachedFileResult(FileInfo file, string contentType = null) - { - Guard.NotNull(file, nameof(file)); - - if (!file.Exists) - { - throw new FileNotFoundException(file.FullName); - } - - Transmitter = new FileStreamTransmitter(file.OpenRead); - ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); - FileLength = file.Length; - LastModifiedUtc = file.LastWriteTimeUtc; - } - - public CachedFileResult(IFile file, string contentType = null) - { - Guard.NotNull(file, nameof(file)); - - if (!file.Exists) - { - throw new FileNotFoundException(file.Path); - } - - Transmitter = new FileStreamTransmitter(file.OpenRead); - ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); - FileLength = file.Size; - Dimensions = file.Dimensions; - LastModifiedUtc = file.LastUpdated; - } - - public CachedFileResult(VirtualFile file, DateTime? lastModifiedUtc = null, string contentType = null) - { - Guard.NotNull(file, nameof(file)); - - try - { - var fi = GetFileInfo(file.VirtualPath); - if (fi.Exists) - { - ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(fi.Name); - FileLength = fi.Length; - LastModifiedUtc = fi.LastWriteTimeUtc; - } - } - finally - { - if (lastModifiedUtc == null) - { - throw new ArgumentNullException(nameof(lastModifiedUtc), "A modification date must be provided if the VirtualFile cannot be mapped to a physical path."); - } - - if (FileLength == null) - { - ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); - using (var stream = file.Open()) - { - FileLength = stream.Length; - } - } - } - - Transmitter = new FileStreamTransmitter(file.Open); - LastModifiedUtc = lastModifiedUtc.Value; - } - - public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) - { - Guard.NotNull(factory, nameof(factory)); - Guard.NotEmpty(contentType, nameof(contentType)); - - Transmitter = new FileStreamTransmitter(factory); - ContentType = contentType; - FileLength = fileLength; - LastModifiedUtc = lastModifiedUtc; - } - - public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) - { - Guard.NotNull(factory, nameof(factory)); - Guard.NotEmpty(contentType, nameof(contentType)); - - Transmitter = new FileBufferTransmitter(factory); - ContentType = contentType; - FileLength = fileLength; - LastModifiedUtc = lastModifiedUtc; - } - - private static FileInfo GetFileInfo(string path) - { - Guard.NotEmpty(path, nameof(path)); - - if (!PathHelper.IsAbsolutePhysicalPath(path)) - { - path = CommonHelper.MapPath(path); - } - - return new FileInfo(path); - } - - #endregion - - #region Properties - - public string ContentType { get; private set; } - - public long? FileLength { get; set; } - - public Size? Dimensions { get; set; } - - public DateTime LastModifiedUtc { get; set; } - - public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(7); - - /// - /// If not set, will be auto-generated based on property. - /// - public string ETag { get; set; } - - public FileTransmitter Transmitter { get; private set; } - - /// - /// A callback that will be invoked on successful result execution - /// - public Action OnExecuted { get; set; } - - #endregion - - public override void ExecuteResult(ControllerContext context) - { - var httpContext = context.HttpContext; - var request = httpContext.Request; - var response = httpContext.Response; - - // Fix Last Modified Time. We might need it soon - // if we encounter a Range: and If-Range header - // Using UTC time to avoid daylight savings time bug 83230 - var lastModified = new DateTime(LastModifiedUtc.Year, - LastModifiedUtc.Month, - LastModifiedUtc.Day, - LastModifiedUtc.Hour, - LastModifiedUtc.Minute, - LastModifiedUtc.Second, - 0, - DateTimeKind.Utc); - - // Because we can't set a "Last-Modified" header to any time - // in the future, check the last modified time and set it to - // DateTime.Now if it's in the future. - // This is to fix VSWhidbey #402323 - var utcNow = DateTime.UtcNow; - if (lastModified > utcNow) - { - // use 1 second resolution - lastModified = new DateTime(utcNow.Ticks - (utcNow.Ticks % TimeSpan.TicksPerSecond), DateTimeKind.Utc); - } - - LastModifiedUtc = lastModified; - - // Generate ETag if empty - if (ETag.IsEmpty()) - { - ETag = GenerateETag(httpContext, lastModified, utcNow); - } - - // Determine applicable file responder - var responder = ResolveResponder(request); - - // Execute response (send file) - if (responder.TrySendHeaders(httpContext)) - { - responder.SendFile(httpContext); - } - - // Finish: invoke the optional callback - OnExecuted?.Invoke(); - } - - private FileResponder ResolveResponder(HttpRequestBase request) - { - // Is this a HEAD request - if (request.HttpMethod == "HEAD") - { - return new HeadFileResponder(this); - } - - // Is this a request for an unmodified file? - var ifNoneMatch = request.Headers["If-None-Match"]; - if (ifNoneMatch.HasValue() && ETag == ifNoneMatch) - { - return new UnmodifiedFileResponder(this); - } - - // Is this a Range request? - var rangeHeader = request.Headers["Range"]; - if (rangeHeader.HasValue() && rangeHeader.StartsWith("bytes", StringComparison.OrdinalIgnoreCase)) - { - return new RangeFileResponder(this, rangeHeader); - } - - // Responder for sending the whole file - return new FullFileResponder(this); - } - - private static string GenerateETag(HttpContextBase context, DateTime lastModified, DateTime now) - { - // Get 64-bit FILETIME stamp - var lastModFileTime = lastModified.ToFileTime(); - var nowFileTime = now.ToFileTime(); - var hexFileTime = lastModFileTime.ToString("X8", CultureInfo.InvariantCulture); - - //// Do what IIS does to determine if this is a weak ETag. - //// Compare the last modified time to now and if the difference is - //// less than or equal to 3 seconds, then it is weak - //if ((nowFileTime - lastModFileTime) <= 30000000) - //{ - // return "W/\"" + hexFileTime + "\""; - //} - - return "\"" + hexFileTime + "\""; - } - } + public class CachedFileResult : ActionResult, IFileResponse + { + #region Ctor + + public CachedFileResult(string path, string contentType = null) + : this(GetFileInfo(path), contentType) + { + } + + public CachedFileResult(FileInfo file, string contentType = null) + { + Guard.NotNull(file, nameof(file)); + + if (!file.Exists) + { + throw new FileNotFoundException(file.FullName); + } + + Transmitter = new FileStreamTransmitter(file.OpenRead); + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); + FileLength = file.Length; + LastModifiedUtc = file.LastWriteTimeUtc; + } + + public CachedFileResult(IFile file, string contentType = null) + { + Guard.NotNull(file, nameof(file)); + + if (!file.Exists) + { + throw new FileNotFoundException(file.Path); + } + + Transmitter = new FileStreamTransmitter(file.OpenRead); + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); + FileLength = file.Size; + LastModifiedUtc = file.LastUpdated; + } + + public CachedFileResult(VirtualFile file, DateTime? lastModifiedUtc = null, string contentType = null) + { + Guard.NotNull(file, nameof(file)); + + try + { + var fi = GetFileInfo(file.VirtualPath); + if (fi.Exists) + { + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(fi.Name); + FileLength = fi.Length; + LastModifiedUtc = fi.LastWriteTimeUtc; + } + } + finally + { + if (lastModifiedUtc == null) + { + throw new ArgumentNullException(nameof(lastModifiedUtc), "A modification date must be provided if the VirtualFile cannot be mapped to a physical path."); + } + + if (FileLength == null) + { + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); + using (var stream = file.Open()) + { + FileLength = stream.Length; + } + } + } + + Transmitter = new FileStreamTransmitter(file.Open); + LastModifiedUtc = lastModifiedUtc.Value; + } + + public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) + { + Guard.NotNull(factory, nameof(factory)); + Guard.NotEmpty(contentType, nameof(contentType)); + + Transmitter = new FileStreamTransmitter(factory); + ContentType = contentType; + FileLength = fileLength; + LastModifiedUtc = lastModifiedUtc; + } + + public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) + { + Guard.NotNull(factory, nameof(factory)); + Guard.NotEmpty(contentType, nameof(contentType)); + + Transmitter = new FileBufferTransmitter(factory); + ContentType = contentType; + FileLength = fileLength; + LastModifiedUtc = lastModifiedUtc; + } + + private static FileInfo GetFileInfo(string path) + { + Guard.NotEmpty(path, nameof(path)); + + if (!PathHelper.IsAbsolutePhysicalPath(path)) + { + path = CommonHelper.MapPath(path); + } + + return new FileInfo(path); + } + + #endregion + + #region Properties + + public string ContentType { get; private set; } + + public long? FileLength { get; set; } + + public DateTime LastModifiedUtc { get; set; } + + public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(7); + + /// + /// If not set, will be auto-generated based on property. + /// + public string ETag { get; set; } + + public FileTransmitter Transmitter { get; private set; } + + /// + /// A callback that will be invoked on successful result execution + /// + public Action OnExecuted { get; set; } + + #endregion + + public override void ExecuteResult(ControllerContext context) + { + var httpContext = context.HttpContext; + var request = httpContext.Request; + var response = httpContext.Response; + + // Fix Last Modified Time. We might need it soon + // if we encounter a Range: and If-Range header + // Using UTC time to avoid daylight savings time bug 83230 + var lastModified = new DateTime(LastModifiedUtc.Year, + LastModifiedUtc.Month, + LastModifiedUtc.Day, + LastModifiedUtc.Hour, + LastModifiedUtc.Minute, + LastModifiedUtc.Second, + 0, + DateTimeKind.Utc); + + // Because we can't set a "Last-Modified" header to any time + // in the future, check the last modified time and set it to + // DateTime.Now if it's in the future. + // This is to fix VSWhidbey #402323 + var utcNow = DateTime.UtcNow; + if (lastModified > utcNow) + { + // use 1 second resolution + lastModified = new DateTime(utcNow.Ticks - (utcNow.Ticks % TimeSpan.TicksPerSecond), DateTimeKind.Utc); + } + + LastModifiedUtc = lastModified; + + // Generate ETag if empty + if (ETag.IsEmpty()) + { + ETag = GenerateETag(httpContext, lastModified, utcNow); + } + + // Determine applicable file responder + var responder = ResolveResponder(request); + + // Execute response (send file) + if (responder.TrySendHeaders(httpContext)) + { + responder.SendFile(httpContext); + } + + // Finish: invoke the optional callback + OnExecuted?.Invoke(); + } + + private FileResponder ResolveResponder(HttpRequestBase request) + { + // Is this a HEAD request + if (request.HttpMethod == "HEAD") + { + return new HeadFileResponder(this); + } + + // Is this a request for an unmodified file? + var ifNoneMatch = request.Headers["If-None-Match"]; + if (ifNoneMatch.HasValue() && ETag == ifNoneMatch) + { + return new UnmodifiedFileResponder(this); + } + + // Is this a Range request? + var rangeHeader = request.Headers["Range"]; + if (rangeHeader.HasValue() && rangeHeader.StartsWith("bytes", StringComparison.OrdinalIgnoreCase)) + { + return new RangeFileResponder(this, rangeHeader); + } + + // Responder for sending the whole file + return new FullFileResponder(this); + } + + private static string GenerateETag(HttpContextBase context, DateTime lastModified, DateTime now) + { + // Get 64-bit FILETIME stamp + var lastModFileTime = lastModified.ToFileTime(); + var nowFileTime = now.ToFileTime(); + var hexFileTime = lastModFileTime.ToString("X8", CultureInfo.InvariantCulture); + + //// Do what IIS does to determine if this is a weak ETag. + //// Compare the last modified time to now and if the difference is + //// less than or equal to 3 seconds, then it is weak + //if ((nowFileTime - lastModFileTime) <= 30000000) + //{ + // return "W/\"" + hexFileTime + "\""; + //} + + return "\"" + hexFileTime + "\""; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs index 6ecda387d3..bbf053767c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs @@ -6,589 +6,589 @@ namespace SmartStore.Web.Framework.Modelling { - internal abstract class FileResponder - { - protected FileResponder(IFileResponse fileResponse) - { - Guard.NotNull(fileResponse, nameof(fileResponse)); - FileResponse = fileResponse; - } - - protected IFileResponse FileResponse { get; private set; } - - public virtual bool TrySendHeaders(HttpContextBase context) - { - var response = context.Response; - - var utcNow = DateTime.UtcNow; - - // Specify content type - response.ContentType = FileResponse.ContentType; - // We support byte ranges - response.AppendHeader("Accept-Ranges", "bytes"); - //// Set the expires header for HTTP 1.0 cliets - //response.Cache.SetExpires(utcNow.Add(FileResponse.MaxAge)); - // How often the browser should check that it has the latest version - response.Cache.SetMaxAge(FileResponse.MaxAge); - // The unique identifier for the entity - response.Cache.SetETag(FileResponse.ETag); - // Proxy and browser can cache response - response.Cache.SetCacheability(HttpCacheability.Public); - // Proxy cache should check with orginal server once cache has expired - response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); - //// The date the file was last modified - //context.Response.Cache.SetLastModified(FileResponse.LastModifiedUtc); + internal abstract class FileResponder + { + protected FileResponder(IFileResponse fileResponse) + { + Guard.NotNull(fileResponse, nameof(fileResponse)); + FileResponse = fileResponse; + } + + protected IFileResponse FileResponse { get; private set; } + + public virtual bool TrySendHeaders(HttpContextBase context) + { + var response = context.Response; + + var utcNow = DateTime.UtcNow; + + // Specify content type + response.ContentType = FileResponse.ContentType; + // We support byte ranges + response.AppendHeader("Accept-Ranges", "bytes"); + //// Set the expires header for HTTP 1.0 cliets + //response.Cache.SetExpires(utcNow.Add(FileResponse.MaxAge)); + // How often the browser should check that it has the latest version + response.Cache.SetMaxAge(FileResponse.MaxAge); + // The unique identifier for the entity + response.Cache.SetETag(FileResponse.ETag); + // Proxy and browser can cache response + response.Cache.SetCacheability(HttpCacheability.Public); + // Proxy cache should check with orginal server once cache has expired + response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + //// The date the file was last modified + //context.Response.Cache.SetLastModified(FileResponse.LastModifiedUtc); - return true; - } - - public abstract void SendFile(HttpContextBase context); - } + return true; + } + + public abstract void SendFile(HttpContextBase context); + } - internal sealed class HeadFileResponder : FileResponder - { - public HeadFileResponder(IFileResponse fileResponse) - : base(fileResponse) - { - } - - public override bool TrySendHeaders(HttpContextBase context) - { - var response = context.Response; - - base.TrySendHeaders(context); + internal sealed class HeadFileResponder : FileResponder + { + public HeadFileResponder(IFileResponse fileResponse) + : base(fileResponse) + { + } + + public override bool TrySendHeaders(HttpContextBase context) + { + var response = context.Response; + + base.TrySendHeaders(context); - response.StatusCode = (int)HttpStatusCode.OK; - response.AddHeader("Content-Length", (FileResponse.FileLength ?? 0).ToString(CultureInfo.InvariantCulture)); - - if (FileResponse.Dimensions != null) - { - response.AddHeader("X-Width", FileResponse.Dimensions.Value.Width.ToString(CultureInfo.InvariantCulture)); - response.AddHeader("X-Height", FileResponse.Dimensions.Value.Height.ToString(CultureInfo.InvariantCulture)); - } - - response.End(); - - return true; - } - - public override void SendFile(HttpContextBase context) - { - // Don't send any file. - } - } - - - internal sealed class UnmodifiedFileResponder : FileResponder - { - public UnmodifiedFileResponder(IFileResponse fileResponse) - : base(fileResponse) - { - } - - public override bool TrySendHeaders(HttpContextBase context) - { - var response = context.Response; - - base.TrySendHeaders(context); - - response.StatusCode = (int)HttpStatusCode.NotModified; - response.StatusDescription = "Not Modified"; - - // Explicitly set the Content-Length header so the client doesn't wait for - // content but keeps the connection open for other requests - response.AddHeader("Content-Length", "0"); - - return true; - } - - public override void SendFile(HttpContextBase context) - { - // Don't send file, it is unmodified. Let browser fetch from its cache. - } - } - - - - internal sealed class FullFileResponder : FileResponder - { - public FullFileResponder(IFileResponse fileResponse) - : base(fileResponse) - { - } - - public override bool TrySendHeaders(HttpContextBase context) - { - base.TrySendHeaders(context); - - // The date the file was last modified - context.Response.Cache.SetLastModified(FileResponse.LastModifiedUtc); - - return true; - } - - public override void SendFile(HttpContextBase context) - { - var fileLength = FileResponse.FileLength ?? FileResponse.Transmitter.GetFileLength(); - FileResponse.Transmitter.TransmitFile(0, fileLength, fileLength, context); - } - } - - - - internal sealed class RangeFileResponder : FileResponder - { - internal struct ByteRange - { - internal long Offset; - internal long Length; - } - - private const string RANGE_BOUNDARY = ""; - private const string MULTIPART_CONTENT_TYPE = "multipart/byteranges; boundary="; - private const string MULTIPART_RANGE_DELIMITER = "--\r\n"; - private const string MULTIPART_RANGE_END = "----\r\n\r\n"; - private const string CONTENT_RANGE_FORMAT = "bytes {0}-{1}/{2}"; - private const int MAX_RANGE_ALLOWED = 5; - - private const int ERROR_ACCESS_DENIED = 5; - - private static readonly string[] _httpDateFormats = new string[] { "r", "dddd, dd-MMM-yy HH':'mm':'ss 'GMT'", "ddd MMM d HH':'mm':'ss yyyy" }; - - private readonly FileResponder _defaultResponder; - private readonly string _rangeHeader; - - public RangeFileResponder(IFileResponse fileResponse, string rangeHeader) - : base(fileResponse) - { - _defaultResponder = new FullFileResponder(fileResponse); - _rangeHeader = rangeHeader; - } - - public override bool TrySendHeaders(HttpContextBase context) - { - var fileLength = FileResponse.FileLength ?? FileResponse.Transmitter.GetFileLength(); - var etag = FileResponse.ETag; - var lastModified = FileResponse.LastModifiedUtc; - - var handled = ExecuteRangeRequest(context, fileLength, _rangeHeader, etag, lastModified); - if (!handled && _defaultResponder.TrySendHeaders(context)) - { - _defaultResponder.SendFile(context); - handled = true; - } - - return handled; - } - - public override void SendFile(HttpContextBase context) - { - // Do nothing here, we have handled everything in 'TrySendHeaders()' already - } - - #region Range Utils - - // Most of the helpers here were copied over from the internal StaticFileHandler.cs - - private bool ExecuteRangeRequest( - HttpContextBase context, - long fileLength, - string rangeHeader, - string etag, - DateTime lastModified) - { - var handled = false; - var request = context.Request; - var response = context.Response; - - // return "416 Requested range not satisfiable" if the file length is zero. - if (fileLength <= 0) - { - SendRangeNotSatisfiable(response, fileLength); - handled = true; - return handled; - } - - var ifRangeHeader = request.Headers["If-Range"]; - if (ifRangeHeader != null && ifRangeHeader.Length > 1) - { - // Is this an ETag or a Date? We only need to check two - // characters; an ETag either begins with W/ or it is quoted. - if (ifRangeHeader[0] == '"') - { - // it's a strong ETag - if (ifRangeHeader != etag) - { - // the etags do not match, and we will therefore return the entire response - return handled; - } - } - else if (ifRangeHeader[0] == 'W' && ifRangeHeader[1] == '/') - { - // it's a weak ETag, and is therefore not usable for sub-range retrieval and - // we will return the entire response - return handled; - } - else - { - // It's a date. If it is greater than or equal to the last-write time of the file, we can send the range. - if (IsOutDated(ifRangeHeader, lastModified)) - { - return handled; - } - } - } - - // the expected format is "bytes = [, , ...]" - // where is "-[]" or "-". - int indexOfEquals = rangeHeader.IndexOf('='); - if (indexOfEquals == -1 || indexOfEquals == rangeHeader.Length - 1) - { - // invalid syntax - return handled; - } - - int startIndex = indexOfEquals + 1; - bool isRangeHeaderSyntacticallyValid = true; - long offset; - long length; - bool isSatisfiable; - bool exceededMax = false; - ByteRange[] byteRanges = null; - int byteRangesCount = 0; - long totalBytes = 0; - while (startIndex < rangeHeader.Length && isRangeHeaderSyntacticallyValid) - { - isRangeHeaderSyntacticallyValid = GetNextRange(rangeHeader, ref startIndex, fileLength, out offset, out length, out isSatisfiable); - - if (!isRangeHeaderSyntacticallyValid) - { - break; - } - - if (!isSatisfiable) - { - continue; - } - - if (byteRanges == null) - { - byteRanges = new ByteRange[16]; - } - - if (byteRangesCount >= byteRanges.Length) - { - // grow byteRanges array - ByteRange[] buffer = new ByteRange[byteRanges.Length * 2]; - Array.Copy(byteRanges, buffer, byteRanges.Length); - byteRanges = buffer; - } - - byteRanges[byteRangesCount].Offset = offset; - byteRanges[byteRangesCount].Length = length; - byteRangesCount++; - - // IIS imposes this limitation too, and sends "400 Bad Request" if exceeded - totalBytes += length; - if (totalBytes > fileLength * MAX_RANGE_ALLOWED) - { - exceededMax = true; - break; - } - } - - if (!isRangeHeaderSyntacticallyValid) - { - return handled; - } - - if (exceededMax) - { - SendBadRequest(response); - handled = true; - return handled; - } - - if (byteRangesCount == 0) - { - // we parsed the Range header and found no satisfiable byte ranges, so return "416 Requested Range Not Satisfiable" - SendRangeNotSatisfiable(response, fileLength); - handled = true; - return handled; - } - - if (byteRangesCount == 1) - { - offset = byteRanges[0].Offset; - length = byteRanges[0].Length; - response.ContentType = FileResponse.ContentType; - string contentRange = String.Format(CultureInfo.InvariantCulture, CONTENT_RANGE_FORMAT, offset, offset + length - 1, fileLength); - response.AppendHeader("Content-Range", contentRange); - - SendResponseHeaders(); - FileResponse.Transmitter.TransmitFile(offset, length, fileLength, context); - } - else - { - response.ContentType = MULTIPART_CONTENT_TYPE; - string contentRange; - string partialContentType = "Content-Type: " + FileResponse.ContentType + "\r\n"; - - SendResponseHeaders(); - - for (int i = 0; i < byteRangesCount; i++) - { - offset = byteRanges[i].Offset; - length = byteRanges[i].Length; - response.Write(MULTIPART_RANGE_DELIMITER); - response.Write(partialContentType); - response.Write("Content-Range: "); - contentRange = String.Format(CultureInfo.InvariantCulture, CONTENT_RANGE_FORMAT, offset, offset + length - 1, fileLength); - response.Write(contentRange); - response.Write("\r\n\r\n"); - // Transmit - FileResponse.Transmitter.TransmitFile(offset, length, fileLength, context); - response.Write("\r\n"); - } - response.Write(MULTIPART_RANGE_END); - } - - handled = true; - return handled; - - void SendResponseHeaders() - { - // Send a "206 Partial Content" status - response.StatusCode = (int)HttpStatusCode.PartialContent; - response.AppendHeader("Accept-Ranges", "bytes"); - response.Cache.SetLastModified(lastModified); - response.Cache.SetETag(etag); - response.Cache.SetCacheability(HttpCacheability.Public); - } - } - - private void SendBadRequest(HttpResponseBase response) - { - response.StatusCode = (int)HttpStatusCode.BadRequest; - response.Write("Bad Request"); - } - - private void SendRangeNotSatisfiable(HttpResponseBase response, long fileLength) - { - response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable; - response.ContentType = null; - response.AppendHeader("Content-Range", "bytes */" + fileLength.ToString(NumberFormatInfo.InvariantInfo)); - } - - private static bool IsOutDated(string ifRangeHeader, DateTime lastModified) - { - try - { - var utcLastModified = lastModified.ToUniversalTime(); - var utc = UtcParse(ifRangeHeader); - return (utc < utcLastModified); - } - catch - { - return true; - } - } - - private static DateTime UtcParse(string time) - { - Guard.NotNull(time, nameof(time)); - - var dtStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; - if (DateTime.TryParseExact(time, _httpDateFormats, null, dtStyles, out var utcDate)) - { - return utcDate; - } - - throw new FormatException($"{time} is an invalid date expression."); - } - - // initial space characters are skipped, and the string of digits up until the first non-digit - // are converted to a long. If digits are found the method returns true; otherwise false. - private static bool GetLongFromSubstring(string s, ref int startIndex, out long result) - { - result = 0; - - // get index of first digit - MovePastSpaceCharacters(s, ref startIndex); - int beginIndex = startIndex; - - // get index of last digit - MovePastDigits(s, ref startIndex); - int endIndex = startIndex - 1; - - // are there any digits? - if (endIndex < beginIndex) - { - return false; - } - - long multipleOfTen = 1; - for (int i = endIndex; i >= beginIndex; i--) - { - int digit = s[i] - '0'; - result += digit * multipleOfTen; - multipleOfTen *= 10; - // check for overflow - if (result < 0) - { - return false; - } - } - - return true; - } - - // The Range header consists of one or more byte range specifiers. E.g, "Range: bytes=0-1024,-1024" is a request - // for the first and last 1024 bytes of a file. Before this method is called, startIndex points to the beginning - // of a byte range specifier; and afterwards it points to the beginning of the next byte range specifier. - // If the current byte range specifier is syntactially inavlid, this function will return false indicating that the - // Range header must be ignored. If the function returns true, then the byte range specifier will be converted to - // an offset and length, and the startIndex will be incremented to the next byte range specifier. The byte range - // specifier (offset and length) returned by this function is satisfiable if and only if isSatisfiable is true. - private static bool GetNextRange(string rangeHeader, ref int startIndex, long fileLength, out long offset, out long length, out bool isSatisfiable) - { - // startIndex is first char after '=', or first char after ',' - Debug.Assert(startIndex < rangeHeader.Length, "startIndex < rangeHeader.Length"); - - offset = 0; - length = 0; - isSatisfiable = false; - - // A Range request to an empty file is never satisfiable, and will always receive a 416 status. - if (fileLength <= 0) - { - // put startIndex at end of string so we don't try to call GetNextRange again - startIndex = rangeHeader.Length; - return true; - } - - MovePastSpaceCharacters(rangeHeader, ref startIndex); - - if (startIndex < rangeHeader.Length && rangeHeader[startIndex] == '-') - { - // this range is of the form "-mmm" - startIndex++; - if (!GetLongFromSubstring(rangeHeader, ref startIndex, out length)) - { - return false; - } - - if (length > fileLength) - { - // send entire file - offset = 0; - length = fileLength; - } - else - { - // send last N bytes - offset = fileLength - length; - } - - isSatisfiable = IsRangeSatisfiable(offset, length, fileLength); - // we parsed the current range, but need to successfully move the startIndex to the next range - return IncrementToNextRange(rangeHeader, ref startIndex); - } - else - { - // this range is of the form "nnn-[mmm]" - if (!GetLongFromSubstring(rangeHeader, ref startIndex, out offset)) - { - return false; - } - - // increment startIndex past '-' - if (startIndex < rangeHeader.Length && rangeHeader[startIndex] == '-') - { - startIndex++; - } - else - { - return false; - } - - if (!GetLongFromSubstring(rangeHeader, ref startIndex, out long endPos)) - { - // assume range is of form "nnn-". If it isn't, - // the call to IncrementToNextRange will return false - length = fileLength - offset; - } - else - { - // if...greater than or equal to the current length of the entity-body, last-byte-pos - // is taken to be equal to one less than the current length of the entity- body in bytes. - if (endPos > fileLength - 1) - { - endPos = fileLength - 1; - } - - length = endPos - offset + 1; - - if (length < 1) - { - // the byte range specifier is syntactially invalid - // because the last-byte-pos < first-byte-pos - return false; - } - } - - isSatisfiable = IsRangeSatisfiable(offset, length, fileLength); - - // we parsed the current range, but need to successfully move the startIndex to the next range - return IncrementToNextRange(rangeHeader, ref startIndex); - } - } - - private static bool IncrementToNextRange(string s, ref int startIndex) - { - // increment startIndex until next token and return true, unless the syntax is invalid - MovePastSpaceCharacters(s, ref startIndex); - - if (startIndex < s.Length) - { - if (s[startIndex] != ',') - { - return false; - } - // move to first char after ',' - startIndex++; - } - - return true; - } - - private static bool IsRangeSatisfiable(long offset, long length, long fileLength) - { - return (offset < fileLength && length > 0); - } - - private static bool IsSecurityError(int ErrorCode) - { - return (ErrorCode == ERROR_ACCESS_DENIED); - } - - private static void MovePastSpaceCharacters(string s, ref int startIndex) - { - while (startIndex < s.Length && s[startIndex] == ' ') - { - startIndex++; - } - } - - private static void MovePastDigits(string s, ref int startIndex) - { - while (startIndex < s.Length && s[startIndex] <= '9' && s[startIndex] >= '0') - { - startIndex++; - } - } - - #endregion - } + response.StatusCode = (int)HttpStatusCode.OK; + response.AddHeader("Content-Length", (FileResponse.FileLength ?? 0).ToString(CultureInfo.InvariantCulture)); + + //if (FileResponse.Dimensions != null) + //{ + // response.AddHeader("X-Width", FileResponse.Dimensions.Value.Width.ToString(CultureInfo.InvariantCulture)); + // response.AddHeader("X-Height", FileResponse.Dimensions.Value.Height.ToString(CultureInfo.InvariantCulture)); + //} + + response.End(); + + return true; + } + + public override void SendFile(HttpContextBase context) + { + // Don't send any file. + } + } + + + internal sealed class UnmodifiedFileResponder : FileResponder + { + public UnmodifiedFileResponder(IFileResponse fileResponse) + : base(fileResponse) + { + } + + public override bool TrySendHeaders(HttpContextBase context) + { + var response = context.Response; + + base.TrySendHeaders(context); + + response.StatusCode = (int)HttpStatusCode.NotModified; + response.StatusDescription = "Not Modified"; + + // Explicitly set the Content-Length header so the client doesn't wait for + // content but keeps the connection open for other requests + response.AddHeader("Content-Length", "0"); + + return true; + } + + public override void SendFile(HttpContextBase context) + { + // Don't send file, it is unmodified. Let browser fetch from its cache. + } + } + + + + internal sealed class FullFileResponder : FileResponder + { + public FullFileResponder(IFileResponse fileResponse) + : base(fileResponse) + { + } + + public override bool TrySendHeaders(HttpContextBase context) + { + base.TrySendHeaders(context); + + // The date the file was last modified + context.Response.Cache.SetLastModified(FileResponse.LastModifiedUtc); + + return true; + } + + public override void SendFile(HttpContextBase context) + { + var fileLength = FileResponse.FileLength ?? FileResponse.Transmitter.GetFileLength(); + FileResponse.Transmitter.TransmitFile(0, fileLength, fileLength, context); + } + } + + + + internal sealed class RangeFileResponder : FileResponder + { + internal struct ByteRange + { + internal long Offset; + internal long Length; + } + + private const string RANGE_BOUNDARY = ""; + private const string MULTIPART_CONTENT_TYPE = "multipart/byteranges; boundary="; + private const string MULTIPART_RANGE_DELIMITER = "--\r\n"; + private const string MULTIPART_RANGE_END = "----\r\n\r\n"; + private const string CONTENT_RANGE_FORMAT = "bytes {0}-{1}/{2}"; + private const int MAX_RANGE_ALLOWED = 5; + + private const int ERROR_ACCESS_DENIED = 5; + + private static readonly string[] _httpDateFormats = new string[] { "r", "dddd, dd-MMM-yy HH':'mm':'ss 'GMT'", "ddd MMM d HH':'mm':'ss yyyy" }; + + private readonly FileResponder _defaultResponder; + private readonly string _rangeHeader; + + public RangeFileResponder(IFileResponse fileResponse, string rangeHeader) + : base(fileResponse) + { + _defaultResponder = new FullFileResponder(fileResponse); + _rangeHeader = rangeHeader; + } + + public override bool TrySendHeaders(HttpContextBase context) + { + var fileLength = FileResponse.FileLength ?? FileResponse.Transmitter.GetFileLength(); + var etag = FileResponse.ETag; + var lastModified = FileResponse.LastModifiedUtc; + + var handled = ExecuteRangeRequest(context, fileLength, _rangeHeader, etag, lastModified); + if (!handled && _defaultResponder.TrySendHeaders(context)) + { + _defaultResponder.SendFile(context); + handled = true; + } + + return handled; + } + + public override void SendFile(HttpContextBase context) + { + // Do nothing here, we have handled everything in 'TrySendHeaders()' already + } + + #region Range Utils + + // Most of the helpers here were copied over from the internal StaticFileHandler.cs + + private bool ExecuteRangeRequest( + HttpContextBase context, + long fileLength, + string rangeHeader, + string etag, + DateTime lastModified) + { + var handled = false; + var request = context.Request; + var response = context.Response; + + // return "416 Requested range not satisfiable" if the file length is zero. + if (fileLength <= 0) + { + SendRangeNotSatisfiable(response, fileLength); + handled = true; + return handled; + } + + var ifRangeHeader = request.Headers["If-Range"]; + if (ifRangeHeader != null && ifRangeHeader.Length > 1) + { + // Is this an ETag or a Date? We only need to check two + // characters; an ETag either begins with W/ or it is quoted. + if (ifRangeHeader[0] == '"') + { + // it's a strong ETag + if (ifRangeHeader != etag) + { + // the etags do not match, and we will therefore return the entire response + return handled; + } + } + else if (ifRangeHeader[0] == 'W' && ifRangeHeader[1] == '/') + { + // it's a weak ETag, and is therefore not usable for sub-range retrieval and + // we will return the entire response + return handled; + } + else + { + // It's a date. If it is greater than or equal to the last-write time of the file, we can send the range. + if (IsOutDated(ifRangeHeader, lastModified)) + { + return handled; + } + } + } + + // the expected format is "bytes = [, , ...]" + // where is "-[]" or "-". + int indexOfEquals = rangeHeader.IndexOf('='); + if (indexOfEquals == -1 || indexOfEquals == rangeHeader.Length - 1) + { + // invalid syntax + return handled; + } + + int startIndex = indexOfEquals + 1; + bool isRangeHeaderSyntacticallyValid = true; + long offset; + long length; + bool isSatisfiable; + bool exceededMax = false; + ByteRange[] byteRanges = null; + int byteRangesCount = 0; + long totalBytes = 0; + while (startIndex < rangeHeader.Length && isRangeHeaderSyntacticallyValid) + { + isRangeHeaderSyntacticallyValid = GetNextRange(rangeHeader, ref startIndex, fileLength, out offset, out length, out isSatisfiable); + + if (!isRangeHeaderSyntacticallyValid) + { + break; + } + + if (!isSatisfiable) + { + continue; + } + + if (byteRanges == null) + { + byteRanges = new ByteRange[16]; + } + + if (byteRangesCount >= byteRanges.Length) + { + // grow byteRanges array + ByteRange[] buffer = new ByteRange[byteRanges.Length * 2]; + Array.Copy(byteRanges, buffer, byteRanges.Length); + byteRanges = buffer; + } + + byteRanges[byteRangesCount].Offset = offset; + byteRanges[byteRangesCount].Length = length; + byteRangesCount++; + + // IIS imposes this limitation too, and sends "400 Bad Request" if exceeded + totalBytes += length; + if (totalBytes > fileLength * MAX_RANGE_ALLOWED) + { + exceededMax = true; + break; + } + } + + if (!isRangeHeaderSyntacticallyValid) + { + return handled; + } + + if (exceededMax) + { + SendBadRequest(response); + handled = true; + return handled; + } + + if (byteRangesCount == 0) + { + // we parsed the Range header and found no satisfiable byte ranges, so return "416 Requested Range Not Satisfiable" + SendRangeNotSatisfiable(response, fileLength); + handled = true; + return handled; + } + + if (byteRangesCount == 1) + { + offset = byteRanges[0].Offset; + length = byteRanges[0].Length; + response.ContentType = FileResponse.ContentType; + string contentRange = String.Format(CultureInfo.InvariantCulture, CONTENT_RANGE_FORMAT, offset, offset + length - 1, fileLength); + response.AppendHeader("Content-Range", contentRange); + + SendResponseHeaders(); + FileResponse.Transmitter.TransmitFile(offset, length, fileLength, context); + } + else + { + response.ContentType = MULTIPART_CONTENT_TYPE; + string contentRange; + string partialContentType = "Content-Type: " + FileResponse.ContentType + "\r\n"; + + SendResponseHeaders(); + + for (int i = 0; i < byteRangesCount; i++) + { + offset = byteRanges[i].Offset; + length = byteRanges[i].Length; + response.Write(MULTIPART_RANGE_DELIMITER); + response.Write(partialContentType); + response.Write("Content-Range: "); + contentRange = String.Format(CultureInfo.InvariantCulture, CONTENT_RANGE_FORMAT, offset, offset + length - 1, fileLength); + response.Write(contentRange); + response.Write("\r\n\r\n"); + // Transmit + FileResponse.Transmitter.TransmitFile(offset, length, fileLength, context); + response.Write("\r\n"); + } + response.Write(MULTIPART_RANGE_END); + } + + handled = true; + return handled; + + void SendResponseHeaders() + { + // Send a "206 Partial Content" status + response.StatusCode = (int)HttpStatusCode.PartialContent; + response.AppendHeader("Accept-Ranges", "bytes"); + response.Cache.SetLastModified(lastModified); + response.Cache.SetETag(etag); + response.Cache.SetCacheability(HttpCacheability.Public); + } + } + + private void SendBadRequest(HttpResponseBase response) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.Write("Bad Request"); + } + + private void SendRangeNotSatisfiable(HttpResponseBase response, long fileLength) + { + response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable; + response.ContentType = null; + response.AppendHeader("Content-Range", "bytes */" + fileLength.ToString(NumberFormatInfo.InvariantInfo)); + } + + private static bool IsOutDated(string ifRangeHeader, DateTime lastModified) + { + try + { + var utcLastModified = lastModified.ToUniversalTime(); + var utc = UtcParse(ifRangeHeader); + return (utc < utcLastModified); + } + catch + { + return true; + } + } + + private static DateTime UtcParse(string time) + { + Guard.NotNull(time, nameof(time)); + + var dtStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; + if (DateTime.TryParseExact(time, _httpDateFormats, null, dtStyles, out var utcDate)) + { + return utcDate; + } + + throw new FormatException($"{time} is an invalid date expression."); + } + + // initial space characters are skipped, and the string of digits up until the first non-digit + // are converted to a long. If digits are found the method returns true; otherwise false. + private static bool GetLongFromSubstring(string s, ref int startIndex, out long result) + { + result = 0; + + // get index of first digit + MovePastSpaceCharacters(s, ref startIndex); + int beginIndex = startIndex; + + // get index of last digit + MovePastDigits(s, ref startIndex); + int endIndex = startIndex - 1; + + // are there any digits? + if (endIndex < beginIndex) + { + return false; + } + + long multipleOfTen = 1; + for (int i = endIndex; i >= beginIndex; i--) + { + int digit = s[i] - '0'; + result += digit * multipleOfTen; + multipleOfTen *= 10; + // check for overflow + if (result < 0) + { + return false; + } + } + + return true; + } + + // The Range header consists of one or more byte range specifiers. E.g, "Range: bytes=0-1024,-1024" is a request + // for the first and last 1024 bytes of a file. Before this method is called, startIndex points to the beginning + // of a byte range specifier; and afterwards it points to the beginning of the next byte range specifier. + // If the current byte range specifier is syntactially inavlid, this function will return false indicating that the + // Range header must be ignored. If the function returns true, then the byte range specifier will be converted to + // an offset and length, and the startIndex will be incremented to the next byte range specifier. The byte range + // specifier (offset and length) returned by this function is satisfiable if and only if isSatisfiable is true. + private static bool GetNextRange(string rangeHeader, ref int startIndex, long fileLength, out long offset, out long length, out bool isSatisfiable) + { + // startIndex is first char after '=', or first char after ',' + Debug.Assert(startIndex < rangeHeader.Length, "startIndex < rangeHeader.Length"); + + offset = 0; + length = 0; + isSatisfiable = false; + + // A Range request to an empty file is never satisfiable, and will always receive a 416 status. + if (fileLength <= 0) + { + // put startIndex at end of string so we don't try to call GetNextRange again + startIndex = rangeHeader.Length; + return true; + } + + MovePastSpaceCharacters(rangeHeader, ref startIndex); + + if (startIndex < rangeHeader.Length && rangeHeader[startIndex] == '-') + { + // this range is of the form "-mmm" + startIndex++; + if (!GetLongFromSubstring(rangeHeader, ref startIndex, out length)) + { + return false; + } + + if (length > fileLength) + { + // send entire file + offset = 0; + length = fileLength; + } + else + { + // send last N bytes + offset = fileLength - length; + } + + isSatisfiable = IsRangeSatisfiable(offset, length, fileLength); + // we parsed the current range, but need to successfully move the startIndex to the next range + return IncrementToNextRange(rangeHeader, ref startIndex); + } + else + { + // this range is of the form "nnn-[mmm]" + if (!GetLongFromSubstring(rangeHeader, ref startIndex, out offset)) + { + return false; + } + + // increment startIndex past '-' + if (startIndex < rangeHeader.Length && rangeHeader[startIndex] == '-') + { + startIndex++; + } + else + { + return false; + } + + if (!GetLongFromSubstring(rangeHeader, ref startIndex, out long endPos)) + { + // assume range is of form "nnn-". If it isn't, + // the call to IncrementToNextRange will return false + length = fileLength - offset; + } + else + { + // if...greater than or equal to the current length of the entity-body, last-byte-pos + // is taken to be equal to one less than the current length of the entity- body in bytes. + if (endPos > fileLength - 1) + { + endPos = fileLength - 1; + } + + length = endPos - offset + 1; + + if (length < 1) + { + // the byte range specifier is syntactially invalid + // because the last-byte-pos < first-byte-pos + return false; + } + } + + isSatisfiable = IsRangeSatisfiable(offset, length, fileLength); + + // we parsed the current range, but need to successfully move the startIndex to the next range + return IncrementToNextRange(rangeHeader, ref startIndex); + } + } + + private static bool IncrementToNextRange(string s, ref int startIndex) + { + // increment startIndex until next token and return true, unless the syntax is invalid + MovePastSpaceCharacters(s, ref startIndex); + + if (startIndex < s.Length) + { + if (s[startIndex] != ',') + { + return false; + } + // move to first char after ',' + startIndex++; + } + + return true; + } + + private static bool IsRangeSatisfiable(long offset, long length, long fileLength) + { + return (offset < fileLength && length > 0); + } + + private static bool IsSecurityError(int ErrorCode) + { + return (ErrorCode == ERROR_ACCESS_DENIED); + } + + private static void MovePastSpaceCharacters(string s, ref int startIndex) + { + while (startIndex < s.Length && s[startIndex] == ' ') + { + startIndex++; + } + } + + private static void MovePastDigits(string s, ref int startIndex) + { + while (startIndex < s.Length && s[startIndex] <= '9' && s[startIndex] >= '0') + { + startIndex++; + } + } + + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs index bed604eaa9..10574118e1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs @@ -4,126 +4,126 @@ namespace SmartStore.Web.Framework.Modelling { - public abstract class FileTransmitter - { - public abstract long GetFileLength(); - public abstract void TransmitFile(long offset, long length, long fileLength, HttpContextBase context); - } - - public sealed class FileBufferTransmitter : FileTransmitter - { - private readonly Func _bufferFactory; - private byte[] _buffer; - - public FileBufferTransmitter(Func bufferFactory) - { - Guard.NotNull(bufferFactory, nameof(bufferFactory)); - _bufferFactory = bufferFactory; - } - - public override long GetFileLength() - { - return GetBuffer().LongLength; - } - - public override void TransmitFile(long offset, long length, long fileLength, HttpContextBase context) - { - context.Response.OutputStream.Write(GetBuffer(), (int)offset, (int)length); - } - - private byte[] GetBuffer() - { - if (_buffer == null) - { - _buffer = _bufferFactory(); - if (_buffer == null) - { - throw new NullReferenceException("File buffer cannot be NULL."); - } - } - - return _buffer; - } - } - - public sealed class FileStreamTransmitter : FileTransmitter - { - // Default buffer size as defined in BufferedStream type - const int DefaultBufferSize = 81920; - - private readonly Func _streamFactory; - private Stream _stream; - - public FileStreamTransmitter(Func streamFactory) - { - Guard.NotNull(streamFactory, nameof(streamFactory)); - _streamFactory = streamFactory; - } - - public override long GetFileLength() - { - return GetStream().Length; - } - - public override void TransmitFile(long offset, long length, long fileLength, HttpContextBase context) - { - var response = context.Response; - var stream = GetStream(); - - response.BufferOutput = false; - //response.Buffer = true; // Buffering leads to high RAM usage - - using (stream) - { - int bufferSize = (int)Math.Min(DefaultBufferSize, length); - byte[] buffer = new byte[bufferSize]; - - int read; - long remaining = length; - - if (stream.CanSeek) - { - stream.Seek(offset, SeekOrigin.Begin); - } - while ((remaining > 0) && (read = stream.Read(buffer, 0, buffer.Length)) != 0) - { - if (response.IsClientConnected) - { - try - { - response.OutputStream.Write(buffer, 0, read); - remaining -= read; - } - catch (Exception ex) - { - var isConClose = ex is HttpException httpEx && httpEx.ErrorCode == -2147023901; - if (!isConClose) - { - throw ex; - } - } - } - else - { - remaining = 0; - break; - } - } - } - } - - private Stream GetStream() - { - if (_stream == null) - { - _stream = _streamFactory(); - if (_stream == null) - { - throw new NullReferenceException("File stream cannot be NULL."); - } - } - - return _stream; - } - } + public abstract class FileTransmitter + { + public abstract long GetFileLength(); + public abstract void TransmitFile(long offset, long length, long fileLength, HttpContextBase context); + } + + public sealed class FileBufferTransmitter : FileTransmitter + { + private readonly Func _bufferFactory; + private byte[] _buffer; + + public FileBufferTransmitter(Func bufferFactory) + { + Guard.NotNull(bufferFactory, nameof(bufferFactory)); + _bufferFactory = bufferFactory; + } + + public override long GetFileLength() + { + return GetBuffer().LongLength; + } + + public override void TransmitFile(long offset, long length, long fileLength, HttpContextBase context) + { + context.Response.OutputStream.Write(GetBuffer(), (int)offset, (int)length); + } + + private byte[] GetBuffer() + { + if (_buffer == null) + { + _buffer = _bufferFactory(); + if (_buffer == null) + { + throw new NullReferenceException("File buffer cannot be NULL."); + } + } + + return _buffer; + } + } + + public sealed class FileStreamTransmitter : FileTransmitter + { + // Default buffer size as defined in BufferedStream type + const int DefaultBufferSize = 81920; + + private readonly Func _streamFactory; + private Stream _stream; + + public FileStreamTransmitter(Func streamFactory) + { + Guard.NotNull(streamFactory, nameof(streamFactory)); + _streamFactory = streamFactory; + } + + public override long GetFileLength() + { + return GetStream().Length; + } + + public override void TransmitFile(long offset, long length, long fileLength, HttpContextBase context) + { + var response = context.Response; + var stream = GetStream(); + + response.BufferOutput = false; + //response.Buffer = true; // Buffering leads to high RAM usage + + using (stream) + { + int bufferSize = (int)Math.Min(DefaultBufferSize, length); + byte[] buffer = new byte[bufferSize]; + + int read; + long remaining = length; + + if (stream.CanSeek) + { + stream.Seek(offset, SeekOrigin.Begin); + } + while ((remaining > 0) && (read = stream.Read(buffer, 0, buffer.Length)) != 0) + { + if (response.IsClientConnected) + { + try + { + response.OutputStream.Write(buffer, 0, read); + remaining -= read; + } + catch (Exception ex) + { + var isConClose = ex is HttpException httpEx && httpEx.ErrorCode == -2147023901; + if (!isConClose) + { + throw ex; + } + } + } + else + { + remaining = 0; + break; + } + } + } + } + + private Stream GetStream() + { + if (_stream == null) + { + _stream = _streamFactory(); + if (_stream == null) + { + throw new NullReferenceException("File stream cannot be NULL."); + } + } + + return _stream; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs index 3bc0b9c483..b24463c932 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs @@ -1,16 +1,15 @@ using System; -using System.Drawing; namespace SmartStore.Web.Framework.Modelling { - public interface IFileResponse - { - string ContentType { get; } - long? FileLength { get; } - Size? Dimensions { get; } - DateTime LastModifiedUtc { get; } - TimeSpan MaxAge { get; } - string ETag { get; } - FileTransmitter Transmitter { get; } - } + public interface IFileResponse + { + string ContentType { get; } + long? FileLength { get; } + //Func LazyDimensions { get; } + DateTime LastModifiedUtc { get; } + TimeSpan MaxAge { get; } + string ETag { get; } + FileTransmitter Transmitter { get; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/JsonNetResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/JsonNetResult.cs index fb9321e1de..aa85bc4b20 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/JsonNetResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/JsonNetResult.cs @@ -8,23 +8,23 @@ // ReSharper disable CheckNamespace namespace SmartStore.Web.Framework.Modelling { - public class JsonNetResult : JsonResult - { + public class JsonNetResult : JsonResult + { private readonly IDateTimeHelper _dateTimeHelper; - private readonly JsonSerializerSettings _settings; + private readonly JsonSerializerSettings _settings; - public JsonNetResult(IDateTimeHelper dateTimeHelper) - : this(dateTimeHelper, null) - { - } + public JsonNetResult(IDateTimeHelper dateTimeHelper) + : this(dateTimeHelper, null) + { + } - public JsonNetResult(IDateTimeHelper dateTimeHelper, JsonSerializerSettings settings) - { - Guard.NotNull(dateTimeHelper, nameof(dateTimeHelper)); + public JsonNetResult(IDateTimeHelper dateTimeHelper, JsonSerializerSettings settings) + { + Guard.NotNull(dateTimeHelper, nameof(dateTimeHelper)); - _dateTimeHelper = dateTimeHelper; - _settings = settings; - } + _dateTimeHelper = dateTimeHelper; + _settings = settings; + } public static JsonSerializerSettings CreateDefaultSerializerSettings() { @@ -49,88 +49,88 @@ public static JsonSerializerSettings CreateDefaultSerializerSettings() } public override void ExecuteResult(ControllerContext context) - { - Guard.NotNull(context, nameof(context)); + { + Guard.NotNull(context, nameof(context)); + + if (this.Data == null) + return; - if (this.Data == null) - return; + if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && context.HttpContext.Request.HttpMethod.IsCaseInsensitiveEqual("GET")) + { + throw new InvalidOperationException("This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request.To allow GET requests, set JsonRequestBehavior to AllowGet."); + } - if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && context.HttpContext.Request.HttpMethod.IsCaseInsensitiveEqual("GET")) - { - throw new InvalidOperationException("This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request.To allow GET requests, set JsonRequestBehavior to AllowGet."); - } + var response = context.HttpContext.Response; - var response = context.HttpContext.Response; + if (this.ContentEncoding != null) + { + response.ContentEncoding = this.ContentEncoding; + } - if (this.ContentEncoding != null) - { - response.ContentEncoding = this.ContentEncoding; - } - - response.ContentType = this.ContentType.NullEmpty() ?? "application/json"; + response.ContentType = this.ContentType.NullEmpty() ?? "application/json"; var serializerSettings = _settings ?? CreateDefaultSerializerSettings(); - if (_settings == null) - { - var utcDateTimeConverter = new UTCDateTimeConverter(_dateTimeHelper, new JavaScriptDateTimeConverter()); - serializerSettings.Converters.Add(utcDateTimeConverter); - } - - using (var jsonWriter = new JsonTextWriter(response.Output)) - { - jsonWriter.CloseOutput = false; - var jsonSerializer = JsonSerializer.Create(serializerSettings); - jsonSerializer.Serialize(jsonWriter, this.Data); + if (_settings == null) + { + var utcDateTimeConverter = new UTCDateTimeConverter(_dateTimeHelper, new JavaScriptDateTimeConverter()); + serializerSettings.Converters.Add(utcDateTimeConverter); + } + + using (var jsonWriter = new JsonTextWriter(response.Output)) + { + jsonWriter.CloseOutput = false; + var jsonSerializer = JsonSerializer.Create(serializerSettings); + jsonSerializer.Serialize(jsonWriter, this.Data); } - } - - class UTCDateTimeConverter : DateTimeConverterBase - { - private readonly IDateTimeHelper _dateTimeHelper; - private readonly DateTimeConverterBase _innerConverter; - - public UTCDateTimeConverter(IDateTimeHelper dateTimeHelper, DateTimeConverterBase innerConverter) - { - Guard.NotNull(innerConverter, nameof(innerConverter)); - - _dateTimeHelper = dateTimeHelper; - _innerConverter = innerConverter; - } - - public override bool CanConvert(Type objectType) - { - return _innerConverter.CanConvert(objectType); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return _innerConverter.ReadJson(reader, objectType, existingValue, serializer); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is DateTime) - { - var d = (DateTime)value; - if (d.Kind == DateTimeKind.Unspecified) - { - // when DateTime kind is "Unspecified", it was very likely converted from UTC to - // SERVER*s preferred local time before (with DateTimeHelper.ConvertToUserTime()). - // While this works fine during server-time rendering, it can lead to wrong UTC offsets - // on the client (e.g. in AJAX mode Grids, where rendering is performed locally with JSON data). - // The issue occurs when the client's time zone is not the same as "CurrentTimeZone" (configured in the backend). - // To fix it, we have to convert the date back to UTC kind, but with the SERVER PREFERRED TIMEZONE - // in order to calculate with the correct UTC offset. Then it's up to the client to display the date - // in the CLIENT's time zone. Which is not perfect of course, because the same date would be displayed in the - // "CurrentTimeZone" if rendered on server. - // But: it fixes the issue and is way better than converting all AJAXable dates to strings on the server. - value = _dateTimeHelper.ConvertToUtcTime(d, _dateTimeHelper.CurrentTimeZone); - } - } - - _innerConverter.WriteJson(writer, value, serializer); - } - } - } + } + + class UTCDateTimeConverter : DateTimeConverterBase + { + private readonly IDateTimeHelper _dateTimeHelper; + private readonly DateTimeConverterBase _innerConverter; + + public UTCDateTimeConverter(IDateTimeHelper dateTimeHelper, DateTimeConverterBase innerConverter) + { + Guard.NotNull(innerConverter, nameof(innerConverter)); + + _dateTimeHelper = dateTimeHelper; + _innerConverter = innerConverter; + } + + public override bool CanConvert(Type objectType) + { + return _innerConverter.CanConvert(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return _innerConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is DateTime) + { + var d = (DateTime)value; + if (d.Kind == DateTimeKind.Unspecified) + { + // when DateTime kind is "Unspecified", it was very likely converted from UTC to + // SERVER*s preferred local time before (with DateTimeHelper.ConvertToUserTime()). + // While this works fine during server-time rendering, it can lead to wrong UTC offsets + // on the client (e.g. in AJAX mode Grids, where rendering is performed locally with JSON data). + // The issue occurs when the client's time zone is not the same as "CurrentTimeZone" (configured in the backend). + // To fix it, we have to convert the date back to UTC kind, but with the SERVER PREFERRED TIMEZONE + // in order to calculate with the correct UTC offset. Then it's up to the client to display the date + // in the CLIENT's time zone. Which is not perfect of course, because the same date would be displayed in the + // "CurrentTimeZone" if rendered on server. + // But: it fixes the issue and is way better than converting all AJAXable dates to strings on the server. + value = _dateTimeHelper.ConvertToUtcTime(d, _dateTimeHelper.CurrentTimeZone); + } + } + + _innerConverter.WriteJson(writer, value, serializer); + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/PermissiveRedirectResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/PermissiveRedirectResult.cs index e394aba608..7c6c729645 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/PermissiveRedirectResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/PermissiveRedirectResult.cs @@ -1,25 +1,24 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; namespace SmartStore.Web.Framework.Modelling { - /// - /// Allows redirects from within child actions while keeping TempData - /// - public class PermissiveRedirectResult : ActionResult - { - private readonly string _url; + /// + /// Allows redirects from within child actions while keeping TempData + /// + public class PermissiveRedirectResult : ActionResult + { + private readonly string _url; - public PermissiveRedirectResult(string url) - { - _url = url; - } + public PermissiveRedirectResult(string url) + { + _url = url; + } - public override void ExecuteResult(ControllerContext context) - { - var url = UrlHelper.GenerateContentUrl(_url, context.HttpContext); - context.Controller.TempData.Keep(); - context.HttpContext.Response.Redirect(url, false); - } - } + public override void ExecuteResult(ControllerContext context) + { + var url = UrlHelper.GenerateContentUrl(_url, context.HttpContext); + context.Controller.TempData.Keep(); + context.HttpContext.Response.Redirect(url, false); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs index 2e9f733b80..a112f0b355 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs @@ -1,31 +1,27 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Web.Mvc; namespace SmartStore.Web.Framework.Modelling.Results { - public class RootActionViewResult : ViewResult - { - protected override ViewEngineResult FindView(ControllerContext context) - { - if (base.ViewName.IsEmpty()) - throw new InvalidOperationException($"{nameof(ViewName)} is required when using {nameof(RootActionViewResult)}."); + public class RootActionViewResult : ViewResult + { + protected override ViewEngineResult FindView(ControllerContext context) + { + if (base.ViewName.IsEmpty()) + throw new InvalidOperationException($"{nameof(ViewName)} is required when using {nameof(RootActionViewResult)}."); - return base.FindView(context.GetRootControllerContext()); - } - } + return base.FindView(context.GetRootControllerContext()); + } + } - public class RootActionPartialViewResult : PartialViewResult - { - protected override ViewEngineResult FindView(ControllerContext context) - { - if (base.ViewName.IsEmpty()) - throw new InvalidOperationException($"{nameof(ViewName)} is required when using {nameof(RootActionPartialViewResult)}."); + public class RootActionPartialViewResult : PartialViewResult + { + protected override ViewEngineResult FindView(ControllerContext context) + { + if (base.ViewName.IsEmpty()) + throw new InvalidOperationException($"{nameof(ViewName)} is required when using {nameof(RootActionPartialViewResult)}."); - return base.FindView(context.GetRootControllerContext()); - } - } + return base.FindView(context.GetRootControllerContext()); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RssActionResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RssActionResult.cs index ead023a777..2d44042448 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RssActionResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RssActionResult.cs @@ -14,7 +14,7 @@ public override void ExecuteResult(ControllerContext context) context.HttpContext.Response.ContentType = "application/rss+xml"; var rssFormatter = new Rss20FeedFormatter(Feed, false); - var settings = new XmlWriterSettings { Indent = true, IndentChars = "\t", CheckCharacters = false }; + var settings = new XmlWriterSettings { Indent = true, IndentChars = "\t", CheckCharacters = false }; using (var writer = XmlWriter.Create(context.HttpContext.Response.Output, settings)) { diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/SmartMetadataProvider.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/SmartMetadataProvider.cs index 65215447ff..2f8edaa461 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/SmartMetadataProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/SmartMetadataProvider.cs @@ -13,39 +13,39 @@ namespace SmartStore.Web.Framework.Modelling ///
public class SmartMetadataProvider : CachedDataAnnotationsModelMetadataProvider { - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] - protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable attributes, Type containerType, Type modelType, string propertyName) - { - var metadata = base.CreateMetadataPrototype(attributes, containerType, modelType, propertyName); - var attrs = attributes.OfType().ToList(); + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] + protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable attributes, Type containerType, Type modelType, string propertyName) + { + var metadata = base.CreateMetadataPrototype(attributes, containerType, modelType, propertyName); + var attrs = attributes.OfType().ToList(); - foreach (var attr in attrs) - { - if (metadata.AdditionalValues.ContainsKey(attr.Name)) - { - throw new SmartException("There is already an attribute with the name of '{0}' on this model.".FormatCurrent(attr.Name)); - } - metadata.AdditionalValues.Add(attr.Name, attr); - } + foreach (var attr in attrs) + { + if (metadata.AdditionalValues.ContainsKey(attr.Name)) + { + throw new SmartException("There is already an attribute with the name of '{0}' on this model.".FormatCurrent(attr.Name)); + } + metadata.AdditionalValues.Add(attr.Name, attr); + } - return metadata; - } + return metadata; + } - protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func modelAccessor) - { - var result = base.CreateMetadataFromPrototype(prototype, modelAccessor); + protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func modelAccessor) + { + var result = base.CreateMetadataFromPrototype(prototype, modelAccessor); - if (prototype.AdditionalValues.Count > 0) - { - var attrs = prototype.AdditionalValues.Where(x => x.Value is IModelAttribute).ToArray(); - if (attrs.Any()) - { - result.AdditionalValues.AddRange(attrs); - } - } + if (prototype.AdditionalValues.Count > 0) + { + var attrs = prototype.AdditionalValues.Where(x => x.Value is IModelAttribute).ToArray(); + if (attrs.Any()) + { + result.AdditionalValues.AddRange(attrs); + } + } - return result; - } + return result; + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/SmartModelBinder.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/SmartModelBinder.cs index fef277e4da..350a009f70 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/SmartModelBinder.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/SmartModelBinder.cs @@ -2,24 +2,25 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Security; using System.Web.Mvc; using SmartStore.Core.Html; using SmartStore.Web.Framework.Security; namespace SmartStore.Web.Framework.Modelling { - public class SmartModelBinder : DefaultModelBinder + public class SmartModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { - var modelType = bindingContext.ModelType; + var modelType = bindingContext.ModelType; - if (modelType == typeof(CustomPropertiesDictionary)) - { - return BindCustomPropertiesDictioary(controllerContext, bindingContext); - } + if (modelType == typeof(CustomPropertiesDictionary)) + { + return BindCustomPropertiesDictioary(controllerContext, bindingContext); + } - var model = base.BindModel(controllerContext, bindingContext); + var model = base.BindModel(controllerContext, bindingContext); if (model is ModelBase) { @@ -30,118 +31,125 @@ public override object BindModel(ControllerContext controllerContext, ModelBindi } protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value) - { - var attrs = property.Attributes; - var sanitizeHtmlAttr = attrs.OfType().FirstOrDefault(); - - if (property.PropertyType == typeof(string) && sanitizeHtmlAttr != null && value is string input && !string.IsNullOrWhiteSpace(input)) - { - value = HtmlUtils.SanitizeHtml(input, sanitizeHtmlAttr.IsFragment); - } - - base.SetProperty(controllerContext, bindingContext, property, value); - } - - private CustomPropertiesDictionary BindCustomPropertiesDictioary(ControllerContext controllerContext, ModelBindingContext bindingContext) - { - var model = bindingContext.Model as CustomPropertiesDictionary ?? new CustomPropertiesDictionary(); - - var keys = GetValueProviderKeys(controllerContext, bindingContext.ModelName + "["); - if (keys.Count == 0) - { - return model; - } - - foreach (var key in keys) - { - var keyName = GetKeyName(key); - if (keyName == null || model.ContainsKey(keyName)) - continue; - - var valueBinder = this.Binders.DefaultBinder; - - var subPropertyName = GetSubPropertyName(key); - if (subPropertyName.IsCaseInsensitiveEqual("__Type__")) - continue; - - if (subPropertyName == null) - { - var simpleBindingContext = new ModelBindingContext - { - ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, GetValueType(keys, key, bindingContext.ValueProvider)), - ModelName = key, - ModelState = bindingContext.ModelState, - PropertyFilter = bindingContext.PropertyFilter, - ValueProvider = bindingContext.ValueProvider - }; - var value = valueBinder.BindModel(controllerContext, simpleBindingContext); - model[keyName] = value; - } - else - { - // Is Complex type - var modelName = key.Substring(0, key.Length - subPropertyName.Length - 1); - var valueType = GetValueType(keys, modelName, bindingContext.ValueProvider); - valueBinder = this.Binders.GetBinder(valueType); - var complexBindingContext = new ModelBindingContext - { - ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType), - ModelName = key.Substring(0, key.Length - subPropertyName.Length - 1), - ModelState = bindingContext.ModelState, - PropertyFilter = bindingContext.PropertyFilter, - ValueProvider = bindingContext.ValueProvider - }; - var value = valueBinder.BindModel(controllerContext, complexBindingContext); - model[keyName] = value; - } - } - - return model; - } - - private HashSet GetValueProviderKeys(ControllerContext context, string prefix) - { - var keys = context.HttpContext.Request.Form.Keys.Cast() - .Concat(((IDictionary)context.RouteData.Values).Keys.Cast()) - .Concat(context.HttpContext.Request.QueryString.Keys.Cast()) - .Concat(context.HttpContext.Request.Files.Keys.Cast()) - .Where(x => x.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)); - - return new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); - } - - private string GetKeyName(string key) - { - int startBracket = key.IndexOf("["); - int endBracket = key.IndexOf("]", startBracket); - - if (endBracket == -1) - return null; - - return key.Substring(startBracket + 1, endBracket - startBracket - 1); - } - - private string GetSubPropertyName(string key) - { - var parts = key.Split('.'); - if (parts.Length > 1) - { - return parts[1]; - } - - return null; - } - - private Type GetValueType(HashSet keys, string prefix, IValueProvider valueProvider) - { - var typeKey = prefix + ".__Type__"; - if (keys.Contains(typeKey)) - { - var type = Type.GetType(valueProvider.GetValue(typeKey).AttemptedValue, true); - return type; - } - - return typeof(object); - } + { + var attrs = property.Attributes; + var sanitizeHtmlAttr = attrs.OfType().FirstOrDefault(); + + if (property.PropertyType == typeof(string) && sanitizeHtmlAttr != null && value is string input && !string.IsNullOrWhiteSpace(input)) + { + value = HtmlUtils.SanitizeHtml(input, sanitizeHtmlAttr.IsFragment); + } + + base.SetProperty(controllerContext, bindingContext, property, value); + } + + private CustomPropertiesDictionary BindCustomPropertiesDictioary(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + var model = bindingContext.Model as CustomPropertiesDictionary ?? new CustomPropertiesDictionary(); + + var keys = GetValueProviderKeys(controllerContext, bindingContext.ModelName + "["); + if (keys.Count == 0) + { + return model; + } + + foreach (var key in keys) + { + var keyName = GetKeyName(key); + if (keyName == null || model.ContainsKey(keyName)) + continue; + + var valueBinder = this.Binders.DefaultBinder; + + var subPropertyName = GetSubPropertyName(key); + if (subPropertyName.IsCaseInsensitiveEqual("__Type__")) + continue; + + if (subPropertyName == null) + { + var simpleBindingContext = new ModelBindingContext + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, GetValueType(keys, key, bindingContext.ValueProvider)), + ModelName = key, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + var value = valueBinder.BindModel(controllerContext, simpleBindingContext); + model[keyName] = value; + } + else + { + // Is Complex type + var modelName = key.Substring(0, key.Length - subPropertyName.Length - 1); + var valueType = GetValueType(keys, modelName, bindingContext.ValueProvider); + if (!valueType.HasAttribute(false)) + { + throw new SecurityException("For security reasons complex types in '{0}' must be decorated with the '{1}' attribute.".FormatInvariant( + typeof(CustomPropertiesDictionary).AssemblyQualifiedNameWithoutVersion(), + typeof(CustomModelPartAttribute).AssemblyQualifiedNameWithoutVersion())); + } + + valueBinder = this.Binders.GetBinder(valueType); + var complexBindingContext = new ModelBindingContext + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType), + ModelName = key.Substring(0, key.Length - subPropertyName.Length - 1), + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + var value = valueBinder.BindModel(controllerContext, complexBindingContext); + model[keyName] = value; + } + } + + return model; + } + + private HashSet GetValueProviderKeys(ControllerContext context, string prefix) + { + var keys = context.HttpContext.Request.Form.Keys.Cast() + .Concat(((IDictionary)context.RouteData.Values).Keys.Cast()) + .Concat(context.HttpContext.Request.QueryString.Keys.Cast()) + .Concat(context.HttpContext.Request.Files.Keys.Cast()) + .Where(x => x.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)); + + return new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); + } + + private string GetKeyName(string key) + { + int startBracket = key.IndexOf("["); + int endBracket = key.IndexOf("]", startBracket); + + if (endBracket == -1) + return null; + + return key.Substring(startBracket + 1, endBracket - startBracket - 1); + } + + private string GetSubPropertyName(string key) + { + var parts = key.Split('.'); + if (parts.Length > 1) + { + return parts[1]; + } + + return null; + } + + private Type GetValueType(HashSet keys, string prefix, IValueProvider valueProvider) + { + var typeKey = prefix + ".__Type__"; + if (keys.Contains(typeKey)) + { + var type = Type.GetType(valueProvider.GetValue(typeKey).AttemptedValue, true); + return type; + } + + return typeof(object); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs index eef903e167..19ad2d26bb 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs @@ -1,14 +1,13 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; using SmartStore.Services.Pdf; namespace SmartStore.Web.Framework.Pdf { - public class PdfPartialViewContent : PdfHtmlContent - { - public PdfPartialViewContent(string partialViewName, object model, ControllerContext controllerContext) - : base(PdfViewContent.ViewToString(partialViewName, null, model, true, controllerContext, false)) - { - } - } + public class PdfPartialViewContent : PdfHtmlContent + { + public PdfPartialViewContent(string partialViewName, object model, ControllerContext controllerContext) + : base(PdfViewContent.ViewToString(partialViewName, null, model, true, controllerContext, false)) + { + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs index 170a240570..67611c5559 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs @@ -1,41 +1,40 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; using System.Web.Routing; using SmartStore.Services.Pdf; namespace SmartStore.Web.Framework.Pdf -{ - public class PdfRouteContent : PdfUrlContent - { - public PdfRouteContent(string routeName, ControllerContext controllerContext) - : this(routeName, null, null, null, controllerContext) - { - } +{ + public class PdfRouteContent : PdfUrlContent + { + public PdfRouteContent(string routeName, ControllerContext controllerContext) + : this(routeName, null, null, null, controllerContext) + { + } - public PdfRouteContent(string routeName, RouteValueDictionary routeValues, ControllerContext controllerContext) - : this(routeName, null, null, routeValues, controllerContext) - { - } + public PdfRouteContent(string routeName, RouteValueDictionary routeValues, ControllerContext controllerContext) + : this(routeName, null, null, routeValues, controllerContext) + { + } - public PdfRouteContent(string action, string controller, ControllerContext controllerContext) - : this(null, action, controller, null, controllerContext) - { - } + public PdfRouteContent(string action, string controller, ControllerContext controllerContext) + : this(null, action, controller, null, controllerContext) + { + } - public PdfRouteContent(string action, string controller, RouteValueDictionary routeValues, ControllerContext controllerContext) - : this(null, action, controller, routeValues, controllerContext) - { - } + public PdfRouteContent(string action, string controller, RouteValueDictionary routeValues, ControllerContext controllerContext) + : this(null, action, controller, routeValues, controllerContext) + { + } - protected PdfRouteContent(string routeName, string action, string controller, RouteValueDictionary routeValues, ControllerContext controllerContext) - : base(UrlHelper.GenerateUrl(routeName, action, controller, routeValues, RouteTable.Routes, controllerContext.RequestContext, true)) - { - //Guard.NotNull(controllerContext, nameof(controllerContext)); - - //if (routeName.IsEmpty() || action.IsEmpty()) - //{ - // throw Error.InvalidOperation("Either 'routeName' or 'action' must be set"); - //} - } - } + protected PdfRouteContent(string routeName, string action, string controller, RouteValueDictionary routeValues, ControllerContext controllerContext) + : base(UrlHelper.GenerateUrl(routeName, action, controller, routeValues, RouteTable.Routes, controllerContext.RequestContext, true)) + { + //Guard.NotNull(controllerContext, nameof(controllerContext)); + + //if (routeName.IsEmpty() || action.IsEmpty()) + //{ + // throw Error.InvalidOperation("Either 'routeName' or 'action' must be set"); + //} + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs index a96f90c4b5..c81c46d4e7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs @@ -1,49 +1,47 @@ using System; -using System.Linq; -using System.Text; using System.Web.Mvc; using SmartStore.Services.Pdf; using SmartStore.Web.Framework.Controllers; namespace SmartStore.Web.Framework.Pdf -{ - public class PdfViewContent : PdfHtmlContent - { - public PdfViewContent(string viewName, object model, ControllerContext controllerContext) - : this(viewName, null, model, controllerContext) - { - } +{ + public class PdfViewContent : PdfHtmlContent + { + public PdfViewContent(string viewName, object model, ControllerContext controllerContext) + : this(viewName, null, model, controllerContext) + { + } - public PdfViewContent(string viewName, string masterName, object model, ControllerContext controllerContext) - : base(ViewToString(viewName, masterName, model, false, controllerContext, true)) - { - } + public PdfViewContent(string viewName, string masterName, object model, ControllerContext controllerContext) + : base(ViewToString(viewName, masterName, model, false, controllerContext, true)) + { + } - protected internal static string ViewToString(string viewName, string masterName, object model, bool isPartial, ControllerContext context, bool throwOnError) - { - Guard.NotNull(context, nameof(context)); + protected internal static string ViewToString(string viewName, string masterName, object model, bool isPartial, ControllerContext context, bool throwOnError) + { + Guard.NotNull(context, nameof(context)); - string html = null; + string html = null; - try - { - html = isPartial - ? context.Controller.RenderPartialViewToString(viewName, model) - : context.Controller.RenderViewToString(viewName, masterName, model); - } - catch (Exception) - { - if (throwOnError) - { - throw; - } - else - { - return null; - } - } + try + { + html = isPartial + ? context.Controller.RenderPartialViewToString(viewName, model) + : context.Controller.RenderViewToString(viewName, masterName, model); + } + catch (Exception) + { + if (throwOnError) + { + throw; + } + else + { + return null; + } + } - return html; - } - } + return html; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfHeaderFooterVariables.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfHeaderFooterVariables.cs index 4b1dd669f6..65a5a86f36 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfHeaderFooterVariables.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfHeaderFooterVariables.cs @@ -1,59 +1,55 @@ -using System; -using System.Collections.Generic; -using System.Web.Mvc; - -namespace SmartStore.Web.Framework.Pdf +namespace SmartStore.Web.Framework.Pdf { - public class PdfHeaderFooterVariables - { - /// - /// The number of the page currently being printed - /// - public int Page { get; set; } - - /// - /// The number of the first page to be printed - /// - public int FromPage { get; set; } - - /// - /// The number of the last page to be printed - /// - public int ToPage { get; set; } - - /// - /// The URL of the page being printed - /// - public string WebPage { get; set; } - - /// - /// The name of the current section - /// - public string Section { get; set; } - - /// - /// The name of the current subsection - /// - public string SubSection { get; set; } - - /// - /// The title of the of the current page object - /// - public string Title { get; set; } - - /// - /// The title of the output document - /// - public string DocTitle { get; set; } - - /// - /// The number of the page in the current site being converted - /// - public int SitePage { get; set; } - - /// - /// The number of pages in the current site being converted - /// - public int SitePages { get; set; } - } + public class PdfHeaderFooterVariables + { + /// + /// The number of the page currently being printed + /// + public int Page { get; set; } + + /// + /// The number of the first page to be printed + /// + public int FromPage { get; set; } + + /// + /// The number of the last page to be printed + /// + public int ToPage { get; set; } + + /// + /// The URL of the page being printed + /// + public string WebPage { get; set; } + + /// + /// The name of the current section + /// + public string Section { get; set; } + + /// + /// The name of the current subsection + /// + public string SubSection { get; set; } + + /// + /// The title of the of the current page object + /// + public string Title { get; set; } + + /// + /// The title of the output document + /// + public string DocTitle { get; set; } + + /// + /// The number of the page in the current site being converted + /// + public int SitePage { get; set; } + + /// + /// The number of pages in the current site being converted + /// + public int SitePages { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs index 67a69d0b5a..a413692334 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs @@ -1,23 +1,21 @@ using SmartStore.Core.Domain.Common; using SmartStore.Web.Framework.Modelling; -using SmartStore.Web.Framework.Pdf; namespace SmartStore.Web.Framework.Pdf { - public partial class PdfReceiptHeaderFooterModel : ModelBase + public partial class PdfReceiptHeaderFooterModel : ModelBase { - public int StoreId { get; set; } + public int StoreId { get; set; } + public string StoreName { get; set; } + public string StoreUrl { get; set; } + public int LogoId { get; set; } - public string StoreName { get; set; } - public string LogoUrl { get; set; } - public string StoreUrl { get; set; } + public CompanyInformationSettings MerchantCompanyInfo { get; set; } + public BankConnectionSettings MerchantBankAccount { get; set; } + public ContactDataSettings MerchantContactData { get; set; } - public CompanyInformationSettings MerchantCompanyInfo { get; set; } - public BankConnectionSettings MerchantBankAccount { get; set; } - public ContactDataSettings MerchantContactData { get; set; } + public string MerchantFormattedAddress { get; set; } - public string MerchantFormattedAddress { get; set; } - - public PdfHeaderFooterVariables Variables { get; set; } + public PdfHeaderFooterVariables Variables { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfResult.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfResult.cs index 19df5bc70a..525e0b070e 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfResult.cs @@ -1,53 +1,50 @@ -using System.IO; -using System.Text.RegularExpressions; -using System.Web; +using System.Web; using System.Web.Mvc; -using System.Web.Security; using SmartStore.Services.Pdf; using SmartStore.Utilities; namespace SmartStore.Web.Framework.Pdf { - public class PdfResult : ActionResult - { - private const string ContentType = "application/pdf"; - - public PdfResult(IPdfConverter converter, PdfConvertSettings settings) - { - Guard.NotNull(converter, nameof(converter)); - - this.Converter = converter; - this.Settings = settings ?? new PdfConvertSettings(); - } - - protected IPdfConverter Converter { get; set; } - - protected PdfConvertSettings Settings { get; set; } - - /// - /// The name of the generated PDF file. - /// - public string FileName { get; set; } - - protected HttpResponseBase PrepareResponse(HttpResponseBase response) - { - response.ContentType = ContentType; - - if (FileName.HasValue()) - { - response.AddHeader("Content-Disposition", "attachment; filename=\"{0}\"".FormatCurrent(PathHelper.SanitizeFileName(FileName))); - } - - response.AddHeader("Content-Type", ContentType); - - return response; - } - - public override void ExecuteResult(ControllerContext context) - { - var buffer = Converter.Convert(Settings); - var response = PrepareResponse(context.HttpContext.Response); - response.OutputStream.Write(buffer, 0, buffer.Length); - } - } + public class PdfResult : ActionResult + { + private const string ContentType = "application/pdf"; + + public PdfResult(IPdfConverter converter, PdfConvertSettings settings) + { + Guard.NotNull(converter, nameof(converter)); + + this.Converter = converter; + this.Settings = settings ?? new PdfConvertSettings(); + } + + protected IPdfConverter Converter { get; set; } + + protected PdfConvertSettings Settings { get; set; } + + /// + /// The name of the generated PDF file. + /// + public string FileName { get; set; } + + protected HttpResponseBase PrepareResponse(HttpResponseBase response) + { + response.ContentType = ContentType; + + if (FileName.HasValue()) + { + response.AddHeader("Content-Disposition", "attachment; filename=\"{0}\"".FormatCurrent(PathHelper.SanitizeFileName(FileName))); + } + + response.AddHeader("Content-Type", ContentType); + + return response; + } + + public override void ExecuteResult(ControllerContext context) + { + var buffer = Converter.Convert(Settings); + var response = PrepareResponse(context.HttpContext.Response); + response.OutputStream.Write(buffer, 0, buffer.Length); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginExtensions.cs index 0925a59f65..e15f9f655a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginExtensions.cs @@ -4,29 +4,29 @@ namespace SmartStore.Web.Framework { - public static class PluginExtensions - { - /// - /// Determines whether a plugin is installed and activated for a particular store. - /// - public static bool IsPluginReady(this IPluginFinder pluginFinder, ISettingService settingService, string systemName, int storeId) - { - try - { - var pluginDescriptor = pluginFinder.GetPluginDescriptorBySystemName(systemName); + public static class PluginExtensions + { + /// + /// Determines whether a plugin is installed and activated for a particular store. + /// + public static bool IsPluginReady(this IPluginFinder pluginFinder, ISettingService settingService, string systemName, int storeId) + { + try + { + var pluginDescriptor = pluginFinder.GetPluginDescriptorBySystemName(systemName); - if (pluginDescriptor != null && pluginDescriptor.Installed) - { - if (storeId == 0 || settingService.GetSettingByKey(pluginDescriptor.GetSettingKey("LimitedToStores")).ToIntArrayContains(storeId, true)) - return true; - } - } - catch (Exception exc) - { - exc.Dump(); - } + if (pluginDescriptor != null && pluginDescriptor.Installed) + { + if (storeId == 0 || settingService.GetSettingByKey(pluginDescriptor.GetSettingKey("LimitedToStores")).ToIntArrayContains(storeId, true)) + return true; + } + } + catch (Exception exc) + { + exc.Dump(); + } - return false; - } - } + return false; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs index 6cd85d9b5f..817fe53f8c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs @@ -2,12 +2,9 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; -using System.Linq.Expressions; -using System.Web.Routing; using Autofac; using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Localization; using SmartStore.Core.Plugins; using SmartStore.Services; using SmartStore.Services.Cms; @@ -16,298 +13,297 @@ namespace SmartStore.Web.Framework.Plugins { - public class PluginMediator - { - private static readonly ConcurrentDictionary _routesCache = new ConcurrentDictionary(); - private readonly ICommonServices _services; - private readonly IComponentContext _ctx; - - public PluginMediator(ICommonServices services, IComponentContext ctx) - { - _services = services; - _ctx = ctx; - } - - public string GetLocalizedFriendlyName(IProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) - { - return GetLocalizedValue(metadata, "FriendlyName", x => x.FriendlyName, languageId, returnDefaultValue); - } - - public string GetLocalizedDescription(IProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) - { - return GetLocalizedValue(metadata, "Description", x => x.Description, languageId, returnDefaultValue); - } - - public string GetLocalizedValue(TMetadata metadata, - string propertyName, - Func fallback, - int languageId = 0, - bool returnDefaultValue = true) - where TMetadata : IProviderMetadata - { - Guard.NotNull(metadata, nameof(metadata)); - - var systemName = metadata.SystemName; - var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); - string result = _services.Localization.GetResource(resourceName, languageId, false, "", true); - - if (result.IsEmpty() && returnDefaultValue) - result = fallback(metadata); - - return result; - } - - public void SaveLocalizedValue(IProviderMetadata metadata, int languageId, string propertyName, string value) - { - Guard.NotNull(metadata, nameof(metadata)); - Guard.IsPositive(languageId, nameof(languageId)); - Guard.NotEmpty(propertyName, nameof(propertyName)); - - var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); - var resource = _services.Localization.GetLocaleStringResourceByName(resourceName, languageId, false); - - if (resource != null) - { - if (value.IsEmpty()) - { - // delete - _services.Localization.DeleteLocaleStringResource(resource); - } - else - { - // update - resource.ResourceValue = value; - _services.Localization.UpdateLocaleStringResource(resource); - } - } - else - { - if (value.HasValue()) - { - // insert - resource = new LocaleStringResource - { - LanguageId = languageId, - ResourceName = resourceName, - ResourceValue = value, - }; - _services.Localization.InsertLocaleStringResource(resource); - } - } - } - - public int? GetUserDisplayOrder(ProviderMetadata metadata) - { - return GetSetting(metadata, "DisplayOrder"); - } - - public T GetSetting(ProviderMetadata metadata, string propertyName) - { - var settingKey = metadata.SettingKeyPattern.FormatInvariant(metadata.SystemName, propertyName); - return _services.Settings.GetSettingByKey(settingKey); - } - - public void SetUserDisplayOrder(ProviderMetadata metadata, int displayOrder) - { - Guard.NotNull(metadata, nameof(metadata)); - - metadata.DisplayOrder = displayOrder; - SetSetting(metadata, "DisplayOrder", displayOrder); - } - - public void SetSetting(ProviderMetadata metadata, string propertyName, T value) - { - Guard.NotNull(metadata, nameof(metadata)); - Guard.NotEmpty(propertyName, nameof(propertyName)); - - var settingKey = metadata.SettingKeyPattern.FormatInvariant(metadata.SystemName, propertyName); - - using (_services.Settings.BeginScope()) - { - if (value != null) - { - _services.Settings.SetSetting(settingKey, value, 0, false); - } - else - { - _services.Settings.DeleteSetting(settingKey); - } - } - } - - public ProviderModel ToProviderModel(Provider provider, bool forEdit = false, Action, ProviderModel> enhancer = null) - { - return ToProviderModel(provider, forEdit, enhancer); - } - - public TModel ToProviderModel(Provider provider, bool forEdit = false, Action, TModel> enhancer = null) - where TModel : ProviderModel, new() - where TProvider : IProvider - { - Guard.NotNull(provider, nameof(provider)); - - var metadata = provider.Metadata; - var model = new TModel(); - model.ProviderType = typeof(TProvider); - model.SystemName = metadata.SystemName; - model.FriendlyName = forEdit ? metadata.FriendlyName : GetLocalizedFriendlyName(metadata); - model.Description = forEdit ? metadata.Description : GetLocalizedDescription(metadata); - model.DisplayOrder = metadata.DisplayOrder; - model.IsEditable = metadata.IsEditable; - model.IconUrl = GetIconUrl(metadata); - model.PluginDescriptor = metadata.PluginDescriptor; - if (model.PluginDescriptor != null) - { - model.ProvidingPluginFriendlyName = model.PluginDescriptor.GetLocalizedValue(_services.Localization, "FriendlyName"); - } - - if (metadata.IsConfigurable) - { - var routeInfo = _routesCache.GetOrAdd(model.SystemName, (key) => - { - var configurable = (IConfigurable)provider.Value; - configurable.GetConfigurationRoute(out var actionName, out var controllerName, out var routeValues); - - if (actionName.IsEmpty()) - { - metadata.IsConfigurable = false; - return null; - } - else - { - return new RouteInfo(actionName, controllerName, routeValues); - } - }); - - if (routeInfo != null) - { - model.ConfigurationRoute = new RouteInfo(routeInfo); - } - } - - if (enhancer != null) - { - enhancer(provider, model); - } - - model.IsConfigurable = metadata.IsConfigurable; - - return model; - } - - public string GetBrandImageUrl(ProviderMetadata metadata) - { - var plugin = metadata.PluginDescriptor; - - if (plugin != null) - { - var filesToCheck = (new string[] { "branding.{0}.png", "branding.{0}.gif", "branding.{0}.jpg", "branding.{0}.jpeg" }).Select(x => x.FormatInvariant(metadata.SystemName)); - var dir = plugin.PhysicalPath; - foreach (var file in filesToCheck) - { - if (File.Exists(Path.Combine(dir, "Content", file))) - { - return "~/Plugins/{0}/Content/{1}".FormatInvariant(plugin.SystemName, file); - } - } - - var fallback = plugin.BrandImageFileName; - if (fallback.HasValue()) - { - return "~/Plugins/{0}/Content/{1}".FormatInvariant(plugin.SystemName, fallback); - } - } - - return null; - } - - public string GetIconUrl(ProviderMetadata metadata) - { - var plugin = metadata.PluginDescriptor; - - if (plugin == null) - { - return GetDefaultIconUrl(metadata.GroupName); - } - - return GetIconUrl(plugin, metadata.SystemName); - } - - /// - /// Returns the absolute path of a plugin/provider icon - /// - /// The plugin descriptor. Used to resolve the physical path - /// Optional system name of provider. If passed, an icon with this name gets being tried to resolve first. - /// The icon's absolute path - public string GetIconUrl(PluginDescriptor plugin, string providerSystemName = null) - { - //var cacheKey = new Tuple(plugin, providerSystemName); - - if (providerSystemName.HasValue()) - { - if (File.Exists(Path.Combine(plugin.PhysicalPath, "Content", "icon-{0}.png".FormatInvariant(providerSystemName)))) - { - return "~/Plugins/{0}/Content/icon-{1}.png".FormatInvariant(plugin.SystemName, providerSystemName); - } - } - - if (File.Exists(Path.Combine(plugin.PhysicalPath, "Content", "icon.png"))) - { - return "~/Plugins/{0}/Content/icon.png".FormatInvariant(plugin.SystemName); - } - else - { - return GetDefaultIconUrl(plugin.Group); - } - } - - public string GetDefaultIconUrl(string groupName) - { - if (groupName.HasValue()) - { - string path = "~/Administration/Content/images/icon-plugin-{0}.png".FormatInvariant(groupName.ToLower()); - if (File.Exists(CommonHelper.MapPath(path, false))) - { - return path; - } - } - - return "~/Administration/Content/images/icon-plugin-default.png"; - } - - public void ActivateDependentWidgets(ProviderMetadata parent, bool activate) - { - Guard.NotNull(parent, nameof(parent)); - - if (parent.DependentWidgets == null || parent.DependentWidgets.Length == 0) - return; - - foreach (var systemName in parent.DependentWidgets) - { - var widget = _ctx.ResolveOptionalNamed>(systemName); - if (widget != null) - { - var widgetSettings = _ctx.Resolve(); - - if (widget.IsWidgetActive(widgetSettings)) - { - if (!activate) - { - widgetSettings.ActiveWidgetSystemNames.Remove(systemName); - _services.Settings.SaveSetting(widgetSettings); - } - } - else - { - if (activate) - { - widgetSettings.ActiveWidgetSystemNames.Add(systemName); - _services.Settings.SaveSetting(widgetSettings); - } - } - } - } - } - - } -} + public class PluginMediator + { + private static readonly ConcurrentDictionary _routesCache = new ConcurrentDictionary(); + private readonly ICommonServices _services; + private readonly IComponentContext _ctx; + + public PluginMediator(ICommonServices services, IComponentContext ctx) + { + _services = services; + _ctx = ctx; + } + + public string GetLocalizedFriendlyName(IProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) + { + return GetLocalizedValue(metadata, "FriendlyName", x => x.FriendlyName, languageId, returnDefaultValue); + } + + public string GetLocalizedDescription(IProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) + { + return GetLocalizedValue(metadata, "Description", x => x.Description, languageId, returnDefaultValue); + } + + public string GetLocalizedValue(TMetadata metadata, + string propertyName, + Func fallback, + int languageId = 0, + bool returnDefaultValue = true) + where TMetadata : IProviderMetadata + { + Guard.NotNull(metadata, nameof(metadata)); + + var systemName = metadata.SystemName; + var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); + string result = _services.Localization.GetResource(resourceName, languageId, false, "", true); + + if (result.IsEmpty() && returnDefaultValue) + result = fallback(metadata); + + return result; + } + + public void SaveLocalizedValue(IProviderMetadata metadata, int languageId, string propertyName, string value) + { + Guard.NotNull(metadata, nameof(metadata)); + Guard.IsPositive(languageId, nameof(languageId)); + Guard.NotEmpty(propertyName, nameof(propertyName)); + + var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); + var resource = _services.Localization.GetLocaleStringResourceByName(resourceName, languageId, false); + + if (resource != null) + { + if (value.IsEmpty()) + { + // delete + _services.Localization.DeleteLocaleStringResource(resource); + } + else + { + // update + resource.ResourceValue = value; + _services.Localization.UpdateLocaleStringResource(resource); + } + } + else + { + if (value.HasValue()) + { + // insert + resource = new LocaleStringResource + { + LanguageId = languageId, + ResourceName = resourceName, + ResourceValue = value, + }; + _services.Localization.InsertLocaleStringResource(resource); + } + } + } + + public int? GetUserDisplayOrder(ProviderMetadata metadata) + { + return GetSetting(metadata, "DisplayOrder"); + } + + public T GetSetting(ProviderMetadata metadata, string propertyName) + { + var settingKey = metadata.SettingKeyPattern.FormatInvariant(metadata.SystemName, propertyName); + return _services.Settings.GetSettingByKey(settingKey); + } + + public void SetUserDisplayOrder(ProviderMetadata metadata, int displayOrder) + { + Guard.NotNull(metadata, nameof(metadata)); + + metadata.DisplayOrder = displayOrder; + SetSetting(metadata, "DisplayOrder", displayOrder); + } + + public void SetSetting(ProviderMetadata metadata, string propertyName, T value) + { + Guard.NotNull(metadata, nameof(metadata)); + Guard.NotEmpty(propertyName, nameof(propertyName)); + + var settingKey = metadata.SettingKeyPattern.FormatInvariant(metadata.SystemName, propertyName); + + using (_services.Settings.BeginScope()) + { + if (value != null) + { + _services.Settings.SetSetting(settingKey, value, 0, false); + } + else + { + _services.Settings.DeleteSetting(settingKey); + } + } + } + + public ProviderModel ToProviderModel(Provider provider, bool forEdit = false, Action, ProviderModel> enhancer = null) + { + return ToProviderModel(provider, forEdit, enhancer); + } + + public TModel ToProviderModel(Provider provider, bool forEdit = false, Action, TModel> enhancer = null) + where TModel : ProviderModel, new() + where TProvider : IProvider + { + Guard.NotNull(provider, nameof(provider)); + + var metadata = provider.Metadata; + var model = new TModel(); + model.ProviderType = typeof(TProvider); + model.SystemName = metadata.SystemName; + model.FriendlyName = forEdit ? metadata.FriendlyName : GetLocalizedFriendlyName(metadata); + model.Description = forEdit ? metadata.Description : GetLocalizedDescription(metadata); + model.DisplayOrder = metadata.DisplayOrder; + model.IsEditable = metadata.IsEditable; + model.IconUrl = GetIconUrl(metadata); + model.PluginDescriptor = metadata.PluginDescriptor; + if (model.PluginDescriptor != null) + { + model.ProvidingPluginFriendlyName = model.PluginDescriptor.GetLocalizedValue(_services.Localization, "FriendlyName"); + } + + if (metadata.IsConfigurable) + { + var routeInfo = _routesCache.GetOrAdd(model.SystemName, (key) => + { + var configurable = (IConfigurable)provider.Value; + configurable.GetConfigurationRoute(out var actionName, out var controllerName, out var routeValues); + + if (actionName.IsEmpty()) + { + metadata.IsConfigurable = false; + return null; + } + else + { + return new RouteInfo(actionName, controllerName, routeValues); + } + }); + + if (routeInfo != null) + { + model.ConfigurationRoute = new RouteInfo(routeInfo); + } + } + + if (enhancer != null) + { + enhancer(provider, model); + } + + model.IsConfigurable = metadata.IsConfigurable; + + return model; + } + + public string GetBrandImageUrl(ProviderMetadata metadata) + { + var plugin = metadata.PluginDescriptor; + + if (plugin != null) + { + var filesToCheck = (new string[] { "branding.{0}.png", "branding.{0}.gif", "branding.{0}.jpg", "branding.{0}.jpeg" }).Select(x => x.FormatInvariant(metadata.SystemName)); + var dir = plugin.PhysicalPath; + foreach (var file in filesToCheck) + { + if (File.Exists(Path.Combine(dir, "Content", file))) + { + return "~/Plugins/{0}/Content/{1}".FormatInvariant(plugin.SystemName, file); + } + } + + var fallback = plugin.BrandImageFileName; + if (fallback.HasValue()) + { + return "~/Plugins/{0}/Content/{1}".FormatInvariant(plugin.SystemName, fallback); + } + } + + return null; + } + + public string GetIconUrl(ProviderMetadata metadata) + { + var plugin = metadata.PluginDescriptor; + + if (plugin == null) + { + return GetDefaultIconUrl(metadata.GroupName); + } + + return GetIconUrl(plugin, metadata.SystemName); + } + + /// + /// Returns the absolute path of a plugin/provider icon + /// + /// The plugin descriptor. Used to resolve the physical path + /// Optional system name of provider. If passed, an icon with this name gets being tried to resolve first. + /// The icon's absolute path + public string GetIconUrl(PluginDescriptor plugin, string providerSystemName = null) + { + //var cacheKey = new Tuple(plugin, providerSystemName); + + if (providerSystemName.HasValue()) + { + if (File.Exists(Path.Combine(plugin.PhysicalPath, "Content", "icon-{0}.png".FormatInvariant(providerSystemName)))) + { + return "~/Plugins/{0}/Content/icon-{1}.png".FormatInvariant(plugin.SystemName, providerSystemName); + } + } + + if (File.Exists(Path.Combine(plugin.PhysicalPath, "Content", "icon.png"))) + { + return "~/Plugins/{0}/Content/icon.png".FormatInvariant(plugin.SystemName); + } + else + { + return GetDefaultIconUrl(plugin.Group); + } + } + + public string GetDefaultIconUrl(string groupName) + { + if (groupName.HasValue()) + { + string path = "~/Administration/Content/images/icon-plugin-{0}.png".FormatInvariant(groupName.ToLower()); + if (File.Exists(CommonHelper.MapPath(path, false))) + { + return path; + } + } + + return "~/Administration/Content/images/icon-plugin-default.png"; + } + + public void ActivateDependentWidgets(ProviderMetadata parent, bool activate) + { + Guard.NotNull(parent, nameof(parent)); + + if (parent.DependentWidgets == null || parent.DependentWidgets.Length == 0) + return; + + foreach (var systemName in parent.DependentWidgets) + { + var widget = _ctx.ResolveOptionalNamed>(systemName); + if (widget != null) + { + var widgetSettings = _ctx.Resolve(); + + if (widget.IsWidgetActive(widgetSettings)) + { + if (!activate) + { + widgetSettings.ActiveWidgetSystemNames.Remove(systemName); + _services.Settings.SaveSetting(widgetSettings); + } + } + else + { + if (activate) + { + widgetSettings.ActiveWidgetSystemNames.Add(systemName); + _services.Settings.SaveSetting(widgetSettings); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginRazorHost.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginRazorHost.cs index 01527d924f..ac2a4cd5fd 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginRazorHost.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginRazorHost.cs @@ -1,5 +1,4 @@ -using System; -using System.Web; +using System.Web; using System.Web.Hosting; using System.Web.Mvc.Razor; using System.Web.WebPages.Razor; @@ -8,35 +7,35 @@ namespace SmartStore.Web.Framework.Plugins { - public class PluginRazorHostFactory : WebRazorHostFactory - { - public override WebPageRazorHost CreateHost(string virtualPath, string physicalPath) - { - // Implementation borrowed from MvcRazorHostFactory - var host = base.CreateHost(virtualPath, physicalPath); + public class PluginRazorHostFactory : WebRazorHostFactory + { + public override WebPageRazorHost CreateHost(string virtualPath, string physicalPath) + { + // Implementation borrowed from MvcRazorHostFactory + var host = base.CreateHost(virtualPath, physicalPath); - if (!host.IsSpecialPage) - { - return new PluginRazorHost(virtualPath, physicalPath); - } + if (!host.IsSpecialPage) + { + return new PluginRazorHost(virtualPath, physicalPath); + } - return host; - } - } + return host; + } + } - public class PluginRazorHost : MvcWebPageRazorHost - { - public PluginRazorHost(string virtualPath, string physicalPath) - : base(virtualPath, physicalPath) - { - if (CommonHelper.IsDevEnvironment && HttpContext.Current.IsDebuggingEnabled) - { - var file = HostingEnvironment.VirtualPathProvider.GetFile(virtualPath) as DebugVirtualFile; - if (file != null) - { - PhysicalPath = file.PhysicalPath; - } - } - } - } + public class PluginRazorHost : MvcWebPageRazorHost + { + public PluginRazorHost(string virtualPath, string physicalPath) + : base(virtualPath, physicalPath) + { + if (CommonHelper.IsDevEnvironment && HttpContext.Current.IsDebuggingEnabled) + { + var file = HostingEnvironment.VirtualPathProvider.GetFile(virtualPath) as DebugVirtualFile; + if (file != null) + { + PhysicalPath = file.PhysicalPath; + } + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginStarter.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginStarter.cs new file mode 100644 index 0000000000..cd7d9e63a2 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginStarter.cs @@ -0,0 +1,47 @@ +using System; +using System.Web; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Plugins; +using SmartStore.Services.Localization; + +namespace SmartStore.Web.Framework.Plugins +{ + /// + /// Checks whether any plugin has changed and refreshes all plugin locale resources. + /// + public sealed class PluginStarter : IPostApplicationStart + { + private readonly IPluginFinder _pluginFinder; + private readonly ILocalizationService _locService; + + public PluginStarter(IPluginFinder pluginFinder, ILocalizationService locService) + { + _pluginFinder = pluginFinder; + _locService = locService; + } + + public int Order => 100; + public bool ThrowOnError => false; + public int MaxAttempts => 1; + + public void Start(HttpContextBase httpContext) + { + //if (!PluginManager.PluginChangeDetected) + // return; + + var descriptors = _pluginFinder.GetPluginDescriptors(true); + foreach (var d in descriptors) + { + var hasher = _locService.CreatePluginResourcesHasher(d); + if (hasher.HasChanged) + { + _locService.ImportPluginResourcesFromXml(d, null, false); + } + } + } + + public void OnFail(Exception exception, bool willRetry) + { + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs index e08ef6561e..e346a0010c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs @@ -7,139 +7,139 @@ namespace SmartStore.Web.Framework.Plugins { - public partial class ProviderManager : IProviderManager - { - private readonly IComponentContext _ctx; - private readonly ICommonServices _services; - private readonly PluginMediator _pluginMediator; - - public ProviderManager(IComponentContext ctx, ICommonServices services, PluginMediator pluginMediator) - { - _ctx = ctx; - _services = services; - _pluginMediator = pluginMediator; - } - - public Provider GetProvider(string systemName, int storeId = 0) where TProvider : IProvider - { - if (systemName.IsEmpty()) - return null; - - var provider = _ctx.ResolveOptionalNamed>(systemName); - if (provider != null) - { - if (storeId > 0) - { - var d = provider.Metadata.PluginDescriptor; - if (d != null && !IsActiveForStore(d, storeId)) - { - return null; - } - } - SetUserData(provider.Metadata); - return new Provider(provider); - } - - return null; - } - - public Provider GetProvider(string systemName, int storeId = 0) - { - Guard.NotEmpty(systemName, nameof(systemName)); - - var provider = _ctx.ResolveOptionalNamed>(systemName); - if (provider != null) - { - if (storeId > 0) - { - var d = provider.Metadata.PluginDescriptor; - if (d != null && !IsActiveForStore(d, storeId)) - { - return null; - } - } - SetUserData(provider.Metadata); - return new Provider(provider); - } - return null; - } - - public IEnumerable> GetAllProviders(int storeId = 0) where TProvider : IProvider - { - var providers = _ctx.Resolve>>(); - if (storeId > 0) - { - providers = from p in providers - let d = p.Metadata.PluginDescriptor - where d == null || IsActiveForStore(d, storeId) - select p; - } - return SortProviders(providers.Select(x => new Provider(x))); - } - - public IEnumerable> GetAllProviders(int storeId = 0) - { - var providers = _ctx.Resolve>>(); - if (storeId > 0) - { - providers = from p in providers - let d = p.Metadata.PluginDescriptor - where d == null || IsActiveForStore(d, storeId) - select p; - } - return SortProviders(providers.Select(x => new Provider(x))); - } - - protected virtual IEnumerable> SortProviders(IEnumerable> providers) where TProvider : IProvider - { - foreach (var m in providers.Select(x => x.Metadata)) - { - SetUserData(m); - } - - return providers.OrderBy(x => x.Metadata.DisplayOrder).ThenBy(x => x.Metadata.FriendlyName); - } - - protected virtual void SetUserData(ProviderMetadata metadata) - { - if (!metadata.IsEditable) - return; - - var displayOrder = _pluginMediator.GetUserDisplayOrder(metadata); - var name = _pluginMediator.GetSetting(metadata, "FriendlyName"); - var description = _pluginMediator.GetSetting(metadata, "Description"); - metadata.FriendlyName = name; - metadata.Description = description; - - if (displayOrder.HasValue) - { - metadata.DisplayOrder = displayOrder.Value; - } - } - - private bool IsActiveForStore(PluginDescriptor plugin, int storeId) - { - if (storeId == 0) - { - return true; - } - - var limitedToStoresSetting = _services.Settings.GetSettingByKey(plugin.GetSettingKey("LimitedToStores")); - if (limitedToStoresSetting.IsEmpty()) - { - return true; - } - - var limitedToStores = limitedToStoresSetting.ToIntArray(); - if (limitedToStores.Length > 0) - { - var flag = limitedToStores.Contains(storeId); - return flag; - } - - return true; - } - - } + public partial class ProviderManager : IProviderManager + { + private readonly IComponentContext _ctx; + private readonly ICommonServices _services; + private readonly PluginMediator _pluginMediator; + + public ProviderManager(IComponentContext ctx, ICommonServices services, PluginMediator pluginMediator) + { + _ctx = ctx; + _services = services; + _pluginMediator = pluginMediator; + } + + public Provider GetProvider(string systemName, int storeId = 0) where TProvider : IProvider + { + if (systemName.IsEmpty()) + return null; + + var provider = _ctx.ResolveOptionalNamed>(systemName); + if (provider != null) + { + if (storeId > 0) + { + var d = provider.Metadata.PluginDescriptor; + if (d != null && !IsActiveForStore(d, storeId)) + { + return null; + } + } + SetUserData(provider.Metadata); + return new Provider(provider); + } + + return null; + } + + public Provider GetProvider(string systemName, int storeId = 0) + { + Guard.NotEmpty(systemName, nameof(systemName)); + + var provider = _ctx.ResolveOptionalNamed>(systemName); + if (provider != null) + { + if (storeId > 0) + { + var d = provider.Metadata.PluginDescriptor; + if (d != null && !IsActiveForStore(d, storeId)) + { + return null; + } + } + SetUserData(provider.Metadata); + return new Provider(provider); + } + return null; + } + + public IEnumerable> GetAllProviders(int storeId = 0) where TProvider : IProvider + { + var providers = _ctx.Resolve>>(); + if (storeId > 0) + { + providers = from p in providers + let d = p.Metadata.PluginDescriptor + where d == null || IsActiveForStore(d, storeId) + select p; + } + return SortProviders(providers.Select(x => new Provider(x))); + } + + public IEnumerable> GetAllProviders(int storeId = 0) + { + var providers = _ctx.Resolve>>(); + if (storeId > 0) + { + providers = from p in providers + let d = p.Metadata.PluginDescriptor + where d == null || IsActiveForStore(d, storeId) + select p; + } + return SortProviders(providers.Select(x => new Provider(x))); + } + + protected virtual IEnumerable> SortProviders(IEnumerable> providers) where TProvider : IProvider + { + foreach (var m in providers.Select(x => x.Metadata)) + { + SetUserData(m); + } + + return providers.OrderBy(x => x.Metadata.DisplayOrder).ThenBy(x => x.Metadata.FriendlyName); + } + + protected virtual void SetUserData(ProviderMetadata metadata) + { + if (!metadata.IsEditable) + return; + + var displayOrder = _pluginMediator.GetUserDisplayOrder(metadata); + var name = _pluginMediator.GetSetting(metadata, "FriendlyName"); + var description = _pluginMediator.GetSetting(metadata, "Description"); + metadata.FriendlyName = name; + metadata.Description = description; + + if (displayOrder.HasValue) + { + metadata.DisplayOrder = displayOrder.Value; + } + } + + private bool IsActiveForStore(PluginDescriptor plugin, int storeId) + { + if (storeId == 0) + { + return true; + } + + var limitedToStoresSetting = _services.Settings.GetSettingByKey(plugin.GetSettingKey("LimitedToStores")); + if (limitedToStoresSetting.IsEmpty()) + { + return true; + } + + var limitedToStores = limitedToStoresSetting.ToIntArray(); + if (limitedToStores.Length > 0) + { + var flag = limitedToStores.Contains(storeId); + return flag; + } + + return true; + } + + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderModel.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderModel.cs index fa02cb9798..39fbe63e21 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderModel.cs @@ -8,81 +8,72 @@ namespace SmartStore.Web.Framework.Plugins { - public class ProviderModel : ModelBase, ILocalizedModel - { - private IList _locales; - - public Type ProviderType { get; set; } - - [SmartResourceDisplayName("Common.SystemName")] - public string SystemName { get; set; } - - [SmartResourceDisplayName("Common.FriendlyName")] - [AllowHtml] - public string FriendlyName { get; set; } - - [SmartResourceDisplayName("Common.Description")] - [AllowHtml] - public string Description { get; set; } - - [SmartResourceDisplayName("Common.DisplayOrder")] - public int DisplayOrder { get; set; } - - public bool IsEditable { get; set; } - - public bool IsConfigurable { get; set; } - - public RouteInfo ConfigurationRoute { get; set; } - - public PluginDescriptor PluginDescriptor { get; set; } - - [SmartResourceDisplayName("Admin.Providers.ProvidingPlugin")] - public string ProvidingPluginFriendlyName { get; set; } - - /// - /// Returns the absolute path of the provider's icon url. - /// - /// - /// The parent plugin's icon url is returned as a fallback, if provider icon cannot be resolved. - /// - public string IconUrl { get; set; } - - public IList Locales - { - get - { - if (_locales == null) - { - _locales = new List(); - } - return _locales; - } - set - { - _locales = value; - } - } - - public bool IsPaymentMethod - { - get - { - return ProviderType == typeof(IPaymentMethod); - } - } - } - - public class ProviderLocalizedModel : ILocalizedModelLocal - { - public int LanguageId { get; set; } - - [SmartResourceDisplayName("Common.FriendlyName")] - [AllowHtml] - public string FriendlyName { get; set; } - - [SmartResourceDisplayName("Common.Description")] - [AllowHtml] - public string Description { get; set; } - } + public class ProviderModel : ModelBase, ILocalizedModel + { + private IList _locales; + + public Type ProviderType { get; set; } + + [SmartResourceDisplayName("Common.SystemName")] + public string SystemName { get; set; } + + [SmartResourceDisplayName("Common.FriendlyName")] + [AllowHtml] + public string FriendlyName { get; set; } + + [SmartResourceDisplayName("Common.Description")] + [AllowHtml] + public string Description { get; set; } + + [SmartResourceDisplayName("Common.DisplayOrder")] + public int DisplayOrder { get; set; } + + public bool IsEditable { get; set; } + + public bool IsConfigurable { get; set; } + + public RouteInfo ConfigurationRoute { get; set; } + + public PluginDescriptor PluginDescriptor { get; set; } + + [SmartResourceDisplayName("Admin.Providers.ProvidingPlugin")] + public string ProvidingPluginFriendlyName { get; set; } + + /// + /// Returns the absolute path of the provider's icon url. + /// + /// + /// The parent plugin's icon url is returned as a fallback, if provider icon cannot be resolved. + /// + public string IconUrl { get; set; } + + public IList Locales + { + get + { + if (_locales == null) + { + _locales = new List(); + } + return _locales; + } + set => _locales = value; + } + + public bool IsPaymentMethod => ProviderType == typeof(IPaymentMethod); + } + + public class ProviderLocalizedModel : ILocalizedModelLocal + { + public int LanguageId { get; set; } + + [SmartResourceDisplayName("Common.FriendlyName")] + [AllowHtml] + public string FriendlyName { get; set; } + + [SmartResourceDisplayName("Common.Description")] + [AllowHtml] + public string Description { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Properties/AssemblyInfo.cs b/src/Presentation/SmartStore.Web.Framework/Properties/AssemblyInfo.cs index 3803039590..7ce572fa25 100644 --- a/src/Presentation/SmartStore.Web.Framework/Properties/AssemblyInfo.cs +++ b/src/Presentation/SmartStore.Web.Framework/Properties/AssemblyInfo.cs @@ -1,6 +1,5 @@ using System.Reflection; using System.Runtime.CompilerServices; -using System.Security; [assembly: AssemblyTitle("SmartStore.Web.Framework")] diff --git a/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs b/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs index 97f1550d1b..b936aaad1e 100644 --- a/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs +++ b/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs @@ -20,7 +20,7 @@ public bool Match(HttpContextBase httpContext, Route route, string parameterName if (!string.IsNullOrEmpty(stringValue)) { - return Guid.TryParse(stringValue, out var guidValue) && + return Guid.TryParse(stringValue, out var guidValue) && (_allowEmpty || guidValue != Guid.Empty); } } diff --git a/src/Presentation/SmartStore.Web.Framework/Routing/RoutePublisher.cs b/src/Presentation/SmartStore.Web.Framework/Routing/RoutePublisher.cs index 60191958db..8906a4a3c8 100644 --- a/src/Presentation/SmartStore.Web.Framework/Routing/RoutePublisher.cs +++ b/src/Presentation/SmartStore.Web.Framework/Routing/RoutePublisher.cs @@ -17,7 +17,7 @@ public RoutePublisher(ITypeFinder typeFinder) public void RegisterRoutes(RouteCollection routes) { - var routeProviderTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); + var routeProviderTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); var routeProviders = new List(); foreach (var providerType in routeProviderTypes) @@ -26,10 +26,10 @@ public void RegisterRoutes(RouteCollection routes) routeProviders.Add(provider); } routeProviders = routeProviders.OrderByDescending(rp => rp.Priority).ToList(); - routeProviders.Each(rp => - { - rp.RegisterRoutes(routes); - }); + routeProviders.Each(rp => + { + rp.RegisterRoutes(routes); + }); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/AdminAuthorizeAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/AdminAuthorizeAttribute.cs index 14b9f93177..4493d1d338 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/AdminAuthorizeAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/AdminAuthorizeAttribute.cs @@ -6,12 +6,12 @@ namespace SmartStore.Web.Framework.Security { - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=true)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] public class AdminAuthorizeAttribute : FilterAttribute, IAuthorizationFilter { - public IPermissionService PermissionService { get; set; } - - private void HandleUnauthorizedRequest(AuthorizationContext filterContext) + public IPermissionService PermissionService { get; set; } + + private void HandleUnauthorizedRequest(AuthorizationContext filterContext) { filterContext.Result = new HttpUnauthorizedResult(); } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs index aca5a27830..9568d12685 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs @@ -3,62 +3,61 @@ using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Domain.Security; -using SmartStore.Core.Infrastructure; namespace SmartStore.Web.Framework.Security { public class AdminValidateIpAddressAttribute : FilterAttribute, IAuthorizationFilter { - public Lazy WebHelper { get; set; } - public Lazy SecuritySettings { get; set; } + public Lazy WebHelper { get; set; } + public Lazy SecuritySettings { get; set; } - public virtual void OnAuthorization(AuthorizationContext filterContext) - { - if (filterContext == null || filterContext.HttpContext == null) - return; + public virtual void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null || filterContext.HttpContext == null) + return; - HttpRequestBase request = filterContext.HttpContext.Request; - if (request == null) - return; + HttpRequestBase request = filterContext.HttpContext.Request; + if (request == null) + return; - // don't apply filter to child methods - if (filterContext.IsChildAction) - return; + // don't apply filter to child methods + if (filterContext.IsChildAction) + return; - // Prevent lockout - if (filterContext.HttpContext.Request.IsLocal) - return; + // Prevent lockout + if (filterContext.HttpContext.Request.IsLocal) + return; - bool ok = false; - var ipAddresses = SecuritySettings.Value.AdminAreaAllowedIpAddresses; - if (ipAddresses != null && ipAddresses.Count > 0) - { - var webHelper = WebHelper.Value; - var curIpAddress = webHelper.GetCurrentIpAddress(); - foreach (string ip in ipAddresses) - if (ip.Equals(curIpAddress, StringComparison.InvariantCultureIgnoreCase)) - { - ok = true; - break; - } - } - else - { - //no restrictions - ok = true; - } + bool ok = false; + var ipAddresses = SecuritySettings.Value.AdminAreaAllowedIpAddresses; + if (ipAddresses != null && ipAddresses.Count > 0) + { + var webHelper = WebHelper.Value; + var curIpAddress = webHelper.GetCurrentIpAddress(); + foreach (string ip in ipAddresses) + if (ip.Equals(curIpAddress, StringComparison.InvariantCultureIgnoreCase)) + { + ok = true; + break; + } + } + else + { + //no restrictions + ok = true; + } - if (!ok) - { - //ensure that it's not 'Access denied' page - var webHelper = WebHelper.Value; - var thisPageUrl = webHelper.GetThisPageUrl(false); - if (!thisPageUrl.StartsWith(string.Format("{0}admin/security/accessdenied", webHelper.GetStoreLocation()), StringComparison.InvariantCultureIgnoreCase)) - { - //redirect to 'Access denied' page - filterContext.Result = new RedirectResult(webHelper.GetStoreLocation() + "admin/security/accessdenied"); - } - } - } - } + if (!ok) + { + //ensure that it's not 'Access denied' page + var webHelper = WebHelper.Value; + var thisPageUrl = webHelper.GetThisPageUrl(false); + if (!thisPageUrl.StartsWith(string.Format("{0}admin/security/accessdenied", webHelper.GetStoreLocation()), StringComparison.InvariantCultureIgnoreCase)) + { + //redirect to 'Access denied' page + filterContext.Result = new RedirectResult(webHelper.GetStoreLocation() + "admin/security/accessdenied"); + } + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/Captcha/CaptchaSettings.cs b/src/Presentation/SmartStore.Web.Framework/Security/Captcha/CaptchaSettings.cs index 799a507216..3ed092b904 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/Captcha/CaptchaSettings.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/Captcha/CaptchaSettings.cs @@ -17,8 +17,8 @@ public class CaptchaSettings : ISettings public bool ShowOnProductReviewPage { get; set; } public string ReCaptchaPublicKey { get; set; } public string ReCaptchaPrivateKey { get; set; } - public bool UseInvisibleReCaptcha { get; set; } + public bool UseInvisibleReCaptcha { get; set; } public bool CanDisplayCaptcha => Enabled && ReCaptchaPublicKey.HasValue() && ReCaptchaPrivateKey.HasValue(); - } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Security/Captcha/HtmlCaptchaExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Security/Captcha/HtmlCaptchaExtensions.cs index c2f4994e96..d00153bfac 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/Captcha/HtmlCaptchaExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/Captcha/HtmlCaptchaExtensions.cs @@ -7,7 +7,7 @@ namespace SmartStore.Web.Framework.Security { public static class HtmlCaptchaExtensions - { + { public static IHtmlString GenerateCaptcha(this HtmlHelper helper) { var captchaSettings = EngineContext.Current.Resolve(); @@ -25,7 +25,7 @@ public static IHtmlString GenerateCaptcha(this HtmlHelper helper) widgetUrl, callbackName, workContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower() - ); + ); var script = new[] { diff --git a/src/Presentation/SmartStore.Web.Framework/Security/Captcha/ValidateCaptchaAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/Captcha/ValidateCaptchaAttribute.cs index 4e1ca7728f..d1f65774f2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/Captcha/ValidateCaptchaAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/Captcha/ValidateCaptchaAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; @@ -15,22 +16,20 @@ namespace SmartStore.Web.Framework.Security { public class ValidateCaptchaAttribute : ActionFilterAttribute { - public ValidateCaptchaAttribute() - { - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } = NullLogger.Instance; public Lazy CaptchaSettings { get; set; } - public Lazy LocalizationService { get; set; } + public Lazy LocalizationService { get; set; } - public override void OnActionExecuting(ActionExecutingContext filterContext) + public override void OnActionExecuting(ActionExecutingContext filterContext) { var valid = false; + var verify = true; + + try + { + verify = CaptchaSettings.Value.CanDisplayCaptcha && VerifyRecaptcha(filterContext); - try - { - if (CaptchaSettings.Value.CanDisplayCaptcha) + if (verify) { var verifyUrl = CommonHelper.GetAppSetting("g:RecaptchaVerifyUrl"); var recaptchaResponse = filterContext.HttpContext.Request.Form["g-recaptcha-response"]; @@ -61,7 +60,8 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) } else { - foreach (var error in result.ErrorCodes) + // Do not log 'missing input'. Could be a regular case. + foreach (var error in result.ErrorCodes.Where(x => x.HasValue() && x != "missing-input-response")) { Logger.Error("Error while getting Google Recaptcha response: " + error); } @@ -70,31 +70,90 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) } } } - } - catch (Exception ex) - { - Logger.ErrorsAll(ex); - } + } + catch (Exception ex) + { + Logger.ErrorsAll(ex); + } - // This will push the result value into a parameter in our Action. - filterContext.ActionParameters["captchaValid"] = valid; + // Push the result values as parameters in our action method. + filterContext.ActionParameters["captchaValid"] = valid; - filterContext.ActionParameters["captchaError"] = !valid && CaptchaSettings.Value.CanDisplayCaptcha + filterContext.ActionParameters["captchaError"] = !valid && verify ? LocalizationService.Value.GetResource(CaptchaSettings.Value.UseInvisibleReCaptcha ? "Common.WrongInvisibleCaptcha" : "Common.WrongCaptcha") : null; base.OnActionExecuting(filterContext); } + + private bool VerifyRecaptcha(ActionExecutingContext context) + { + // Poor method to avoid unnecessary requests to the Google API. + var controller = context?.ActionDescriptor?.ControllerDescriptor?.ControllerName.EmptyNull(); + var action = context?.ActionDescriptor?.ActionName.EmptyNull(); + + if (controller == "Blog" && action == "BlogCommentAdd") + { + return CaptchaSettings.Value.ShowOnBlogCommentPage; + } + else if (controller == "Boards") + { + switch (action) + { + case "TopicCreate": + case "TopicEdit": + case "PostCreate": + case "PostEdit": + return CaptchaSettings.Value.ShowOnForumPage; + } + } + else if (controller == "Customer") + { + switch (action) + { + case "Login": + return CaptchaSettings.Value.ShowOnLoginPage; + case "Register": + return CaptchaSettings.Value.ShowOnRegistrationPage; + } + } + else if (controller == "Home" && action == "ContactUsSend") + { + return CaptchaSettings.Value.ShowOnContactUsPage; + } + else if (controller == "News" && action == "NewsCommentAdd") + { + return CaptchaSettings.Value.ShowOnNewsCommentPage; + } + else if (controller == "Product") + { + switch (action) + { + case "ReviewsAdd": + return CaptchaSettings.Value.ShowOnProductReviewPage; + case "AskQuestionSend": + return CaptchaSettings.Value.ShowOnAskQuestionPage; + case "EmailAFriendSend": + return CaptchaSettings.Value.ShowOnEmailProductToFriendPage; + } + } + else if (controller == "ShoppingCart" && action == "EmailWishlistSend") + { + return CaptchaSettings.Value.ShowOnEmailWishlistToFriendPage; + } + + return true; + } } - [DataContract] - public class GoogleRecaptchaApiResponse - { - [DataMember(Name = "success")] - public bool Success { get; set; } + [DataContract] + public class GoogleRecaptchaApiResponse + { + [DataMember(Name = "success")] + public bool Success { get; set; } - [DataMember(Name = "error-codes")] - public List ErrorCodes { get; set; } - } + [DataMember(Name = "error-codes")] + public List ErrorCodes { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs b/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs index 35b5720dd3..1ee8d1b697 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs @@ -156,12 +156,12 @@ public static IEnumerable GetDirectoriesWrite(IWebHelper webHelper) string rootDir = webHelper.MapPath("~/"); var dirsToCheck = new List(); dirsToCheck.Add(Path.Combine(rootDir, "App_Data")); - dirsToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName)); - dirsToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "Media")); - dirsToCheck.Add(Path.Combine(rootDir, "bin")); + dirsToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName)); + dirsToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "Media")); + dirsToCheck.Add(Path.Combine(rootDir, "bin")); dirsToCheck.Add(Path.Combine(rootDir, "content")); - dirsToCheck.Add(Path.Combine(rootDir, "Exchange")); - dirsToCheck.Add(Path.Combine(rootDir, "plugins")); + dirsToCheck.Add(Path.Combine(rootDir, "Exchange")); + dirsToCheck.Add(Path.Combine(rootDir, "plugins")); dirsToCheck.Add(Path.Combine(rootDir, "plugins\\bin")); return dirsToCheck; } @@ -175,12 +175,12 @@ public static IEnumerable GetFilesWrite(IWebHelper webHelper) { string rootDir = webHelper.MapPath("~/"); var filesToCheck = new List(); - //filesToCheck.Add(Path.Combine(rootDir, "Global.asax")); - //filesToCheck.Add(Path.Combine(rootDir, "web.config")); - filesToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "InstalledPlugins.txt")); - filesToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "Settings.txt")); + //filesToCheck.Add(Path.Combine(rootDir, "Global.asax")); + //filesToCheck.Add(Path.Combine(rootDir, "web.config")); + filesToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "InstalledPlugins.txt")); + filesToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "Settings.txt")); - return filesToCheck; + return filesToCheck; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/Honeypot.cs b/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/Honeypot.cs index c9e58fe770..8bad998c40 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/Honeypot.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/Honeypot.cs @@ -7,70 +7,70 @@ namespace SmartStore.Web.Framework.Security { - public class HoneypotField - { - public string Name { get; set; } - public DateTime CreatedOnUtc { get; set; } - } + public class HoneypotField + { + public string Name { get; set; } + public DateTime CreatedOnUtc { get; set; } + } - internal static class Honeypot - { - internal const string TokenFieldName = "__hpToken"; + internal static class Honeypot + { + internal const string TokenFieldName = "__hpToken"; - private static readonly string[] _fieldNames = new[] { "Phone", "Fax", "Email", "Age", "Name", "FirstName", "LastName", "Type", "Custom", "Reason", "Pet", "Question", "Region" }; - private static readonly string _fieldSuffix = CommonHelper.GenerateRandomDigitCode(5); + private static readonly string[] _fieldNames = new[] { "Phone", "Fax", "Email", "Age", "Name", "FirstName", "LastName", "Type", "Custom", "Reason", "Pet", "Question", "Region" }; + private static readonly string _fieldSuffix = CommonHelper.GenerateRandomDigitCode(5); - public static HoneypotField CreateToken() - { - var r = new Random(); + public static HoneypotField CreateToken() + { + var r = new Random(); - // Create a rondom field name with pattern "[random1]-[random2][suffix]" - var len = _fieldNames.Length; - var fieldName = string.Concat(_fieldNames[r.Next(0, len)], "-", _fieldNames[r.Next(0, len)], _fieldSuffix); + // Create a rondom field name with pattern "[random1]-[random2][suffix]" + var len = _fieldNames.Length; + var fieldName = string.Concat(_fieldNames[r.Next(0, len)], "-", _fieldNames[r.Next(0, len)], _fieldSuffix); - return new HoneypotField - { - Name = fieldName, - CreatedOnUtc = DateTime.UtcNow - }; - } + return new HoneypotField + { + Name = fieldName, + CreatedOnUtc = DateTime.UtcNow + }; + } - public static string SerializeToken(HoneypotField token) - { - Guard.NotNull(token, nameof(token)); + public static string SerializeToken(HoneypotField token) + { + Guard.NotNull(token, nameof(token)); - var json = JsonConvert.SerializeObject(token); - var encoded = MachineKey.Protect(Encoding.UTF8.GetBytes(json)); + var json = JsonConvert.SerializeObject(token); + var encoded = MachineKey.Protect(Encoding.UTF8.GetBytes(json)); - var result = Convert.ToBase64String(encoded); - return result; - } + var result = Convert.ToBase64String(encoded); + return result; + } - public static HoneypotField DeserializeToken(string token) - { - Guard.NotEmpty(token, nameof(token)); + public static HoneypotField DeserializeToken(string token) + { + Guard.NotEmpty(token, nameof(token)); - var encoded = Convert.FromBase64String(token); - var decoded = MachineKey.Unprotect(encoded); - var json = Encoding.UTF8.GetString(decoded); + var encoded = Convert.FromBase64String(token); + var decoded = MachineKey.Unprotect(encoded); + var json = Encoding.UTF8.GetString(decoded); - var result = JsonConvert.DeserializeObject(json); - return result; - } + var result = JsonConvert.DeserializeObject(json); + return result; + } - public static bool IsBot(HttpContextBase httpContext) - { - var tokenString = httpContext.Request.Form[TokenFieldName]; - if (tokenString.IsEmpty()) - { - throw new InvalidOperationException("The required honeypot form field is missing. Please render the field with 'Html.HoneypotField()'."); - } + public static bool IsBot(HttpContextBase httpContext) + { + var tokenString = httpContext.Request.Form[TokenFieldName]; + if (tokenString.IsEmpty()) + { + throw new InvalidOperationException("The required honeypot form field is missing. Please render the field with 'Html.HoneypotField()'."); + } - var token = DeserializeToken(tokenString); - var trap = httpContext.Request.Form[token.Name]; - var isBot = trap == null || trap.Length > 0 || (DateTime.UtcNow - token.CreatedOnUtc).TotalMilliseconds < 2000; + var token = DeserializeToken(tokenString); + var trap = httpContext.Request.Form[token.Name]; + var isBot = trap == null || trap.Length > 0 || (DateTime.UtcNow - token.CreatedOnUtc).TotalMilliseconds < 2000; - return isBot; - } - } + return isBot; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/HtmlHoneypotExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/HtmlHoneypotExtensions.cs index 49593920a1..328b3ce01d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/HtmlHoneypotExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/HtmlHoneypotExtensions.cs @@ -1,20 +1,19 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; using System.Web.Mvc.Html; namespace SmartStore.Web.Framework.Security { - public static class HtmlHoneypotExtensions - { - public static MvcHtmlString HoneypotField(this HtmlHelper html) - { - var token = Honeypot.CreateToken(); - var serializedToken = Honeypot.SerializeToken(token); + public static class HtmlHoneypotExtensions + { + public static MvcHtmlString HoneypotField(this HtmlHelper html) + { + var token = Honeypot.CreateToken(); + var serializedToken = Honeypot.SerializeToken(token); - var textField = html.TextBox(token.Name, string.Empty, new { @class = "required-text-input", autocomplete = "off" }).ToHtmlString(); - var hiddenField = html.Hidden(Honeypot.TokenFieldName, serializedToken).ToHtmlString(); + var textField = html.TextBox(token.Name, string.Empty, new { @class = "required-text-input", autocomplete = "off" }).ToHtmlString(); + var hiddenField = html.Hidden(Honeypot.TokenFieldName, serializedToken).ToHtmlString(); - return MvcHtmlString.Create(textField + hiddenField); - } - } + return MvcHtmlString.Create(textField + hiddenField); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/ValidateHoneypotAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/ValidateHoneypotAttribute.cs index 3d026fc75f..b064401d34 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/ValidateHoneypotAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/Honeypot/ValidateHoneypotAttribute.cs @@ -7,32 +7,32 @@ namespace SmartStore.Web.Framework.Security { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class ValidateHoneypotAttribute : FilterAttribute, IAuthorizationFilter + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + public class ValidateHoneypotAttribute : FilterAttribute, IAuthorizationFilter { - public ValidateHoneypotAttribute() - { - Logger = NullLogger.Instance; - } + public ValidateHoneypotAttribute() + { + Logger = NullLogger.Instance; + } - public SecuritySettings SecuritySettings { get; set; } - public ILogger Logger { get; set; } - public Localizer T { get; set; } - public Lazy WebHelper { get; set; } + public SecuritySettings SecuritySettings { get; set; } + public ILogger Logger { get; set; } + public Localizer T { get; set; } + public Lazy WebHelper { get; set; } - public void OnAuthorization(AuthorizationContext filterContext) - { - if (!SecuritySettings.EnableHoneypotProtection) - return; + public void OnAuthorization(AuthorizationContext filterContext) + { + if (!SecuritySettings.EnableHoneypotProtection) + return; - var isBot = Honeypot.IsBot(filterContext.HttpContext); - if (!isBot) - return; - - Logger.Warn("Honeypot detected a bot and rejected the request."); + var isBot = Honeypot.IsBot(filterContext.HttpContext); + if (!isBot) + return; - var redirectUrl = WebHelper.Value.GetThisPageUrl(true); - filterContext.Result = new RedirectResult(redirectUrl); - } - } + Logger.Warn("Honeypot detected a bot and rejected the request."); + + var redirectUrl = WebHelper.Value.GetThisPageUrl(true); + filterContext.Result = new RedirectResult(redirectUrl); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/SameSiteBrowserDetector.cs b/src/Presentation/SmartStore.Web.Framework/Security/SameSiteBrowserDetector.cs new file mode 100644 index 0000000000..ad0d832ead --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Security/SameSiteBrowserDetector.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +namespace SmartStore.Web.Framework.Security +{ + public static class SameSiteBrowserDetector + { + // Same as https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ + public static bool DisallowsSameSiteNone(string userAgent) + { + if (string.IsNullOrEmpty(userAgent)) + { + return true; + } + + // Note that these detections are a starting point. See https://www.chromium.org/updates/same-site/incompatible-clients for more detections. + + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking stack + if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12")) + { + return true; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.Contains("Version/") && userAgent.Contains("Safari")) + { + return true; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) + { + return true; + } + + // Unreal Engine runs Chromium 59, but does not advertise as Chrome until 4.23. Treat versions of Unreal + // that don't specify their Chrome version as lacking support for SameSite=None. + if (userAgent.Contains("UnrealEngine") && !userAgent.Contains("Chrome")) + { + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AllowsSameSiteNone(string userAgent) + { + return !DisallowsSameSiteNone(userAgent); + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Security/SanitizeHtmlAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/SanitizeHtmlAttribute.cs index ddfb9cec9f..34939ca5e2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/SanitizeHtmlAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/SanitizeHtmlAttribute.cs @@ -3,25 +3,25 @@ namespace SmartStore.Web.Framework.Security { - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class SanitizeHtmlAttribute : Attribute, IMetadataAware - { - public SanitizeHtmlAttribute() - { - IsFragment = true; - } + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class SanitizeHtmlAttribute : Attribute, IMetadataAware + { + public SanitizeHtmlAttribute() + { + IsFragment = true; + } - public bool IsFragment - { - get; - set; - } + public bool IsFragment + { + get; + set; + } - public void OnMetadataCreated(ModelMetadata metadata) - { - Guard.NotNull(metadata, nameof(metadata)); + public void OnMetadataCreated(ModelMetadata metadata) + { + Guard.NotNull(metadata, nameof(metadata)); - metadata.RequestValidationEnabled = false; - } - } + metadata.RequestValidationEnabled = false; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs index 1b476d0284..c5ebc451c7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRoute.cs @@ -96,22 +96,22 @@ public static void RegisterPaths(params GenericPath[] paths) public static IEnumerable Paths { get; } = _paths.Values.OrderBy(x => x.Order); - public static void RegisterUrlPrefix(string prefix, params string[] entityNames) - { - Guard.NotEmpty(prefix, nameof(prefix)); + public static void RegisterUrlPrefix(string prefix, params string[] entityNames) + { + Guard.NotEmpty(prefix, nameof(prefix)); - _urlPrefixes.AddRange(prefix, entityNames); - } - - public static string GetUrlPrefixFor(string entityName) - { - Guard.NotEmpty(entityName, nameof(entityName)); + _urlPrefixes.AddRange(prefix, entityNames); + } - if (_urlPrefixes.Count == 0) - return null; + public static string GetUrlPrefixFor(string entityName) + { + Guard.NotEmpty(entityName, nameof(entityName)); - return _urlPrefixes.FirstOrDefault(x => x.Value.Contains(entityName, StringComparer.OrdinalIgnoreCase)).Key; - } + if (_urlPrefixes.Count == 0) + return null; + + return _urlPrefixes.FirstOrDefault(x => x.Value.Contains(entityName, StringComparer.OrdinalIgnoreCase)).Key; + } /// /// Returns information about the requested route. @@ -128,17 +128,17 @@ public override RouteData GetRouteData(HttpContextBase httpContext) { var slug = NormalizeSlug(data.Values); - if (TryResolveUrlPrefix(slug, out var urlPrefix, out var actualSlug, out var entityNames)) - { - slug = actualSlug; - } - - var urlRecordService = EngineContext.Current.Resolve(); - var urlRecord = urlRecordService.GetBySlug(slug); + if (TryResolveUrlPrefix(slug, out var urlPrefix, out var actualSlug, out var entityNames)) + { + slug = actualSlug; + } + + var urlRecordService = EngineContext.Current.Resolve(); + var urlRecord = urlRecordService.GetBySlug(slug); if (urlRecord == null) { // no URL record found - return NotFound(data); + return NotFound(data); } if (!urlRecord.IsActive) @@ -151,33 +151,33 @@ public override RouteData GetRouteData(HttpContextBase httpContext) var webHelper = EngineContext.Current.Resolve(); var response = httpContext.Response; response.Status = "301 Moved Permanently"; - if (urlPrefix.HasValue()) - { - activeSlug = urlPrefix + "/" + activeSlug; - } - response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), activeSlug); + if (urlPrefix.HasValue()) + { + activeSlug = urlPrefix + "/" + activeSlug; + } + response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(), activeSlug); response.End(); return null; } else { - // no active slug found - return NotFound(data); - } + // no active slug found + return NotFound(data); + } } - // Verify prefix matches any assigned entity name - if (entityNames != null && !entityNames.Contains(urlRecord.EntityName, StringComparer.OrdinalIgnoreCase)) - { - // does NOT match - return NotFound(data); - } + // Verify prefix matches any assigned entity name + if (entityNames != null && !entityNames.Contains(urlRecord.EntityName, StringComparer.OrdinalIgnoreCase)) + { + // does NOT match + return NotFound(data); + } - // process URL - data.DataTokens["UrlRecord"] = urlRecord; - data.Values["SeName"] = slug; + // process URL + data.DataTokens["UrlRecord"] = urlRecord; + data.Values["SeName"] = slug; - //string controller, action, paramName; + //string controller, action, paramName; if (!_paths.TryGetValue(urlRecord.EntityName, out var path)) { @@ -208,36 +208,36 @@ private string NormalizeSlug(RouteValueDictionary routeValues) } private RouteData NotFound(RouteData data) - { - data.Values["controller"] = "Error"; - data.Values["action"] = "NotFound"; - - return data; - } - - private bool TryResolveUrlPrefix(string slug, out string urlPrefix, out string actualSlug, out ICollection entityNames) - { - urlPrefix = null; - actualSlug = null; - entityNames = null; - - if (_urlPrefixes.Count > 0) - { - var firstSepIndex = slug.IndexOf('/'); - if (firstSepIndex > 0) - { - var prefix = slug.Substring(0, firstSepIndex); - if (_urlPrefixes.ContainsKey(prefix)) - { - urlPrefix = prefix; - entityNames = _urlPrefixes[prefix]; - actualSlug = slug.Substring(prefix.Length + 1); - return true; - } - } - } - - return false; - } + { + data.Values["controller"] = "Error"; + data.Values["action"] = "NotFound"; + + return data; + } + + private bool TryResolveUrlPrefix(string slug, out string urlPrefix, out string actualSlug, out ICollection entityNames) + { + urlPrefix = null; + actualSlug = null; + entityNames = null; + + if (_urlPrefixes.Count > 0) + { + var firstSepIndex = slug.IndexOf('/'); + if (firstSepIndex > 0) + { + var prefix = slug.Substring(0, firstSepIndex); + if (_urlPrefixes.ContainsKey(prefix)) + { + urlPrefix = prefix; + entityNames = _urlPrefixes[prefix]; + actualSlug = slug.Substring(prefix.Length + 1); + return true; + } + } + } + + return false; + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRouteExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRouteExtensions.cs index d402d26925..e83872c01a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRouteExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Seo/GenericPathRouteExtensions.cs @@ -53,29 +53,29 @@ public static Route MapGenericPathRoute(this RouteCollection routes, string name return route; } - /// - /// Changes the url pattern of an existing named route. - /// - /// Route collection - /// Name of the route - /// The new url pattern - /// The route instance - public static Route ChangeRouteUrl(this RouteCollection routes, string name, string url) - { - Guard.NotNull(routes, nameof(routes)); - Guard.NotEmpty(name, nameof(name)); - Guard.NotEmpty(url, nameof(url)); - - var route = routes[name] as Route; + /// + /// Changes the url pattern of an existing named route. + /// + /// Route collection + /// Name of the route + /// The new url pattern + /// The route instance + public static Route ChangeRouteUrl(this RouteCollection routes, string name, string url) + { + Guard.NotNull(routes, nameof(routes)); + Guard.NotEmpty(name, nameof(name)); + Guard.NotEmpty(url, nameof(url)); + + var route = routes[name] as Route; + + if (route == null) + { + throw new ArgumentException("The route '{0}' does not exist or is not assignable from 'Route'.".FormatInvariant(name), nameof(name)); + } - if (route == null) - { - throw new ArgumentException("The route '{0}' does not exist or is not assignable from 'Route'.".FormatInvariant(name), nameof(name)); - } + route.Url = url; - route.Url = url; - - return route; - } + return route; + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/ISeoModel.cs b/src/Presentation/SmartStore.Web.Framework/Seo/ISeoModel.cs new file mode 100644 index 0000000000..d39485def5 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Seo/ISeoModel.cs @@ -0,0 +1,37 @@ +using System.Web.Mvc; +using SmartStore.Web.Framework.Localization; + +namespace SmartStore.Web.Framework.Seo +{ + public interface ISeoModel : ILocalizedModel + { + [SmartResourceDisplayName("Admin.Configuration.Seo.MetaTitle")] + [AllowHtml] + string MetaTitle { get; } + + [SmartResourceDisplayName("Admin.Configuration.Seo.MetaDescription")] + [AllowHtml] + string MetaDescription { get; } + + [SmartResourceDisplayName("Admin.Configuration.Seo.MetaKeywords")] + [AllowHtml] + string MetaKeywords { get; } + } + + public class SeoModelLocal : ILocalizedModelLocal + { + public int LanguageId { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Seo.MetaTitle")] + [AllowHtml] + public string MetaTitle { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Seo.MetaDescription")] + [AllowHtml] + public string MetaDescription { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Seo.MetaKeywords")] + [AllowHtml] + public string MetaKeywords { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/RewriteUrlAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/RewriteUrlAttribute.cs index 59dc094c71..f17ffe3e30 100644 --- a/src/Presentation/SmartStore.Web.Framework/Seo/RewriteUrlAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Seo/RewriteUrlAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Web.Hosting; using System.Web.Mvc; using System.Web.Routing; using SmartStore.Core; @@ -11,267 +12,267 @@ namespace SmartStore.Web.Framework.Seo { public class RewriteUrlAttribute : FilterAttribute, IAuthorizationFilter { - public RewriteUrlAttribute(SslRequirement sslRequirement) + public RewriteUrlAttribute(SslRequirement sslRequirement) { SslRequirement = sslRequirement; - AppendTrailingSlash = RouteTable.Routes.AppendTrailingSlash; - LowercaseUrls = RouteTable.Routes.LowercaseUrls; - Order = 100; + AppendTrailingSlash = RouteTable.Routes.AppendTrailingSlash; + LowercaseUrls = RouteTable.Routes.LowercaseUrls; + Order = 100; } - public Lazy SeoSettings { get; set; } - public Lazy SecuritySettings { get; set; } - public Lazy StoreContext { get; set; } - public Lazy WebHelper { get; set; } - public Lazy WorkContext { get; set; } + public Lazy SeoSettings { get; set; } + public Lazy SecuritySettings { get; set; } + public Lazy StoreContext { get; set; } + public Lazy WebHelper { get; set; } + public Lazy WorkContext { get; set; } - public SslRequirement SslRequirement { get; set; } - public bool AppendTrailingSlash { get; set; } - public bool LowercaseUrls { get; set; } + public SslRequirement SslRequirement { get; set; } + public bool AppendTrailingSlash { get; set; } + public bool LowercaseUrls { get; set; } - public virtual void OnAuthorization(AuthorizationContext filterContext) + public virtual void OnAuthorization(AuthorizationContext filterContext) { // don't apply filter to child methods if (filterContext.IsChildAction) return; - // only redirect for GET requests, - // otherwise the browser might not propagate the verb and request body correctly. - if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + // only redirect for GET requests, + // otherwise the browser might not propagate the verb and request body correctly. + if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return; - if (!DataSettings.DatabaseIsInstalled()) + if (HostingEnvironment.IsHosted && !DataSettings.DatabaseIsInstalled()) return; - var currentStore = StoreContext.Value.CurrentStore; - var uri = filterContext.HttpContext.Request.Url; - var originalUrl = new Uri(uri.GetLeftPart(UriPartial.Authority) + filterContext.HttpContext.Request.RawUrl); - - var rewriteContext = new RewriteContext - { - ControllerContext = filterContext, - OriginalUrl = originalUrl, - IsLoopback = uri.IsLoopback || filterContext.HttpContext.Request.IsLocal, - CurrentStore = currentStore, - SecurityMode = currentStore.GetSecurityMode(), - IsAdmin = WorkContext.Value.IsAdmin - }; - - // Applies HTTP protocol rule - TryRewriteScheme(rewriteContext); - - // Applies canonical host name rule - TryRewriteHostName(rewriteContext); - - // Applies trailing slash and lowercase rules - TryRewritePath(rewriteContext); - - if (rewriteContext.Url.HasValue()) - { - filterContext.Result = new RedirectResult(rewriteContext.Url, rewriteContext.Permanent ?? !rewriteContext.IsLoopback); - } + var currentStore = StoreContext.Value.CurrentStore; + var uri = filterContext.HttpContext.Request.Url; + var originalUrl = new Uri(uri.GetLeftPart(UriPartial.Authority) + filterContext.HttpContext.Request.RawUrl); + + var rewriteContext = new RewriteContext + { + ControllerContext = filterContext, + OriginalUrl = originalUrl, + IsLoopback = uri.IsLoopback || filterContext.HttpContext.Request.IsLocal, + CurrentStore = currentStore, + SecurityMode = currentStore.GetSecurityMode(), + IsAdmin = WorkContext.Value.IsAdmin + }; + + // Applies HTTP protocol rule + TryRewriteScheme(rewriteContext); + + // Applies canonical host name rule + TryRewriteHostName(rewriteContext); + + // Applies trailing slash and lowercase rules + TryRewritePath(rewriteContext); + + if (rewriteContext.Url.HasValue()) + { + filterContext.Result = new RedirectResult(rewriteContext.Url, rewriteContext.Permanent ?? !rewriteContext.IsLoopback); + } } - /// - /// Applies HTTP protocol rule - /// - private bool TryRewriteScheme(RewriteContext context) - { - if (context.IsLoopback && !SecuritySettings.Value.UseSslOnLocalhost) - return false; - - var webHelper = WebHelper.Value; - - var currentConnectionSecured = webHelper.IsCurrentConnectionSecured(); - var currentStore = context.CurrentStore; - - if (currentStore.ForceSslForAllPages) - { - // all pages are forced to be SSL no matter of the specified value - this.SslRequirement = SslRequirement.Yes; - } - - switch (this.SslRequirement) - { - case SslRequirement.Yes: - { - if (!currentConnectionSecured && context.SecurityMode > HttpSecurityMode.Unsecured) - { - // Redirect to HTTPS version of page - context.Url = webHelper.GetThisPageUrl(true, true); - return true; - } - } - break; - case SslRequirement.No: - { - if (currentConnectionSecured) - { - // Redirect to HTTP version of page - context.Url = webHelper.GetThisPageUrl(true, false); - return true; - } - } - break; - default: - return false; - } - - return false; - } - - /// - /// Applies canonical host name rule - /// - private bool TryRewriteHostName(RewriteContext context) - { - var rule = SeoSettings.Value.CanonicalHostNameRule; - if (rule == CanonicalHostNameRule.NoRule) - return false; - - if (context.IsLoopback) - { - // Allows testing of "localtest.me" - return false; - } - - var uri = context.Url == null ? context.OriginalUrl : new Uri(context.Url); - var url = context.Url ?? uri.ToString(); - var protocol = uri.Scheme.ToLower(); - var isHttps = protocol.IsCaseInsensitiveEqual("https"); - var startsWith = "{0}://www.".FormatInvariant(protocol); - var hasPrefix = url.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase); - - if (isHttps) - { - if (context.SecurityMode == HttpSecurityMode.SharedSsl) - { - // Don't attempt to redirect when shared SSL is being used and current request is secured. - // Redirecting from http://ssl.bla.com to https://www.ssl.bla.com will most probably fail. - return false; - } - } - - if (hasPrefix && rule == CanonicalHostNameRule.OmitWww) - { - context.Url = url.Replace("://www.", "://"); - return true; - } - - if (!hasPrefix && rule == CanonicalHostNameRule.RequireWww) - { - context.Url = url.Replace("{0}://".FormatInvariant(protocol), startsWith); - return true; - } - - return false; - } - - /// - /// Applies trailing slash and lowercase rules - /// - private bool TryRewritePath(RewriteContext context) - { - // Be tolerant in backend - if (context.IsAdmin) - return false; - - // Don't try to fix root path - if (context.ControllerContext.HttpContext.Request.RawUrl == "/") - return false; - - bool rewritten = false; - - var url = context.Url ?? context.OriginalUrl.ToString(); - var queryIndex = url.IndexOf('?'); - - if (queryIndex == -1) - { - bool hasTrailingSlash = url[url.Length - 1] == '/'; - - if (this.AppendTrailingSlash) - { - // Append a trailing slash to the end of the URL. - if (!hasTrailingSlash) - { - url += "/"; - rewritten = true; - } - } - else - { - // Trim a trailing slash from the end of the URL. - if (hasTrailingSlash) - { - url = url.TrimEnd('/'); - rewritten = true; - } - } - } - else - { - bool hasTrailingSlash = url[queryIndex - 1] == '/'; - - if (this.AppendTrailingSlash) - { - // Append a trailing slash to the end of the URL but before the query string. - if (!hasTrailingSlash) - { - url = url.Insert(queryIndex, "/"); - rewritten = true; - } - } - else - { - // Trim a trailing slash to the end of the URL but before the query string. - if (hasTrailingSlash) - { - url = url.Remove(queryIndex - 1, 1); - rewritten = true; - } - } - } - - if (this.LowercaseUrls) - { - foreach (char c in url) - { - if (char.IsUpper(c)) - { - var qIndex = url.IndexOf('?'); - url = qIndex == -1 - ? url.ToLower() - : url.Substring(0, qIndex).ToLower() + url.Substring(qIndex); - - rewritten = true; - break; - } - if (c == '?') - { - break; - } - } - } - - if (rewritten) - { - context.Url = url; - } - - return rewritten; - } - - class RewriteContext - { - public Uri OriginalUrl { get; set; } - public bool IsLoopback { get; set; } - public HttpSecurityMode SecurityMode { get; set; } - public ControllerContext ControllerContext { get; set; } - public Store CurrentStore { get; set; } - public bool IsAdmin { get; set; } - - public string Url { get; set; } - public bool? Permanent { get; set; } - } + /// + /// Applies HTTP protocol rule + /// + private bool TryRewriteScheme(RewriteContext context) + { + if (context.IsLoopback && !SecuritySettings.Value.UseSslOnLocalhost) + return false; + + var webHelper = WebHelper.Value; + + var currentConnectionSecured = webHelper.IsCurrentConnectionSecured(); + var currentStore = context.CurrentStore; + + if (currentStore.ForceSslForAllPages) + { + // all pages are forced to be SSL no matter of the specified value + this.SslRequirement = SslRequirement.Yes; + } + + switch (this.SslRequirement) + { + case SslRequirement.Yes: + { + if (!currentConnectionSecured && context.SecurityMode > HttpSecurityMode.Unsecured) + { + // Redirect to HTTPS version of page + context.Url = webHelper.GetThisPageUrl(true, true); + return true; + } + } + break; + case SslRequirement.No: + { + if (currentConnectionSecured) + { + // Redirect to HTTP version of page + context.Url = webHelper.GetThisPageUrl(true, false); + return true; + } + } + break; + default: + return false; + } + + return false; + } + + /// + /// Applies canonical host name rule + /// + private bool TryRewriteHostName(RewriteContext context) + { + var rule = SeoSettings.Value.CanonicalHostNameRule; + if (rule == CanonicalHostNameRule.NoRule) + return false; + + if (context.IsLoopback) + { + // Allows testing of "localtest.me" + return false; + } + + var uri = context.Url == null ? context.OriginalUrl : new Uri(context.Url); + var url = context.Url ?? uri.ToString(); + var protocol = uri.Scheme.ToLower(); + var isHttps = protocol.IsCaseInsensitiveEqual("https"); + var startsWith = "{0}://www.".FormatInvariant(protocol); + var hasPrefix = url.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase); + + if (isHttps) + { + if (context.SecurityMode == HttpSecurityMode.SharedSsl) + { + // Don't attempt to redirect when shared SSL is being used and current request is secured. + // Redirecting from http://ssl.bla.com to https://www.ssl.bla.com will most probably fail. + return false; + } + } + + if (hasPrefix && rule == CanonicalHostNameRule.OmitWww) + { + context.Url = url.Replace("://www.", "://"); + return true; + } + + if (!hasPrefix && rule == CanonicalHostNameRule.RequireWww) + { + context.Url = url.Replace("{0}://".FormatInvariant(protocol), startsWith); + return true; + } + + return false; + } + + /// + /// Applies trailing slash and lowercase rules + /// + private bool TryRewritePath(RewriteContext context) + { + // Be tolerant in backend + if (context.IsAdmin) + return false; + + // Don't try to fix root path + if (context.ControllerContext.HttpContext.Request.Path == "/") + return false; + + bool rewritten = false; + + var url = context.Url ?? context.OriginalUrl.ToString(); + var queryIndex = url.IndexOf('?'); + + if (queryIndex == -1) + { + bool hasTrailingSlash = url[url.Length - 1] == '/'; + + if (this.AppendTrailingSlash) + { + // Append a trailing slash to the end of the URL. + if (!hasTrailingSlash) + { + url += "/"; + rewritten = true; + } + } + else + { + // Trim a trailing slash from the end of the URL. + if (hasTrailingSlash) + { + url = url.TrimEnd('/'); + rewritten = true; + } + } + } + else + { + bool hasTrailingSlash = url[queryIndex - 1] == '/'; + + if (this.AppendTrailingSlash) + { + // Append a trailing slash to the end of the URL but before the query string. + if (!hasTrailingSlash) + { + url = url.Insert(queryIndex, "/"); + rewritten = true; + } + } + else + { + // Trim a trailing slash to the end of the URL but before the query string. + if (hasTrailingSlash) + { + url = url.Remove(queryIndex - 1, 1); + rewritten = true; + } + } + } + + if (this.LowercaseUrls) + { + foreach (char c in url) + { + if (char.IsUpper(c)) + { + var qIndex = url.IndexOf('?'); + url = qIndex == -1 + ? url.ToLower() + : url.Substring(0, qIndex).ToLower() + url.Substring(qIndex); + + rewritten = true; + break; + } + if (c == '?') + { + break; + } + } + } + + if (rewritten) + { + context.Url = url; + } + + return rewritten; + } + + class RewriteContext + { + public Uri OriginalUrl { get; set; } + public bool IsLoopback { get; set; } + public HttpSecurityMode SecurityMode { get; set; } + public ControllerContext ControllerContext { get; set; } + public Store CurrentStore { get; set; } + public bool IsAdmin { get; set; } + + public string Url { get; set; } + public bool? Permanent { get; set; } + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs index df6e25159f..9ab06a0c57 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Web.Mvc; using SmartStore.Core.Configuration; using SmartStore.Services; @@ -8,132 +9,150 @@ namespace SmartStore.Web.Framework.Settings { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - public class LoadSettingAttribute : FilterAttribute, IActionFilter - { - public sealed class SettingParam - { - public ISettings Instance { get; set; } - public ParameterDescriptor Parameter { get; set; } - } - - protected int _storeId; - protected SettingParam[] _settingParams; - - public LoadSettingAttribute() - : this(true) - { - } - - public LoadSettingAttribute(bool updateParameterFromStore) - { - UpdateParameterFromStore = updateParameterFromStore; - } - - public bool UpdateParameterFromStore { get; set; } - public ICommonServices Services { get; set; } - - public virtual void OnActionExecuting(ActionExecutingContext filterContext) - { - // Get the current configured store id - _storeId = filterContext.Controller.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - Func predicate = (x) => new[] { "storescope", "storeid" }.Contains(x.ParameterName, StringComparer.OrdinalIgnoreCase); - var storeScopeParam = FindActionParameters(filterContext.ActionDescriptor, false, false, predicate).FirstOrDefault(); - if (storeScopeParam != null) - { - // We found an action param named storeScope with type int. Assign our storeId to it. - filterContext.ActionParameters[storeScopeParam.ParameterName] = _storeId; - } - - // Find the required ISettings concrete types in ActionDescriptor.GetParameters() - _settingParams = FindActionParameters(filterContext.ActionDescriptor) - .Select(x => - { - // Load settings for the settings type obtained with FindActionParameters() - var settings = UpdateParameterFromStore - ? Services.Settings.LoadSetting(x.ParameterType, _storeId) - : filterContext.ActionParameters[x.ParameterName] as ISettings; - - if (settings == null) - { - throw new InvalidOperationException($"Could not load settings for type '{x.ParameterType.FullName}'."); - } - - // Replace settings from action parameters with our loaded settings - if (UpdateParameterFromStore) - { - filterContext.ActionParameters[x.ParameterName] = settings; - } - - return new SettingParam - { - Instance = settings, - Parameter = x - }; - }) - .ToArray(); - } - - public virtual void OnActionExecuted(ActionExecutedContext filterContext) - { - var viewResult = filterContext.Result as ViewResultBase; - - if (viewResult != null) - { - var model = viewResult.Model; - - if (model == null) - { - // Nothing to override. E.g. insufficient permission. - return; - } - - var settingsHelper = new StoreDependingSettingHelper(filterContext.Controller.ViewData); - - foreach (var param in _settingParams) - { - settingsHelper.GetOverrideKeys(param.Instance, model, _storeId, Services.Settings); - } - } - } - - protected IEnumerable FindActionParameters( - ActionDescriptor actionDescriptor, - bool requireDefaultConstructor = true, - bool throwIfNotFound = true, - Func predicate = null) - { - Guard.NotNull(actionDescriptor, nameof(actionDescriptor)); - - var t = typeof(T); - - var query = actionDescriptor - .GetParameters() - .Where(x => t.IsAssignableFrom(x.ParameterType)); - - if (predicate != null) - { - query = query.Where(predicate); - } - - if (throwIfNotFound && !query.Any()) - { - throw new InvalidOperationException( - $"A controller action method with a '{this.GetType().Name}' attribute requires an action parameter of type '{t.Name}' in order to execute properly."); - } - - if (requireDefaultConstructor) - { - foreach (var param in query) - { - if (!param.ParameterType.HasDefaultConstructor()) - { - throw new InvalidOperationException($"The parameter '{param.ParameterName}' must have a default parameterless constructor."); - } - } - } - - return query; - } - } + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public class LoadSettingAttribute : FilterAttribute, IActionFilter + { + public sealed class SettingParam + { + public ISettings Instance { get; set; } + public ParameterDescriptor Parameter { get; set; } + } + + protected int _storeId; + protected SettingParam[] _settingParams; + + public LoadSettingAttribute() + : this(true) + { + } + + public LoadSettingAttribute(bool updateParameterFromStore) + { + UpdateParameterFromStore = updateParameterFromStore; + } + + public bool UpdateParameterFromStore { get; set; } + public bool IsRootedModel { get; set; } + public ICommonServices Services { get; set; } + + public virtual void OnActionExecuting(ActionExecutingContext filterContext) + { + // Get the current configured store id + _storeId = filterContext.Controller.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + Func predicate = (x) => new[] { "storescope", "storeid" }.Contains(x.ParameterName, StringComparer.OrdinalIgnoreCase); + var storeScopeParam = FindActionParameters(filterContext.ActionDescriptor, false, false, predicate).FirstOrDefault(); + if (storeScopeParam != null) + { + // We found an action param named storeScope with type int. Assign our storeId to it. + filterContext.ActionParameters[storeScopeParam.ParameterName] = _storeId; + } + + // Find the required ISettings concrete types in ActionDescriptor.GetParameters() + _settingParams = FindActionParameters(filterContext.ActionDescriptor) + .Select(x => + { + // Load settings for the settings type obtained with FindActionParameters() + var settings = UpdateParameterFromStore + ? Services.Settings.LoadSetting(x.ParameterType, _storeId) + : filterContext.ActionParameters[x.ParameterName] as ISettings; + + if (settings == null) + { + throw new InvalidOperationException($"Could not load settings for type '{x.ParameterType.FullName}'."); + } + + // Replace settings from action parameters with our loaded settings + if (UpdateParameterFromStore) + { + filterContext.ActionParameters[x.ParameterName] = settings; + } + + return new SettingParam + { + Instance = settings, + Parameter = x + }; + }) + .ToArray(); + } + + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + var viewResult = filterContext.Result as ViewResultBase; + + if (viewResult != null) + { + var model = viewResult.Model; + + if (model == null) + { + // Nothing to override. E.g. insufficient permission. + return; + } + + var modelType = model.GetType(); + var settingsHelper = new StoreDependingSettingHelper(filterContext.Controller.ViewData); + if (IsRootedModel) + { + settingsHelper.CreateViewDataObject(_storeId); + } + + foreach (var param in _settingParams) + { + var settingInstance = param.Instance; + var modelInstance = model; + + if (IsRootedModel) + { + modelInstance = modelType.GetProperty(settingInstance.GetType().Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)?.GetValue(model); + if (modelInstance == null) + { + continue; + } + } + + settingsHelper.GetOverrideKeys(settingInstance, modelInstance, _storeId, Services.Settings, !IsRootedModel); + } + } + } + + protected IEnumerable FindActionParameters( + ActionDescriptor actionDescriptor, + bool requireDefaultConstructor = true, + bool throwIfNotFound = true, + Func predicate = null) + { + Guard.NotNull(actionDescriptor, nameof(actionDescriptor)); + + var t = typeof(T); + + var query = actionDescriptor + .GetParameters() + .Where(x => t.IsAssignableFrom(x.ParameterType)); + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (throwIfNotFound && !query.Any()) + { + throw new InvalidOperationException( + $"A controller action method with a '{this.GetType().Name}' attribute requires an action parameter of type '{t.Name}' in order to execute properly."); + } + + if (requireDefaultConstructor) + { + foreach (var param in query) + { + if (!param.ParameterType.HasDefaultConstructor()) + { + throw new InvalidOperationException($"The parameter '{param.ParameterName}' must have a default parameterless constructor."); + } + } + } + + return query; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs index 278679dbde..e84d13adb9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs @@ -4,90 +4,90 @@ namespace SmartStore.Web.Framework.Settings { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - public sealed class SaveSettingAttribute : LoadSettingAttribute - { - private FormCollection _form; - private IDisposable _settingsWriteBatch; - - public SaveSettingAttribute() - : base(true) - { - } - - public SaveSettingAttribute(bool updateParameterFromStore) - : base(updateParameterFromStore) - { - } - - public override void OnActionExecuting(ActionExecutingContext filterContext) - { - base.OnActionExecuting(filterContext); - - if (!filterContext.Controller.ViewData.ModelState.IsValid) - { - return; - } - - // Find the required FormCollection parameter in ActionDescriptor.GetParameters() - var formParam = FindActionParameters(filterContext.ActionDescriptor, requireDefaultConstructor: false, throwIfNotFound: false).FirstOrDefault(); - _form = formParam != null - ? (FormCollection)filterContext.ActionParameters[formParam.ParameterName] - : BindFormCollection(filterContext.Controller.ControllerContext); - - - _settingsWriteBatch = Services.Settings.BeginScope(); - } - - public override void OnActionExecuted(ActionExecutedContext filterContext) - { - if (filterContext.Controller.ViewData.ModelState.IsValid) - { - var updateSettings = true; - var redirectResult = filterContext.Result as RedirectToRouteResult; - if (redirectResult != null) - { - var controllerName = redirectResult.RouteValues["controller"] as string; - var areaName = redirectResult.RouteValues["area"] as string; - if (controllerName.IsCaseInsensitiveEqual("security") && areaName.IsCaseInsensitiveEqual("admin")) - { - // Insufficient permission. We must not save because the action did not run. - updateSettings = false; - } - } - - if (updateSettings) - { - var settingHelper = new StoreDependingSettingHelper(filterContext.Controller.ViewData); - - foreach (var param in _settingParams) - { - settingHelper.UpdateSettings(param.Instance, _form, _storeId, Services.Settings); - } - } - } - - if (_settingsWriteBatch != null) - { - _settingsWriteBatch.Dispose(); - _settingsWriteBatch = null; - } - - base.OnActionExecuted(filterContext); - } - - private FormCollection BindFormCollection(ControllerContext controllerContext) - { - var bindingContext = new ModelBindingContext - { - ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(FormCollection)), - ModelState = controllerContext.Controller.ViewData.ModelState, - ValueProvider = controllerContext.Controller.ValueProvider - }; - - var modelBinder = ModelBinders.Binders.GetBinder(typeof(FormCollection)); - - return (FormCollection)modelBinder.BindModel(controllerContext, bindingContext); - } - } + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public sealed class SaveSettingAttribute : LoadSettingAttribute + { + private FormCollection _form; + private IDisposable _settingsWriteBatch; + + public SaveSettingAttribute() + : base(true) + { + } + + public SaveSettingAttribute(bool updateParameterFromStore) + : base(updateParameterFromStore) + { + } + + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + base.OnActionExecuting(filterContext); + + if (!filterContext.Controller.ViewData.ModelState.IsValid) + { + return; + } + + // Find the required FormCollection parameter in ActionDescriptor.GetParameters() + var formParam = FindActionParameters(filterContext.ActionDescriptor, requireDefaultConstructor: false, throwIfNotFound: false).FirstOrDefault(); + _form = formParam != null + ? (FormCollection)filterContext.ActionParameters[formParam.ParameterName] + : BindFormCollection(filterContext.Controller.ControllerContext); + + + _settingsWriteBatch = Services.Settings.BeginScope(); + } + + public override void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext.Controller.ViewData.ModelState.IsValid) + { + var updateSettings = true; + var redirectResult = filterContext.Result as RedirectToRouteResult; + if (redirectResult != null) + { + var controllerName = redirectResult.RouteValues["controller"] as string; + var areaName = redirectResult.RouteValues["area"] as string; + if (controllerName.IsCaseInsensitiveEqual("security") && areaName.IsCaseInsensitiveEqual("admin")) + { + // Insufficient permission. We must not save because the action did not run. + updateSettings = false; + } + } + + if (updateSettings) + { + var settingHelper = new StoreDependingSettingHelper(filterContext.Controller.ViewData); + + foreach (var param in _settingParams) + { + settingHelper.UpdateSettings(param.Instance, _form, _storeId, Services.Settings); + } + } + } + + if (_settingsWriteBatch != null) + { + _settingsWriteBatch.Dispose(); + _settingsWriteBatch = null; + } + + base.OnActionExecuted(filterContext); + } + + private FormCollection BindFormCollection(ControllerContext controllerContext) + { + var bindingContext = new ModelBindingContext + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(FormCollection)), + ModelState = controllerContext.Controller.ViewData.ModelState, + ValueProvider = controllerContext.Controller.ValueProvider + }; + + var modelBinder = ModelBinders.Binders.GetBinder(typeof(FormCollection)); + + return (FormCollection)modelBinder.BindModel(controllerContext, bindingContext); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingData.cs b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingData.cs index 5894b7ab6f..a82194f63d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingData.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingData.cs @@ -1,16 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace SmartStore.Web.Framework.Settings { - public class StoreDependingSettingData - { - public StoreDependingSettingData() - { - OverrideSettingKeys = new List(); - } - - public int ActiveStoreScopeConfiguration { get; set; } - public List OverrideSettingKeys { get; set; } - public string RootSettingClass { get; set; } - } + public class StoreDependingSettingData + { + public int ActiveStoreScopeConfiguration { get; set; } + public HashSet OverrideSettingKeys { get; set; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public string RootSettingClass { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs index 7605dd7934..9c1adb51de 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs @@ -1,5 +1,8 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Web.Mvc; using SmartStore.ComponentModel; using SmartStore.Core.Infrastructure; @@ -9,121 +12,128 @@ namespace SmartStore.Web.Framework.Settings { - public class StoreDependingSettingHelper - { - private ViewDataDictionary _viewData; - - public StoreDependingSettingHelper(ViewDataDictionary viewData) - { - _viewData = viewData; - } - - public static string ViewDataKey => "StoreDependingSettingData"; - - public StoreDependingSettingData Data - { - get - { - return _viewData[ViewDataKey] as StoreDependingSettingData; - } - } - - private bool IsOverrideChecked(string settingKey, FormCollection form) - { - var rawOverrideKey = form.AllKeys.FirstOrDefault(k => k.IsCaseInsensitiveEqual(settingKey + "_OverrideForStore")); - - if (rawOverrideKey.HasValue()) - { - var checkboxValue = form[rawOverrideKey].EmptyNull().ToLower(); - return checkboxValue.Contains("on") || checkboxValue.Contains("true"); - } - return false; - } - - public bool IsOverrideChecked(object settings, string name, FormCollection form) - { - var key = settings.GetType().Name + "." + name; - return IsOverrideChecked(key, form); - } - - public void AddOverrideKey(object settings, string name) - { - if (Data == null) - { - throw new SmartException("You must call GetOverrideKeys or CreateViewDataObject before AddOverrideKey."); - } - - var key = settings.GetType().Name + "." + name; - Data.OverrideSettingKeys.Add(key); - } - - public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootSettingClass = null) - { - _viewData[ViewDataKey] = new StoreDependingSettingData - { - ActiveStoreScopeConfiguration = activeStoreScopeConfiguration, - RootSettingClass = rootSettingClass - }; - } + public class StoreDependingSettingHelper + { + private ViewDataDictionary _viewData; + + public StoreDependingSettingHelper(ViewDataDictionary viewData) + { + _viewData = viewData; + } + + public static string ViewDataKey => "StoreDependingSettingData"; + + public StoreDependingSettingData Data => _viewData[ViewDataKey] as StoreDependingSettingData; + + public bool IsOverrideChecked(object settings, string name, FormCollection form) + { + var key = settings.GetType().Name + "." + name; + return IsOverrideChecked(key, form); + } + + private bool IsOverrideChecked(string settingKey, FormCollection form) + { + var rawOverrideKey = form.AllKeys.FirstOrDefault(k => k.IsCaseInsensitiveEqual(settingKey + "_OverrideForStore")); + + if (rawOverrideKey.HasValue()) + { + var checkboxValue = form[rawOverrideKey].EmptyNull().ToLower(); + return checkboxValue.Contains("on") || checkboxValue.Contains("true"); + } + + return false; + } + + public void AddOverrideKey(object settings, string name) + { + if (Data == null) + { + throw new SmartException("You must call GetOverrideKeys or CreateViewDataObject before AddOverrideKey."); + } + + var key = settings.GetType().Name + "." + name; + Data.OverrideSettingKeys.Add(key); + } + + public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootSettingClass = null) + { + _viewData[ViewDataKey] = new StoreDependingSettingData + { + ActiveStoreScopeConfiguration = activeStoreScopeConfiguration, + RootSettingClass = rootSettingClass + }; + } public void GetOverrideKeys( - object settings, - object model, - int storeId, - ISettingService settingService, - bool isRootModel = true, - ILocalizedModelLocal localized = null, - int? index = null) + object settings, + object model, + int storeId, + ISettingService settingService, + bool isRootModel = true, + Func propertyNameMapper = null) { - if (storeId <= 0) - { - // Single store mode -> there are no overrides. - return; - } + GetOverrideKeysInternal(settings, model, storeId, settingService, isRootModel, propertyNameMapper, null); + } - var data = Data ?? new StoreDependingSettingData(); - var settingName = settings.GetType().Name; - var properties = settings.GetType().GetProperties(); - var localizedEntityService = EngineContext.Current.Resolve(); + private void GetOverrideKeysInternal( + object settings, + object model, + int storeId, + ISettingService settingService, + bool isRootModel, + Func propertyNameMapper, + int? localeIndex) + { + if (storeId <= 0) + { + // Single store mode -> there are no overrides. + return; + } + var data = Data ?? new StoreDependingSettingData(); + var settingType = settings.GetType(); var modelType = model.GetType(); + var settingName = settingType.Name; + var modelProperties = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var localizedModelLocal = model as ILocalizedModelLocal; + var localizedEntityService = EngineContext.Current.Resolve(); - foreach (var prop in properties) + foreach (var prop in modelProperties) { - var name = prop.Name; - var modelProperty = modelType.GetProperty(name); - - if (modelProperty == null) - { - // Setting is not configurable or missing or whatever... however we don't need the override info. - continue; - } - - string key = null; - - if (localized == null) - { - key = settingName + "." + name; - - if (settingService.GetSettingByKey(key, storeId: storeId) == null) - { - key = null; - } - } - else - { - var value = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); - if (!string.IsNullOrEmpty(value)) - { - key = string.Concat("Locales[", index.ToString(), "].", name); - } - } - - if (key != null) - { - data.OverrideSettingKeys.Add(key); - } - } + var name = propertyNameMapper?.Invoke(prop.Name) ?? prop.Name; + + var settingProperty = settingType.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + + if (settingProperty == null) + { + // Setting is not configurable or missing or whatever... however we don't need the override info. + continue; + } + + string key = null; + + if (localizedModelLocal == null) + { + key = settingName + "." + name; + if (settingService.GetSettingByKey(key, storeId: storeId) == null) + { + key = null; + } + } + else if (localeIndex.HasValue) + { + var value = localizedEntityService.GetLocalizedValue(localizedModelLocal.LanguageId, storeId, settingName, name); + if (!string.IsNullOrEmpty(value)) + { + key = settingName + "." + "Locales[" + localeIndex.ToString() + "]." + name; + } + } + + if (key != null) + { + data.OverrideSettingKeys.Add(key); + } + } if (isRootModel) { @@ -132,118 +142,113 @@ public void GetOverrideKeys( _viewData[ViewDataKey] = data; } + + if (model is ILocalizedModel) + { + var localesProperty = modelType.GetProperty("Locales", BindingFlags.Public | BindingFlags.Instance); + if (localesProperty != null) + { + if (localesProperty.GetValue(model) is IEnumerable locales) + { + int i = 0; + foreach (var locale in locales) + { + GetOverrideKeysInternal(settings, locale, storeId, settingService, false, propertyNameMapper, i); + i++; + } + } + } + } } - public void GetOverrideKey( - string formKey, - string settingName, - object settings, - int storeId, - ISettingService settingService, - ILocalizedModelLocal localized = null, - int? index = null) - { - if (storeId <= 0) - { - // Single store mode -> there are no overrides. - return; - } - - var key = formKey; - - if (localized == null) - { - if (settingService.GetSettingByKey(string.Concat(settings.GetType().Name, ".", settingName), storeId: storeId) == null) - { - key = null; - } - } - else - { - // TODO if required - throw new ArgumentException("Localized override key not supported yet."); - } - - if (key != null) - { - var data = Data ?? new StoreDependingSettingData(); - data.OverrideSettingKeys.Add(key); - } - } - - /// - /// Updates settings for a store. - /// - /// Settings class instance. - /// Form value collection. - /// Store identifier. - /// Setting service. - /// Localized model. - /// Function to map property names. Return null to skip a property. - public void UpdateSettings( - object settings, - FormCollection form, - int storeId, - ISettingService settingService, - ILocalizedModelLocal localized = null, - Func propertyNameMapper = null) + public void GetOverrideKey(string formKey, string settingName, object settings, int storeId, ISettingService settingService) { - var settingName = settings.GetType().Name; - var properties = FastProperty.GetProperties(localized == null ? settings.GetType() : localized.GetType()).Values; - - foreach (var prop in properties) - { - var name = prop.Name; - - if (propertyNameMapper != null) - { - name = propertyNameMapper(name); - } - - if (string.IsNullOrWhiteSpace(name)) - { - continue; - } - - var key = string.Concat(settingName, ".", name); - - if (storeId == 0 || IsOverrideChecked(key, form)) - { - dynamic value = prop.GetValue(localized == null ? settings : localized); - settingService.SetSetting(key, value == null ? "" : value, storeId, false); - } - else if (storeId > 0) - { - settingService.DeleteSetting(key, storeId); - } - } + if (storeId <= 0) + { + // Single store mode -> there are no overrides. + return; + } + + var key = formKey; + if (settingService.GetSettingByKey(string.Concat(settings.GetType().Name, ".", settingName), storeId: storeId) == null) + { + key = null; + } + + if (key != null) + { + var data = Data ?? new StoreDependingSettingData(); + data.OverrideSettingKeys.Add(key); + } + } + + /// + /// Updates settings for a store. + /// + /// Settings class instance. + /// Form value collection. + /// Store identifier. + /// Setting service. + /// Function to map property names. Return null to skip a property. + public void UpdateSettings( + object settings, + FormCollection form, + int storeId, + ISettingService settingService, + Func propertyNameMapper = null) + { + var settingType = settings.GetType(); + var settingName = settingType.Name; + var settingProperties = FastProperty.GetProperties(settingType).Values; + + foreach (var prop in settingProperties) + { + var name = propertyNameMapper?.Invoke(prop.Name) ?? prop.Name; + + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + var key = string.Concat(settingName, ".", name); + + if (storeId == 0 || IsOverrideChecked(key, form)) + { + dynamic value = prop.GetValue(settings); + settingService.SetSetting(key, value ?? string.Empty, storeId, false); + } + else if (storeId > 0) + { + settingService.DeleteSetting(key, storeId); + } + } } - public void UpdateSetting( - string formKey, - string settingName, - object settings, - FormCollection form, - int storeId, - ISettingService settingService, - ILocalizedModelLocal localized = null) - { - if (storeId == 0 || IsOverrideChecked(formKey, form)) - { - var prop = FastProperty.GetProperty(localized == null ? settings.GetType() : localized.GetType(), settingName); - if (prop != null) - { - dynamic value = prop.GetValue(localized == null ? settings : localized); - var key = string.Concat(settings.GetType().Name, ".", settingName); - - settingService.SetSetting(key, value == null ? "" : value, storeId, false); - } - } - else if (storeId > 0) - { - var key = string.Concat(settings.GetType().Name, ".", settingName); - settingService.DeleteSetting(key, storeId); - } - } - } + public void UpdateSetting( + string formKey, + string settingName, + object settings, + FormCollection form, + int storeId, + ISettingService settingService) + { + var settingType = settings.GetType(); + + if (storeId == 0 || IsOverrideChecked(formKey, form)) + { + var prop = FastProperty.GetProperty(settingType, settingName); + if (prop != null) + { + dynamic value = prop.GetValue(settings); + var key = string.Concat(settingType.Name, ".", settingName); + settingService.SetSetting(key, value ?? string.Empty, storeId, false); + } + } + else if (storeId > 0) + { + var key = string.Concat(settingType.Name, ".", settingName); + settingService.DeleteSetting(key, storeId); + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index 40d6b9639d..3af5f9ff89 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Web.Framework SmartStore.Web.Framework - v4.6.1 + v4.7.2 512 @@ -71,65 +71,73 @@ False ..\..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll - - ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll + + ..\..\packages\Autofac.5.2.0\lib\net461\Autofac.dll - - ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll - True + + ..\..\packages\Autofac.Mvc5.5.0.0\lib\net461\Autofac.Integration.Mvc.dll + + + ..\..\packages\Autofac.WebApi2.5.0.0\lib\net461\Autofac.Integration.WebApi.dll - - ..\..\packages\Autofac.WebApi2.4.2.0\lib\net45\Autofac.Integration.WebApi.dll + + ..\..\packages\AutoprefixerHost.1.1.10\lib\net45\AutoprefixerHost.dll - - ..\..\packages\BundleTransformer.Autoprefixer.1.10.2\lib\net40\BundleTransformer.Autoprefixer.dll + + ..\..\packages\BundleTransformer.Autoprefixer.1.12.1\lib\net40\BundleTransformer.Autoprefixer.dll ..\..\packages\BundleTransformer.Core.1.10.0\lib\net40\BundleTransformer.Core.dll - - ..\..\packages\BundleTransformer.SassAndScss.1.10.0\lib\net40\BundleTransformer.SassAndScss.dll + + ..\..\packages\BundleTransformer.SassAndScss.1.12.1\lib\net40\BundleTransformer.SassAndScss.dll - - ..\..\packages\CommonServiceLocator.2.0.4\lib\net46\CommonServiceLocator.dll + + ..\..\packages\CommonServiceLocator.2.0.5\lib\net47\CommonServiceLocator.dll ..\..\packages\DotLiquid.2.0.254\lib\net45\DotLiquid.dll - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll True - ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll True ..\..\packages\FluentValidation.7.4.0\lib\net45\FluentValidation.dll - - ..\..\packages\JavaScriptEngineSwitcher.Core.3.0.0\lib\net45\JavaScriptEngineSwitcher.Core.dll + + ..\..\packages\JavaScriptEngineSwitcher.Core.3.3.0\lib\net45\JavaScriptEngineSwitcher.Core.dll + + + ..\..\packages\LibSassHost.1.2.6\lib\net471\LibSassHost.dll - - ..\..\packages\LibSassHost.1.2.2\lib\net45\LibSassHost.dll + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll - - ..\..\packages\Microsoft.Data.Edm.5.8.4\lib\net40\Microsoft.Data.Edm.dll + + ..\..\packages\Microsoft.OData.Core.6.15.0\lib\portable-net45+win+wpa81\Microsoft.OData.Core.dll - - ..\..\packages\Microsoft.Data.OData.5.8.4\lib\net40\Microsoft.Data.OData.dll + + ..\..\packages\Microsoft.OData.Edm.6.15.0\lib\portable-net45+win+wpa81\Microsoft.OData.Edm.dll - - ..\..\packages\Microsoft.Data.Services.Client.5.8.4\lib\net40\Microsoft.Data.Services.Client.dll + + ..\..\packages\Microsoft.Spatial.6.15.0\lib\portable-net45+win+wpa81\Microsoft.Spatial.dll True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + @@ -140,16 +148,16 @@ ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + ..\..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - - ..\..\packages\System.Spatial.5.8.4\lib\net40\System.Spatial.dll - - - ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll @@ -165,16 +173,15 @@ ..\..\packages\Microsoft.AspNet.WebApi.Cors.5.2.7\lib\net45\System.Web.Http.Cors.dll - - ..\..\packages\Microsoft.AspNet.WebApi.OData.5.7.0\lib\net45\System.Web.Http.OData.dll - True - ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll ..\..\packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll + + ..\..\packages\Microsoft.AspNet.OData.5.10.0\lib\net45\System.Web.OData.dll + False ..\..\packages\Microsoft.AspNet.Web.Optimization.1.1.3\lib\net40\System.Web.Optimization.dll @@ -223,9 +230,12 @@ + + + @@ -234,11 +244,14 @@ + + + @@ -460,6 +473,8 @@ + + @@ -467,8 +482,7 @@ - - + diff --git a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs index 41e123dea0..271d4c8e31 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs +++ b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs @@ -1,238 +1,293 @@ using System; using System.Collections.Generic; -using System.Collections.Concurrent; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; using System.Web; +using System.Web.Hosting; using System.Web.Routing; -using System.Text.RegularExpressions; -using SmartStore.Utilities; -using SmartStore.Core; +using SmartStore.Collections; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Infrastructure; using SmartStore.Core.IO; -using System.IO; -using System.Web.Hosting; -using System.Reflection; +using SmartStore.Utilities; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Theming; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Events; -using SmartStore.Core.Data; -using SmartStore.Collections; namespace SmartStore.Web.Framework { - /// - /// Request event sequence: - /// - BeginRequest - /// - AuthenticateRequest - /// - PostAuthenticateRequest - /// - AuthorizeRequest - /// - PostAuthorizeRequest - /// - ResolveRequestCache - /// - PostResolveRequestCache - /// - MapRequestHandler - /// - PostMapRequestHandler - /// - AcquireRequestState - /// - PostAcquireRequestState - /// - PreRequestHandlerExecute - /// - PostRequestHandlerExecute - /// - ReleaseRequestState - /// - PostReleaseRequestState - /// - UpdateRequestCache - /// - PostUpdateRequestCache - /// - LogRequest - /// - PostLogRequest - /// - EndRequest - /// - PreSendRequestHeaders - /// - PreSendRequestContent - /// - public class SmartUrlRoutingModule : IHttpModule - { - private static readonly object _contextKey = new object(); - - private static readonly ICollection> _actions = - new SyncedCollection>(new List>()) { ReadLockFree = true }; - - private static readonly ICollection _routes = - new SyncedCollection(new List()) { ReadLockFree = true }; - - static SmartUrlRoutingModule() - { - StopSubDirMonitoring(); - } - - private static void StopSubDirMonitoring() - { - try - { - // http://stackoverflow.com/questions/2248825/asp-net-restarts-when-a-folder-is-created-renamed-or-deleted - var prop = typeof(HttpRuntime).GetProperty("FileChangesMonitor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - var o = prop.GetValue(null, null); - - var fi = o.GetType().GetField("_dirMonSubdirs", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase); - var monitor = fi.GetValue(o); - var mi = monitor.GetType().GetMethod("StopMonitoring", BindingFlags.Instance | BindingFlags.NonPublic); - mi.Invoke(monitor, new object[] { }); - } - catch { } - } - - public void Init(HttpApplication application) - { - if (!DataSettings.DatabaseIsInstalled()) - return; - - if (application.Context.Items[_contextKey] == null) - { - application.Context.Items[_contextKey] = _contextKey; - - if (CommonHelper.IsDevEnvironment && HttpContext.Current.IsDebuggingEnabled) - { - // Handle plugin static file in DevMode - application.PostAuthorizeRequest += (s, e) => PostAuthorizeRequest(new HttpContextWrapper(((HttpApplication)s).Context)); - application.PreSendRequestHeaders += (s, e) => PreSendRequestHeaders(new HttpContextWrapper(((HttpApplication)s).Context)); - } - - application.PostResolveRequestCache += (s, e) => PostResolveRequestCache(new HttpContextWrapper(((HttpApplication)s).Context)); - - // Publish event to give plugins the chance to register custom event handlers for the request lifecycle. - foreach (var action in _actions) - { - action(application); - } - - // Set app to fully initialized state on very first request - EngineContext.Current.IsFullyInitialized = true; - } - } - - /// - /// Registers an action that is called on application init. Call this to register HTTP request lifecycle callbacks / event handlers. - /// - /// Action - public static void RegisterAction(Action action) - { - Guard.NotNull(action, nameof(action)); - - _actions.Add(action); - } - - /// - /// Registers a path pattern which should be handled by the - /// - /// The app relative path to handle (regular expression, e.g. /sitemap.xml or /mini-profiler-resources/(.*)) - /// The http method constraint (regular expression, e.g. GET|POST) - /// - /// For performance reasons SmartStore.NET is configured to serve static files (files with extensions other than those mapped to managed handlers) - /// through the native StaticFileModule. This method lets you define exceptions to this default rule: every path registered as routable gets - /// handled by the and therefore enables dynamic processing of (physically non-existent) static files. - /// - public static void RegisterRoutablePath(string path, string verb = ".*") - { - Guard.NotEmpty(path, nameof(path)); - - if (path.IsWebUrl()) - { - throw new ArgumentException("Only relative paths are allowed.", "path"); - } - - var rgPath = new Regex(path, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - var rgVerb = new Regex(verb, RegexOptions.CultureInvariant | RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase); - - _routes.Add(new RoutablePath { PathPattern = rgPath, HttpMethodPattern = rgVerb }); - } - - public static bool HasMatchingPathHandler(string path, string method = "GET") - { - Guard.NotEmpty(path, nameof(path)); - Guard.NotEmpty(method, nameof(method)); - - if (_routes.Count > 0) - { - foreach (var route in _routes) - { - if (route.PathPattern.IsMatch(path) && route.HttpMethodPattern.IsMatch(method)) - { - return true; - } - } - } - - return false; - } - - public virtual void PostAuthorizeRequest(HttpContextBase context) - { - var request = context?.Request; - if (request == null) - return; - - if (IsExtensionPath(request) && WebHelper.IsStaticResourceRequested(request)) - { - // We're in debug mode and in dev environment - var file = HostingEnvironment.VirtualPathProvider.GetFile(request.AppRelativeCurrentExecutionFilePath) as DebugVirtualFile; - if (file != null) - { + /// + /// Request event sequence: + /// - BeginRequest + /// - AuthenticateRequest + /// - PostAuthenticateRequest + /// - AuthorizeRequest + /// - PostAuthorizeRequest + /// - ResolveRequestCache + /// - PostResolveRequestCache + /// - MapRequestHandler + /// - PostMapRequestHandler + /// - AcquireRequestState + /// - PostAcquireRequestState + /// - PreRequestHandlerExecute + /// - PostRequestHandlerExecute + /// - ReleaseRequestState + /// - PostReleaseRequestState + /// - UpdateRequestCache + /// - PostUpdateRequestCache + /// - LogRequest + /// - PostLogRequest + /// - EndRequest + /// - PreSendRequestHeaders + /// - PreSendRequestContent + /// + public class SmartUrlRoutingModule : IHttpModule + { + private static readonly object _contextKey = new object(); + + private static readonly ICollection> _actions = + new SyncedCollection>(new List>()) { ReadLockFree = true }; + + private static readonly ICollection _routes = + new SyncedCollection(new List()) { ReadLockFree = true }; + + static SmartUrlRoutingModule() + { + StopSubDirMonitoring(); + } + + private static void StopSubDirMonitoring() + { + try + { + // http://stackoverflow.com/questions/2248825/asp-net-restarts-when-a-folder-is-created-renamed-or-deleted + var prop = typeof(HttpRuntime).GetProperty("FileChangesMonitor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + var o = prop.GetValue(null, null); + + var fi = o.GetType().GetField("_dirMonSubdirs", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase); + var monitor = fi.GetValue(o); + var mi = monitor.GetType().GetMethod("StopMonitoring", BindingFlags.Instance | BindingFlags.NonPublic); + mi.Invoke(monitor, new object[] { }); + } + catch { } + } + + public void Init(HttpApplication application) + { + if (!DataSettings.DatabaseIsInstalled()) + return; + + if (application.Context.Items[_contextKey] == null) + { + application.Context.Items[_contextKey] = _contextKey; + + application.BeginRequest += (s, e) => FilterSameSiteNoneForIncompatibleUserAgents(application); + + if (CommonHelper.IsDevEnvironment && HttpContext.Current.IsDebuggingEnabled) + { + // Handle plugin static file in DevMode + application.PostAuthorizeRequest += (s, e) => RewritePluginStaticFilePaths((HttpApplication)s); + application.PreSendRequestHeaders += (s, e) => HandlePluginStaticFileCaching((HttpApplication)s); + } + + application.PreSendRequestHeaders += (s, e) => HandleSameSiteForAntiForgeryCookie((HttpApplication)s); + application.PostResolveRequestCache += (s, e) => TryResolveRoutablePath((HttpApplication)s); + + // Publish event to give plugins the chance to register custom event handlers for the request lifecycle. + foreach (var action in _actions) + { + action(application); + } + + // Set app to fully initialized state on very first request + EngineContext.Current.IsFullyInitialized = true; + } + } + + #region Handlers + + private static void RewritePluginStaticFilePaths(HttpApplication app) + { + var context = app.Context; + var request = context?.Request; + if (request == null) + return; + + if (IsExtensionPath(request) && context.IsStaticResourceRequested()) + { + // We're in debug mode and in dev environment + var file = HostingEnvironment.VirtualPathProvider.GetFile(request.AppRelativeCurrentExecutionFilePath) as DebugVirtualFile; + if (file != null) + { context.Items["DebugFile"] = file; context.Response.TransmitFile(file.PhysicalPath); context.Response.Flush(); // TBD: optional? context.Response.SuppressContent = true; // TBD: optional? context.ApplicationInstance.CompleteRequest(); } - } - } - - public virtual void PreSendRequestHeaders(HttpContextBase context) - { - if (context?.Response == null) - return; - - var file = context.Items?["DebugFile"] as DebugVirtualFile; - if (file != null) - { - context.Response.AddFileDependency(file.PhysicalPath); - context.Response.ContentType = MimeTypes.MapNameToMimeType(Path.GetFileName(file.PhysicalPath)); - context.Response.Cache.SetNoStore(); - context.Response.Cache.SetLastModifiedFromFileDependencies(); - } - } - - public virtual void PostResolveRequestCache(HttpContextBase context) - { - var request = context?.Request; - if (request == null) - return; - - if (_routes.Count > 0) - { - var path = request.AppRelativeCurrentExecutionFilePath.TrimStart('~').TrimEnd('/'); - var method = request.HttpMethod.EmptyNull(); - - foreach (var route in _routes) - { - if (route.PathPattern.IsMatch(path) && route.HttpMethodPattern.IsMatch(method)) - { - var module = new UrlRoutingModule(); - module.PostResolveRequestCache(context); - return; - } - } - } - } - - private bool IsExtensionPath(HttpRequestBase request) - { - var path = request.AppRelativeCurrentExecutionFilePath.ToLower(); - var result = path.StartsWith("~/plugins/") || path.StartsWith("~/themes/"); - return result; - } - - public void Dispose() - { - // nothing to dispose - } - - private class RoutablePath - { - public Regex PathPattern { get; set; } - public Regex HttpMethodPattern { get; set; } - } - } + } + } + + private static void HandlePluginStaticFileCaching(HttpApplication app) + { + var context = app.Context; + if (context?.Response == null) + return; + + var file = context.Items?["DebugFile"] as DebugVirtualFile; + if (file != null) + { + context.Response.AddFileDependency(file.PhysicalPath); + context.Response.ContentType = MimeTypes.MapNameToMimeType(Path.GetFileName(file.PhysicalPath)); + context.Response.Cache.SetNoStore(); + context.Response.Cache.SetLastModifiedFromFileDependencies(); + } + } + + private static void HandleSameSiteForAntiForgeryCookie(HttpApplication app) + { + var context = app.Context; + if (context?.Request == null || context?.Response == null) + return; + + if (context.IsStaticResourceRequested()) + return; + + if (SameSiteBrowserDetector.AllowsSameSiteNone(context.Request.UserAgent)) + { + // Set SameSite attribute for antiforgery token. + var privacySettings = EngineContext.Current.Resolve(); + var antiForgeryCookie = context.Request.Cookies["__requestverificationtoken"]; + + if (antiForgeryCookie != null) + { + antiForgeryCookie.HttpOnly = true; + antiForgeryCookie.Secure = context.Request.IsHttps(); + antiForgeryCookie.SameSite = antiForgeryCookie.Secure ? (SameSiteMode)privacySettings.SameSiteMode : SameSiteMode.Lax; + + context.Response.Cookies.Set(antiForgeryCookie); + } + } + } + + private static void TryResolveRoutablePath(HttpApplication app) + { + var context = app.Context; + var request = context?.Request; + if (request == null) + return; + + if (_routes.Count > 0) + { + var path = request.AppRelativeCurrentExecutionFilePath.TrimStart('~').TrimEnd('/'); + var method = request.HttpMethod.EmptyNull(); + + foreach (var route in _routes) + { + if (route.PathPattern.IsMatch(path) && route.HttpMethodPattern.IsMatch(method)) + { + var module = new UrlRoutingModule(); + module.PostResolveRequestCache(new HttpContextWrapper(context)); + return; + } + } + } + } + + private static void FilterSameSiteNoneForIncompatibleUserAgents(HttpApplication app) + { + var context = app?.Context; + if (context == null || context.IsStaticResourceRequested() || SameSiteBrowserDetector.AllowsSameSiteNone(context.Request?.UserAgent)) + return; + + app.Response.AddOnSendingHeaders(ctx => + { + var cookies = ctx.Response.Cookies; + for (var i = 0; i < cookies.Count; i++) + { + var cookie = cookies[i]; + if (cookie.SameSite == SameSiteMode.None) + { + cookie.SameSite = (SameSiteMode)(-1); // Unspecified + } + } + }); + } + + #endregion + + /// + /// Registers an action that is called on application init. Call this to register HTTP request lifecycle callbacks / event handlers. + /// + /// Action + public static void RegisterAction(Action action) + { + Guard.NotNull(action, nameof(action)); + + _actions.Add(action); + } + + /// + /// Registers a path pattern which should be handled by the + /// + /// The app relative path to handle (regular expression, e.g. /sitemap.xml or /mini-profiler-resources/(.*)) + /// The http method constraint (regular expression, e.g. GET|POST) + /// + /// For performance reasons Smartstore is configured to serve static files (files with extensions other than those mapped to managed handlers) + /// through the native StaticFileModule. This method lets you define exceptions to this default rule: every path registered as routable gets + /// handled by the and therefore enables dynamic processing of (physically non-existent) static files. + /// + public static void RegisterRoutablePath(string path, string verb = ".*") + { + Guard.NotEmpty(path, nameof(path)); + + if (path.IsWebUrl()) + { + throw new ArgumentException("Only relative paths are allowed.", "path"); + } + + var rgPath = new Regex(path, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + var rgVerb = new Regex(verb, RegexOptions.CultureInvariant | RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase); + + _routes.Add(new RoutablePath { PathPattern = rgPath, HttpMethodPattern = rgVerb }); + } + + public static bool HasMatchingPathHandler(string path, string method = "GET") + { + Guard.NotEmpty(path, nameof(path)); + Guard.NotEmpty(method, nameof(method)); + + if (_routes.Count > 0) + { + foreach (var route in _routes) + { + if (route.PathPattern.IsMatch(path) && route.HttpMethodPattern.IsMatch(method)) + { + return true; + } + } + } + + return false; + } + + private static bool IsExtensionPath(HttpRequest request) + { + var path = request.AppRelativeCurrentExecutionFilePath.ToLower(); + var result = path.StartsWith("~/plugins/") || path.StartsWith("~/themes/"); + return result; + } + + public void Dispose() + { + // nothing to dispose + } + + private class RoutablePath + { + public Regex PathPattern { get; set; } + public Regex HttpMethodPattern { get; set; } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs index 7754edbf54..b86c81639b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs @@ -1,96 +1,89 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using DotLiquid; namespace SmartStore.Templating { - /// - /// Published when a template zone is about to be rendered. - /// By subscribing to this event, implementors can inject custom - /// content to specific template zones. - /// - public sealed class ZoneRenderingEvent - { - private IList _snippets; + /// + /// Published when a template zone is about to be rendered. + /// By subscribing to this event, implementors can inject custom + /// content to specific template zones. + /// + public sealed class ZoneRenderingEvent + { + private IList _snippets; - public ZoneRenderingEvent(string zoneName, IDictionary model) - { - ZoneName = zoneName; - Model = model; - } + public ZoneRenderingEvent(string zoneName, IDictionary model) + { + ZoneName = zoneName; + Model = model; + } - internal Context LiquidContext { get; set; } + internal Context LiquidContext { get; set; } - /// - /// The name of the rendered template. - /// - public string TemplateName - { - get => Evaluate("Context.TemplateName") as string; - } + /// + /// The name of the rendered template. + /// + public string TemplateName => Evaluate("Context.TemplateName") as string; - /// - /// The name of the zone which is rendered. - /// - public string ZoneName { get; private set; } + /// + /// The name of the zone which is rendered. + /// + public string ZoneName { get; private set; } - /// - /// The template model - /// - public IDictionary Model { get; private set; } + /// + /// The template model + /// + public IDictionary Model { get; private set; } - /// - /// Evaluates an expression - e.g. Product.Sku - and returns it's value. - /// - /// - /// - public object Evaluate(string expression) - { - return LiquidContext[expression, false]; - } + /// + /// Evaluates an expression - e.g. Product.Sku - and returns it's value. + /// + /// + /// + public object Evaluate(string expression) + { + return LiquidContext[expression, false]; + } - /// - /// Specifies the custom content which the template engine should parse and inject. - /// - /// The content - public void InjectContent(string content) - { - if (content.HasValue()) - { - AddSnippet(new Snippet { Content = content, Parse = true }); - } - } + /// + /// Specifies the custom content which the template engine should parse and inject. + /// + /// The content + public void InjectContent(string content) + { + if (content.HasValue()) + { + AddSnippet(new Snippet { Content = content, Parse = true }); + } + } - /// - /// Specifies the custom content to inject. - /// - /// The content - /// This should be true if the content contains template syntax. - public void InjectContent(string content, bool parse) - { - if (content.HasValue()) - { - AddSnippet(new Snippet { Content = content, Parse = parse }); - } - } + /// + /// Specifies the custom content to inject. + /// + /// The content + /// This should be true if the content contains template syntax. + public void InjectContent(string content, bool parse) + { + if (content.HasValue()) + { + AddSnippet(new Snippet { Content = content, Parse = parse }); + } + } - private void AddSnippet(Snippet snippet) - { - if (_snippets == null) - _snippets = new List(); + private void AddSnippet(Snippet snippet) + { + if (_snippets == null) + _snippets = new List(); - _snippets.Add(snippet); - } + _snippets.Add(snippet); + } - internal IList Snippets - { - get => _snippets; - } + internal IList Snippets => _snippets; - internal class Snippet - { - public string Content { get; set; } - public bool Parse { get; set; } - } - } + internal class Snippet + { + public string Content { get; set; } + public bool Parse { get; set; } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs index e23556ddab..e061b9976d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs @@ -1,34 +1,33 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Templating.Liquid { - internal class DictionaryDrop : SafeDropBase - { - private readonly IDictionary _inner; + internal class DictionaryDrop : SafeDropBase + { + private readonly IDictionary _inner; - public DictionaryDrop(IDictionary data) - { - Guard.NotNull(data, nameof(data)); + public DictionaryDrop(IDictionary data) + { + Guard.NotNull(data, nameof(data)); - _inner = data; - } + _inner = data; + } - public override bool ContainsKey(object key) - { - return (key is string s) - ? _inner.ContainsKey(s) - : false; - } + public override bool ContainsKey(object key) + { + return (key is string s) + ? _inner.ContainsKey(s) + : false; + } - protected override object InvokeMember(string name) - { - return _inner.Get(name); - } + protected override object InvokeMember(string name) + { + return _inner.Get(name); + } - public override object GetWrappedObject() - { - return _inner; - } - } + public override object GetWrappedObject() + { + return _inner; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs index 721e73d533..e416575d82 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs @@ -1,40 +1,42 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; using DotLiquid; namespace SmartStore.Templating.Liquid { - internal class EnumerableWrapper : IEnumerable, ISafeObject - { - private readonly IEnumerable _enumerable; + internal class EnumerableWrapper : IEnumerable, ISafeObject + { + private readonly IEnumerable _enumerable; - public EnumerableWrapper(IEnumerable enumerable) - { - Guard.NotNull(enumerable, nameof(enumerable)); - _enumerable = enumerable; - } + public EnumerableWrapper(IEnumerable enumerable) + { + Guard.NotNull(enumerable, nameof(enumerable)); + _enumerable = enumerable; + } - public IEnumerator GetEnumerator() - { - return GetEnumeratorInternal(); - } + public IEnumerator GetEnumerator() + { + return GetEnumeratorInternal(); + } - public object GetWrappedObject() => _enumerable; + public object GetWrappedObject() + { + return _enumerable; + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumeratorInternal(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumeratorInternal(); + } - private IEnumerator GetEnumeratorInternal() - { - return _enumerable - .Cast() - .Select(x => LiquidUtil.CreateSafeObject(x)) - .OfType() - .GetEnumerator(); - } - } + private IEnumerator GetEnumeratorInternal() + { + return _enumerable + .Cast() + .Select(x => LiquidUtil.CreateSafeObject(x)) + .OfType() + .GetEnumerator(); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs index 259f6ef028..8a384a5c9b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs @@ -6,54 +6,54 @@ namespace SmartStore.Templating.Liquid { - internal class ObjectDrop : SafeDropBase - { - private readonly object _data; - private readonly Type _type; - - public ObjectDrop(object data) - { - Guard.NotNull(data, nameof(data)); - - _data = data; - - if (data is BaseEntity be) - { - _type = be.GetUnproxiedType(); - } - else - { - _type = data.GetType(); - } - } - - public override bool ContainsKey(object key) - { - return true; - } - - protected override object InvokeMember(string name) - { - var prop = FastProperty.GetProperty(_type, name); - if (prop != null) - { - return prop.Property.HasAttribute(true) - ? null - : prop.GetValue(_data); - } - - var method = _type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public); - if (method != null && method.GetParameters().Length == 0) - { - return method.Invoke(_data, null); - } - - return null; - } - - public override object GetWrappedObject() - { - return _data; - } - } + internal class ObjectDrop : SafeDropBase + { + private readonly object _data; + private readonly Type _type; + + public ObjectDrop(object data) + { + Guard.NotNull(data, nameof(data)); + + _data = data; + + if (data is BaseEntity be) + { + _type = be.GetUnproxiedType(); + } + else + { + _type = data.GetType(); + } + } + + public override bool ContainsKey(object key) + { + return true; + } + + protected override object InvokeMember(string name) + { + var prop = FastProperty.GetProperty(_type, name); + if (prop != null) + { + return prop.Property.HasAttribute(true) + ? null + : prop.GetValue(_data); + } + + var method = _type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public); + if (method != null && method.GetParameters().Length == 0) + { + return method.Invoke(_data, null); + } + + return null; + } + + public override object GetWrappedObject() + { + return _data; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs index 2974c3cacf..87d4423df6 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs @@ -1,49 +1,42 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using DotLiquid; namespace SmartStore.Templating.Liquid { - internal interface ISafeObject - { - object GetWrappedObject(); - } - - internal abstract class SafeDropBase : ILiquidizable, IIndexable, ISafeObject - { - private readonly IDictionary _safeObjects = new Dictionary(); - - protected object GetOrCreateSafeObject(string name) - { - if (!_safeObjects.TryGetValue(name, out var safeObject)) - { - safeObject = LiquidUtil.CreateSafeObject(InvokeMember(name)); - if (safeObject is ISafeObject) - { - _safeObjects[name] = safeObject; - } - } - - return safeObject; - } - - public abstract bool ContainsKey(object key); - - public object this[object key] - { - get - { - return (key is string s) ? GetOrCreateSafeObject(s) : null; - } - } - - protected abstract object InvokeMember(string name); - - public object ToLiquid() - { - return this; - } - - public abstract object GetWrappedObject(); - } + internal interface ISafeObject + { + object GetWrappedObject(); + } + + internal abstract class SafeDropBase : ILiquidizable, IIndexable, ISafeObject + { + private readonly IDictionary _safeObjects = new Dictionary(); + + protected object GetOrCreateSafeObject(string name) + { + if (!_safeObjects.TryGetValue(name, out var safeObject)) + { + safeObject = LiquidUtil.CreateSafeObject(InvokeMember(name)); + if (safeObject is ISafeObject) + { + _safeObjects[name] = safeObject; + } + } + + return safeObject; + } + + public abstract bool ContainsKey(object key); + + public object this[object key] => (key is string s) ? GetOrCreateSafeObject(s) : null; + + protected abstract object InvokeMember(string name); + + public object ToLiquid() + { + return this; + } + + public abstract object GetWrappedObject(); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs index 0c02cfc4c2..91e0c00b40 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs @@ -6,91 +6,88 @@ namespace SmartStore.Templating.Liquid { - internal class TestDrop : ITestModel, ILiquidizable, IIndexable, ISafeObject - { - private readonly BaseEntity _entity; - private readonly Type _type; - private readonly string _modelPrefix; + internal class TestDrop : ITestModel, ILiquidizable, IIndexable, ISafeObject + { + private readonly BaseEntity _entity; + private readonly Type _type; + private readonly string _modelPrefix; - public TestDrop(BaseEntity entity, string modelPrefix) - { - _entity = entity; - _type = entity.GetUnproxiedType(); + public TestDrop(BaseEntity entity, string modelPrefix) + { + _entity = entity; + _type = entity.GetUnproxiedType(); - if (modelPrefix.HasValue()) - { - _modelPrefix = modelPrefix.EnsureEndsWith("."); - } + if (modelPrefix.HasValue()) + { + _modelPrefix = modelPrefix.EnsureEndsWith("."); + } - _modelPrefix = _modelPrefix ?? string.Empty; - } + _modelPrefix = _modelPrefix ?? string.Empty; + } - public string ModelName - { - get => _type.Name; - } + public string ModelName => _type.Name; - public object GetWrappedObject() - { - return _entity; - } + public object GetWrappedObject() + { + return _entity; + } - public bool ContainsKey(object key) - { - return true; - } + public bool ContainsKey(object key) + { + return true; + } - public object this[object key] - { - get - { - object value = null; + public object this[object key] + { + get + { + object value = null; - if (key is string name) - { - var modelPrefix = _modelPrefix + name; - var fastProp = FastProperty.GetProperty(_type, name); - var pi = fastProp?.Property; + if (key is string name) + { + var modelPrefix = _modelPrefix + name; + var fastProp = FastProperty.GetProperty(_type, name); + var pi = fastProp?.Property; - if (pi == null) - { - value = "{{ " + modelPrefix + " }}"; - } - else if (pi.PropertyType.IsPredefinedType()) - { - value = "{{ " + modelPrefix + " }}"; - } - else if (fastProp.IsSequenceType) - { - var seqType = pi.PropertyType.GetGenericArguments()[0]; - if (typeof(BaseEntity).IsAssignableFrom(seqType)) - { - var testObj1 = new TestDrop((BaseEntity)Activator.CreateInstance(seqType), "it"); - var testObj2 = new TestDrop((BaseEntity)Activator.CreateInstance(seqType), "it"); - var list = Activator.CreateInstance(typeof(List)) as List; - list.Add(testObj1); - list.Add(testObj2); - value = list; - } - } - else if (typeof(BaseEntity).IsAssignableFrom(pi.PropertyType)) - { - value = new TestDrop((BaseEntity)Activator.CreateInstance(pi.PropertyType), modelPrefix); - } + if (pi == null) + { + value = "{{ " + modelPrefix + " }}"; + } + else if (pi.PropertyType.IsPredefinedType()) + { + value = "{{ " + modelPrefix + " }}"; + } + else if (fastProp.IsSequenceType) + { + var seqType = pi.PropertyType.GetGenericArguments()[0]; + if (typeof(BaseEntity).IsAssignableFrom(seqType)) + { + var testObj1 = new TestDrop((BaseEntity)Activator.CreateInstance(seqType), "it"); + var testObj2 = new TestDrop((BaseEntity)Activator.CreateInstance(seqType), "it"); + var list = Activator.CreateInstance(typeof(List)) as List; + list.Add(testObj1); + list.Add(testObj2); + value = list; + } + } + else if (typeof(BaseEntity).IsAssignableFrom(pi.PropertyType)) + { + value = new TestDrop((BaseEntity)Activator.CreateInstance(pi.PropertyType), modelPrefix); + } - //if (value is string s) - //{ - // value = "{1}".FormatInvariant(invalid ? " invalid" : "", value); - //} - } + //if (value is string s) + //{ + // value = "{1}".FormatInvariant(invalid ? " invalid" : "", value); + //} + } - return value; - } - } + return value; + } + } - public object ToLiquid() - { - return this; - } - } + public object ToLiquid() + { + return this; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs index c373fa09fb..a4bd7e1bb0 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs @@ -5,244 +5,239 @@ using System.Text; using DotLiquid; using Newtonsoft.Json; -using SmartStore.Utilities; -using SmartStore.Services; -using SmartStore.Services.Directory; +using SmartStore.ComponentModel; using SmartStore.Core.Domain.Directory; +using SmartStore.Services; using SmartStore.Services.Common; -using SmartStore.ComponentModel; +using SmartStore.Services.Directory; +using SmartStore.Utilities; namespace SmartStore.Templating.Liquid { - public static class AdditionalFilters - { - private static LiquidTemplateEngine GetEngine() - { - return (LiquidTemplateEngine)Template.FileSystem; - } - - #region Common Filters - - public static object Default(object input, object value) - { - return input ?? value; - } - - public static string Json(object input) - { - if (input == null) - return null; - - return JsonConvert.SerializeObject(input, new JsonSerializerSettings - { + public static class AdditionalFilters + { + private static LiquidTemplateEngine GetEngine() + { + return (LiquidTemplateEngine)Template.FileSystem; + } + + #region Common Filters + + public static object Default(object input, object value) + { + return input ?? value; + } + + public static string Json(object input) + { + if (input == null) + return null; + + return JsonConvert.SerializeObject(input, new JsonSerializerSettings + { ContractResolver = SmartContractResolver.Instance, ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }); - } + }); + } + + public static string FormatAddress(Context context, object input) + { + if (input == null) + return null; + + var engine = GetEngine(); + var countryService = engine.Services.Resolve(); + var addressService = engine.Services.Resolve(); + + Country country = null; + + // We know that we converted Address entity to a dictionary. - public static string FormatAddress(Context context, object input) - { - if (input == null) - return null; - - var engine = GetEngine(); - var countryService = engine.Services.Resolve(); - var addressService = engine.Services.Resolve(); + if (input is IDictionary dict) + { + country = countryService.GetCountryById(dict.Get("CountryId").Convert().GetValueOrDefault()); + } + else if (input is IIndexable lq) + { + country = countryService.GetCountryById(lq["CountryId"].Convert().GetValueOrDefault()); + } + + return addressService.FormatAddress(input, country?.AddressFormat, context.FormatProvider); + } + + #endregion + + #region Array Filters + + public static IEnumerable Uniq(IEnumerable input) + { + if (input == null) + return null; + + return input.Cast().Distinct(); + } + + public static IEnumerable Compact(IEnumerable input) + { + if (input == null) + return null; + + return input.Cast().Where(x => x != null); + } + + public static IEnumerable Reverse(IEnumerable input) + { + if (input == null) + return null; + + return input.Cast().Reverse(); + } + + #endregion + + #region Number Filters + + public static object Ceil(object input) + { + if (input == null) + return null; + + return CommonHelper.TryConvert(input, out var d) + ? Math.Ceiling(d) + : 0; + } - Country country = null; + public static object Floor(object input) + { + if (input == null) + return null; - // We know that we converted Address entity to a dictionary. + return CommonHelper.TryConvert(input, out var d) + ? Math.Floor(d) + : 0; + } - if (input is IDictionary dict) - { - country = countryService.GetCountryById(dict.Get("CountryId").Convert().GetValueOrDefault()); - } - else if (input is IIndexable lq) - { - country = countryService.GetCountryById(lq["CountryId"].Convert().GetValueOrDefault()); - } - - return addressService.FormatAddress(input, country?.AddressFormat, context.FormatProvider); - } - - #endregion - - #region Array Filters - - public static IEnumerable Uniq(IEnumerable input) - { - if (input == null) - return null; - - return input.Cast().Distinct(); - } - - public static IEnumerable Compact(IEnumerable input) - { - if (input == null) - return null; - - return input.Cast().Where(x => x != null); - } - - public static IEnumerable Reverse(IEnumerable input) - { - if (input == null) - return null; - - return input.Cast().Reverse(); - } - - #endregion - - #region Number Filters - - public static object Ceil(object input) - { - if (input == null) - return null; - - return CommonHelper.TryConvert(input, out var d) - ? Math.Ceiling(d) - : 0; - } - - public static object Floor(object input) - { - if (input == null) - return null; + public static object Abs(object input) + { + if (input == null) + return null; - return CommonHelper.TryConvert(input, out var d) - ? Math.Floor(d) - : 0; - } + return CommonHelper.TryConvert(input, out var d) + ? Math.Abs(d) + : 0; + } - public static object Abs(object input) - { - if (input == null) - return null; + public static object AtMost(object input, object operand) + { + if (input == null || operand == null) + return null; - return CommonHelper.TryConvert(input, out var d) - ? Math.Abs(d) - : 0; - } + return CommonHelper.TryConvert(input, out var d) && CommonHelper.TryConvert(operand, out var o) + ? Math.Min(d, o) + : 0; + } - public static object AtMost(object input, object operand) - { - if (input == null || operand == null) - return null; + public static object AtLeast(object input, object operand) + { + if (input == null || operand == null) + return null; - return CommonHelper.TryConvert(input, out var d) && CommonHelper.TryConvert(operand, out var o) - ? Math.Min(d, o) - : 0; - } + return CommonHelper.TryConvert(input, out var d) && CommonHelper.TryConvert(operand, out var o) + ? Math.Max(d, o) + : 0; + } - public static object AtLeast(object input, object operand) - { - if (input == null || operand == null) - return null; + #endregion - return CommonHelper.TryConvert(input, out var d) && CommonHelper.TryConvert(operand, out var o) - ? Math.Max(d, o) - : 0; - } + #region String Filters - #endregion + public static string Prettify(object input, bool allowSpace) + { + if (CommonHelper.TryConvert(input, out var l)) + { + return Prettifier.BytesToString(l); + } + else if (input is string s) + { + return s.Slugify(allowSpace); + } - #region String Filters + return null; + } - public static string Prettify(object input, bool allowSpace) - { - if (CommonHelper.TryConvert(input, out var l)) - { - return Prettifier.BytesToString(l); - } - else if (input is string s) - { - return s.Slugify(allowSpace); - } + public static string SanitizeHtmlId(string input) + { + return input?.SanitizeHtmlId(); + } - return null; - } + public static string Md5(string input) + { + return input?.Hash(Encoding.UTF8); + } - public static string SanitizeHtmlId(string input) - { - return input?.SanitizeHtmlId(); - } - - public static string Md5(string input) - { - return input?.Hash(Encoding.UTF8); - } + public static string UrlDecode(string input) + { + return input?.UrlDecode(); + } - public static string Sha1(string input) - { - return input?.Sha(Encoding.UTF8); - } + public static string Handleize(string input) + { + return Inflector.Handleize(input.EmptyNull()); + } - public static string UrlDecode(string input) - { - return input?.UrlDecode(); - } + public static string Pluralize(string input) + { + return Inflector.Pluralize(input.EmptyNull()); + } - public static string Handleize(string input) - { - return Inflector.Handleize(input.EmptyNull()); - } + #endregion - public static string Pluralize(string input) - { - return Inflector.Pluralize(input.EmptyNull()); - } + #region Localization Filters - #endregion - - #region Localization Filters - - public static string T(Context context, string key, object arg1 = null, object arg2 = null, object arg3 = null, object arg4 = null) - { - var engine = GetEngine(); - - int languageId = 0; - - if (context["Context.LanguageId"] is int lid) - { - languageId = lid; - } - - var args = (new object[] { arg1, arg2, arg3, arg4 }).ToArray(); + public static string T(Context context, string key, object arg1 = null, object arg2 = null, object arg3 = null, object arg4 = null) + { + var engine = GetEngine(); + + int languageId = 0; + + if (context["Context.LanguageId"] is int lid) + { + languageId = lid; + } + + var args = (new object[] { arg1, arg2, arg3, arg4 }).ToArray(); - return engine.T(key, languageId, args); - } + return engine.T(key, languageId, args); + } - #endregion + #endregion - #region Url Filters + #region Url Filters - #endregion + #endregion - #region Html Filters + #region Html Filters - public static string ScriptTag(string input) - { - return String.Format("", input); - } + public static string ScriptTag(string input) + { + return String.Format("", input); + } - public static string StylesheetTag(string input) - { - return String.Format("", input); - } + public static string StylesheetTag(string input) + { + return String.Format("", input); + } - public static string ImgTag(string input, string alt = "", string css = "") - { - return input == null ? null : GetImageTag(input, alt, css); - } + public static string ImgTag(string input, string alt = "", string css = "") + { + return input == null ? null : GetImageTag(input, alt, css); + } - private static string GetImageTag(string src, string alt, string css) - { - return String.Format("\"{1}\"", src, alt, css); - } + private static string GetImageTag(string src, string alt, string css) + { + return String.Format("\"{1}\"", src, alt, css); + } - #endregion - } + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs index 004877840d..3f6638c705 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Web.Hosting; using DotLiquid; @@ -7,81 +6,81 @@ namespace SmartStore.Templating.Liquid { - internal class LiquidTemplate : ITemplate - { - public LiquidTemplate(Template template, string source) - { - Guard.NotNull(template, nameof(template)); - Guard.NotNull(source, nameof(source)); + internal class LiquidTemplate : ITemplate + { + public LiquidTemplate(Template template, string source) + { + Guard.NotNull(template, nameof(template)); + Guard.NotNull(source, nameof(source)); - Template = template; - Source = source; - } + Template = template; + Source = source; + } - public string Source - { - get; - internal set; - } + public string Source + { + get; + internal set; + } - public Template Template - { - get; - internal set; - } + public Template Template + { + get; + internal set; + } - public string Render(object model, IFormatProvider formatProvider) - { - Guard.NotNull(model, nameof(model)); - Guard.NotNull(formatProvider, nameof(formatProvider)); - - var p = CreateParameters(model, formatProvider); - return Template.Render(p); - } + public string Render(object model, IFormatProvider formatProvider) + { + Guard.NotNull(model, nameof(model)); + Guard.NotNull(formatProvider, nameof(formatProvider)); - private RenderParameters CreateParameters(object data, IFormatProvider formatProvider) - { - var p = new RenderParameters(formatProvider); + var p = CreateParameters(model, formatProvider); + return Template.Render(p); + } - Hash hash = null; - - if (data is ISafeObject so) - { - if (so.GetWrappedObject() is IDictionary soDict) - { - hash = Hash.FromDictionary(soDict); - } - else - { - data = so.GetWrappedObject(); - } - } + private RenderParameters CreateParameters(object data, IFormatProvider formatProvider) + { + var p = new RenderParameters(formatProvider); - if (hash == null) - { - hash = new Hash(); + Hash hash = null; - if (data is IDictionary dict) - { - foreach (var kvp in dict) - { - hash[kvp.Key] = LiquidUtil.CreateSafeObject(kvp.Value); - } - } - else - { - var props = FastProperty.GetProperties(data.GetType()); - foreach (var prop in props) - { - hash[prop.Key] = LiquidUtil.CreateSafeObject(prop.Value.GetValue(data)); - } - } - } + if (data is ISafeObject so) + { + if (so.GetWrappedObject() is IDictionary soDict) + { + hash = Hash.FromDictionary(soDict); + } + else + { + data = so.GetWrappedObject(); + } + } - p.LocalVariables = hash; - p.ErrorsOutputMode = HostingEnvironment.IsHosted ? ErrorsOutputMode.Display : ErrorsOutputMode.Rethrow; + if (hash == null) + { + hash = new Hash(); - return p; - } - } + if (data is IDictionary dict) + { + foreach (var kvp in dict) + { + hash[kvp.Key] = LiquidUtil.CreateSafeObject(kvp.Value); + } + } + else + { + var props = FastProperty.GetProperties(data.GetType()); + foreach (var prop in props) + { + hash[prop.Key] = LiquidUtil.CreateSafeObject(prop.Value.GetValue(data)); + } + } + } + + p.LocalVariables = hash; + p.ErrorsOutputMode = HostingEnvironment.IsHosted ? ErrorsOutputMode.Display : ErrorsOutputMode.Rethrow; + + return p; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs index a229d69208..d2ee3c46bd 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs @@ -13,157 +13,157 @@ namespace SmartStore.Templating.Liquid { - public partial class LiquidTemplateEngine : ITemplateEngine, ITemplateFileSystem - { - private readonly Work _services; - private readonly Work _localizer; - private readonly Work _themeContext; - private readonly IVirtualPathProvider _vpp; - - public LiquidTemplateEngine( - Work services, - IVirtualPathProvider vpp, - Work themeContext, - Work localizer) - { - _services = services; - _vpp = vpp; - _localizer = localizer; - _themeContext = themeContext; - - // Register Value type transformers - var allowedMoneyProps = new[] - { - nameof(Money.Amount), - nameof(Money.RoundedAmount), - nameof(Money.TruncatedAmount), - nameof(Money.Formatted), - nameof(Money.DecimalDigits) - }; - Template.RegisterSafeType(typeof(Money), allowedMoneyProps, x => x); - - // Register tag "zone" - Template.RegisterTagFactory(new ZoneTagFactory(_services.Value.EventPublisher)); - - // Register Filters - Template.RegisterFilter(typeof(AdditionalFilters)); - - Template.NamingConvention = new CSharpNamingConvention(); - Template.FileSystem = this; - } - - #region Services - - public ICommonServices Services => _services.Value; - - public LocalizedString T(string key, int languageId, params object[] args) - { - return _localizer.Value(key, languageId, args); - } - - #endregion - - #region ITemplateEngine - - public ITemplate Compile(string source) - { - Guard.NotNull(source, nameof(source)); - - return new LiquidTemplate(Template.Parse(source), source); - } - - public string Render(string source, object model, IFormatProvider formatProvider) - { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(model, nameof(model)); - Guard.NotNull(formatProvider, nameof(formatProvider)); - - return Compile(source).Render(model, formatProvider); - } - - public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix) - { - Guard.NotNull(entity, nameof(entity)); - - return new TestDrop(entity, modelPrefix); - } - - #endregion - - #region ITemplateFileSystem - - public Template GetTemplate(Context context, string templateName) - { - var virtualPath = ResolveVirtualPath(context, templateName); - - if (virtualPath.IsEmpty()) - { - return null; - } - - var cacheKey = HttpRuntime.Cache.BuildScopedKey("LiquidPartial://" + virtualPath); - var cachedTemplate = HttpRuntime.Cache.Get(cacheKey); - - if (cachedTemplate == null) - { - // Read from file, compile and put to cache with file dependeny - var source = ReadTemplateFileInternal(virtualPath); - cachedTemplate = Template.Parse(source); - var cacheDependency = _vpp.GetCacheDependency(virtualPath, DateTime.UtcNow); - HttpRuntime.Cache.Insert(cacheKey, cachedTemplate, cacheDependency); - } - - return (Template)cachedTemplate; - } - - public string ReadTemplateFile(Context context, string templateName) - { - var virtualPath = ResolveVirtualPath(context, templateName); - - return ReadTemplateFileInternal(virtualPath); - } - - private string ReadTemplateFileInternal(string virtualPath) - { - if (virtualPath.IsEmpty()) - { - return string.Empty; - } - - if (!_vpp.FileExists(virtualPath)) - { - throw new FileNotFoundException($"Include file '{virtualPath}' does not exist."); - } - - using (var stream = _vpp.OpenFile(virtualPath)) - { - return stream.AsString(); - } - } + public partial class LiquidTemplateEngine : ITemplateEngine, ITemplateFileSystem + { + private readonly Work _services; + private readonly Work _localizer; + private readonly Work _themeContext; + private readonly IVirtualPathProvider _vpp; + + public LiquidTemplateEngine( + Work services, + IVirtualPathProvider vpp, + Work themeContext, + Work localizer) + { + _services = services; + _vpp = vpp; + _localizer = localizer; + _themeContext = themeContext; + + // Register Value type transformers + var allowedMoneyProps = new[] + { + nameof(Money.Amount), + nameof(Money.RoundedAmount), + nameof(Money.TruncatedAmount), + nameof(Money.Formatted), + nameof(Money.DecimalDigits) + }; + Template.RegisterSafeType(typeof(Money), allowedMoneyProps, x => x); + + // Register tag "zone" + Template.RegisterTagFactory(new ZoneTagFactory(_services.Value.EventPublisher)); + + // Register Filters + Template.RegisterFilter(typeof(AdditionalFilters)); + + Template.NamingConvention = new CSharpNamingConvention(); + Template.FileSystem = this; + } + + #region Services + + public ICommonServices Services => _services.Value; + + public LocalizedString T(string key, int languageId, params object[] args) + { + return _localizer.Value(key, languageId, args); + } + + #endregion + + #region ITemplateEngine + + public ITemplate Compile(string source) + { + Guard.NotNull(source, nameof(source)); + + return new LiquidTemplate(Template.Parse(source), source); + } + + public string Render(string source, object model, IFormatProvider formatProvider) + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(model, nameof(model)); + Guard.NotNull(formatProvider, nameof(formatProvider)); + + return Compile(source).Render(model, formatProvider); + } + + public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix) + { + Guard.NotNull(entity, nameof(entity)); + + return new TestDrop(entity, modelPrefix); + } + + #endregion + + #region ITemplateFileSystem + + public Template GetTemplate(Context context, string templateName) + { + var virtualPath = ResolveVirtualPath(context, templateName); + + if (virtualPath.IsEmpty()) + { + return null; + } + + var cacheKey = HttpRuntime.Cache.BuildScopedKey("LiquidPartial://" + virtualPath); + var cachedTemplate = HttpRuntime.Cache.Get(cacheKey); + + if (cachedTemplate == null) + { + // Read from file, compile and put to cache with file dependeny + var source = ReadTemplateFileInternal(virtualPath); + cachedTemplate = Template.Parse(source); + var cacheDependency = _vpp.GetCacheDependency(virtualPath, DateTime.UtcNow); + HttpRuntime.Cache.Insert(cacheKey, cachedTemplate, cacheDependency); + } + + return (Template)cachedTemplate; + } + + public string ReadTemplateFile(Context context, string templateName) + { + var virtualPath = ResolveVirtualPath(context, templateName); + + return ReadTemplateFileInternal(virtualPath); + } + + private string ReadTemplateFileInternal(string virtualPath) + { + if (virtualPath.IsEmpty()) + { + return string.Empty; + } + + if (!_vpp.FileExists(virtualPath)) + { + throw new FileNotFoundException($"Include file '{virtualPath}' does not exist."); + } + + using (var stream = _vpp.OpenFile(virtualPath)) + { + return stream.AsString(); + } + } - private string ResolveVirtualPath(Context context, string templateName) - { - var path = ((string)context[templateName]).NullEmpty() ?? templateName; + private string ResolveVirtualPath(Context context, string templateName) + { + var path = ((string)context[templateName]).NullEmpty() ?? templateName; - if (path.IsEmpty()) - return string.Empty; + if (path.IsEmpty()) + return string.Empty; - path = path.EnsureEndsWith(".liquid"); + path = path.EnsureEndsWith(".liquid"); - string virtualPath = null; + string virtualPath = null; - if (!path.StartsWith("~/")) - { - var currentTheme = _themeContext.Value.CurrentTheme; - virtualPath = _vpp.Combine(currentTheme.Location, currentTheme.ThemeName, "Views/Shared/EmailTemplates", path); - } - else - { - virtualPath = VirtualPathUtility.ToAppRelative(path); - } - return virtualPath; - } - - #endregion - } + if (!path.StartsWith("~/")) + { + var currentTheme = _themeContext.Value.CurrentTheme; + virtualPath = _vpp.Combine(currentTheme.Location, currentTheme.ThemeName, "Views/Shared/EmailTemplates", path); + } + else + { + virtualPath = VirtualPathUtility.ToAppRelative(path); + } + return virtualPath; + } + + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs index 9363346814..5937ea4fc6 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs @@ -4,58 +4,58 @@ namespace SmartStore.Templating.Liquid { - internal static class LiquidUtil - { - private static readonly IDictionary> _typeWrapperCache - = new Dictionary>(); - - internal static object CreateSafeObject(object value) - { - if (value == null) - { - return null; - } - - if (value is TestDrop || value is IFormattable) - { - return value; - } - - var valueType = value.GetType(); - - if (!_typeWrapperCache.TryGetValue(valueType, out var fn)) - { - if (value is IDictionary dict) - { - fn = x => new DictionaryDrop((IDictionary)x); - } - else if (valueType.IsSequenceType()) - { - var genericArgs = valueType.GetGenericArguments(); - var isEnumerable = genericArgs.Length == 1 && valueType.IsSubClass(typeof(IEnumerable<>)); - if (isEnumerable) - { - var seqType = genericArgs[0]; - if (!IsSafeType(seqType)) - { - fn = x => new EnumerableWrapper((IEnumerable)x); - } - } - } - else if (valueType.IsPlainObjectType()) - { - fn = x => new ObjectDrop(x); - } - - _typeWrapperCache[valueType] = fn; - } - - return fn?.Invoke(value) ?? value; - } - - public static bool IsSafeType(Type type) - { - return type.IsPredefinedType(); - } - } + internal static class LiquidUtil + { + private static readonly IDictionary> _typeWrapperCache + = new Dictionary>(); + + internal static object CreateSafeObject(object value) + { + if (value == null) + { + return null; + } + + if (value is TestDrop || value is IFormattable) + { + return value; + } + + var valueType = value.GetType(); + + if (!_typeWrapperCache.TryGetValue(valueType, out var fn)) + { + if (value is IDictionary dict) + { + fn = x => new DictionaryDrop((IDictionary)x); + } + else if (valueType.IsSequenceType()) + { + var genericArgs = valueType.GetGenericArguments(); + var isEnumerable = genericArgs.Length == 1 && valueType.IsSubClass(typeof(IEnumerable<>)); + if (isEnumerable) + { + var seqType = genericArgs[0]; + if (!IsSafeType(seqType)) + { + fn = x => new EnumerableWrapper((IEnumerable)x); + } + } + } + else if (valueType.IsPlainObjectType()) + { + fn = x => new ObjectDrop(x); + } + + _typeWrapperCache[valueType] = fn; + } + + return fn?.Invoke(value) ?? value; + } + + public static bool IsSafeType(Type type) + { + return type.IsPredefinedType(); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs index ed1113cf1c..4e9fb8037e 100644 --- a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -10,76 +9,76 @@ namespace SmartStore.Templating.Liquid { - internal sealed class ZoneTagFactory : ITagFactory - { - private readonly IEventPublisher _eventPublisher; - - public ZoneTagFactory(IEventPublisher eventPublisher) - { - _eventPublisher = eventPublisher; - } - - public string TagName => "zone"; - - public Tag Create() - { - return new Zone { EventPublisher = _eventPublisher }; - } - - class Zone : Tag - { - private static readonly Regex Syntax = R.B(@"^({0})", DotLiquid.Liquid.QuotedFragment); - - private string _zoneName; - - public IEventPublisher EventPublisher { get; set; } - - public override void Initialize(string tagName, string markup, List tokens) - { - Match syntaxMatch = Syntax.Match(markup); - - if (syntaxMatch.Success) - { - _zoneName = syntaxMatch.Groups[1].Value; - } - else - { - throw new SyntaxException("Syntax Error in 'zone' tag - Valid syntax: zone '[ZoneName]'."); - } - - base.Initialize(tagName, markup, tokens); - } - - public override void Render(Context context, TextWriter result) - { - var zoneName = (string)context[_zoneName] ?? _zoneName; - - if (zoneName.IsEmpty()) - return; - - var model = context.Environments.First(); - var evt = new ZoneRenderingEvent(zoneName, model); - - evt.LiquidContext = context; - - EventPublisher.Publish(evt); - - if (evt.Snippets != null && evt.Snippets.Count > 0) - { - foreach (var snippet in evt.Snippets) - { - if (snippet.Parse) - { - Template.Parse(snippet.Content) - .Render(result, new RenderParameters(context.FormatProvider) { LocalVariables = model }); - } - else - { - result.Write(snippet.Content); - } - } - } - } - } - } + internal sealed class ZoneTagFactory : ITagFactory + { + private readonly IEventPublisher _eventPublisher; + + public ZoneTagFactory(IEventPublisher eventPublisher) + { + _eventPublisher = eventPublisher; + } + + public string TagName => "zone"; + + public Tag Create() + { + return new Zone { EventPublisher = _eventPublisher }; + } + + class Zone : Tag + { + private static readonly Regex Syntax = R.B(@"^({0})", DotLiquid.Liquid.QuotedFragment); + + private string _zoneName; + + public IEventPublisher EventPublisher { get; set; } + + public override void Initialize(string tagName, string markup, List tokens) + { + Match syntaxMatch = Syntax.Match(markup); + + if (syntaxMatch.Success) + { + _zoneName = syntaxMatch.Groups[1].Value; + } + else + { + throw new SyntaxException("Syntax Error in 'zone' tag - Valid syntax: zone '[ZoneName]'."); + } + + base.Initialize(tagName, markup, tokens); + } + + public override void Render(Context context, TextWriter result) + { + var zoneName = (string)context[_zoneName] ?? _zoneName; + + if (zoneName.IsEmpty()) + return; + + var model = context.Environments.First(); + var evt = new ZoneRenderingEvent(zoneName, model); + + evt.LiquidContext = context; + + EventPublisher.Publish(evt); + + if (evt.Snippets != null && evt.Snippets.Count > 0) + { + foreach (var snippet in evt.Snippets) + { + if (snippet.Parse) + { + Template.Parse(snippet.Content) + .Render(result, new RenderParameters(context.FormatProvider) { LocalVariables = model }); + } + else + { + result.Write(snippet.Content); + } + } + } + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs index 5698f20851..59d32b7132 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs @@ -4,53 +4,53 @@ namespace SmartStore.Web.Framework.Theming { - /// - /// Instructs the view engine to additionally search in the admin area for views. - /// - /// - /// The "admin area" corresponds to the ~/Administration base folder. - /// This attribute is useful in plugins - which usually are areas on its own - where views - /// should be rendered as part of the admin backend. - /// Without this attribute the view resolver would directly fallback to the default nameless area - /// when a view could not be resolved from within the plugin area. - /// - public class AdminThemedAttribute : FilterAttribute, IResultFilter - { - public readonly static string[] ExtraAreaViewLocations = new string[] - { - "~/Administration/Views/{1}/{0}", - "~/Administration/Views/Shared/{0}" - }; + /// + /// Instructs the view engine to additionally search in the admin area for views. + /// + /// + /// The "admin area" corresponds to the ~/Administration base folder. + /// This attribute is useful in plugins - which usually are areas on its own - where views + /// should be rendered as part of the admin backend. + /// Without this attribute the view resolver would directly fallback to the default nameless area + /// when a view could not be resolved from within the plugin area. + /// + public class AdminThemedAttribute : FilterAttribute, IResultFilter + { + public readonly static string[] ExtraAreaViewLocations = new string[] + { + "~/Administration/Views/{1}/{0}", + "~/Administration/Views/Shared/{0}" + }; - public Lazy WorkContext { get; set; } + public Lazy WorkContext { get; set; } - public virtual void OnResultExecuting(ResultExecutingContext filterContext) - { - if (filterContext?.Result == null) - return; + public virtual void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext?.Result == null) + return; - if (!filterContext.Result.IsHtmlViewResult()) - return; + if (!filterContext.Result.IsHtmlViewResult()) + return; - if (filterContext.HttpContext.Response.StatusCode < 300) - { - var isNonAdmin = filterContext.HttpContext.GetItem("IsNonAdmin", forceCreation: false); - if(!isNonAdmin) - { - // Add extra view location formats to all view results (even the partial ones) - // {0} is appended by view engine - filterContext.RouteData.DataTokens["ExtraAreaViewLocations"] = ExtraAreaViewLocations; + if (filterContext.HttpContext.Response.StatusCode < 300) + { + var isNonAdmin = filterContext.HttpContext.GetItem("IsNonAdmin", forceCreation: false); + if (!isNonAdmin) + { + // Add extra view location formats to all view results (even the partial ones) + // {0} is appended by view engine + filterContext.RouteData.DataTokens["ExtraAreaViewLocations"] = ExtraAreaViewLocations; - if (!filterContext.IsChildAction) - { - WorkContext.Value.IsAdmin = true; - } - } - } - } + if (!filterContext.IsChildAction) + { + WorkContext.Value.IsAdmin = true; + } + } + } + } - public virtual void OnResultExecuted(ResultExecutedContext filterContext) - { - } - } + public virtual void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs index 62bf5fd53e..a77a3cee23 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using BundleTransformer.Core.Assets; using BundleTransformer.Core.Constants; @@ -11,144 +10,138 @@ namespace SmartStore.Web.Framework.Theming.Assets { - public abstract class AssetTranslator : ITranslator - where TTranslator : ITranslator, new() - { - private readonly ITranslator _inner; - - public AssetTranslator() - { - _inner = new TTranslator(); - } - - protected abstract string[] ValidTypeCodes { get; } - - private IAssetCache AssetCache - { - get { return EngineContext.Current.Resolve(); } - } - - public bool IsDebugMode - { - get { return _inner.IsDebugMode; } - set { _inner.IsDebugMode = value; } - } - - public IList Translate(IList assets) - { - var assets2 = WalkAndFindCachedAssets(assets).ToList(); - var result = _inner.Translate(assets2); - return result; - } - - private IEnumerable WalkAndFindCachedAssets(IList assets) - { - foreach (var asset in assets) - { - if (ValidTypeCodes.Contains(asset.AssetTypeCode.ToLowerInvariant())) - { - yield return TranslateInternal(asset); - } - else - { - yield return asset; - } - } - } - - public IAsset Translate(IAsset asset) - { - return TranslateInternal(asset); - } - - private IAsset TranslateInternal(IAsset asset) - { - IAsset result; - var chronometer = EngineContext.Current.Resolve(); - - using (chronometer.Step("Translate asset {0}".FormatInvariant(asset.VirtualPath))) - { - bool validationMode = ThemeHelper.IsStyleValidationRequest(); - - if (validationMode || !TryGetCachedAsset(asset, out result)) - { - using (KeyedLock.Lock("CachedAsset:" + asset.VirtualPath)) - { - if (validationMode || !TryGetCachedAsset(asset, out result)) - { - using (chronometer.Step("Compile asset {0}".FormatInvariant(asset.VirtualPath))) - { - result = _inner.Translate(asset); - - var cachedAsset = new CachedAsset - { - AssetTypeCode = AssetTypeCode.Css, - IsStylesheet = true, - Minified = result.Minified, - Combined = result.Combined, - Content = result.Content, - OriginalAssets = asset.OriginalAssets, - VirtualPath = asset.VirtualPath, - VirtualPathDependencies = result.VirtualPathDependencies, - Url = asset.Url - }; - - result = AssetTranslorUtil.PostProcessAsset(cachedAsset, this.IsDebugMode); - - if (!validationMode) - { - var pCodes = new List(3); - if (cachedAsset.Minified) pCodes.Add(DefaultAssetCache.MinificationCode); - if (cachedAsset.RelativePathsResolved) pCodes.Add(DefaultAssetCache.UrlRewriteCode); - if (cachedAsset.Autoprefixed) pCodes.Add(DefaultAssetCache.AutoprefixCode); - - AssetCache.InsertAsset( - cachedAsset.VirtualPath, - cachedAsset.VirtualPathDependencies, - cachedAsset.Content, - pCodes.ToArray()); - } - } - } - } - } - } - - return result; - } - - private bool TryGetCachedAsset(IAsset asset, out IAsset cachedAsset) - { - cachedAsset = null; - var entry = AssetCache.GetAsset(asset.VirtualPath); - - if (entry != null) - { - cachedAsset = new CachedAsset - { - AssetTypeCode = AssetTypeCode.Css, - Combined = true, - Content = entry.Content, - IsStylesheet = true, - Minified = entry.ProcessorCodes.Contains(DefaultAssetCache.MinificationCode), - RelativePathsResolved = entry.ProcessorCodes.Contains(DefaultAssetCache.UrlRewriteCode), - Autoprefixed = entry.ProcessorCodes.Contains(DefaultAssetCache.AutoprefixCode), - OriginalAssets = new List(), - VirtualPath = asset.VirtualPath, - VirtualPathDependencies = entry.VirtualPathDependencies.ToList(), - Url = asset.Url - }; - } - - return cachedAsset != null; - } - } - - public sealed class SassTranslator : AssetTranslator - { - protected override string[] ValidTypeCodes - { - get { return new[] { "sass", "scss" }; } - } - } + public abstract class AssetTranslator : ITranslator + where TTranslator : ITranslator, new() + { + private readonly ITranslator _inner; + + public AssetTranslator() + { + _inner = new TTranslator(); + } + + protected abstract string[] ValidTypeCodes { get; } + + private IAssetCache AssetCache => EngineContext.Current.Resolve(); + + public bool IsDebugMode + { + get => _inner.IsDebugMode; + set => _inner.IsDebugMode = value; + } + + public IList Translate(IList assets) + { + var assets2 = WalkAndFindCachedAssets(assets).ToList(); + var result = _inner.Translate(assets2); + return result; + } + + private IEnumerable WalkAndFindCachedAssets(IList assets) + { + foreach (var asset in assets) + { + if (ValidTypeCodes.Contains(asset.AssetTypeCode.ToLowerInvariant())) + { + yield return TranslateInternal(asset); + } + else + { + yield return asset; + } + } + } + + public IAsset Translate(IAsset asset) + { + return TranslateInternal(asset); + } + + private IAsset TranslateInternal(IAsset asset) + { + IAsset result; + var chronometer = EngineContext.Current.Resolve(); + + using (chronometer.Step("Translate asset {0}".FormatInvariant(asset.VirtualPath))) + { + bool validationMode = ThemeHelper.IsStyleValidationRequest(); + + if (validationMode || !TryGetCachedAsset(asset, out result)) + { + using (KeyedLock.Lock("CachedAsset:" + asset.VirtualPath)) + { + if (validationMode || !TryGetCachedAsset(asset, out result)) + { + using (chronometer.Step("Compile asset {0}".FormatInvariant(asset.VirtualPath))) + { + result = _inner.Translate(asset); + + var cachedAsset = new CachedAsset + { + AssetTypeCode = AssetTypeCode.Css, + IsStylesheet = true, + Minified = result.Minified, + Combined = result.Combined, + Content = result.Content, + OriginalAssets = asset.OriginalAssets, + VirtualPath = asset.VirtualPath, + VirtualPathDependencies = result.VirtualPathDependencies, + Url = asset.Url + }; + + result = AssetTranslorUtil.PostProcessAsset(cachedAsset, this.IsDebugMode); + + if (!validationMode) + { + var pCodes = new List(3); + if (cachedAsset.Minified) pCodes.Add(DefaultAssetCache.MinificationCode); + if (cachedAsset.RelativePathsResolved) pCodes.Add(DefaultAssetCache.UrlRewriteCode); + if (cachedAsset.Autoprefixed) pCodes.Add(DefaultAssetCache.AutoprefixCode); + + AssetCache.InsertAsset( + cachedAsset.VirtualPath, + cachedAsset.VirtualPathDependencies, + cachedAsset.Content, + pCodes.ToArray()); + } + } + } + } + } + } + + return result; + } + + private bool TryGetCachedAsset(IAsset asset, out IAsset cachedAsset) + { + cachedAsset = null; + var entry = AssetCache.GetAsset(asset.VirtualPath); + + if (entry != null) + { + cachedAsset = new CachedAsset + { + AssetTypeCode = AssetTypeCode.Css, + Combined = true, + Content = entry.Content, + IsStylesheet = true, + Minified = entry.ProcessorCodes.Contains(DefaultAssetCache.MinificationCode), + RelativePathsResolved = entry.ProcessorCodes.Contains(DefaultAssetCache.UrlRewriteCode), + Autoprefixed = entry.ProcessorCodes.Contains(DefaultAssetCache.AutoprefixCode), + OriginalAssets = new List(), + VirtualPath = asset.VirtualPath, + VirtualPathDependencies = entry.VirtualPathDependencies.ToList(), + Url = asset.Url + }; + } + + return cachedAsset != null; + } + } + + public sealed class SassTranslator : AssetTranslator + { + protected override string[] ValidTypeCodes => new[] { "sass", "scss" }; + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslorUtil.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslorUtil.cs index fc7d0b2c2f..834154cee3 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslorUtil.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslorUtil.cs @@ -1,26 +1,25 @@ -using System; -using System.Linq; +using System.Linq; using BundleTransformer.Core; using BundleTransformer.Core.Assets; using BundleTransformer.Core.Transformers; namespace SmartStore.Web.Framework.Theming.Assets { - internal static class AssetTranslorUtil - { - internal static IAsset PostProcessAsset(IAsset asset, bool isDebugMode) - { - var transformer = BundleTransformerContext.Current.Styles.GetDefaultTransformInstance() as ITransformer; - if (transformer != null) - { - var processors = transformer.PostProcessors.Where(x => x.UseInDebugMode || !isDebugMode); - foreach (var processor in processors) - { - asset = processor.PostProcess(asset); - } - } + internal static class AssetTranslorUtil + { + internal static IAsset PostProcessAsset(IAsset asset, bool isDebugMode) + { + var transformer = BundleTransformerContext.Current.Styles.GetDefaultTransformInstance() as ITransformer; + if (transformer != null) + { + var processors = transformer.PostProcessors.Where(x => x.UseInDebugMode || !isDebugMode); + foreach (var processor in processors) + { + asset = processor.PostProcess(asset); + } + } - return asset; - } - } + return asset; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AutoPrefixPostProcessor.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AutoPrefixPostProcessor.cs index 254d8a6bfd..694cbcd37a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AutoPrefixPostProcessor.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AutoPrefixPostProcessor.cs @@ -1,71 +1,70 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using BundleTransformer.Core.Assets; using BundleTransformer.Core.PostProcessors; namespace SmartStore.Web.Framework.Theming.Assets { - public class AutoprefixPostProcessor : IPostProcessor - { - private readonly BundleTransformer.Autoprefixer.PostProcessors.AutoprefixCssPostProcessor _inner; + public class AutoprefixPostProcessor : IPostProcessor + { + private readonly BundleTransformer.Autoprefixer.PostProcessors.AutoprefixCssPostProcessor _inner; - public AutoprefixPostProcessor() - { - _inner = new BundleTransformer.Autoprefixer.PostProcessors.AutoprefixCssPostProcessor(); - } + public AutoprefixPostProcessor() + { + _inner = new BundleTransformer.Autoprefixer.PostProcessors.AutoprefixCssPostProcessor(); + } - public bool UseInDebugMode - { - get { return _inner.UseInDebugMode; } - set { _inner.UseInDebugMode = value; } - } + public bool UseInDebugMode + { + get => _inner.UseInDebugMode; + set => _inner.UseInDebugMode = value; + } - public IList PostProcess(IList assets) - { - Guard.NotNull(assets, nameof(assets)); + public IList PostProcess(IList assets) + { + Guard.NotNull(assets, nameof(assets)); - var list = assets.Where(CanProcess).ToList(); + var list = assets.Where(CanProcess).ToList(); - if (list.Count == 0) - { - return assets; - } + if (list.Count == 0) + { + return assets; + } - // Process only unprocessed assets... - _inner.PostProcess(list); + // Process only unprocessed assets... + _inner.PostProcess(list); - // Set Autoprefixed flag to true in our custom cached assets - list.OfType().Each(x => x.Autoprefixed = true); + // Set Autoprefixed flag to true in our custom cached assets + list.OfType().Each(x => x.Autoprefixed = true); - // ...but return the original list - return assets; - } + // ...but return the original list + return assets; + } - public IAsset PostProcess(IAsset asset) - { - Guard.NotNull(asset, nameof(asset)); + public IAsset PostProcess(IAsset asset) + { + Guard.NotNull(asset, nameof(asset)); - var cachedAsset = asset as CachedAsset; - if (cachedAsset == null || cachedAsset.Autoprefixed == false) - { - asset = _inner.PostProcess(asset); + var cachedAsset = asset as CachedAsset; + if (cachedAsset == null || cachedAsset.Autoprefixed == false) + { + asset = _inner.PostProcess(asset); - if (cachedAsset != null) - { - cachedAsset.Autoprefixed = true; - } + if (cachedAsset != null) + { + cachedAsset.Autoprefixed = true; + } - return asset; - } + return asset; + } - return asset; - } + return asset; + } - private static bool CanProcess(IAsset asset) - { - var cachedAsset = asset as CachedAsset; - return asset.IsStylesheet && (cachedAsset == null || cachedAsset.Autoprefixed == false); - } - } + private static bool CanProcess(IAsset asset) + { + var cachedAsset = asset as CachedAsset; + return asset.IsStylesheet && (cachedAsset == null || cachedAsset.Autoprefixed == false); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs index edb20c5c8b..9c9b79918f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs @@ -1,205 +1,205 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; using System.Web.Caching; using System.Web.Hosting; using SmartStore.Core.Infrastructure; namespace SmartStore.Web.Framework.Theming.Assets { - public sealed class BundlingVirtualPathProvider : ThemingVirtualPathProvider - { - private readonly SassCheckedPathStack _sassCheckedPathStack; + public sealed class BundlingVirtualPathProvider : ThemingVirtualPathProvider + { + private readonly SassCheckedPathStack _sassCheckedPathStack; - public BundlingVirtualPathProvider(VirtualPathProvider previous) - : base(previous) + public BundlingVirtualPathProvider(VirtualPathProvider previous) + : base(previous) { - _sassCheckedPathStack = new SassCheckedPathStack(); - } + _sassCheckedPathStack = new SassCheckedPathStack(); + } public override bool FileExists(string virtualPath) { - var exists = false; - var styleResult = ThemeHelper.IsStyleSheet(virtualPath); - - TokenizedSassPath sassPath = null; - - if (styleResult != null) - { - exists = _sassCheckedPathStack.Check(styleResult, out sassPath); - if (exists) - { - // It seems awkward to return false when check result actually says true. - // But true in this context means: a check for THIS sass file pattern (e.g. _slick.scss.sass) - // yielded true previously (because _slick.scss really exists on disks), therefore _slick.scss.sass - // CANNOT exist (or SHOULD not by convention). By returning false we prevent that a real filesystem check - // is made against _slick.scss.sass (which is huge performace saver considering that IThemeFileResolver - // also does some additional checks). - return false; - } - - if (styleResult.IsThemeVars || styleResult.IsModuleImports) - { - _sassCheckedPathStack.PushExistingPath(sassPath); - return true; - } - } - - exists = base.FileExists(virtualPath); - - if (exists && sassPath != null) - { - _sassCheckedPathStack.PushExistingPath(sassPath); - } - - return exists; + var exists = false; + var styleResult = ThemeHelper.IsStyleSheet(virtualPath); + + TokenizedSassPath sassPath = null; + + if (styleResult != null) + { + exists = _sassCheckedPathStack.Check(styleResult, out sassPath); + if (exists) + { + // It seems awkward to return false when check result actually says true. + // But true in this context means: a check for THIS sass file pattern (e.g. _slick.scss.sass) + // yielded true previously (because _slick.scss really exists on disks), therefore _slick.scss.sass + // CANNOT exist (or SHOULD not by convention). By returning false we prevent that a real filesystem check + // is made against _slick.scss.sass (which is huge performace saver considering that IThemeFileResolver + // also does some additional checks). + return false; + } + + if (styleResult.IsThemeVars || styleResult.IsModuleImports) + { + _sassCheckedPathStack.PushExistingPath(sassPath); + return true; + } + } + + exists = base.FileExists(virtualPath); + + if (exists && sassPath != null) + { + _sassCheckedPathStack.PushExistingPath(sassPath); + } + + return exists; } - + public override VirtualFile GetFile(string virtualPath) { - var styleResult = ThemeHelper.IsStyleSheet(virtualPath); - if (styleResult != null) - { - if (styleResult.IsThemeVars) - { - var theme = ThemeHelper.ResolveCurrentTheme(); - int storeId = ThemeHelper.ResolveCurrentStoreId(); - return new ThemeVarsVirtualFile(virtualPath, styleResult.Extension, theme.ThemeName, storeId); - } - else if (styleResult.IsModuleImports) - { - return new ModuleImportsVirtualFile(virtualPath, ThemeHelper.IsAdminArea()); - } - } - - return base.GetFile(virtualPath); + var styleResult = ThemeHelper.IsStyleSheet(virtualPath); + if (styleResult != null) + { + if (styleResult.IsThemeVars) + { + var theme = ThemeHelper.ResolveCurrentTheme(); + int storeId = ThemeHelper.ResolveCurrentStoreId(); + return new ThemeVarsVirtualFile(virtualPath, styleResult.Extension, theme.ThemeName, storeId); + } + else if (styleResult.IsModuleImports) + { + return new ModuleImportsVirtualFile(virtualPath, ThemeHelper.IsAdminArea()); + } + } + + return base.GetFile(virtualPath); + } + + public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) + { + if (ThemeHelper.IsStyleValidationRequest()) + { + var themeVarsPath = virtualPathDependencies.Cast().FirstOrDefault(x => ThemeHelper.PathIsThemeVars(x)); + if (themeVarsPath.HasValue()) + { + return base.GetCacheDependency(virtualPath, new[] { themeVarsPath }, utcStart); + } + } + + return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } + } + + internal class SassCheckedPathStack + { + private readonly string[] _styleExtensions = new[] { ".scss", ".sass", ".css" }; - public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) + private readonly ContextState> _state + = new ContextState>("SassCheckedPathStack.State", () => new Stack()); + + public SassCheckedPathStack() { - if (ThemeHelper.IsStyleValidationRequest()) - { - var themeVarsPath = virtualPathDependencies.Cast().FirstOrDefault(x => ThemeHelper.PathIsThemeVars(x)); - if (themeVarsPath.HasValue()) - { - return base.GetCacheDependency(virtualPath, new[] { themeVarsPath }, utcStart); - } - } - - return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } - } - - internal class SassCheckedPathStack - { - private readonly string[] _styleExtensions = new[] { ".scss", ".sass", ".css" }; - - private readonly ContextState> _state - = new ContextState>("SassCheckedPathStack.State", () => new Stack()); - - public SassCheckedPathStack() - { - } - - /// - /// Checks last path existence - /// - /// true = does exist, no need to check | false = not checked yet - public bool Check(StyleSheetResult styleResult, out TokenizedSassPath path) - { - path = new TokenizedSassPath(styleResult); - - var state = _state.GetState(); - if (state.Count == 0) - return false; - - var currentPath = path; - var lastPath = _state.GetState().Peek(); - - if (currentPath.Extension.IsEmpty()) - { - // We dont't allow extension-less Sass files, so no need to check. - return true; - } - - if (lastPath.Dir != currentPath.Dir) - { - return false; - } - - if (currentPath.StyleResult.IsBaseImport && lastPath.StyleResult.IsBaseImport && currentPath.FileName == lastPath.FileName) - { - return true; - } - - if (currentPath.StyleResult.IsModuleImports && lastPath.StyleResult.IsModuleImports) - { - return true; - } - - if (currentPath.StyleResult.IsThemeVars && lastPath.StyleResult.IsThemeVars) - { - return true; - } - - // slick.scss.(scss|sass|css) > slick.scss - if (Path.GetExtension(currentPath.FileNameWithoutExtension) == ".scss") - { - return true; - } - - // slick.(sass|css) > slick.scss - if (!currentPath.StyleResult.IsBaseImport && _styleExtensions.Contains(currentPath.Extension) && currentPath.FileNameWithoutExtension == lastPath.FileNameWithoutExtension) - { - return true; - } - - // _slick.scss > slick.scss - if (currentPath.FileName.StartsWith("_")) - { - if (currentPath.FileName.Substring(1) == lastPath.FileName) - { - return true; - } - } - - // slick.(scss|sass|css) > _slick.scss - if (lastPath.FileNameWithoutExtension.StartsWith("_")) - { - if (lastPath.FileNameWithoutExtension == "_" + currentPath.FileNameWithoutExtension) - { - return true; - } - } - - return false; - } - - public void PushExistingPath(TokenizedSassPath path) - { - _state.GetState().Push(path); - } - } - - internal class TokenizedSassPath - { - public TokenizedSassPath(StyleSheetResult styleResult) - { - StyleResult = styleResult; - VirtualPath = styleResult.Path; - Extension = styleResult.Extension.EmptyNull(); - FileName = Path.GetFileName(styleResult.Path); - FileNameWithoutExtension = FileName.Substring(0, FileName.Length - Extension.Length); - Dir = VirtualPath.Substring(0, VirtualPath.Length - FileName.Length); - } - - public StyleSheetResult StyleResult { get; set; } - public string VirtualPath { get; private set; } - public string Dir { get; private set; } - public string FileName { get; private set; } - public string FileNameWithoutExtension { get; private set; } - public string Extension { get; private set; } - } + + /// + /// Checks last path existence + /// + /// true = does exist, no need to check | false = not checked yet + public bool Check(StyleSheetResult styleResult, out TokenizedSassPath path) + { + path = new TokenizedSassPath(styleResult); + + var state = _state.GetState(); + if (state.Count == 0) + return false; + + var currentPath = path; + var lastPath = _state.GetState().Peek(); + + if (currentPath.Extension.IsEmpty()) + { + // We dont't allow extension-less Sass files, so no need to check. + return true; + } + + if (lastPath.Dir != currentPath.Dir) + { + return false; + } + + if (currentPath.StyleResult.IsBaseImport && lastPath.StyleResult.IsBaseImport && currentPath.FileName == lastPath.FileName) + { + return true; + } + + if (currentPath.StyleResult.IsModuleImports && lastPath.StyleResult.IsModuleImports) + { + return true; + } + + if (currentPath.StyleResult.IsThemeVars && lastPath.StyleResult.IsThemeVars) + { + return true; + } + + // slick.scss.(scss|sass|css) > slick.scss + if (Path.GetExtension(currentPath.FileNameWithoutExtension) == ".scss") + { + return true; + } + + // slick.(sass|css) > slick.scss + if (!currentPath.StyleResult.IsBaseImport && _styleExtensions.Contains(currentPath.Extension) && currentPath.FileNameWithoutExtension == lastPath.FileNameWithoutExtension) + { + return true; + } + + // _slick.scss > slick.scss + if (currentPath.FileName.StartsWith("_")) + { + if (currentPath.FileName.Substring(1) == lastPath.FileName) + { + return true; + } + } + + // slick.(scss|sass|css) > _slick.scss + if (lastPath.FileNameWithoutExtension.StartsWith("_")) + { + if (lastPath.FileNameWithoutExtension == "_" + currentPath.FileNameWithoutExtension) + { + return true; + } + } + + return false; + } + + public void PushExistingPath(TokenizedSassPath path) + { + _state.GetState().Push(path); + } + } + + internal class TokenizedSassPath + { + public TokenizedSassPath(StyleSheetResult styleResult) + { + StyleResult = styleResult; + VirtualPath = styleResult.Path; + Extension = styleResult.Extension.EmptyNull(); + FileName = Path.GetFileName(styleResult.Path); + FileNameWithoutExtension = FileName.Substring(0, FileName.Length - Extension.Length); + Dir = VirtualPath.Substring(0, VirtualPath.Length - FileName.Length); + } + + public StyleSheetResult StyleResult { get; set; } + public string VirtualPath { get; private set; } + public string Dir { get; private set; } + public string FileName { get; private set; } + public string FileNameWithoutExtension { get; private set; } + public string Extension { get; private set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/CachedAsset.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/CachedAsset.cs index 6844f235cc..d4f43fe25b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/CachedAsset.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/CachedAsset.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using BundleTransformer.Core.Assets; namespace SmartStore.Web.Framework.Theming.Assets { - internal class CachedAsset : IAsset - { - public string AssetTypeCode { get; set; } - public bool Combined { get; set; } - public string Content { get; set; } - public bool IsScript { get; set; } - public bool IsStylesheet { get; set; } - public bool Minified { get; set; } - public bool RelativePathsResolved { get; set; } - public bool Autoprefixed { get; set; } - public IList OriginalAssets { get; set; } - public string Url { get; set; } - public string VirtualPath { get; set; } - public IList VirtualPathDependencies { get; set; } - } + internal class CachedAsset : IAsset + { + public string AssetTypeCode { get; set; } + public bool Combined { get; set; } + public string Content { get; set; } + public bool IsScript { get; set; } + public bool IsStylesheet { get; set; } + public bool Minified { get; set; } + public bool RelativePathsResolved { get; set; } + public bool Autoprefixed { get; set; } + public IList OriginalAssets { get; set; } + public string Url { get; set; } + public string VirtualPath { get; set; } + public IList VirtualPathDependencies { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs index f1eaf270da..c86420202b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs @@ -17,421 +17,421 @@ namespace SmartStore.Web.Framework.Theming.Assets { - public class DefaultAssetCache : IAssetCache - { - public const string MinificationCode = "min"; - public const string AutoprefixCode = "autoprefix"; - public const string UrlRewriteCode = "urlrewrite"; - - public static readonly IAssetCache Null = new NullCache(); - const string CacheKeyPrefix = "sm:AssetCacheEntry:"; - - private readonly bool _isEnabled; - private readonly ThemeSettings _themeSettings; - private readonly IApplicationEnvironment _env; - private readonly IThemeContext _themeContext; - private readonly IThemeRegistry _themeRegistry; - private readonly IThemeFileResolver _themeFileResolver; - private readonly ICommonServices _services; - - private VirtualFolder _cacheFolder; - - public DefaultAssetCache( - ThemeSettings themeSettings, - IApplicationEnvironment env, - IThemeFileResolver themeFileResolver, - IThemeContext themeContext, - IThemeRegistry themeRegistry, - ICommonServices services) - { - _themeSettings = themeSettings; - _isEnabled = _themeSettings.AssetCachingEnabled == 2 || (_themeSettings.AssetCachingEnabled == 0 && !HttpContext.Current.IsDebuggingEnabled); - - _env = env; - _themeContext = themeContext; - _themeFileResolver = themeFileResolver; - _themeRegistry = themeRegistry; - _services = services; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - public IVirtualFolder CacheFolder - { - get - { - if (_cacheFolder == null) - { - _cacheFolder = new VirtualFolder( - _env.TenantFolder.GetVirtualPath("AssetCache"), - _env.TenantFolder.VirtualPathProvider, - Logger); - } - - return _cacheFolder; - } - } - - public CachedAssetEntry GetAsset(string virtualPath) - { - if (!_isEnabled) - return null; - - Guard.NotEmpty(virtualPath, nameof(virtualPath)); - - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); - - if (CacheFolder.DirectoryExists(cacheDirectoryName)) - { - try - { - var deps = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.dependencies")); - var hash = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.hash")); - - if (!TryValidate(virtualPath, deps, hash, out IEnumerable parsedDeps, out string currentHash)) - { - Logger.DebugFormat("Invalidating cached asset for '{0}' because it is not valid anymore.", virtualPath); - InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); - return null; - } - - // TODO: determine correct file extension - var content = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.css")); - if (content == null) - { - using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) - { - InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); - return null; - } - } - - var codes = ParseFileContent(CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.pcodes"))); - - var entry = new CachedAssetEntry - { - Content = content, - HashCode = currentHash, - OriginalVirtualPath = virtualPath, - VirtualPathDependencies = parsedDeps, - PhysicalPath = CacheFolder.MapPath(cacheDirectoryName), - ThemeName = themeName, - StoreId = storeId, - ProcessorCodes = codes.ToArray() - }; - - SetupEvictionObserver(entry); - - Logger.DebugFormat("Succesfully read asset '{0}' from cache.", virtualPath); - - return entry; - } - catch (Exception ex) - { - Logger.ErrorFormat(ex, "Error while resolving asset '{0}' from the asset cache.", virtualPath); - } - } - - return null; - } - - public CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virtualPathDependencies, string content, params string[] processorCodes) - { - if (!_isEnabled) - return null; - - Guard.NotEmpty(virtualPath, nameof(virtualPath)); - Guard.NotEmpty(content, nameof(content)); - - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); - - using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) - { - CacheFolder.CreateDirectory(cacheDirectoryName); - - try - { - // Save main content file - // TODO: determine correct file extension - CreateFileFromEntries(cacheDirectoryName, "asset.css", new[] { content }); - - // Save dependencies file - var deps = ResolveVirtualPathDependencies(virtualPath, virtualPathDependencies, themeName); - CreateFileFromEntries(cacheDirectoryName, "asset.dependencies", deps); - - // Save hash file - var currentHash = BundleTable.VirtualPathProvider.GetFileHash(virtualPath, deps); - CreateFileFromEntries(cacheDirectoryName, "asset.hash", new[] { currentHash }); - - // Save codes file - CreateFileFromEntries(cacheDirectoryName, "asset.pcodes", processorCodes); - - var entry = new CachedAssetEntry - { - Content = content, - HashCode = currentHash, - OriginalVirtualPath = virtualPath, - VirtualPathDependencies = deps, - PhysicalPath = CacheFolder.MapPath(cacheDirectoryName), - ThemeName = themeName, - StoreId = storeId, - ProcessorCodes = processorCodes - }; - - SetupEvictionObserver(entry); - - Logger.DebugFormat("Succesfully inserted asset '{0}' to cache.", virtualPath); - - return entry; - } - catch (Exception ex) - { - Logger.ErrorFormat(ex, "Error while inserting asset '{0}' to the asset cache.", virtualPath); - InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); - } - } - - return null; - } - - public void Clear() - { - if (_env.TenantFolder.DirectoryExists("AssetCache")) - { - _env.TenantFolder.TryDeleteDirectory("AssetCache"); - // Remove the eviction observer also - HttpRuntime.Cache.RemoveByPattern(CacheKeyPrefix); - } - } - - public bool InvalidateAsset(string virtualPath) - { - Guard.NotEmpty(virtualPath, nameof(virtualPath)); - - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); - - using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) - { - if (CacheFolder.DirectoryExists(cacheDirectoryName)) - { - return InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); - } - - return false; - } - } - - private string BuildLockKey(string dirName) - { - return "AssetCache.Dir." + dirName; - } - - /// - /// Invalidates a cached asset when any themevar was changed or the theme was touched on file system - /// - /// - private static void SetupEvictionObserver(CachedAssetEntry entry) - { - if (entry.ThemeName == null) - return; - - var cacheKey = CacheKeyPrefix + "{0}:{1}".FormatInvariant(entry.ThemeName, entry.StoreId); - - var cacheDependency = new CacheDependency( - new string[0], - new[] { FrameworkCacheConsumer.BuildThemeVarsCacheKey(entry.ThemeName, entry.StoreId) }, - DateTime.UtcNow); - - HttpRuntime.Cache.Insert( - cacheKey, - entry.PhysicalPath, - cacheDependency, - Cache.NoAbsoluteExpiration, - Cache.NoSlidingExpiration, - CacheItemPriority.Default, - OnCacheItemRemoved); - } - - private static void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) - { - if (HostingEnvironment.ShutdownReason > ApplicationShutdownReason.None) - { - // Don't evict cached files during app shutdown. Dependant cache keys change - // during a shutdown caused by HttpRuntime.Cache full eviction. - return; - } - - // Keep this low level - var path = value as string; - if (path.HasValue() && reason == CacheItemRemovedReason.DependencyChanged) - { - try - { - Directory.Delete(path, true); - } - catch { } - } - } - - private IEnumerable ResolveVirtualPathDependencies(string virtualPath, IEnumerable deps, string themeName) - { - var list = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var d in (deps ?? new string[0]).Concat(new[] { virtualPath })) - { - var result = _themeFileResolver.Resolve(d); - list.Add(result != null ? result.ResultVirtualPath : d); - } - - // Add all theme.config files in the theme hierarchy chain - var manifest = _themeRegistry.GetThemeManifest(themeName); - while (manifest != null) - { - list.Add(VirtualPathUtility.ToAbsolute(CacheFolder.Combine(manifest.Location, manifest.ThemeName, "theme.config"))); - manifest = manifest.BaseTheme; - } - - return list; - } - - private bool InvalidateAssetInternal(string cacheDirectoryName, string theme, int storeId) - { - if (CacheFolder.TryDeleteDirectory(cacheDirectoryName)) - { - if (theme.HasValue()) - { - // Remove the eviction observer - var cacheKey = CacheKeyPrefix + "{0}:{1}".FormatInvariant(theme, storeId); - HttpRuntime.Cache.Remove(cacheKey); - - } - - return true; - } - - return false; - } - - /// - /// Checks whether asset is up-to-date. - /// - /// - /// - /// - /// - /// false if asset is not valid anymore and must be evicted from cache - private bool TryValidate(string virtualPath, string lastDeps, string lastHash, out IEnumerable parsedDeps, out string currentHash) - { - parsedDeps = null; - currentHash = null; - - try - { - if (lastDeps.IsEmpty() || lastHash.IsEmpty()) - { - return false; - } - - parsedDeps = ParseFileContent(lastDeps); - - // Check if dependency files hash matches the last saved hash - currentHash = BundleTable.VirtualPathProvider.GetFileHash(virtualPath, parsedDeps); - - return lastHash == currentHash; - } - catch - { - return false; - } - } - - private string ResolveCacheDirectoryName(string virtualPath, out string themeName, out int storeId) - { - themeName = null; - storeId = 0; - - if (virtualPath[0] != '~') - { - virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); - } - - // Build a dir name: ~/Themes/Flex/Content/theme.scss > themes-flex-content-theme.scss-flex-1 - var folderName = virtualPath.TrimStart('~', '/', '\\') - .Replace('/', '-') - .Replace('\\', '-'); - - if (ThemeHelper.PathIsInheritableThemeFile(virtualPath)) - { - themeName = _themeContext.CurrentTheme.ThemeName; - storeId = _services.StoreContext.CurrentStore.Id; - folderName += "-" + themeName + "-" + storeId; - } - - return folderName.ToLowerInvariant(); - } - - private IEnumerable ParseFileContent(string content) - { - var list = new HashSet(StringComparer.OrdinalIgnoreCase); - if (content.IsEmpty()) return list; - - var sr = new StringReader(content); - while (true) - { - var f = sr.ReadLine(); - if (f != null && f.HasValue()) - { - list.Add(f); - } - else - { - break; - } - } - - return list; - } - - private void CreateFileFromEntries(string dirName, string fileName, IEnumerable entries) - { - if (entries == null || !entries.Any()) - return; - - var sb = new StringBuilder(); - foreach (var f in entries) - { - sb.AppendLine(f); - } - - var content = sb.ToString().TrimEnd(); - if (content.HasValue()) - { - CacheFolder.CreateTextFile(CacheFolder.Combine(dirName, fileName), content); - } - } - - class NullCache : IAssetCache - { - public void Clear() - { - } - - public CachedAssetEntry GetAsset(string virtualPath) - { - return null; - } - - public CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virtualPathDependencies, string content, params string[] processorCodes) - { - return null; - } - - public bool InvalidateAsset(string virtualPath) - { - return false; - } - } - } + public class DefaultAssetCache : IAssetCache + { + public const string MinificationCode = "min"; + public const string AutoprefixCode = "autoprefix"; + public const string UrlRewriteCode = "urlrewrite"; + + public static readonly IAssetCache Null = new NullCache(); + const string CacheKeyPrefix = "sm:AssetCacheEntry:"; + + private readonly bool _isEnabled; + private readonly ThemeSettings _themeSettings; + private readonly IApplicationEnvironment _env; + private readonly IThemeContext _themeContext; + private readonly IThemeRegistry _themeRegistry; + private readonly IThemeFileResolver _themeFileResolver; + private readonly ICommonServices _services; + + private VirtualFolder _cacheFolder; + + public DefaultAssetCache( + ThemeSettings themeSettings, + IApplicationEnvironment env, + IThemeFileResolver themeFileResolver, + IThemeContext themeContext, + IThemeRegistry themeRegistry, + ICommonServices services) + { + _themeSettings = themeSettings; + _isEnabled = _themeSettings.AssetCachingEnabled == 2 || (_themeSettings.AssetCachingEnabled == 0 && !HttpContext.Current.IsDebuggingEnabled); + + _env = env; + _themeContext = themeContext; + _themeFileResolver = themeFileResolver; + _themeRegistry = themeRegistry; + _services = services; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public IVirtualFolder CacheFolder + { + get + { + if (_cacheFolder == null) + { + _cacheFolder = new VirtualFolder( + _env.TenantFolder.GetVirtualPath("AssetCache"), + _env.TenantFolder.VirtualPathProvider, + Logger); + } + + return _cacheFolder; + } + } + + public CachedAssetEntry GetAsset(string virtualPath) + { + if (!_isEnabled) + return null; + + Guard.NotEmpty(virtualPath, nameof(virtualPath)); + + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + + if (CacheFolder.DirectoryExists(cacheDirectoryName)) + { + try + { + var deps = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.dependencies")); + var hash = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.hash")); + + if (!TryValidate(virtualPath, deps, hash, out IEnumerable parsedDeps, out string currentHash)) + { + Logger.DebugFormat("Invalidating cached asset for '{0}' because it is not valid anymore.", virtualPath); + InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); + return null; + } + + // TODO: determine correct file extension + var content = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.css")); + if (content == null) + { + using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) + { + InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); + return null; + } + } + + var codes = ParseFileContent(CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.pcodes"))); + + var entry = new CachedAssetEntry + { + Content = content, + HashCode = currentHash, + OriginalVirtualPath = virtualPath, + VirtualPathDependencies = parsedDeps, + PhysicalPath = CacheFolder.MapPath(cacheDirectoryName), + ThemeName = themeName, + StoreId = storeId, + ProcessorCodes = codes.ToArray() + }; + + SetupEvictionObserver(entry); + + Logger.DebugFormat("Succesfully read asset '{0}' from cache.", virtualPath); + + return entry; + } + catch (Exception ex) + { + Logger.ErrorFormat(ex, "Error while resolving asset '{0}' from the asset cache.", virtualPath); + } + } + + return null; + } + + public CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virtualPathDependencies, string content, params string[] processorCodes) + { + if (!_isEnabled) + return null; + + Guard.NotEmpty(virtualPath, nameof(virtualPath)); + Guard.NotEmpty(content, nameof(content)); + + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + + using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) + { + CacheFolder.CreateDirectory(cacheDirectoryName); + + try + { + // Save main content file + // TODO: determine correct file extension + CreateFileFromEntries(cacheDirectoryName, "asset.css", new[] { content }); + + // Save dependencies file + var deps = ResolveVirtualPathDependencies(virtualPath, virtualPathDependencies, themeName); + CreateFileFromEntries(cacheDirectoryName, "asset.dependencies", deps); + + // Save hash file + var currentHash = BundleTable.VirtualPathProvider.GetFileHash(virtualPath, deps); + CreateFileFromEntries(cacheDirectoryName, "asset.hash", new[] { currentHash }); + + // Save codes file + CreateFileFromEntries(cacheDirectoryName, "asset.pcodes", processorCodes); + + var entry = new CachedAssetEntry + { + Content = content, + HashCode = currentHash, + OriginalVirtualPath = virtualPath, + VirtualPathDependencies = deps, + PhysicalPath = CacheFolder.MapPath(cacheDirectoryName), + ThemeName = themeName, + StoreId = storeId, + ProcessorCodes = processorCodes + }; + + SetupEvictionObserver(entry); + + Logger.DebugFormat("Succesfully inserted asset '{0}' to cache.", virtualPath); + + return entry; + } + catch (Exception ex) + { + Logger.ErrorFormat(ex, "Error while inserting asset '{0}' to the asset cache.", virtualPath); + InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); + } + } + + return null; + } + + public void Clear() + { + if (_env.TenantFolder.DirectoryExists("AssetCache")) + { + _env.TenantFolder.TryDeleteDirectory("AssetCache"); + // Remove the eviction observer also + HttpRuntime.Cache.RemoveByPattern(CacheKeyPrefix); + } + } + + public bool InvalidateAsset(string virtualPath) + { + Guard.NotEmpty(virtualPath, nameof(virtualPath)); + + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + + using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) + { + if (CacheFolder.DirectoryExists(cacheDirectoryName)) + { + return InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); + } + + return false; + } + } + + private string BuildLockKey(string dirName) + { + return "AssetCache.Dir." + dirName; + } + + /// + /// Invalidates a cached asset when any themevar was changed or the theme was touched on file system + /// + /// + private static void SetupEvictionObserver(CachedAssetEntry entry) + { + if (entry.ThemeName == null) + return; + + var cacheKey = CacheKeyPrefix + "{0}:{1}".FormatInvariant(entry.ThemeName, entry.StoreId); + + var cacheDependency = new CacheDependency( + new string[0], + new[] { FrameworkCacheConsumer.BuildThemeVarsCacheKey(entry.ThemeName, entry.StoreId) }, + DateTime.UtcNow); + + HttpRuntime.Cache.Insert( + cacheKey, + entry.PhysicalPath, + cacheDependency, + Cache.NoAbsoluteExpiration, + Cache.NoSlidingExpiration, + CacheItemPriority.Default, + OnCacheItemRemoved); + } + + private static void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) + { + if (HostingEnvironment.ShutdownReason > ApplicationShutdownReason.None) + { + // Don't evict cached files during app shutdown. Dependant cache keys change + // during a shutdown caused by HttpRuntime.Cache full eviction. + return; + } + + // Keep this low level + var path = value as string; + if (path.HasValue() && reason == CacheItemRemovedReason.DependencyChanged) + { + try + { + Directory.Delete(path, true); + } + catch { } + } + } + + private IEnumerable ResolveVirtualPathDependencies(string virtualPath, IEnumerable deps, string themeName) + { + var list = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var d in (deps ?? new string[0]).Concat(new[] { virtualPath })) + { + var result = _themeFileResolver.Resolve(d); + list.Add(result != null ? result.ResultVirtualPath : d); + } + + // Add all theme.config files in the theme hierarchy chain + var manifest = _themeRegistry.GetThemeManifest(themeName); + while (manifest != null) + { + list.Add(VirtualPathUtility.ToAbsolute(CacheFolder.Combine(manifest.Location, manifest.ThemeName, "theme.config"))); + manifest = manifest.BaseTheme; + } + + return list; + } + + private bool InvalidateAssetInternal(string cacheDirectoryName, string theme, int storeId) + { + if (CacheFolder.TryDeleteDirectory(cacheDirectoryName)) + { + if (theme.HasValue()) + { + // Remove the eviction observer + var cacheKey = CacheKeyPrefix + "{0}:{1}".FormatInvariant(theme, storeId); + HttpRuntime.Cache.Remove(cacheKey); + + } + + return true; + } + + return false; + } + + /// + /// Checks whether asset is up-to-date. + /// + /// + /// + /// + /// + /// false if asset is not valid anymore and must be evicted from cache + private bool TryValidate(string virtualPath, string lastDeps, string lastHash, out IEnumerable parsedDeps, out string currentHash) + { + parsedDeps = null; + currentHash = null; + + try + { + if (lastDeps.IsEmpty() || lastHash.IsEmpty()) + { + return false; + } + + parsedDeps = ParseFileContent(lastDeps); + + // Check if dependency files hash matches the last saved hash + currentHash = BundleTable.VirtualPathProvider.GetFileHash(virtualPath, parsedDeps); + + return lastHash == currentHash; + } + catch + { + return false; + } + } + + private string ResolveCacheDirectoryName(string virtualPath, out string themeName, out int storeId) + { + themeName = null; + storeId = 0; + + if (virtualPath[0] != '~') + { + virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); + } + + // Build a dir name: ~/Themes/Flex/Content/theme.scss > themes-flex-content-theme.scss-flex-1 + var folderName = virtualPath.TrimStart('~', '/', '\\') + .Replace('/', '-') + .Replace('\\', '-'); + + if (ThemeHelper.PathIsInheritableThemeFile(virtualPath)) + { + themeName = _themeContext.CurrentTheme.ThemeName; + storeId = _services.StoreContext.CurrentStore.Id; + folderName += "-" + themeName + "-" + storeId; + } + + return folderName.ToLowerInvariant(); + } + + private IEnumerable ParseFileContent(string content) + { + var list = new HashSet(StringComparer.OrdinalIgnoreCase); + if (content.IsEmpty()) return list; + + var sr = new StringReader(content); + while (true) + { + var f = sr.ReadLine(); + if (f != null && f.HasValue()) + { + list.Add(f); + } + else + { + break; + } + } + + return list; + } + + private void CreateFileFromEntries(string dirName, string fileName, IEnumerable entries) + { + if (entries == null || !entries.Any()) + return; + + var sb = new StringBuilder(); + foreach (var f in entries) + { + sb.AppendLine(f); + } + + var content = sb.ToString().TrimEnd(); + if (content.HasValue()) + { + CacheFolder.CreateTextFile(CacheFolder.Combine(dirName, fileName), content); + } + } + + class NullCache : IAssetCache + { + public void Clear() + { + } + + public CachedAssetEntry GetAsset(string virtualPath) + { + return null; + } + + public CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virtualPathDependencies, string content, params string[] processorCodes) + { + return null; + } + + public bool InvalidateAsset(string virtualPath) + { + return false; + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/IAssetCache.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/IAssetCache.cs index b0f888ef03..e8e4eee1dd 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/IAssetCache.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/IAssetCache.cs @@ -1,34 +1,30 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace SmartStore.Web.Framework.Theming.Assets { - public class CachedAssetEntry - { - public string Content { get; set; } - public string OriginalVirtualPath { get; set; } - public string PhysicalPath { get; set; } - public IEnumerable VirtualPathDependencies { get; set; } - public string HashCode { get; set; } - public string ThemeName { get; set; } - public int StoreId { get; set; } - public string[] ProcessorCodes { get; set; } - } + public class CachedAssetEntry + { + public string Content { get; set; } + public string OriginalVirtualPath { get; set; } + public string PhysicalPath { get; set; } + public IEnumerable VirtualPathDependencies { get; set; } + public string HashCode { get; set; } + public string ThemeName { get; set; } + public int StoreId { get; set; } + public string[] ProcessorCodes { get; set; } + } - /// - /// A file system based caching mechanism for dynamically translated assets like Sass, Less etc. - /// - public interface IAssetCache - { - CachedAssetEntry GetAsset(string virtualPath); + /// + /// A file system based caching mechanism for dynamically translated assets like Sass, Less etc. + /// + public interface IAssetCache + { + CachedAssetEntry GetAsset(string virtualPath); - CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virtualPathDependencies, string content, params string[] processorCodes); + CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virtualPathDependencies, string content, params string[] processorCodes); - bool InvalidateAsset(string virtualPath); + bool InvalidateAsset(string virtualPath); - void Clear(); - } + void Clear(); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs index 34b4557fe5..3b357666e7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs @@ -1,159 +1,150 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; -using System.Text; using System.Linq; +using System.Text; using System.Web.Hosting; -using SmartStore.Core.Plugins; using SmartStore.Core.Data; -using System; +using SmartStore.Core.Plugins; using SmartStore.Utilities; namespace SmartStore.Web.Framework.Theming.Assets { - public sealed class ModuleImport - { - public string VirtualPath { get; internal set; } - public string PhysicalPath { get; internal set; } - public PluginDescriptor PluginDescriptor { get; internal set; } - public bool IsAdmin { get; internal set; } - - public override string ToString() - { - return VirtualPath; - } - - public override int GetHashCode() - { - return VirtualPath.GetHashCode(); - } - - public override bool Equals(object obj) - { - if (!(obj is ModuleImport other)) - return false; - - return string.Equals(this.VirtualPath, other.VirtualPath, StringComparison.OrdinalIgnoreCase); - } - } - - public class ModuleImportsVirtualFile : VirtualFile, IFileDependencyProvider - { - private static readonly HashSet _adminImports; - private static readonly HashSet _publicImports; - - static ModuleImportsVirtualFile() - { - _adminImports = new HashSet(); - _publicImports = new HashSet(); - - if (DataSettings.DatabaseIsInstalled()) - { - CollectModuleImports(); - } - } - - private static void CollectModuleImports() - { - var installedPlugins = PluginManager.ReferencedPlugins.Where(x => x.Installed); - var root = PluginManager.PluginsLocation; - - foreach (var plugin in installedPlugins) - { - var contentDir = Path.Combine(plugin.PhysicalPath, "Content"); - if (!Directory.Exists(contentDir)) - continue; - - TryAddImport(plugin, _publicImports, "public.scss"); - TryAddImport(plugin, _adminImports, "admin.scss"); - } - - void TryAddImport(PluginDescriptor plugin, HashSet imports, string name) - { - var physicalPath = Path.Combine(plugin.PhysicalPath, "Content", name); - if (File.Exists(physicalPath)) - { - imports.Add(new ModuleImport - { - PhysicalPath = physicalPath, - VirtualPath = $"{root}/{plugin.FolderName}/Content/{name}", - PluginDescriptor = plugin, - IsAdmin = name.Contains("admin") - }); - } - } - } - - public static ModuleImport[] PublicImports - { - get => _publicImports.ToArray(); - } - - public static ModuleImport[] AdminImports - { - get => _adminImports.ToArray(); - } - - public ModuleImportsVirtualFile(string virtualPath, bool isAdmin) - : base(virtualPath) - { - IsAdmin = isAdmin; - } - - public override bool IsDirectory - { - get { return false; } - } - - public bool IsAdmin - { - get; - private set; - } - - public override Stream Open() - { - var sb = new StringBuilder(); - - var imports = IsAdmin ? _adminImports : _publicImports; - foreach (var imp in imports) - { - sb.AppendLine($"@import '{imp.VirtualPath}';"); - } - - return GenerateStreamFromString(sb.ToString()); - } - - public void HashCombine(HashCodeCombiner combiner) - { - Guard.NotNull(combiner, nameof(combiner)); - - var imports = IsAdmin ? _adminImports : _publicImports; - foreach (var imp in imports) - { - combiner.Add(new FileInfo(imp.PhysicalPath)); - } - } - - private Stream GenerateStreamFromString(string value) - { - var stream = new MemoryStream(); - - using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true)) - { - writer.Write(value); - writer.Flush(); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } - } - - public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) - { - var imports = IsAdmin ? _adminImports : _publicImports; - foreach (var imp in imports) - { - mappedPaths.Add(imp.PhysicalPath); - } - } - } + public sealed class ModuleImport + { + public string VirtualPath { get; internal set; } + public string PhysicalPath { get; internal set; } + public PluginDescriptor PluginDescriptor { get; internal set; } + public bool IsAdmin { get; internal set; } + + public override string ToString() + { + return VirtualPath; + } + + public override int GetHashCode() + { + return VirtualPath.GetHashCode(); + } + + public override bool Equals(object obj) + { + if (!(obj is ModuleImport other)) + return false; + + return string.Equals(this.VirtualPath, other.VirtualPath, StringComparison.OrdinalIgnoreCase); + } + } + + public class ModuleImportsVirtualFile : VirtualFile, IFileDependencyProvider + { + private static readonly HashSet _adminImports; + private static readonly HashSet _publicImports; + + static ModuleImportsVirtualFile() + { + _adminImports = new HashSet(); + _publicImports = new HashSet(); + + if (DataSettings.DatabaseIsInstalled()) + { + CollectModuleImports(); + } + } + + private static void CollectModuleImports() + { + var installedPlugins = PluginManager.ReferencedPlugins.Where(x => x.Installed); + var root = PluginManager.PluginsLocation; + + foreach (var plugin in installedPlugins) + { + var contentDir = Path.Combine(plugin.PhysicalPath, "Content"); + if (!Directory.Exists(contentDir)) + continue; + + TryAddImport(plugin, _publicImports, "public.scss"); + TryAddImport(plugin, _adminImports, "admin.scss"); + } + + void TryAddImport(PluginDescriptor plugin, HashSet imports, string name) + { + var physicalPath = Path.Combine(plugin.PhysicalPath, "Content", name); + if (File.Exists(physicalPath)) + { + imports.Add(new ModuleImport + { + PhysicalPath = physicalPath, + VirtualPath = $"{root}/{plugin.FolderName}/Content/{name}", + PluginDescriptor = plugin, + IsAdmin = name.Contains("admin") + }); + } + } + } + + public static ModuleImport[] PublicImports => _publicImports.ToArray(); + + public static ModuleImport[] AdminImports => _adminImports.ToArray(); + + public ModuleImportsVirtualFile(string virtualPath, bool isAdmin) + : base(virtualPath) + { + IsAdmin = isAdmin; + } + + public override bool IsDirectory => false; + + public bool IsAdmin + { + get; + private set; + } + + public override Stream Open() + { + var sb = new StringBuilder(); + + var imports = IsAdmin ? _adminImports : _publicImports; + foreach (var imp in imports) + { + sb.AppendLine($"@import '{imp.VirtualPath}';"); + } + + return GenerateStreamFromString(sb.ToString()); + } + + public void HashCombine(HashCodeCombiner combiner) + { + Guard.NotNull(combiner, nameof(combiner)); + + var imports = IsAdmin ? _adminImports : _publicImports; + foreach (var imp in imports) + { + combiner.Add(new FileInfo(imp.PhysicalPath)); + } + } + + private Stream GenerateStreamFromString(string value) + { + var stream = new MemoryStream(); + + using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true)) + { + writer.Write(value); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + } + + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + var imports = IsAdmin ? _adminImports : _publicImports; + foreach (var imp in imports) + { + mappedPaths.Add(imp.PhysicalPath); + } + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/SmartStyleBundle.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/SmartStyleBundle.cs index d2930486a7..185c640531 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/SmartStyleBundle.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/SmartStyleBundle.cs @@ -18,8 +18,8 @@ public SmartStyleBundle(string virtualPath) : this(virtualPath, null) { } - [SuppressMessage("ReSharper", "VirtualMemberCallInContructor")] - public SmartStyleBundle(string virtualPath, string cdnPath) + [SuppressMessage("ReSharper", "VirtualMemberCallInContructor")] + public SmartStyleBundle(string virtualPath, string cdnPath) : base(virtualPath, cdnPath, new IBundleTransform[] { BundleTransformerContext.Current.Styles.GetDefaultTransformInstance() }) { Builder = new NullBuilder(); @@ -31,10 +31,10 @@ public override BundleResponse GenerateBundleResponse(BundleContext context) // to the Bundle.Files collection. This is bad as we allow switching // Optimization mode per UI. Switching from true to false would also include // ALL LESS/SASS imports in the generated output ('link' tags) - + // get all ORIGINAL bundle parts (including LESS/SASS parents, no @imports) var files = this.EnumerateFiles(context); - + // replace file pattern like {version} and let Bundler resolve min/debug extensions. files = context.BundleCollection.FileExtensionReplacementList.ReplaceFileExtensions(context, files); // save originals for later use @@ -53,7 +53,7 @@ public override BundleResponse GenerateBundleResponse(BundleContext context) return response; } - + public override void UpdateCache(BundleContext context, BundleResponse response) { if (_wasCached && _transformedBundleFiles != null && _transformedBundleFiles.Any()) @@ -89,7 +89,7 @@ where _originalBundleFilePathes.Any(x => virtualPath.EndsWith(x, StringCompariso private static bool CacheIsEnabled(BundleContext context) { - Guard.NotNull(context, nameof(context)); + Guard.NotNull(context, nameof(context)); return context.HttpContext?.Cache != null && !context.EnableInstrumentation; diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs index e2fd7aa151..d0d3900fde 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs @@ -15,39 +15,36 @@ namespace SmartStore.Web.Framework.Theming { - public class SassCssHttpHandler : CssHttpHandlerBase - { - protected override IAsset TranslateAssetCore(IAsset asset, ITransformer transformer, bool isDebugMode) - { - return InnerTranslateAsset("SassTranslator", asset, transformer, isDebugMode); - } - } - - public abstract class CssHttpHandlerBase : BundleTransformer.Core.HttpHandlers.StyleAssetHandlerBase - { - protected CssHttpHandlerBase() + public class SassCssHttpHandler : CssHttpHandlerBase + { + protected override IAsset TranslateAssetCore(IAsset asset, ITransformer transformer, bool isDebugMode) + { + return InnerTranslateAsset("SassTranslator", asset, transformer, isDebugMode); + } + } + + public abstract class CssHttpHandlerBase : BundleTransformer.Core.HttpHandlers.StyleAssetHandlerBase + { + protected CssHttpHandlerBase() : this(HttpContext.Current.Cache, - BundleTransformerContext.Current.FileSystem.GetVirtualFileSystemWrapper(), - BundleTransformerContext.Current.Configuration.GetCoreSettings().AssetHandler) + BundleTransformerContext.Current.FileSystem.GetVirtualFileSystemWrapper(), + BundleTransformerContext.Current.Configuration.GetCoreSettings().AssetHandler) { - } + } - protected CssHttpHandlerBase( + protected CssHttpHandlerBase( Cache cache, IVirtualFileSystemWrapper virtualFileSystemWrapper, AssetHandlerSettings assetHandlerConfig) : base(cache, virtualFileSystemWrapper, assetHandlerConfig) - { - } + { + } - protected override bool IsStaticAsset - { - get { return false; } - } + protected override bool IsStaticAsset => false; - private bool IsThemeableRequest() + private bool IsThemeableRequest() { - if (!DataSettings.DatabaseIsInstalled()) + if (!DataSettings.DatabaseIsInstalled()) { return false; } @@ -63,73 +60,73 @@ private bool IsThemeableRequest() } } - protected override string GetCacheKey(string assetVirtualPath, string bundleVirtualPath) - { - string cacheKey = base.GetCacheKey(assetVirtualPath, bundleVirtualPath); - - if (IsThemeableRequest()) - { - if (HttpContext.Current?.Request != null) - { - var qs = QueryString.Current; - if (qs.Count > 0) - { - // required for Theme editing validation: See Admin.Controllers.ThemeController.ValidateLess() - if (qs.Contains("theme")) - { - EngineContext.Current.Resolve().SetRequestTheme(qs["theme"]); - } - if (qs.Contains("storeId")) - { - EngineContext.Current.Resolve().SetRequestStore(qs["storeId"].ToInt()); - } - } - } - - cacheKey += "_" + EngineContext.Current.Resolve().CurrentTheme.ThemeName + "_" + EngineContext.Current.Resolve().CurrentStore.Id; - - if (ThemeHelper.IsStyleValidationRequest()) - { - cacheKey += "_Validation"; - } - } - - return cacheKey; - } - - protected override IAsset TranslateAsset(IAsset asset, ITransformer transformer, bool isDebugMode) - { - bool validationMode = ThemeHelper.IsStyleValidationRequest(); - - try - { - var processedAsset = TranslateAssetCore(asset, transformer, validationMode || isDebugMode); - - if (transformer == null && !validationMode) - { - // BundleTransformer does NOT PostProcess when transformer instance is null, - // therefore we handle it ourselves, because we desperately need - // UrlRewrite even in debug mode. - return AssetTranslorUtil.PostProcessAsset(processedAsset, isDebugMode); - } - else - { - return processedAsset; - } - } - catch (Exception ex) - { - if (validationMode) - { - _context.Response.Write(ex.Message); - _context.Response.StatusCode = 500; - _context.Response.End(); - } - - throw; - } - } - - protected abstract IAsset TranslateAssetCore(IAsset asset, ITransformer transformer, bool isDebugMode); - } + protected override string GetCacheKey(string assetVirtualPath, string bundleVirtualPath) + { + string cacheKey = base.GetCacheKey(assetVirtualPath, bundleVirtualPath); + + if (IsThemeableRequest()) + { + if (HttpContext.Current?.Request != null) + { + var qs = QueryString.Current; + if (qs.Count > 0) + { + // required for Theme editing validation: See Admin.Controllers.ThemeController.ValidateLess() + if (qs.Contains("theme")) + { + EngineContext.Current.Resolve().SetRequestTheme(qs["theme"]); + } + if (qs.Contains("storeId")) + { + EngineContext.Current.Resolve().SetRequestStore(qs["storeId"].ToInt()); + } + } + } + + cacheKey += "_" + EngineContext.Current.Resolve().CurrentTheme.ThemeName + "_" + EngineContext.Current.Resolve().CurrentStore.Id; + + if (ThemeHelper.IsStyleValidationRequest()) + { + cacheKey += "_Validation"; + } + } + + return cacheKey; + } + + protected override IAsset TranslateAsset(IAsset asset, ITransformer transformer, bool isDebugMode) + { + bool validationMode = ThemeHelper.IsStyleValidationRequest(); + + try + { + var processedAsset = TranslateAssetCore(asset, transformer, validationMode || isDebugMode); + + if (transformer == null && !validationMode) + { + // BundleTransformer does NOT PostProcess when transformer instance is null, + // therefore we handle it ourselves, because we desperately need + // UrlRewrite even in debug mode. + return AssetTranslorUtil.PostProcessAsset(processedAsset, isDebugMode); + } + else + { + return processedAsset; + } + } + catch (Exception ex) + { + if (validationMode) + { + _context.Response.Write(ex.Message); + _context.Response.StatusCode = 500; + _context.Response.End(); + } + + throw; + } + } + + protected abstract IAsset TranslateAssetCore(IAsset asset, ITransformer transformer, bool isDebugMode); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs b/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs index dbbc5a5ab4..f40f05d73c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs @@ -1,309 +1,299 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Web; using System.Web.Hosting; -using System.Collections.Generic; -using System.Threading; -using SmartStore.Utilities.Threading; using SmartStore.Core; using SmartStore.Core.Infrastructure; using SmartStore.Core.Themes; +using SmartStore.Utilities.Threading; namespace SmartStore.Web.Framework.Theming { - public class DefaultThemeFileResolver : DisposableObject, IThemeFileResolver - { - private readonly Dictionary _files = new Dictionary(); - private readonly IThemeRegistry _themeRegistry; - private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); - - public DefaultThemeFileResolver(IThemeRegistry themeRegistry) - { - _themeRegistry = themeRegistry; - - // listen to file monitoring events - _themeRegistry.ThemeFolderDeleted += OnThemeFolderDeleted; - _themeRegistry.ThemeFolderRenamed += OnThemeFolderRenamed; - _themeRegistry.BaseThemeChanged += OnBaseThemeChanged; - _themeRegistry.ThemeFileChanged += OnThemeFileChanged; - } - - private void OnThemeFolderDeleted(object sender, ThemeFolderDeletedEventArgs e) - { - OnThemeRemoved(e.Name); - } - - private void OnThemeFolderRenamed(object sender, ThemeFolderRenamedEventArgs e) - { - OnThemeRemoved(e.OldName); - } - - private void OnBaseThemeChanged(object sender, BaseThemeChangedEventArgs e) - { - // We should be smarter than just clearing the whole cache here. BUT: - // Changing the base theme is a very rare case, whereas determining all dependant files is rather sophisticated. - // So, who cares ;-) - _files.Clear(); - } - - private void OnThemeRemoved(string themeName) - { - using (_rwLock.GetWriteLock()) - { - var keys = _files.Keys.Where(x => x.ThemeName.IsCaseInsensitiveEqual(themeName)).ToList(); - foreach (var key in keys) - { - if (key.ThemeName.IsCaseInsensitiveEqual(themeName) || _themeRegistry.IsChildThemeOf(key.ThemeName, themeName)) - { - // remove all cached pathes for this theme (also in all derived themes) - _files.Remove(key); - } - } - } - } - - private void OnThemeFileChanged(object sender, ThemeFileChangedEventArgs e) - { - if (e.IsConfigurationFile) - return; - - using (_rwLock.GetWriteLock()) - { - // get keys with same relative path - var keys = _files.Keys.Where(x => x.RelativePath.IsCaseInsensitiveEqual(e.RelativePath)).ToList(); - foreach (var key in keys) - { - if (key.ThemeName.IsCaseInsensitiveEqual(e.ThemeName) || _themeRegistry.IsChildThemeOf(key.ThemeName, e.ThemeName)) - { - // remove all cached pathes for this file/theme combination (also in all derived themes) - InheritedThemeFileResult result; - if (_files.TryGetValue(key, out result)) - { - _files.Remove(key); - if (e.ChangeType == ThemeFileChangeType.Created) - { - // The file is new: no chance that our VPP dependencies - // could have been notified about this (only deletions/changes are monitored). - // Therefore we brutally set the result file's last write time - // to enforece VPP to invalidate cache. - try - { - File.SetLastWriteTimeUtc(result.ResultPhysicalPath, DateTime.UtcNow); - } - catch { } - } - } - } - } - } - } - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - _themeRegistry.ThemeFileChanged -= OnThemeFileChanged; - _themeRegistry.ThemeFolderDeleted -= OnThemeFolderDeleted; - _themeRegistry.BaseThemeChanged -= OnBaseThemeChanged; - _themeRegistry.ThemeFolderRenamed -= OnThemeFolderRenamed; - } - } - - /// - /// Tries to resolve a file up in the current theme's hierarchy chain. - /// - /// The original virtual path of the theme file - /// - /// If the current working themme is based on another theme AND the requested file - /// was physically found in the theme's hierarchy chain, an instance of will be returned. - /// In any other case the return value is null. - /// - public InheritedThemeFileResult Resolve(string virtualPath) - { - Guard.NotEmpty(virtualPath, nameof(virtualPath)); - - if (virtualPath[0] != '~') - { - virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); - } - - if (!ThemeHelper.PathIsInheritableThemeFile(virtualPath)) - { - return null; - } - - bool isBased = false; - - virtualPath = ThemeHelper.TokenizePath(virtualPath, out var requestedThemeName, out var relativePath, out var query); - - Func nullOrFile = () => - { - return isBased - ? new InheritedThemeFileResult { IsBased = true, OriginalVirtualPath = virtualPath, Query = query } - : null; - }; - - ThemeManifest currentTheme = ResolveTheme(requestedThemeName, relativePath, query, out isBased); - - if (currentTheme?.BaseTheme == null) - { - // dont't bother resolving files: the current theme is not inherited. - // Let the current VPP do the work. - return nullOrFile(); - } - - if (!currentTheme.ThemeName.Equals(requestedThemeName, StringComparison.OrdinalIgnoreCase)) - { - if (!_themeRegistry.IsChildThemeOf(currentTheme.ThemeName, requestedThemeName)) - { - return nullOrFile(); - } - } - else if (isBased && currentTheme.BaseTheme != null) - { - // A file from the base theme has been requested - currentTheme = currentTheme.BaseTheme; - } - - var fileKey = new FileKey(currentTheme.ThemeName, relativePath, query); - InheritedThemeFileResult result; - - using (_rwLock.GetUpgradeableReadLock()) - { - if (!_files.TryGetValue(fileKey, out result)) - { - using (_rwLock.GetWriteLock()) - { - // ALWAYS begin the search with the current working theme's location! - string actualLocation = LocateFile(currentTheme.ThemeName, relativePath, out var resultVirtualPath, out var resultPhysicalPath); - - if (actualLocation != null) - { - result = new InheritedThemeFileResult - { - RelativePath = relativePath, - OriginalVirtualPath = virtualPath, - ResultVirtualPath = resultVirtualPath, - ResultPhysicalPath = resultPhysicalPath, - OriginalThemeName = requestedThemeName, - ResultThemeName = actualLocation, - IsBased = isBased, - Query = query - }; - } - - _files[fileKey] = result; - } - } - } - - if (result == null) - return nullOrFile(); - - return result; - } - - private ThemeManifest ResolveTheme(string requestedThemeName, string relativePath, string query, out bool isBased) - { - isBased = false; - ThemeManifest currentTheme; - - var isAdmin = EngineContext.Current.Resolve().IsAdmin; - if (isAdmin) - { - currentTheme = _themeRegistry.GetThemeManifest(requestedThemeName); - } - else - { - var styleResult = ThemeHelper.IsStyleSheet(relativePath); - var isPreprocessor = styleResult != null && styleResult.IsPreprocessor; - if (isPreprocessor) - { - // special consideration for SASS/LESS files: they can be validated - // in the backend. For validation, a "theme" query is appended - // to the url. During validation we must work with the actual - // requested theme instead of dynamically resolving the working theme. - var httpContext = HttpContext.Current; - if (httpContext != null && httpContext.Request != null) - { - var qs = httpContext.Request.QueryString; - if (qs["theme"].HasValue()) - { - EngineContext.Current.Resolve().SetRequestTheme(qs["theme"]); - } - } - } - - currentTheme = ThemeHelper.ResolveCurrentTheme(); - - if (isPreprocessor && query != null && query.StartsWith("base", StringComparison.OrdinalIgnoreCase)) - { - // special case to support SASS @import declarations - // within inherited SASS files. Snenario: an inheritor wishes to - // include the same file from it's base theme (e.g. custom.scss) just to tweak it - // a bit for his child theme. Without the 'base' query the resolution starting point - // for custom.scss would be the CURRENT theme's folder, and NOT the requested one's, - // which inevitably would result in a cyclic dependency. - currentTheme = _themeRegistry.GetThemeManifest(requestedThemeName); - isBased = true; - } - } - - return currentTheme; - } - - /// - /// Tries to locate the file - /// - /// the theme's name where the file is actually located - private string LocateFile(string themeName, string relativePath, out string virtualPath, out string physicalPath) - { - virtualPath = null; - physicalPath = null; - - virtualPath = VirtualPathUtility.Combine("{0}{1}/".FormatInvariant(ThemeHelper.ThemesBasePath, themeName), relativePath); - physicalPath = HostingEnvironment.MapPath(virtualPath); - if (File.Exists(physicalPath)) - { - return themeName; - } - - var manifest = _themeRegistry.GetThemeManifest(themeName); - if (manifest != null && manifest.BaseTheme != null) - { - var baseLocation = LocateFile(manifest.BaseTheme.ThemeName, relativePath, out virtualPath, out physicalPath); - return baseLocation; - } - - virtualPath = null; - physicalPath = null; - return null; - } - - - private class FileKey : Tuple - { - public FileKey(string themeName, string relativePath, string query) - : base(themeName.ToLower(), relativePath.ToLower(), query?.ToLower()) - { - } - - public string ThemeName - { - get { return base.Item1; } - } - - public string RelativePath - { - get { return base.Item2; } - } - - public string Query - { - get { return base.Item3; } - } - } - - } + public class DefaultThemeFileResolver : DisposableObject, IThemeFileResolver + { + private readonly Dictionary _files = new Dictionary(); + private readonly IThemeRegistry _themeRegistry; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + public DefaultThemeFileResolver(IThemeRegistry themeRegistry) + { + _themeRegistry = themeRegistry; + + // listen to file monitoring events + _themeRegistry.ThemeFolderDeleted += OnThemeFolderDeleted; + _themeRegistry.ThemeFolderRenamed += OnThemeFolderRenamed; + _themeRegistry.BaseThemeChanged += OnBaseThemeChanged; + _themeRegistry.ThemeFileChanged += OnThemeFileChanged; + } + + private void OnThemeFolderDeleted(object sender, ThemeFolderDeletedEventArgs e) + { + OnThemeRemoved(e.Name); + } + + private void OnThemeFolderRenamed(object sender, ThemeFolderRenamedEventArgs e) + { + OnThemeRemoved(e.OldName); + } + + private void OnBaseThemeChanged(object sender, BaseThemeChangedEventArgs e) + { + // We should be smarter than just clearing the whole cache here. BUT: + // Changing the base theme is a very rare case, whereas determining all dependant files is rather sophisticated. + // So, who cares ;-) + _files.Clear(); + } + + private void OnThemeRemoved(string themeName) + { + using (_rwLock.GetWriteLock()) + { + var keys = _files.Keys.Where(x => x.ThemeName.IsCaseInsensitiveEqual(themeName)).ToList(); + foreach (var key in keys) + { + if (key.ThemeName.IsCaseInsensitiveEqual(themeName) || _themeRegistry.IsChildThemeOf(key.ThemeName, themeName)) + { + // remove all cached pathes for this theme (also in all derived themes) + _files.Remove(key); + } + } + } + } + + private void OnThemeFileChanged(object sender, ThemeFileChangedEventArgs e) + { + if (e.IsConfigurationFile) + return; + + using (_rwLock.GetWriteLock()) + { + // get keys with same relative path + var keys = _files.Keys.Where(x => x.RelativePath.IsCaseInsensitiveEqual(e.RelativePath)).ToList(); + foreach (var key in keys) + { + if (key.ThemeName.IsCaseInsensitiveEqual(e.ThemeName) || _themeRegistry.IsChildThemeOf(key.ThemeName, e.ThemeName)) + { + // remove all cached pathes for this file/theme combination (also in all derived themes) + if (_files.TryGetValue(key, out var result)) + { + _files.Remove(key); + if (e.ChangeType == ThemeFileChangeType.Created) + { + // The file is new: no chance that our VPP dependencies + // could have been notified about this (only deletions/changes are monitored). + // Therefore we brutally set the result file's last write time + // to enforece VPP to invalidate cache. + try + { + File.SetLastWriteTimeUtc(result.ResultPhysicalPath, DateTime.UtcNow); + } + catch { } + } + } + } + } + } + } + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + _themeRegistry.ThemeFileChanged -= OnThemeFileChanged; + _themeRegistry.ThemeFolderDeleted -= OnThemeFolderDeleted; + _themeRegistry.BaseThemeChanged -= OnBaseThemeChanged; + _themeRegistry.ThemeFolderRenamed -= OnThemeFolderRenamed; + } + } + + /// + /// Tries to resolve a file up in the current theme's hierarchy chain. + /// + /// The original virtual path of the theme file + /// + /// If the current working themme is based on another theme AND the requested file + /// was physically found in the theme's hierarchy chain, an instance of will be returned. + /// In any other case the return value is null. + /// + public InheritedThemeFileResult Resolve(string virtualPath) + { + Guard.NotEmpty(virtualPath, nameof(virtualPath)); + + if (virtualPath[0] != '~') + { + virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); + } + + if (!ThemeHelper.PathIsInheritableThemeFile(virtualPath)) + { + return null; + } + + bool isBased = false; + + virtualPath = ThemeHelper.TokenizePath(virtualPath, out var requestedThemeName, out var relativePath, out var query); + + Func nullOrFile = () => + { + return isBased + ? new InheritedThemeFileResult { IsBased = true, OriginalVirtualPath = virtualPath, Query = query } + : null; + }; + + ThemeManifest currentTheme = ResolveTheme(requestedThemeName, relativePath, query, out isBased); + + if (currentTheme?.BaseTheme == null) + { + // dont't bother resolving files: the current theme is not inherited. + // Let the current VPP do the work. + return nullOrFile(); + } + + if (!currentTheme.ThemeName.Equals(requestedThemeName, StringComparison.OrdinalIgnoreCase)) + { + if (!_themeRegistry.IsChildThemeOf(currentTheme.ThemeName, requestedThemeName)) + { + return nullOrFile(); + } + } + else if (isBased && currentTheme.BaseTheme != null) + { + // A file from the base theme has been requested + currentTheme = currentTheme.BaseTheme; + } + + var fileKey = new FileKey(currentTheme.ThemeName, relativePath, query); + InheritedThemeFileResult result; + + using (_rwLock.GetUpgradeableReadLock()) + { + if (!_files.TryGetValue(fileKey, out result)) + { + using (_rwLock.GetWriteLock()) + { + // ALWAYS begin the search with the current working theme's location! + string actualLocation = LocateFile(currentTheme.ThemeName, relativePath, out var resultVirtualPath, out var resultPhysicalPath); + + if (actualLocation != null) + { + result = new InheritedThemeFileResult + { + RelativePath = relativePath, + OriginalVirtualPath = virtualPath, + ResultVirtualPath = resultVirtualPath, + ResultPhysicalPath = resultPhysicalPath, + OriginalThemeName = requestedThemeName, + ResultThemeName = actualLocation, + IsBased = isBased, + Query = query + }; + } + + _files[fileKey] = result; + } + } + } + + if (result == null) + return nullOrFile(); + + return result; + } + + private ThemeManifest ResolveTheme(string requestedThemeName, string relativePath, string query, out bool isBased) + { + isBased = false; + ThemeManifest currentTheme; + + var isAdmin = EngineContext.Current.Resolve().IsAdmin; + if (isAdmin) + { + currentTheme = _themeRegistry.GetThemeManifest(requestedThemeName); + } + else + { + var styleResult = ThemeHelper.IsStyleSheet(relativePath); + var isPreprocessor = styleResult != null && styleResult.IsPreprocessor; + if (isPreprocessor) + { + // special consideration for SASS/LESS files: they can be validated + // in the backend. For validation, a "theme" query is appended + // to the url. During validation we must work with the actual + // requested theme instead of dynamically resolving the working theme. + var httpContext = HttpContext.Current; + if (httpContext != null && httpContext.Request != null) + { + var qs = httpContext.Request.QueryString; + if (qs["theme"].HasValue()) + { + EngineContext.Current.Resolve().SetRequestTheme(qs["theme"]); + } + } + } + + currentTheme = ThemeHelper.ResolveCurrentTheme(); + + if (isPreprocessor && query != null && query.StartsWith("base", StringComparison.OrdinalIgnoreCase)) + { + // special case to support SASS @import declarations + // within inherited SASS files. Snenario: an inheritor wishes to + // include the same file from it's base theme (e.g. custom.scss) just to tweak it + // a bit for his child theme. Without the 'base' query the resolution starting point + // for custom.scss would be the CURRENT theme's folder, and NOT the requested one's, + // which inevitably would result in a cyclic dependency. + currentTheme = _themeRegistry.GetThemeManifest(requestedThemeName); + isBased = true; + } + } + + return currentTheme; + } + + /// + /// Tries to locate the file + /// + /// the theme's name where the file is actually located + private string LocateFile(string themeName, string relativePath, out string virtualPath, out string physicalPath) + { + virtualPath = null; + physicalPath = null; + + virtualPath = VirtualPathUtility.Combine("{0}{1}/".FormatInvariant(ThemeHelper.ThemesBasePath, themeName), relativePath); + physicalPath = HostingEnvironment.MapPath(virtualPath); + if (File.Exists(physicalPath)) + { + return themeName; + } + + var manifest = _themeRegistry.GetThemeManifest(themeName); + if (manifest != null && manifest.BaseTheme != null) + { + var baseLocation = LocateFile(manifest.BaseTheme.ThemeName, relativePath, out virtualPath, out physicalPath); + return baseLocation; + } + + virtualPath = null; + physicalPath = null; + return null; + } + + + private class FileKey : Tuple + { + public FileKey(string themeName, string relativePath, string query) + : base(themeName.ToLower(), relativePath.ToLower(), query?.ToLower()) + { + } + + public string ThemeName => base.Item1; + + public string RelativePath => base.Item2; + + public string Query => base.Item3; + } + + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs index e36f9c9fd4..eaf0c9b6b1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Web.Framework.Theming { - internal interface IFileDependencyProvider - { - void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys); - } + internal interface IFileDependencyProvider + { + void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs index ad49bed1d2..a0c19b1495 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs @@ -8,41 +8,38 @@ namespace SmartStore.Web.Framework.Theming { internal class InheritedVirtualThemeFile : VirtualFile, IFileDependencyProvider { - public InheritedVirtualThemeFile(InheritedThemeFileResult resolveResult) - : base(DetermineVirtualPath(resolveResult)) + public InheritedVirtualThemeFile(InheritedThemeFileResult resolveResult) + : base(DetermineVirtualPath(resolveResult)) { ResolveResult = resolveResult; } - public InheritedThemeFileResult ResolveResult { get; } + public InheritedThemeFileResult ResolveResult { get; } - public string ResultVirtualPath - { - get { return ResolveResult.ResultVirtualPath ?? ResolveResult.OriginalVirtualPath; } - } + public string ResultVirtualPath => ResolveResult.ResultVirtualPath ?? ResolveResult.OriginalVirtualPath; - public override Stream Open() + public override Stream Open() { - return new FileStream(ResolveResult.ResultPhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + return new FileStream(ResolveResult.ResultPhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } - private static string DetermineVirtualPath(InheritedThemeFileResult resolveResult) - { - if (resolveResult.RelativePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) - { - // ASP.NET BuildManager requires the original path for razor views, - // otherwise an exception is thrown - return resolveResult.OriginalVirtualPath; - } - else - { - return resolveResult.ResultVirtualPath; - } - } + private static string DetermineVirtualPath(InheritedThemeFileResult resolveResult) + { + if (resolveResult.RelativePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) + { + // ASP.NET BuildManager requires the original path for razor views, + // otherwise an exception is thrown + return resolveResult.OriginalVirtualPath; + } + else + { + return resolveResult.ResultVirtualPath; + } + } - public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) - { - mappedPaths.Add(ResolveResult.ResultPhysicalPath); - } - } + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + mappedPaths.Add(ResolveResult.ResultPhysicalPath); + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/LocalizedDisplayMode.cs b/src/Presentation/SmartStore.Web.Framework/Theming/LocalizedDisplayMode.cs index 860d0560a2..58f46a29c1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/LocalizedDisplayMode.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/LocalizedDisplayMode.cs @@ -6,94 +6,88 @@ namespace SmartStore.Web.Framework.Theming { - public class LocalizedDisplayMode : DefaultDisplayMode - { - private readonly bool _enabled; - - public LocalizedDisplayMode(bool enabled) - : this(DisplayModeProvider.DefaultDisplayModeId, enabled) - { - } - - public LocalizedDisplayMode(string suffix, bool enables) - : base(suffix) - { - _enabled = enables; - } - - public override DisplayInfo GetDisplayInfo(HttpContextBase httpContext, string virtualPath, Func virtualPathExists) - { - if (!_enabled) - { - // default behaviour, because localized views are not enabled - return base.GetDisplayInfo(httpContext, virtualPath, virtualPathExists); - } - - var lang = GetCurrentLanguageSeoCode(); - - var result = - GetDisplayInfoInternal(httpContext, virtualPath, lang, virtualPathExists) ?? - GetDisplayInfoInternal(httpContext, virtualPath, null, virtualPathExists); - - return result; - } - - private DisplayInfo GetDisplayInfoInternal(HttpContextBase httpContext, string virtualPath, string lang, Func virtualPathExists) - { - string path = this.TransformPath(virtualPath, "{0}{1}".FormatInvariant(base.DisplayModeId, lang.IsEmpty() ? "" : "." + lang)); - if (path != null && virtualPathExists(path)) - { - return new LocalizedDisplayInfo(path, this, lang); - } - - return null; - } - - public override string DisplayModeId - { - get - { - if (!_enabled) - { - return base.DisplayModeId; - } - - var lang = GetCurrentLanguageSeoCode(); - return "{0}{1}".FormatInvariant(base.DisplayModeId, lang.IsEmpty() ? "" : "." + lang); - } - } - - protected string GetCurrentLanguageSeoCode() - { - string result = null; - - var workContext = EngineContext.Current.Resolve(); - if (workContext != null && workContext.WorkingLanguage != null) - { - result = workContext.WorkingLanguage.UniqueSeoCode; - } - - return result.NullEmpty() ?? "en"; - } - } - - public class LocalizedDisplayInfo : DisplayInfo - { - public LocalizedDisplayInfo(string filePath, IDisplayMode displayMode, string lang) - : base(filePath, displayMode) - { - this.Lang = lang; - } - - public string Lang { get; set; } - - public string DisplayModeId - { - get - { - return "{0}{1}".FormatInvariant(DisplayMode.DisplayModeId, Lang.IsEmpty() ? "" : "." + Lang); - } - } - } + public class LocalizedDisplayMode : DefaultDisplayMode + { + private readonly bool _enabled; + + public LocalizedDisplayMode(bool enabled) + : this(DisplayModeProvider.DefaultDisplayModeId, enabled) + { + } + + public LocalizedDisplayMode(string suffix, bool enables) + : base(suffix) + { + _enabled = enables; + } + + public override DisplayInfo GetDisplayInfo(HttpContextBase httpContext, string virtualPath, Func virtualPathExists) + { + if (!_enabled) + { + // default behaviour, because localized views are not enabled + return base.GetDisplayInfo(httpContext, virtualPath, virtualPathExists); + } + + var lang = GetCurrentLanguageSeoCode(); + + var result = + GetDisplayInfoInternal(httpContext, virtualPath, lang, virtualPathExists) ?? + GetDisplayInfoInternal(httpContext, virtualPath, null, virtualPathExists); + + return result; + } + + private DisplayInfo GetDisplayInfoInternal(HttpContextBase httpContext, string virtualPath, string lang, Func virtualPathExists) + { + string path = this.TransformPath(virtualPath, "{0}{1}".FormatInvariant(base.DisplayModeId, lang.IsEmpty() ? "" : "." + lang)); + if (path != null && virtualPathExists(path)) + { + return new LocalizedDisplayInfo(path, this, lang); + } + + return null; + } + + public override string DisplayModeId + { + get + { + if (!_enabled) + { + return base.DisplayModeId; + } + + var lang = GetCurrentLanguageSeoCode(); + return "{0}{1}".FormatInvariant(base.DisplayModeId, lang.IsEmpty() ? "" : "." + lang); + } + } + + protected string GetCurrentLanguageSeoCode() + { + string result = null; + + var workContext = EngineContext.Current.Resolve(); + if (workContext != null && workContext.WorkingLanguage != null) + { + result = workContext.WorkingLanguage.UniqueSeoCode; + } + + return result.NullEmpty() ?? "en"; + } + } + + public class LocalizedDisplayInfo : DisplayInfo + { + public LocalizedDisplayInfo(string filePath, IDisplayMode displayMode, string lang) + : base(filePath, displayMode) + { + this.Lang = lang; + } + + public string Lang { get; set; } + + public string DisplayModeId => "{0}{1}".FormatInvariant(DisplayMode.DisplayModeId, Lang.IsEmpty() ? "" : "." + Lang); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs index 32c6d31fe8..f05bcca22a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs @@ -1,175 +1,172 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Web; using System.Web.Hosting; -using System.Threading; using SmartStore.Core.Infrastructure; -using SmartStore.Utilities.Threading; -using SmartStore.Utilities; using SmartStore.Core.Themes; +using SmartStore.Utilities; +using SmartStore.Utilities.Threading; namespace SmartStore.Web.Framework.Theming { - public abstract class SmartVirtualPathProvider : VirtualPathProvider - { - private readonly IThemeRegistry _themeRegistry; - private readonly Dictionary _cachedDebugFilePaths = new Dictionary(); - private readonly ContextState> _requestState = new ContextState>("PluginDebugViewVPP.RequestCache", () => new Dictionary()); - private readonly DirectoryInfo _pluginsDebugDir; - private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); - - protected static readonly bool _isDebug; - - static SmartVirtualPathProvider() - { - _isDebug = HttpContext.Current.IsDebuggingEnabled && CommonHelper.IsDevEnvironment; - } - - protected SmartVirtualPathProvider() - { - var appRootPath = HostingEnvironment.MapPath("~/").EnsureEndsWith("\\"); - - var pluginsDebugPath = Path.GetFullPath(Path.Combine(appRootPath, @"..\..\Plugins")); - if (Directory.Exists(pluginsDebugPath)) - { - _pluginsDebugDir = new DirectoryInfo(pluginsDebugPath); - } - - _themeRegistry = EngineContext.Current.Resolve(); - } - - protected internal string ResolveDebugFilePath(string virtualPath) - { - if (!_isDebug) - return null; - - // Two-Level caching: RequestCache > AppCache - var d = _requestState.GetState(); - - if (!d.TryGetValue(virtualPath, out var debugPath)) - { - if (!IsExtensionPath(virtualPath, out var root, out var appRelativePath)) - { - // don't query again in this request - d[virtualPath] = null; - return null; - } - - if (!d.TryGetValue(appRelativePath, out debugPath)) - { - // (perf) concurrency with ReaderWriterLockSlim seems way faster than ConcurrentDictionary - using (_rwLock.GetUpgradeableReadLock()) - { - if (!_cachedDebugFilePaths.TryGetValue(appRelativePath, out debugPath)) - { - using (_rwLock.GetWriteLock()) - { - debugPath = FindDebugFile(appRelativePath, root); - _cachedDebugFilePaths[appRelativePath] = d[appRelativePath] = debugPath; - } - } - } - } - } - - return debugPath; - } - - private string FindDebugFile(string appRelativePath, string root) - { - if (_pluginsDebugDir == null) - return null; - - // strip "~/Plugins/" or "~/Themes/" - var unrooted = appRelativePath.Substring(root.Length); - - // either plugin or theme name - var extensionName = unrooted.Substring(0, unrooted.IndexOf('/')); - - // get "Views/Something/View.cshtml" - var relativePath = unrooted.Substring(extensionName.Length + 1); - - if (root == "~/Themes/") - { - var theme = _themeRegistry.GetThemeManifest(extensionName); - if (theme != null && theme.IsSymbolicLink) - { - // Linked theme folders cannot compute cache dependencies correctly when - // working with source paths. We must determine the link target path, - var finalPath = Path.Combine(theme.Path, relativePath.Replace('/', '\\')); - return File.Exists(finalPath) ? finalPath : null; - } - } - else - { - // Root is "~/Plugin/" - var foldersToCheck = new[] { extensionName, extensionName + "-sym" }; - - foreach (var folder in foldersToCheck) - { - var pluginDir = new DirectoryInfo(Path.Combine(_pluginsDebugDir.FullName, folder)); - if (pluginDir != null && pluginDir.Exists) - { - var result = Path.Combine(pluginDir.FullName, relativePath).Replace("/", "\\"); - return File.Exists(result) ? result : null; - } - } - } - - return null; - } - - private bool IsExtensionPath(string virtualPath, out string root, out string appRelativePath) - { - root = null; - appRelativePath = virtualPath; - - if (virtualPath != null && virtualPath.Length > 0 && virtualPath[0] != '~') - { - appRelativePath = VirtualPathUtility.ToAppRelative(virtualPath); - } - - if (appRelativePath.StartsWith("~/Plugins/", StringComparison.InvariantCultureIgnoreCase)) - { - root = "~/Plugins/"; - return true; - } - - if (appRelativePath.StartsWith("~/Themes/", StringComparison.InvariantCultureIgnoreCase)) - { - root = "~/Themes/"; - return true; - } - - return false; - } - } - - internal class DebugVirtualFile : VirtualFile, IFileDependencyProvider - { - public DebugVirtualFile(string virtualPath, string debugPath) - : base(virtualPath) - { - this.PhysicalPath = debugPath; - } - - public string PhysicalPath { get; } - - public override bool IsDirectory - { - get { return false; } - } - - public override Stream Open() - { - var fileStream = new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - return fileStream; - } - - public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) - { - mappedPaths.Add(PhysicalPath); - } - } + public abstract class SmartVirtualPathProvider : VirtualPathProvider + { + private readonly IThemeRegistry _themeRegistry; + private readonly Dictionary _cachedDebugFilePaths = new Dictionary(); + private readonly ContextState> _requestState = new ContextState>("PluginDebugViewVPP.RequestCache", () => new Dictionary()); + private readonly DirectoryInfo _pluginsDebugDir; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + protected static readonly bool _isDebug; + + static SmartVirtualPathProvider() + { + _isDebug = HttpContext.Current.IsDebuggingEnabled && CommonHelper.IsDevEnvironment; + } + + protected SmartVirtualPathProvider() + { + var appRootPath = HostingEnvironment.MapPath("~/").EnsureEndsWith("\\"); + + var pluginsDebugPath = Path.GetFullPath(Path.Combine(appRootPath, @"..\..\Plugins")); + if (Directory.Exists(pluginsDebugPath)) + { + _pluginsDebugDir = new DirectoryInfo(pluginsDebugPath); + } + + _themeRegistry = EngineContext.Current.Resolve(); + } + + protected internal string ResolveDebugFilePath(string virtualPath) + { + if (!_isDebug) + return null; + + // Two-Level caching: RequestCache > AppCache + var d = _requestState.GetState(); + + if (!d.TryGetValue(virtualPath, out var debugPath)) + { + if (!IsExtensionPath(virtualPath, out var root, out var appRelativePath)) + { + // don't query again in this request + d[virtualPath] = null; + return null; + } + + if (!d.TryGetValue(appRelativePath, out debugPath)) + { + // (perf) concurrency with ReaderWriterLockSlim seems way faster than ConcurrentDictionary + using (_rwLock.GetUpgradeableReadLock()) + { + if (!_cachedDebugFilePaths.TryGetValue(appRelativePath, out debugPath)) + { + using (_rwLock.GetWriteLock()) + { + debugPath = FindDebugFile(appRelativePath, root); + _cachedDebugFilePaths[appRelativePath] = d[appRelativePath] = debugPath; + } + } + } + } + } + + return debugPath; + } + + private string FindDebugFile(string appRelativePath, string root) + { + if (_pluginsDebugDir == null) + return null; + + // strip "~/Plugins/" or "~/Themes/" + var unrooted = appRelativePath.Substring(root.Length); + + // either plugin or theme name + var extensionName = unrooted.Substring(0, unrooted.IndexOf('/')); + + // get "Views/Something/View.cshtml" + var relativePath = unrooted.Substring(extensionName.Length + 1); + + if (root == "~/Themes/") + { + var theme = _themeRegistry.GetThemeManifest(extensionName); + if (theme != null && theme.IsSymbolicLink) + { + // Linked theme folders cannot compute cache dependencies correctly when + // working with source paths. We must determine the link target path, + var finalPath = Path.Combine(theme.Path, relativePath.Replace('/', '\\')); + return File.Exists(finalPath) ? finalPath : null; + } + } + else + { + // Root is "~/Plugin/" + var foldersToCheck = new[] { extensionName, extensionName + "-sym" }; + + foreach (var folder in foldersToCheck) + { + var pluginDir = new DirectoryInfo(Path.Combine(_pluginsDebugDir.FullName, folder)); + if (pluginDir != null && pluginDir.Exists) + { + var result = Path.Combine(pluginDir.FullName, relativePath).Replace("/", "\\"); + return File.Exists(result) ? result : null; + } + } + } + + return null; + } + + private bool IsExtensionPath(string virtualPath, out string root, out string appRelativePath) + { + root = null; + appRelativePath = virtualPath; + + if (virtualPath != null && virtualPath.Length > 0 && virtualPath[0] != '~') + { + appRelativePath = VirtualPathUtility.ToAppRelative(virtualPath); + } + + if (appRelativePath.StartsWith("~/Plugins/", StringComparison.InvariantCultureIgnoreCase)) + { + root = "~/Plugins/"; + return true; + } + + if (appRelativePath.StartsWith("~/Themes/", StringComparison.InvariantCultureIgnoreCase)) + { + root = "~/Themes/"; + return true; + } + + return false; + } + } + + internal class DebugVirtualFile : VirtualFile, IFileDependencyProvider + { + public DebugVirtualFile(string virtualPath, string debugPath) + : base(virtualPath) + { + this.PhysicalPath = debugPath; + } + + public string PhysicalPath { get; } + + public override bool IsDirectory => false; + + public override Stream Open() + { + var fileStream = new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + return fileStream; + } + + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + mappedPaths.Add(PhysicalPath); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs index ce5b268dbf..3349b0df00 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs @@ -10,15 +10,15 @@ namespace SmartStore.Web.Framework.Theming { public partial class ThemeContext : IThemeContext { - internal const string OverriddenThemeNameKey = "OverriddenThemeName"; - - private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; + internal const string OverriddenThemeNameKey = "OverriddenThemeName"; + + private readonly IWorkContext _workContext; + private readonly IStoreContext _storeContext; private readonly IGenericAttributeService _genericAttributeService; private readonly ThemeSettings _themeSettings; private readonly IThemeRegistry _themeRegistry; private readonly IMobileDeviceHelper _mobileDeviceHelper; - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; private bool _themeIsCached; private string _cachedThemeName; @@ -27,20 +27,20 @@ public partial class ThemeContext : IThemeContext public ThemeContext( IWorkContext workContext, - IStoreContext storeContext, + IStoreContext storeContext, IGenericAttributeService genericAttributeService, - ThemeSettings themeSettings, + ThemeSettings themeSettings, IThemeRegistry themeRegistry, IMobileDeviceHelper mobileDeviceHelper, - HttpContextBase httpContext) + HttpContextBase httpContext) { this._workContext = workContext; - this._storeContext = storeContext; + this._storeContext = storeContext; this._genericAttributeService = genericAttributeService; this._themeSettings = themeSettings; this._themeRegistry = themeRegistry; this._mobileDeviceHelper = mobileDeviceHelper; - this._httpContext = httpContext; + this._httpContext = httpContext; } public string WorkingThemeName @@ -52,24 +52,24 @@ public string WorkingThemeName return _cachedThemeName; } - var customer = _workContext.CurrentCustomer; + var customer = _workContext.CurrentCustomer; bool isUserSpecific = false; string theme = ""; if (_themeSettings.AllowCustomerToSelectTheme) { - if (_themeSettings.SaveThemeChoiceInCookie) - { - theme = _httpContext.GetUserThemeChoiceFromCookie(); - } - else - { - if (customer != null) - { - theme = customer.GetAttribute(SystemCustomerAttributeNames.WorkingThemeName, _genericAttributeService, _storeContext.CurrentStore.Id); - } - } - - isUserSpecific = theme.HasValue(); + if (_themeSettings.SaveThemeChoiceInCookie) + { + theme = _httpContext.GetUserThemeChoiceFromCookie(); + } + else + { + if (customer != null) + { + theme = customer.GetAttribute(SystemCustomerAttributeNames.WorkingThemeName, _genericAttributeService, _storeContext.CurrentStore.Id); + } + } + + isUserSpecific = theme.HasValue(); } // default store theme @@ -82,23 +82,23 @@ public string WorkingThemeName if (!_themeRegistry.ThemeManifestExists(theme)) { var manifest = _themeRegistry.GetThemeManifests().FirstOrDefault(); - if (manifest == null) - { - // no active theme in system. Throw! - throw Error.Application("At least one theme must be in active state, but the theme registry does not contain a valid theme package."); - } - theme = manifest.ThemeName; + if (manifest == null) + { + // no active theme in system. Throw! + throw Error.Application("At least one theme must be in active state, but the theme registry does not contain a valid theme package."); + } + theme = manifest.ThemeName; if (isUserSpecific) { - // the customer chosen theme does not exists (anymore). Invalidate it! - _httpContext.SetUserThemeChoiceInCookie(null); - if (customer != null) - { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.WorkingThemeName, string.Empty, _storeContext.CurrentStore.Id); - } + // the customer chosen theme does not exists (anymore). Invalidate it! + _httpContext.SetUserThemeChoiceInCookie(null); + if (customer != null) + { + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.WorkingThemeName, string.Empty, _storeContext.CurrentStore.Id); + } } } - + // cache theme this._cachedThemeName = theme; this._themeIsCached = true; @@ -109,96 +109,96 @@ public string WorkingThemeName if (!_themeSettings.AllowCustomerToSelectTheme) return; - if (value.HasValue() && !_themeRegistry.ThemeManifestExists(value)) - return; + if (value.HasValue() && !_themeRegistry.ThemeManifestExists(value)) + return; - _httpContext.SetUserThemeChoiceInCookie(value.NullEmpty()); + _httpContext.SetUserThemeChoiceInCookie(value.NullEmpty()); - if (_workContext.CurrentCustomer != null) - { - _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.WorkingThemeName, value.EmptyNull(), _storeContext.CurrentStore.Id); - } + if (_workContext.CurrentCustomer != null) + { + _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.WorkingThemeName, value.EmptyNull(), _storeContext.CurrentStore.Id); + } // clear cache this._themeIsCached = false; } } - public void SetRequestTheme(string theme) - { - try - { - var dataTokens = _httpContext.Request.RequestContext.RouteData.DataTokens; - if (theme.HasValue()) - { - dataTokens[OverriddenThemeNameKey] = theme; - } - else if (dataTokens.ContainsKey(OverriddenThemeNameKey)) - { - dataTokens.Remove(OverriddenThemeNameKey); - } - - _currentTheme = null; - } - catch { } - } - - public string GetRequestTheme() - { - try - { - return (string)_httpContext.Request?.RequestContext?.RouteData?.DataTokens[OverriddenThemeNameKey]; - } - catch - { - return null; - } - } - - public void SetPreviewTheme(string theme) - { - try - { - _httpContext.SetPreviewModeValue(OverriddenThemeNameKey, theme); - _currentTheme = null; - } - catch { } - } - - public string GetPreviewTheme() - { - try - { - var cookie = _httpContext.GetPreviewModeCookie(false); - if (cookie != null) - { - return cookie.Values[OverriddenThemeNameKey]; - } - - return null; - } - catch - { - return null; - } - } + public void SetRequestTheme(string theme) + { + try + { + var dataTokens = _httpContext.Request.RequestContext.RouteData.DataTokens; + if (theme.HasValue()) + { + dataTokens[OverriddenThemeNameKey] = theme; + } + else if (dataTokens.ContainsKey(OverriddenThemeNameKey)) + { + dataTokens.Remove(OverriddenThemeNameKey); + } + + _currentTheme = null; + } + catch { } + } + + public string GetRequestTheme() + { + try + { + return (string)_httpContext.Request?.RequestContext?.RouteData?.DataTokens[OverriddenThemeNameKey]; + } + catch + { + return null; + } + } + + public void SetPreviewTheme(string theme) + { + try + { + _httpContext.SetPreviewModeValue(OverriddenThemeNameKey, theme); + _currentTheme = null; + } + catch { } + } + + public string GetPreviewTheme() + { + try + { + var cookie = _httpContext.GetPreviewModeCookie(false); + if (cookie != null) + { + return cookie.Values[OverriddenThemeNameKey]; + } + + return null; + } + catch + { + return null; + } + } public ThemeManifest CurrentTheme { get { - if (_currentTheme == null) + if (_currentTheme == null) { - var themeOverride = GetRequestTheme() ?? GetPreviewTheme(); - if (themeOverride != null) - { - // the theme to be used can be overwritten on request/session basis (e.g. for live preview, editing etc.) - _currentTheme = _themeRegistry.GetThemeManifest(themeOverride); - } - else - { - _currentTheme = _themeRegistry.GetThemeManifest(this.WorkingThemeName); - } + var themeOverride = GetRequestTheme() ?? GetPreviewTheme(); + if (themeOverride != null) + { + // the theme to be used can be overwritten on request/session basis (e.g. for live preview, editing etc.) + _currentTheme = _themeRegistry.GetThemeManifest(themeOverride); + } + else + { + _currentTheme = _themeRegistry.GetThemeManifest(this.WorkingThemeName); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs index 074b0e48cb..bb16495453 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs @@ -15,215 +15,206 @@ namespace SmartStore.Web.Framework.Theming { internal static class ThemeHelper { - private static readonly Regex s_inheritableThemeFilePattern; - private static readonly Regex s_themeVarsPattern; - private static readonly Regex s_moduleImportsPattern; - private static readonly Regex s_extensionlessPathPattern; - - public static readonly string ThemesBasePath; - - static ThemeHelper() - { - ThemesBasePath = CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/").EnsureEndsWith("/"); - - var pattern = @"^{0}(.*)/(.+)(\.)(png|gif|jpg|jpeg|css|scss|js|cshtml|svg|json|liquid)(\?base)*$".FormatInvariant(ThemesBasePath); - s_inheritableThemeFilePattern = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - s_themeVarsPattern = new Regex(@"\.(db|app)/[_]?themevars(.scss)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - s_moduleImportsPattern = new Regex(@"\.app/[_]?moduleimports.scss", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - s_extensionlessPathPattern = new Regex(@"~/(.+)/([^/.]*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - } - - internal static IEnumerable RemoveVirtualImports(IEnumerable virtualPathDependencies) - { - Guard.NotNull(virtualPathDependencies, nameof(virtualPathDependencies)); - - // determine the virtual themevars scss import reference - var themeVarsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsThemeVars(x)).FirstOrDefault(); - var moduleImportsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsModuleImports(x)).FirstOrDefault(); - - if (themeVarsFile == null && moduleImportsFile == null) - { - // no themevars or moduleimports import... so no special considerations here - return virtualPathDependencies; - } - - // exclude the special imports from the file dependencies list, - // 'cause this one cannot be monitored by the physical file system - return virtualPathDependencies - .Except((new string[] { themeVarsFile, moduleImportsFile }) - .Where(x => x.HasValue())) - .ToArray(); - } + private static readonly Regex s_inheritableThemeFilePattern; + private static readonly Regex s_themeVarsPattern; + private static readonly Regex s_moduleImportsPattern; + private static readonly Regex s_extensionlessPathPattern; + + public static readonly string ThemesBasePath; + + static ThemeHelper() + { + ThemesBasePath = CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/").EnsureEndsWith("/"); + + var pattern = @"^{0}(.*)/(.+)(\.)(png|gif|jpg|jpeg|css|scss|js|cshtml|svg|json|liquid)(\?base)*$".FormatInvariant(ThemesBasePath); + s_inheritableThemeFilePattern = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + s_themeVarsPattern = new Regex(@"\.(db|app)/[_]?themevars(.scss)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + s_moduleImportsPattern = new Regex(@"\.app/[_]?moduleimports.scss", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + s_extensionlessPathPattern = new Regex(@"~/(.+)/([^/.]*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + + internal static IEnumerable RemoveVirtualImports(IEnumerable virtualPathDependencies) + { + Guard.NotNull(virtualPathDependencies, nameof(virtualPathDependencies)); + + // determine the virtual themevars scss import reference + var themeVarsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsThemeVars(x)).FirstOrDefault(); + var moduleImportsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsModuleImports(x)).FirstOrDefault(); + + if (themeVarsFile == null && moduleImportsFile == null) + { + // no themevars or moduleimports import... so no special considerations here + return virtualPathDependencies; + } + + // exclude the special imports from the file dependencies list, + // 'cause this one cannot be monitored by the physical file system + return virtualPathDependencies + .Except((new string[] { themeVarsFile, moduleImportsFile }) + .Where(x => x.HasValue())) + .ToArray(); + } internal static bool PathIsThemeVars(string virtualPath) { - string extension = null; - return PathIsThemeVars(virtualPath, out extension); + string extension = null; + return PathIsThemeVars(virtualPath, out extension); + } + + internal static bool PathIsThemeVars(string virtualPath, out string extension) + { + extension = null; + + if (string.IsNullOrEmpty(virtualPath)) + return false; + + var match = s_themeVarsPattern.Match(virtualPath); + if (match.Success) + { + extension = match.Groups[2].Value; + return true; + } + + return false; + } + + internal static bool PathIsModuleImports(string virtualPath) + { + if (string.IsNullOrEmpty(virtualPath)) + return false; + + return s_moduleImportsPattern.IsMatch(virtualPath); + } + + internal static bool PathIsInheritableThemeFile(string virtualPath) + { + if (string.IsNullOrWhiteSpace(virtualPath)) + return false; + + if (virtualPath[0] != '~') + { + virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); + } + + return s_inheritableThemeFilePattern.IsMatch(virtualPath); + } + + internal static bool IsAdminArea() + { + if (HttpContext.Current != null) + { + return HttpContext.Current.Request.IsAdminArea(); + } + + return false; + } + + internal static bool IsStyleValidationRequest() + { + return HttpContext.Current?.Request?.QueryString["validate"] != null; + } + + internal static StyleSheetResult IsStyleSheet(string path) + { + // Handle virtual Sass imports with '?base' query + // TBD: (mc) other query params could exist + var qindex = path.IndexOf('?'); + if (qindex > -1) + { + var pathWithoutQuery = path.Substring(0, qindex); + var query = path.Substring(pathWithoutQuery.Length); + if (query.StartsWith("?base", StringComparison.OrdinalIgnoreCase)) + { + // Process again, this time without query + var result = IsStyleSheet(pathWithoutQuery); + result.IsBaseImport = true; + return result; + } + } + + var extension = Path.GetExtension(path).ToLowerInvariant(); + + if (extension == ".cshtml") + { + // Perf + return null; + } + else if (extension == ".css") + { + return new StyleSheetResult { Path = path, IsCss = true, Extension = extension }; + } + else if (extension == ".scss" || extension == ".sass") + { + return new StyleSheetResult { Path = path, IsSass = true, Extension = extension }; + } + else if (extension.IsEmpty()) + { + if (path.Contains("/scss/")) + { + // Bootstrap and other libaries may import SASS files without extension + return new StyleSheetResult { Path = path, IsSass = true }; + } + + // StyleBundles are extension-less, so we have to ask 'BundleTable' + // if a style bundle has been registered for the given path. + if (s_extensionlessPathPattern.IsMatch(path)) + { + var bundle = BundleTable.Bundles.GetBundleFor(path); + if (bundle != null && ((bundle is SmartStyleBundle || bundle is StyleBundle))) + { + return new StyleSheetResult { Path = path, IsBundle = true }; + } + } + } + + return null; } - internal static bool PathIsThemeVars(string virtualPath, out string extension) - { - extension = null; - - if (string.IsNullOrEmpty(virtualPath)) - return false; - - var match = s_themeVarsPattern.Match(virtualPath); - if (match.Success) - { - extension = match.Groups[2].Value; - return true; - } - - return false; - } - - internal static bool PathIsModuleImports(string virtualPath) - { - if (string.IsNullOrEmpty(virtualPath)) - return false; - - return s_moduleImportsPattern.IsMatch(virtualPath); - } - - internal static bool PathIsInheritableThemeFile(string virtualPath) - { - if (string.IsNullOrWhiteSpace(virtualPath)) - return false; - - if (virtualPath[0] != '~') - { - virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); - } - - return s_inheritableThemeFilePattern.IsMatch(virtualPath); - } - - internal static bool IsAdminArea() - { - if (HttpContext.Current != null) - { - return HttpContext.Current.Request.IsAdminArea(); - } - - return false; - } - - internal static bool IsStyleValidationRequest() - { - return HttpContext.Current?.Request?.QueryString["validate"] != null; - } - - internal static StyleSheetResult IsStyleSheet(string path) - { - // Handle virtual Sass imports with '?base' query - // TBD: (mc) other query params could exist - var qindex = path.IndexOf('?'); - if (qindex > -1) - { - var pathWithoutQuery = path.Substring(0, qindex); - var query = path.Substring(pathWithoutQuery.Length); - if (query.StartsWith("?base", StringComparison.OrdinalIgnoreCase)) - { - // Process again, this time without query - var result = IsStyleSheet(pathWithoutQuery); - result.IsBaseImport = true; - return result; - } - } - - var extension = Path.GetExtension(path).ToLowerInvariant(); - - if (extension == ".cshtml") - { - // Perf - return null; - } - else if (extension == ".css") - { - return new StyleSheetResult { Path = path, IsCss = true, Extension = extension }; - } - else if (extension == ".scss" || extension == ".sass") - { - return new StyleSheetResult { Path = path, IsSass = true, Extension = extension }; - } - else if (extension.IsEmpty()) - { - if (path.Contains("/scss/")) - { - // Bootstrap and other libaries may import SASS files without extension - return new StyleSheetResult { Path = path, IsSass = true }; - } - - // StyleBundles are extension-less, so we have to ask 'BundleTable' - // if a style bundle has been registered for the given path. - if (s_extensionlessPathPattern.IsMatch(path)) - { - var bundle = BundleTable.Bundles.GetBundleFor(path); - if (bundle != null && ((bundle is SmartStyleBundle || bundle is StyleBundle))) - { - return new StyleSheetResult { Path = path, IsBundle = true }; - } - } - } - - return null; - } - - internal static ThemeManifest ResolveCurrentTheme() - { - return EngineContext.Current.Resolve().CurrentTheme; - } - - internal static int ResolveCurrentStoreId() - { - return EngineContext.Current.Resolve().CurrentStore.Id; - } - - internal static string TokenizePath(string virtualPath, out string themeName, out string relativePath, out string query) - { - themeName = null; - relativePath = null; - query = null; - - var unrooted = virtualPath.Substring(ThemesBasePath.Length); // strip "~/Themes/" - themeName = unrooted.Substring(0, unrooted.IndexOf('/')); - relativePath = unrooted.Substring(themeName.Length + 1); - - var idx = relativePath.IndexOf('?'); - if (idx > 0) - { - query = relativePath.Substring(idx + 1); - relativePath = relativePath.Substring(0, idx); - } - - // strip out query - return "{0}{1}/{2}".FormatCurrent(ThemesBasePath, themeName, relativePath); - } - } - - internal class StyleSheetResult - { - public string Path { get; set; } - public string Extension { get; set; } - public bool IsCss { get; set; } - public bool IsSass { get; set; } - public bool IsBundle { get; set; } - public bool IsBaseImport { get; set; } - - public bool IsPreprocessor - { - get { return IsSass; } - } - - public bool IsThemeVars - { - get { return IsPreprocessor && ThemeHelper.PathIsThemeVars(Path); } - } - - public bool IsModuleImports - { - get { return IsSass && ThemeHelper.PathIsModuleImports(Path); } - } - } + internal static ThemeManifest ResolveCurrentTheme() + { + return EngineContext.Current.Resolve().CurrentTheme; + } + + internal static int ResolveCurrentStoreId() + { + return EngineContext.Current.Resolve().CurrentStore.Id; + } + + internal static string TokenizePath(string virtualPath, out string themeName, out string relativePath, out string query) + { + themeName = null; + relativePath = null; + query = null; + + var unrooted = virtualPath.Substring(ThemesBasePath.Length); // strip "~/Themes/" + themeName = unrooted.Substring(0, unrooted.IndexOf('/')); + relativePath = unrooted.Substring(themeName.Length + 1); + + var idx = relativePath.IndexOf('?'); + if (idx > 0) + { + query = relativePath.Substring(idx + 1); + relativePath = relativePath.Substring(0, idx); + } + + // strip out query + return "{0}{1}/{2}".FormatCurrent(ThemesBasePath, themeName, relativePath); + } + } + + internal class StyleSheetResult + { + public string Path { get; set; } + public string Extension { get; set; } + public bool IsCss { get; set; } + public bool IsSass { get; set; } + public bool IsBundle { get; set; } + public bool IsBaseImport { get; set; } + + public bool IsPreprocessor => IsSass; + + public bool IsThemeVars => IsPreprocessor && ThemeHelper.PathIsThemeVars(Path); + + public bool IsModuleImports => IsSass && ThemeHelper.PathIsModuleImports(Path); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs index f70718141f..2933507b6a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs @@ -1,19 +1,15 @@ -using System; -using System.Collections.Generic; +using System.IO; using System.Linq; -using System.IO; using System.Text; using System.Web.Mvc; using System.Web.Mvc.Html; using SmartStore.Core; using SmartStore.Core.Infrastructure; using SmartStore.Core.Themes; -using SmartStore.Services.Common; -using SmartStore.Utilities; using SmartStore.Services.Localization; namespace SmartStore.Web.Framework.Theming -{ +{ public static class ThemeHtmlExtensions { @@ -30,110 +26,110 @@ public static MvcHtmlString ThemeVarLabel(this HtmlHelper html, ThemeVariableInf var displayName = locService.GetResource(resKey, langId, false, "", true); - if (displayName.HasValue() && hint.IsEmpty()) - { - hint = locService.GetResource(resKey + ".Hint", langId, false, "", true); - hint = "${0}{1}".FormatInvariant(info.Name, hint.HasValue() ? "\n" + hint : ""); - } + if (displayName.HasValue() && hint.IsEmpty()) + { + hint = locService.GetResource(resKey + ".Hint", langId, false, "", true); + hint = "${0}{1}".FormatInvariant(info.Name, hint.HasValue() ? "\n" + hint : ""); + } result.Append("
"); result.Append(html.Label(html.NameForThemeVar(info), displayName.NullEmpty() ?? "$" + info.Name, new { @class = "x-col-form-label" })); - if (hint.HasValue()) - { - result.Append(html.Hint(hint).ToHtmlString()); - } + if (hint.HasValue()) + { + result.Append(html.Hint(hint).ToHtmlString()); + } result.Append("
"); return MvcHtmlString.Create(result.ToString()); } - + public static MvcHtmlString ThemeVarEditor(this HtmlHelper html, ThemeVariableInfo info, object value) { Guard.NotNull(info, "info"); string expression = html.NameForThemeVar(info); - var strValue = string.Empty; + var strValue = string.Empty; - var arrValue = value as string[]; - if (arrValue != null) - { - strValue = arrValue.Length > 0 ? arrValue[0] : value.ToString(); - } - else - { - strValue = value.ToString(); - } + var arrValue = value as string[]; + if (arrValue != null) + { + strValue = arrValue.Length > 0 ? arrValue[0] : value.ToString(); + } + else + { + strValue = value.ToString(); + } - var isDefault = strValue.IsCaseInsensitiveEqual(info.DefaultValue); - var isValidColor = info.Type == ThemeVariableType.Color - && ((strValue.HasValue() && ThemeVarsRepository.IsValidColor(strValue)) || (strValue.IsEmpty() && ThemeVarsRepository.IsValidColor(info.DefaultValue))); + var isDefault = strValue.IsCaseInsensitiveEqual(info.DefaultValue); + var isValidColor = info.Type == ThemeVariableType.Color + && ((strValue.HasValue() && ThemeVarsRepository.IsValidColor(strValue)) || (strValue.IsEmpty() && ThemeVarsRepository.IsValidColor(info.DefaultValue))); - MvcHtmlString control; + MvcHtmlString control; if (isValidColor) { - control = html.ColorBox(expression, strValue, info.DefaultValue); + control = html.ColorBox(expression, strValue, info.DefaultValue); } else if (info.Type == ThemeVariableType.Boolean) { - var locService = EngineContext.Current.Resolve(); - control = html.CheckBox(expression, strValue.ToBool()); - var custom = "".FormatInvariant( - control.ToString(), - locService.GetResource("Common.On").Truncate(3), - locService.GetResource("Common.Off").Truncate(3)); - - control = MvcHtmlString.Create(custom); + var locService = EngineContext.Current.Resolve(); + control = html.CheckBox(expression, strValue.ToBool()); + var custom = "".FormatInvariant( + control.ToString(), + locService.GetResource("Common.On").Truncate(3), + locService.GetResource("Common.Off").Truncate(3)); + + control = MvcHtmlString.Create(custom); + } + else if (info.Type == ThemeVariableType.Select) + { + control = ThemeVarSelectEditor(html, info, expression, strValue); + } + else + { + control = html.TextBox(expression, isDefault ? "" : strValue, new { placeholder = info.DefaultValue, @class = "form-control" }); } - else if (info.Type == ThemeVariableType.Select) - { - control = ThemeVarSelectEditor(html, info, expression, strValue); - } - else - { - control = html.TextBox(expression, isDefault ? "" : strValue, new { placeholder = info.DefaultValue, @class = "form-control" }); - } - return control; - } + return control; + } - public static MvcHtmlString ThemeVarChainInfo(this HtmlHelper html, ThemeVariableInfo info) - { - Guard.NotNull(info, "info"); + public static MvcHtmlString ThemeVarChainInfo(this HtmlHelper html, ThemeVariableInfo info) + { + Guard.NotNull(info, "info"); - var currentTheme = ThemeHelper.ResolveCurrentTheme(); + var currentTheme = ThemeHelper.ResolveCurrentTheme(); - if (currentTheme != info.Manifest) - { - // the variable is inherited from a base theme: display an info badge - var chainInfo = "{0}".FormatCurrent(info.Manifest.ThemeName); - return MvcHtmlString.Create(chainInfo); - } + if (currentTheme != info.Manifest) + { + // the variable is inherited from a base theme: display an info badge + var chainInfo = "{0}".FormatCurrent(info.Manifest.ThemeName); + return MvcHtmlString.Create(chainInfo); + } - return MvcHtmlString.Empty; - } + return MvcHtmlString.Empty; + } - private static MvcHtmlString ThemeVarSelectEditor(HtmlHelper html, ThemeVariableInfo info, string expression, string value) + private static MvcHtmlString ThemeVarSelectEditor(HtmlHelper html, ThemeVariableInfo info, string expression, string value) { - var manifest = info.Manifest; + var manifest = info.Manifest; if (!manifest.Selects.ContainsKey(info.SelectRef)) { throw new SmartException("A select list with id '{0}' was not specified. Please specify a 'Select' element with at least one 'Option' child.", info.SelectRef); } - //var isDefault = value.IsCaseInsensitiveEqual(info.DefaultValue); + //var isDefault = value.IsCaseInsensitiveEqual(info.DefaultValue); var selectList = from x in manifest.Selects[info.SelectRef] - select new SelectListItem - { - Value = x, + select new SelectListItem + { + Value = x, Text = x, // TODO: (mc) Localize - Selected = x.IsCaseInsensitiveEqual(value) + Selected = x.IsCaseInsensitiveEqual(value) }; - return html.DropDownList(expression, selectList, new { placeholder = info.DefaultValue, @class = "form-control" }); + return html.DropDownList(expression, selectList, new { placeholder = info.DefaultValue, @class = "form-control" }); } public static string NameForThemeVar(this HtmlHelper html, ThemeVariableInfo info) @@ -166,9 +162,9 @@ public static string ThemeAwareContent(this UrlHelper url, string path) public static string ThemeAwareContent(this UrlHelper url, ThemeManifest manifest, string path) { path = EnsurePath(path); - + string fullPath = Path.Combine(manifest.Path, path); - if (File.Exists(fullPath)) + if (File.Exists(fullPath)) { return url.Content(manifest.Location + "/" + Path.Combine(manifest.ThemeName, path)); } @@ -178,7 +174,7 @@ public static string ThemeAwareContent(this UrlHelper url, ThemeManifest manifes { return url.Content("~/" + path); } - + return string.Empty; } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs index 29cc34de82..255a417b68 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs @@ -3,24 +3,23 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Web; using SmartStore.Core.Infrastructure; using SmartStore.Services.Themes; -using System.Web; -using System.IO; namespace SmartStore.Web.Framework.Theming -{ +{ internal class ThemeVarsRepository { private static readonly Regex s_keyWhitelist = new Regex(@"^[a-zA-Z0-9_-]+$", RegexOptions.Compiled); - private static readonly Regex s_valueBlacklist = new Regex(@"[:;]+", RegexOptions.Compiled); - private static readonly Regex s_valueSassVars = new Regex(@"[$][a-zA-Z0-9_-]+", RegexOptions.Compiled); - private static readonly Regex s_valueLessVars = new Regex(@"[@][a-zA-Z0-9_-]+", RegexOptions.Compiled); - //private static readonly Regex s_valueWhitelist = new Regex(@"^[#@]?[a-zA-Z0-9""' _\.,-]*$"); + private static readonly Regex s_valueBlacklist = new Regex(@"[:;]+", RegexOptions.Compiled); + private static readonly Regex s_valueSassVars = new Regex(@"[$][a-zA-Z0-9_-]+", RegexOptions.Compiled); + private static readonly Regex s_valueLessVars = new Regex(@"[@][a-zA-Z0-9_-]+", RegexOptions.Compiled); + //private static readonly Regex s_valueWhitelist = new Regex(@"^[#@]?[a-zA-Z0-9""' _\.,-]*$"); - const string SassVarPrefix = "$"; + const string SassVarPrefix = "$"; - public string GetPreprocessorCss(string extension, string themeName, int storeId) + public string GetPreprocessorCss(string extension, string themeName, int storeId) { Guard.NotEmpty(themeName, nameof(themeName)); Guard.IsPositive(storeId, nameof(storeId)); @@ -35,7 +34,7 @@ private IDictionary GetVariables(string themeName, int storeId) { var result = new Dictionary(); - var rawVars = this.GetRawVariables(themeName, storeId); + var rawVars = this.GetRawVariables(themeName, storeId); foreach (var v in rawVars) { @@ -46,13 +45,13 @@ private IDictionary GetVariables(string themeName, int storeId) string value = v.Value.ToString(); - if (s_valueBlacklist.IsMatch(value)) - continue; + if (s_valueBlacklist.IsMatch(value)) + continue; - //if (!s_valueWhitelist.IsMatch(value)) - // continue; + //if (!s_valueWhitelist.IsMatch(value)) + // continue; - result.Add(key, value); + result.Add(key, value); } return result; @@ -60,75 +59,75 @@ private IDictionary GetVariables(string themeName, int storeId) internal virtual ExpandoObject GetRawVariables(string themeName, int storeId) { - // we need the Asp.Net cache here in order to define cacheKey dependencies - bool validationMode = ThemeHelper.IsStyleValidationRequest(); - - if (validationMode) - { - // Return uncached fresh data (the variables is not nuked yet) - return GetRawVariablesCore(themeName, storeId); - } - else - { - string cacheKey = FrameworkCacheConsumer.BuildThemeVarsCacheKey(themeName, storeId); - return HttpRuntime.Cache.GetOrAdd(cacheKey, () => - { - return GetRawVariablesCore(themeName, storeId); - }); - } + // we need the Asp.Net cache here in order to define cacheKey dependencies + bool validationMode = ThemeHelper.IsStyleValidationRequest(); + + if (validationMode) + { + // Return uncached fresh data (the variables is not nuked yet) + return GetRawVariablesCore(themeName, storeId); + } + else + { + string cacheKey = FrameworkCacheConsumer.BuildThemeVarsCacheKey(themeName, storeId); + return HttpRuntime.Cache.GetOrAdd(cacheKey, () => + { + return GetRawVariablesCore(themeName, storeId); + }); + } + } + + private ExpandoObject GetRawVariablesCore(string themeName, int storeId) + { + var themeVarService = EngineContext.Current.Resolve(); + return themeVarService.GetThemeVariables(themeName, storeId) ?? new ExpandoObject(); + } + + private string Transform(IDictionary parameters) + { + if (parameters.Count == 0) + return string.Empty; + + var prefix = SassVarPrefix; + + var sb = new StringBuilder(); + foreach (var parameter in parameters.Where(kvp => kvp.Value.HasValue())) + { + sb.AppendFormat("{0}{1}: {2};\n", prefix, parameter.Key, parameter.Value); + } + + return sb.ToString(); } - private ExpandoObject GetRawVariablesCore(string themeName, int storeId) - { - var themeVarService = EngineContext.Current.Resolve(); - return themeVarService.GetThemeVariables(themeName, storeId) ?? new ExpandoObject(); - } - - private string Transform(IDictionary parameters) - { - if (parameters.Count == 0) - return string.Empty; - - var prefix = SassVarPrefix; - - var sb = new StringBuilder(); - foreach (var parameter in parameters.Where(kvp => kvp.Value.HasValue())) - { - sb.AppendFormat("{0}{1}: {2};\n", prefix, parameter.Key, parameter.Value); - } - - return sb.ToString(); - } - - /// - /// Checks whether the passed SASS value is a valid, displayable HTML color, - /// e.g.: "lighten($red, 20%)" would return false. - /// - /// The SASS value to test - /// - /// We need this helper during theme configuration: we just can't render - /// color pickers for color values containing var references or SASS functions. - /// - internal static bool IsValidColor(string value) - { - if (value.IsEmpty()) - { - return true; - } - - if (s_valueSassVars.IsMatch(value)) - { - return false; - } - - if (value[0] == '#' || value.StartsWith("rgb(") || value.StartsWith("rgba(") || value.StartsWith("hsl(") || value.StartsWith("hsla(")) - { - return true; - } - - // Let pass all color names (red, blue etc.), but reject functions, e.g. "lighten(#fff, 10%)" - return !value.Contains("("); - } - } + /// + /// Checks whether the passed SASS value is a valid, displayable HTML color, + /// e.g.: "lighten($red, 20%)" would return false. + /// + /// The SASS value to test + /// + /// We need this helper during theme configuration: we just can't render + /// color pickers for color values containing var references or SASS functions. + /// + internal static bool IsValidColor(string value) + { + if (value.IsEmpty()) + { + return true; + } + + if (s_valueSassVars.IsMatch(value)) + { + return false; + } + + if (value[0] == '#' || value.StartsWith("rgb(") || value.StartsWith("rgba(") || value.StartsWith("hsl(") || value.StartsWith("hsla(")) + { + return true; + } + + // Let pass all color names (red, blue etc.), but reject functions, e.g. "lighten(#fff, 10%)" + return !value.Contains("("); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs index b4bd7ab49b..9ac6d1fe4c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs @@ -12,24 +12,21 @@ public ThemeVarsVirtualFile(string virtualPath, string themeName, int storeId) { } - internal ThemeVarsVirtualFile(string virtualPath, string extension, string themeName, int storeId) - : base(virtualPath) - { - Extension = extension; - ThemeName = themeName; - StoreId = storeId; - } - - public override bool IsDirectory + internal ThemeVarsVirtualFile(string virtualPath, string extension, string themeName, int storeId) + : base(virtualPath) { - get { return false; } + Extension = extension; + ThemeName = themeName; + StoreId = storeId; } - public string Extension { get; private set; } - public string ThemeName { get; private set; } - public int StoreId { get; private set; } + public override bool IsDirectory => false; - public override Stream Open() + public string Extension { get; private set; } + public string ThemeName { get; private set; } + public int StoreId { get; private set; } + + public override Stream Open() { var repo = new ThemeVarsRepository(); @@ -43,19 +40,19 @@ public override Stream Open() private Stream GenerateStreamFromString(string value) { var stream = new MemoryStream(); - - using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true)) - { - writer.Write(value); - writer.Flush(); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } + + using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true)) + { + writer.Write(value); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } } - public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) - { - cacheKeys.Add(FrameworkCacheConsumer.BuildThemeVarsCacheKey(ThemeName, StoreId)); - } - } + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + cacheKeys.Add(FrameworkCacheConsumer.BuildThemeVarsCacheKey(ThemeName, StoreId)); + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs index de832a6cd2..bc0fd04532 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs @@ -5,52 +5,52 @@ namespace SmartStore.Web.Framework.Theming { - public class ThemeableRazorViewEngine : ThemeableVirtualPathProviderViewEngine - { - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - public ThemeableRazorViewEngine() - { - var areaBasePathsSetting = CommonHelper.GetAppSetting("sm:AreaBasePaths", "~/Plugins/"); - var areaBasePaths = areaBasePathsSetting.Split(',').Select(x => x.Trim().EnsureEndsWith("/")).ToArray(); - - // 0: view, 1: controller, 2: area - var areaFormats = new[] - { - "{2}/Views/{1}/{0}", - "{2}/Views/Shared/{0}" - }; - var areaLocationFormats = areaBasePaths.SelectMany(x => areaFormats.Select(f => x + f)); - - AreaViewLocationFormats = ExpandLocationFormats(areaLocationFormats, ViewType.Layout).ToArray(); - AreaMasterLocationFormats = ExpandLocationFormats(areaLocationFormats, ViewType.Layout).ToArray(); - AreaPartialViewLocationFormats = ExpandLocationFormats(areaLocationFormats, ViewType.Partial).ToArray(); - - // 0: view, 1: controller, 2: theme - var locationFormats = new[] + public class ThemeableRazorViewEngine : ThemeableVirtualPathProviderViewEngine + { + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + public ThemeableRazorViewEngine() + { + var areaBasePathsSetting = CommonHelper.GetAppSetting("sm:AreaBasePaths", "~/Plugins/"); + var areaBasePaths = areaBasePathsSetting.Split(',').Select(x => x.Trim().EnsureEndsWith("/")).ToArray(); + + // 0: view, 1: controller, 2: area + var areaFormats = new[] { - "~/Themes/{2}/Views/{1}/{0}", - "~/Views/{1}/{0}", - "~/Themes/{2}/Views/Shared/{0}", - "~/Views/Shared/{0}" - }; + "{2}/Views/{1}/{0}", + "{2}/Views/Shared/{0}" + }; + var areaLocationFormats = areaBasePaths.SelectMany(x => areaFormats.Select(f => x + f)); + + AreaViewLocationFormats = ExpandLocationFormats(areaLocationFormats, ViewType.Layout).ToArray(); + AreaMasterLocationFormats = ExpandLocationFormats(areaLocationFormats, ViewType.Layout).ToArray(); + AreaPartialViewLocationFormats = ExpandLocationFormats(areaLocationFormats, ViewType.Partial).ToArray(); + + // 0: view, 1: controller, 2: theme + var locationFormats = new[] + { + "~/Themes/{2}/Views/{1}/{0}", + "~/Views/{1}/{0}", + "~/Themes/{2}/Views/Shared/{0}", + "~/Views/Shared/{0}" + }; ViewLocationFormats = ExpandLocationFormats(locationFormats, ViewType.Layout).ToArray(); MasterLocationFormats = ExpandLocationFormats(locationFormats, ViewType.Layout).ToArray(); - PartialViewLocationFormats = ExpandLocationFormats(locationFormats, ViewType.Partial).ToArray(); - - FileExtensions = EnableVbViews - ? new[] { "cshtml", "vbhtml" } - : new[] { "cshtml" }; - } - - protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) - { - return new RazorView(controllerContext, partialPath, null, false, base.FileExtensions, base.ViewPageActivator); - } - - protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) - { - return new RazorView(controllerContext, viewPath, masterPath, true, base.FileExtensions, base.ViewPageActivator); - } - } + PartialViewLocationFormats = ExpandLocationFormats(locationFormats, ViewType.Partial).ToArray(); + + FileExtensions = EnableVbViews + ? new[] { "cshtml", "vbhtml" } + : new[] { "cshtml" }; + } + + protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) + { + return new RazorView(controllerContext, partialPath, null, false, base.FileExtensions, base.ViewPageActivator); + } + + protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) + { + return new RazorView(controllerContext, viewPath, masterPath, true, base.FileExtensions, base.ViewPageActivator); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs index d7b0e936d2..7bfa8c1161 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs @@ -13,487 +13,481 @@ namespace SmartStore.Web.Framework.Theming { - public abstract class ThemeableVirtualPathProviderViewEngine : BuildManagerViewEngine - { - internal Func GetExtensionThunk = VirtualPathUtility.GetExtension; + public abstract class ThemeableVirtualPathProviderViewEngine : BuildManagerViewEngine + { + internal Func GetExtensionThunk = VirtualPathUtility.GetExtension; - private static readonly string[] _emptyLocations = new string[0]; + private static readonly string[] _emptyLocations = new string[0]; - private static bool? _enableLocalizedViews; + private static bool? _enableLocalizedViews; private static bool? _enableVbViews; private readonly string _cacheKeyType = typeof(ThemeableRazorViewEngine).Name; - private readonly string _cacheKeyEntry = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}"; - - protected ThemeableVirtualPathProviderViewEngine() - { - this.ViewLocationCache = new TwoLevelViewLocationCache(); - - // prepare localized mobile & desktop display modes - DisplayModeProvider.Modes.Clear(); - var mobileDisplayMode = new LocalizedDisplayMode(DisplayModeProvider.MobileDisplayModeId, EnableLocalizedViews) - { - ContextCondition = IsMobileDevice - }; - var desktopDisplayMode = new LocalizedDisplayMode(DisplayModeProvider.DefaultDisplayModeId, EnableLocalizedViews); - - DisplayModeProvider.Modes.Add(mobileDisplayMode); - DisplayModeProvider.Modes.Add(desktopDisplayMode); - } - - public static bool EnableLocalizedViews - { - get - { - if (!_enableLocalizedViews.HasValue) - { - _enableLocalizedViews = CommonHelper.GetAppSetting("sm:EnableLocalizedViews", false); - } - - return _enableLocalizedViews.Value; - } - set - { - _enableLocalizedViews = value; - } - } - - public static bool EnableVbViews - { - get - { - if (!_enableVbViews.HasValue) - { - _enableVbViews = CommonHelper.GetAppSetting("sm:EnableVbViews", false); - } - - return _enableVbViews.Value; - } - set - { - _enableVbViews = value; - } - } - - public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) - { - Guard.NotNull(controllerContext, nameof(controllerContext)); - Guard.NotEmpty(viewName, nameof(viewName)); - - var chronometer = EngineContext.Current.Resolve(); - using (chronometer.Step("Find view '{0}'".FormatInvariant(viewName))) - { - var themeName = GetCurrentThemeName(controllerContext); - var controllerName = controllerContext.RouteData.GetRequiredString("controller"); - var areaName = controllerContext.RouteData.GetAreaName(); - - var viewPath = ResolveViewPath( - controllerContext, - areaName, - ViewLocationFormats, - AreaViewLocationFormats, - "ViewLocationFormats", - viewName, - controllerName, - themeName, - "View", - useCache, - out var viewLocationsSearched); - var masterPath = ResolveViewPath( - controllerContext, - areaName, - MasterLocationFormats, - AreaMasterLocationFormats, - "MasterLocationFormats", - masterName, - controllerName, - themeName, - "Master", - useCache, - out var masterLocationsSearched); - - if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName))) - { - return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this); - } - - return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched)); - } - } - - public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) - { - Guard.NotNull(controllerContext, nameof(controllerContext)); - Guard.NotEmpty(partialViewName, nameof(partialViewName)); - - var chronometer = EngineContext.Current.Resolve(); - using (chronometer.Step("Find partial view '{0}'".FormatInvariant(partialViewName))) - { - string[] searchedLocations; - - var themeName = GetCurrentThemeName(controllerContext); - var controllerName = controllerContext.RouteData.GetRequiredString("controller"); - var areaName = controllerContext.RouteData.GetAreaName(); - - string partialPath = ResolveViewPath( - controllerContext, - areaName, - PartialViewLocationFormats, - AreaPartialViewLocationFormats, - "PartialViewLocationFormats", - partialViewName, - controllerName, - themeName, - "Partial", - useCache, - out searchedLocations); - - if (string.IsNullOrEmpty(partialPath)) - { - return new ViewEngineResult(searchedLocations); - } - - return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this); - } - } + private readonly string _cacheKeyEntry = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}"; + + protected ThemeableVirtualPathProviderViewEngine() + { + this.ViewLocationCache = new TwoLevelViewLocationCache(); + + // prepare localized mobile & desktop display modes + DisplayModeProvider.Modes.Clear(); + var mobileDisplayMode = new LocalizedDisplayMode(DisplayModeProvider.MobileDisplayModeId, EnableLocalizedViews) + { + ContextCondition = IsMobileDevice + }; + var desktopDisplayMode = new LocalizedDisplayMode(DisplayModeProvider.DefaultDisplayModeId, EnableLocalizedViews); + + DisplayModeProvider.Modes.Add(mobileDisplayMode); + DisplayModeProvider.Modes.Add(desktopDisplayMode); + } + + public static bool EnableLocalizedViews + { + get + { + if (!_enableLocalizedViews.HasValue) + { + _enableLocalizedViews = CommonHelper.GetAppSetting("sm:EnableLocalizedViews", false); + } + + return _enableLocalizedViews.Value; + } + set => _enableLocalizedViews = value; + } + + public static bool EnableVbViews + { + get + { + if (!_enableVbViews.HasValue) + { + _enableVbViews = CommonHelper.GetAppSetting("sm:EnableVbViews", false); + } + + return _enableVbViews.Value; + } + set => _enableVbViews = value; + } + + public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) + { + Guard.NotNull(controllerContext, nameof(controllerContext)); + Guard.NotEmpty(viewName, nameof(viewName)); + + var chronometer = EngineContext.Current.Resolve(); + using (chronometer.Step("Find view '{0}'".FormatInvariant(viewName))) + { + var themeName = GetCurrentThemeName(controllerContext); + var controllerName = controllerContext.RouteData.GetRequiredString("controller"); + var areaName = controllerContext.RouteData.GetAreaName(); + + var viewPath = ResolveViewPath( + controllerContext, + areaName, + ViewLocationFormats, + AreaViewLocationFormats, + "ViewLocationFormats", + viewName, + controllerName, + themeName, + "View", + useCache, + out var viewLocationsSearched); + var masterPath = ResolveViewPath( + controllerContext, + areaName, + MasterLocationFormats, + AreaMasterLocationFormats, + "MasterLocationFormats", + masterName, + controllerName, + themeName, + "Master", + useCache, + out var masterLocationsSearched); + + if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName))) + { + return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this); + } + + return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched)); + } + } + + public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) + { + Guard.NotNull(controllerContext, nameof(controllerContext)); + Guard.NotEmpty(partialViewName, nameof(partialViewName)); + + var chronometer = EngineContext.Current.Resolve(); + using (chronometer.Step("Find partial view '{0}'".FormatInvariant(partialViewName))) + { + string[] searchedLocations; + + var themeName = GetCurrentThemeName(controllerContext); + var controllerName = controllerContext.RouteData.GetRequiredString("controller"); + var areaName = controllerContext.RouteData.GetAreaName(); + + string partialPath = ResolveViewPath( + controllerContext, + areaName, + PartialViewLocationFormats, + AreaPartialViewLocationFormats, + "PartialViewLocationFormats", + partialViewName, + controllerName, + themeName, + "Partial", + useCache, + out searchedLocations); + + if (string.IsNullOrEmpty(partialPath)) + { + return new ViewEngineResult(searchedLocations); + } + + return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this); + } + } protected virtual string ResolveViewPath( - ControllerContext controllerContext, - string areaName, - string[] locations, - string[] areaLocations, - string locationsPropertyName, - string name, - string controllerName, - string theme, - string cacheKeyPrefix, - bool useCache, - out string[] searchedLocations) - { - searchedLocations = _emptyLocations; - - if (String.IsNullOrEmpty(name)) - { - return String.Empty; - } - - bool usingAreas = !String.IsNullOrEmpty(areaName); - - if (usingAreas) - { - var isAdminArea = areaName.IsCaseInsensitiveEqual("admin"); - - // "ExtraAreaViewLocations" gets injected by AdminThemedAttribute - var extraAreaViewLocations = controllerContext.RouteData.DataTokens["ExtraAreaViewLocations"] as string[]; - - if (isAdminArea && extraAreaViewLocations == null && controllerContext.HttpContext.Request.IsAjaxRequest()) - { - // In admin area AJAX requests DataTokens["ExtraAreaViewLocations"] is not set. - extraAreaViewLocations = AdminThemedAttribute.ExtraAreaViewLocations; - } - - if (extraAreaViewLocations != null && extraAreaViewLocations.Length > 0) - { - var newLocations = areaLocations.ToList(); - var viewType = cacheKeyPrefix == "Partial" - ? ViewType.Partial - : ViewType.Layout; - - if (isAdminArea) - { - // the admin area cannot fallback to itself. Prepend to list. - ExpandLocationFormats(extraAreaViewLocations, viewType).Reverse().Each(x => newLocations.Insert(0, x)); - } - else - { - newLocations.AddRange(ExpandLocationFormats(extraAreaViewLocations, viewType)); - } - - areaLocations = newLocations.ToArray(); - } - } - - var viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null); - - if (viewLocations.Count == 0) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Properties cannot be null or empty.", new object[] { locationsPropertyName })); - } - - bool nameRepresentsPath = IsSpecificPath(name); - string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName, theme); - - if (useCache) - { - // Only look at cached display modes that can handle the context. - var possibleDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(controllerContext.HttpContext, controllerContext.DisplayMode); - foreach (var displayMode in possibleDisplayModes) - { - string cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId)); - - if (cachedLocation == null) - { - // If any matching display mode location is not in the cache, fall back to the uncached behavior, which will repopulate all of our caches. - return null; - } - - // A non-empty cachedLocation indicates that we have a matching file on disk. Return that result. - if (cachedLocation.Length > 0) - { - if (controllerContext.DisplayMode == null) - { - controllerContext.DisplayMode = displayMode; - } - - return cachedLocation; - } - // An empty cachedLocation value indicates that we don't have a matching file on disk. Keep going down the list of possible display modes. - } - - // ResolveViewPath is called again without using the cache. - return null; - } - else - { - return nameRepresentsPath - ? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations) - : GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, theme, cacheKey, ref searchedLocations); - } - } - - protected virtual string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations) - { - string result = name; - - if (!(FilePathIsSupported(name) && FileExists(controllerContext, name))) - { - result = String.Empty; - searchedLocations = new[] { name }; - } - - ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result); - return result; - } - - protected virtual string GetPathFromGeneralName( - ControllerContext controllerContext, - List locations, - string name, - string controllerName, - string areaName, - string theme, - string cacheKey, - ref string[] searchedLocations) - { - string result = String.Empty; - searchedLocations = new string[locations.Count]; - - for (int i = 0; i < locations.Count; i++) - { - ViewLocation location = locations[i]; - string virtualPath = location.Format(name, controllerName, areaName, theme); - DisplayInfo virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode); - - if (virtualPathDisplayInfo != null) - { - string resolvedVirtualPath = virtualPathDisplayInfo.FilePath; - - searchedLocations = _emptyLocations; - result = resolvedVirtualPath; - ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, GetDisplayModeId(virtualPathDisplayInfo)), result); - - if (controllerContext.DisplayMode == null) - { - controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode; - } - - // Populate the cache for all other display modes. We want to cache both file system hits and misses so that we can distinguish - // in future requests whether a file's status was evicted from the cache (null value) or if the file doesn't exist (empty string). - IEnumerable allDisplayModes = DisplayModeProvider.Modes; - foreach (IDisplayMode displayMode in allDisplayModes) - { - if (displayMode.DisplayModeId != virtualPathDisplayInfo.DisplayMode.DisplayModeId) - { - DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path)); - - string displayModeId = String.Empty; - string cacheValue = String.Empty; - if (displayInfoToCache != null && displayInfoToCache.FilePath != null) - { - cacheValue = displayInfoToCache.FilePath; - displayModeId = GetDisplayModeId(displayInfoToCache); - } - else - { - displayModeId = displayMode.DisplayModeId; - } - - ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayModeId), cacheValue); - } - } - break; - } - - searchedLocations[i] = virtualPath; - } - - return result; - } - - protected virtual bool FilePathIsSupported(string virtualPath) - { - if (this.FileExtensions == null) - { - // legacy behavior for custom ViewEngine that might not set the FileExtensions property - return true; - } - - // get rid of the '.' because the FileExtensions property expects extensions withouth a dot. - string extension = GetExtensionThunk(virtualPath).TrimStart('.'); - return FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); - } - - private string GetDisplayModeId(DisplayInfo displayInfo) - { - var localizedDisplayInfo = displayInfo as LocalizedDisplayInfo; - - return localizedDisplayInfo != null - ? localizedDisplayInfo.DisplayModeId - : displayInfo.DisplayMode.DisplayModeId; - } - - protected virtual string CreateCacheKey(string prefix, string name, string controllerName, string areaName, string theme/*, string lang*/) - { - return string.Format(_cacheKeyEntry, - _cacheKeyType, - prefix, - name, - controllerName, - areaName, - theme); - } - - internal static string AppendDisplayModeToCacheKey(string cacheKey, string displayMode) - { - // key format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:{theme}" - // so append ":{displayMode}" to the key - return string.IsNullOrWhiteSpace(displayMode) - ? cacheKey - : cacheKey + ":" + displayMode; - } - - protected virtual IEnumerable ExpandLocationFormats(IEnumerable formats, ViewType viewType) - { - // Appends razor view file extensions to location formats - Guard.NotNull(formats, nameof(formats)); - - var subfolder = viewType == ViewType.Layout ? "Layouts" : "Partials"; - - foreach (var format in formats) - { - if (viewType > ViewType.View) - { - yield return format.Replace("{0}", subfolder + "/{0}.cshtml"); - - if (EnableVbViews) - { - yield return format.Replace("{0}", subfolder + "/{0}.vbhtml"); - } - } - - yield return format + ".cshtml"; - - if (EnableVbViews) - { - yield return format + ".vbhtml"; - } - } - } - - protected virtual List GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats) - { - List locations = new List( - (viewLocationFormats?.Length ?? 0) + - (areaViewLocationFormats?.Length ?? 0)); - - if (areaViewLocationFormats != null) - { - foreach (string areaViewLocationFormat in areaViewLocationFormats) - { - locations.Add(new AreaAwareViewLocation(areaViewLocationFormat)); - } - } - - if (viewLocationFormats != null) - { - foreach (string viewLocationFormat in viewLocationFormats) - { - locations.Add(new ViewLocation(viewLocationFormat)); - } - } - - return locations; - } - - protected virtual bool IsSpecificPath(string name) - { - char c = name[0]; - return (c == '~' || c == '/'); - } - - protected virtual bool IsMobileDevice(HttpContextBase httpContext) - { - var mobileDeviceHelper = EngineContext.Current.Resolve(); - var result = mobileDeviceHelper.IsMobileDevice(); - return result; - } - - protected virtual string GetCurrentThemeName(ControllerContext controllerContext) - { - var theme = EngineContext.Current.Resolve().CurrentTheme; - return theme.ThemeName; - } - - public enum ViewType - { - View, - Layout, - Partial - } - } - - public class AreaAwareViewLocation : ViewLocation - { - public AreaAwareViewLocation(string virtualPathFormatString) - : base(virtualPathFormatString) - { - } - - public override string Format(string viewName, string controllerName, string areaName, string theme) - { - return string.Format(_virtualPathFormatString, - viewName, - controllerName, - areaName, - theme); - } - } - - public class ViewLocation - { - protected readonly string _virtualPathFormatString; - - public ViewLocation(string virtualPathFormatString) - { - _virtualPathFormatString = virtualPathFormatString; - } - - public virtual string Format(string viewName, string controllerName, string areaName, string theme) - { - return string.Format(_virtualPathFormatString, - viewName, - controllerName, - theme); - } - } + ControllerContext controllerContext, + string areaName, + string[] locations, + string[] areaLocations, + string locationsPropertyName, + string name, + string controllerName, + string theme, + string cacheKeyPrefix, + bool useCache, + out string[] searchedLocations) + { + searchedLocations = _emptyLocations; + + if (String.IsNullOrEmpty(name)) + { + return String.Empty; + } + + bool usingAreas = !String.IsNullOrEmpty(areaName); + + if (usingAreas) + { + var isAdminArea = areaName.IsCaseInsensitiveEqual("admin"); + + // "ExtraAreaViewLocations" gets injected by AdminThemedAttribute + var extraAreaViewLocations = controllerContext.RouteData.DataTokens["ExtraAreaViewLocations"] as string[]; + + if (isAdminArea && extraAreaViewLocations == null && controllerContext.HttpContext.Request.IsAjaxRequest()) + { + // In admin area AJAX requests DataTokens["ExtraAreaViewLocations"] is not set. + extraAreaViewLocations = AdminThemedAttribute.ExtraAreaViewLocations; + } + + if (extraAreaViewLocations != null && extraAreaViewLocations.Length > 0) + { + var newLocations = areaLocations.ToList(); + var viewType = cacheKeyPrefix == "Partial" + ? ViewType.Partial + : ViewType.Layout; + + if (isAdminArea) + { + // the admin area cannot fallback to itself. Prepend to list. + ExpandLocationFormats(extraAreaViewLocations, viewType).Reverse().Each(x => newLocations.Insert(0, x)); + } + else + { + newLocations.AddRange(ExpandLocationFormats(extraAreaViewLocations, viewType)); + } + + areaLocations = newLocations.ToArray(); + } + } + + var viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null); + + if (viewLocations.Count == 0) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Properties cannot be null or empty.", new object[] { locationsPropertyName })); + } + + bool nameRepresentsPath = IsSpecificPath(name); + string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName, theme); + + if (useCache) + { + // Only look at cached display modes that can handle the context. + var possibleDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(controllerContext.HttpContext, controllerContext.DisplayMode); + foreach (var displayMode in possibleDisplayModes) + { + string cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId)); + + if (cachedLocation == null) + { + // If any matching display mode location is not in the cache, fall back to the uncached behavior, which will repopulate all of our caches. + return null; + } + + // A non-empty cachedLocation indicates that we have a matching file on disk. Return that result. + if (cachedLocation.Length > 0) + { + if (controllerContext.DisplayMode == null) + { + controllerContext.DisplayMode = displayMode; + } + + return cachedLocation; + } + // An empty cachedLocation value indicates that we don't have a matching file on disk. Keep going down the list of possible display modes. + } + + // ResolveViewPath is called again without using the cache. + return null; + } + else + { + return nameRepresentsPath + ? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations) + : GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, theme, cacheKey, ref searchedLocations); + } + } + + protected virtual string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations) + { + string result = name; + + if (!(FilePathIsSupported(name) && FileExists(controllerContext, name))) + { + result = String.Empty; + searchedLocations = new[] { name }; + } + + ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result); + return result; + } + + protected virtual string GetPathFromGeneralName( + ControllerContext controllerContext, + List locations, + string name, + string controllerName, + string areaName, + string theme, + string cacheKey, + ref string[] searchedLocations) + { + string result = String.Empty; + searchedLocations = new string[locations.Count]; + + for (int i = 0; i < locations.Count; i++) + { + ViewLocation location = locations[i]; + string virtualPath = location.Format(name, controllerName, areaName, theme); + DisplayInfo virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode); + + if (virtualPathDisplayInfo != null) + { + string resolvedVirtualPath = virtualPathDisplayInfo.FilePath; + + searchedLocations = _emptyLocations; + result = resolvedVirtualPath; + ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, GetDisplayModeId(virtualPathDisplayInfo)), result); + + if (controllerContext.DisplayMode == null) + { + controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode; + } + + // Populate the cache for all other display modes. We want to cache both file system hits and misses so that we can distinguish + // in future requests whether a file's status was evicted from the cache (null value) or if the file doesn't exist (empty string). + IEnumerable allDisplayModes = DisplayModeProvider.Modes; + foreach (IDisplayMode displayMode in allDisplayModes) + { + if (displayMode.DisplayModeId != virtualPathDisplayInfo.DisplayMode.DisplayModeId) + { + DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path)); + + string displayModeId = String.Empty; + string cacheValue = String.Empty; + if (displayInfoToCache != null && displayInfoToCache.FilePath != null) + { + cacheValue = displayInfoToCache.FilePath; + displayModeId = GetDisplayModeId(displayInfoToCache); + } + else + { + displayModeId = displayMode.DisplayModeId; + } + + ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayModeId), cacheValue); + } + } + break; + } + + searchedLocations[i] = virtualPath; + } + + return result; + } + + protected virtual bool FilePathIsSupported(string virtualPath) + { + if (this.FileExtensions == null) + { + // legacy behavior for custom ViewEngine that might not set the FileExtensions property + return true; + } + + // get rid of the '.' because the FileExtensions property expects extensions withouth a dot. + string extension = GetExtensionThunk(virtualPath).TrimStart('.'); + return FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + } + + private string GetDisplayModeId(DisplayInfo displayInfo) + { + var localizedDisplayInfo = displayInfo as LocalizedDisplayInfo; + + return localizedDisplayInfo != null + ? localizedDisplayInfo.DisplayModeId + : displayInfo.DisplayMode.DisplayModeId; + } + + protected virtual string CreateCacheKey(string prefix, string name, string controllerName, string areaName, string theme/*, string lang*/) + { + return string.Format(_cacheKeyEntry, + _cacheKeyType, + prefix, + name, + controllerName, + areaName, + theme); + } + + internal static string AppendDisplayModeToCacheKey(string cacheKey, string displayMode) + { + // key format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:{theme}" + // so append ":{displayMode}" to the key + return string.IsNullOrWhiteSpace(displayMode) + ? cacheKey + : cacheKey + ":" + displayMode; + } + + protected virtual IEnumerable ExpandLocationFormats(IEnumerable formats, ViewType viewType) + { + // Appends razor view file extensions to location formats + Guard.NotNull(formats, nameof(formats)); + + var subfolder = viewType == ViewType.Layout ? "Layouts" : "Partials"; + + foreach (var format in formats) + { + if (viewType > ViewType.View) + { + yield return format.Replace("{0}", subfolder + "/{0}.cshtml"); + + if (EnableVbViews) + { + yield return format.Replace("{0}", subfolder + "/{0}.vbhtml"); + } + } + + yield return format + ".cshtml"; + + if (EnableVbViews) + { + yield return format + ".vbhtml"; + } + } + } + + protected virtual List GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats) + { + List locations = new List( + (viewLocationFormats?.Length ?? 0) + + (areaViewLocationFormats?.Length ?? 0)); + + if (areaViewLocationFormats != null) + { + foreach (string areaViewLocationFormat in areaViewLocationFormats) + { + locations.Add(new AreaAwareViewLocation(areaViewLocationFormat)); + } + } + + if (viewLocationFormats != null) + { + foreach (string viewLocationFormat in viewLocationFormats) + { + locations.Add(new ViewLocation(viewLocationFormat)); + } + } + + return locations; + } + + protected virtual bool IsSpecificPath(string name) + { + char c = name[0]; + return (c == '~' || c == '/'); + } + + protected virtual bool IsMobileDevice(HttpContextBase httpContext) + { + var mobileDeviceHelper = EngineContext.Current.Resolve(); + var result = mobileDeviceHelper.IsMobileDevice(); + return result; + } + + protected virtual string GetCurrentThemeName(ControllerContext controllerContext) + { + var theme = EngineContext.Current.Resolve().CurrentTheme; + return theme.ThemeName; + } + + public enum ViewType + { + View, + Layout, + Partial + } + } + + public class AreaAwareViewLocation : ViewLocation + { + public AreaAwareViewLocation(string virtualPathFormatString) + : base(virtualPathFormatString) + { + } + + public override string Format(string viewName, string controllerName, string areaName, string theme) + { + return string.Format(_virtualPathFormatString, + viewName, + controllerName, + areaName, + theme); + } + } + + public class ViewLocation + { + protected readonly string _virtualPathFormatString; + + public ViewLocation(string virtualPathFormatString) + { + _virtualPathFormatString = virtualPathFormatString; + } + + public virtual string Format(string viewName, string controllerName, string areaName, string theme) + { + return string.Format(_virtualPathFormatString, + viewName, + controllerName, + theme); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs index cef62c8959..78195934d9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs @@ -8,154 +8,153 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Themes; using SmartStore.Utilities; -using SmartStore.Web.Framework.Theming.Assets; namespace SmartStore.Web.Framework.Theming { public class ThemingVirtualPathProvider : SmartVirtualPathProvider { - private readonly VirtualPathProvider _previous; - private static readonly ContextState> _requestState; + private readonly VirtualPathProvider _previous; + private static readonly ContextState> _requestState; - static ThemingVirtualPathProvider() - { - _requestState = new ContextState>("ThemeFileResolver.RequestCache", () => new Dictionary()); - } + static ThemingVirtualPathProvider() + { + _requestState = new ContextState>("ThemeFileResolver.RequestCache", () => new Dictionary()); + } - public ThemingVirtualPathProvider(VirtualPathProvider previous) + public ThemingVirtualPathProvider(VirtualPathProvider previous) { _previous = previous; - } + } public override bool FileExists(string virtualPath) { - var result = GetResolveResult(virtualPath); - if (result != null) - { - if (!result.IsBased) - { - return true; - } - else - { - if (result.Query.HasValue() && result.Query.IndexOf('.') >= 0) - { - // libSass tries to locate files by appending .[s]css extension to our querystring. Prevent this shit! - return false; - } - else - { - // Let system VPP check for this file - virtualPath = result.ResultVirtualPath ?? result.OriginalVirtualPath; - } - } - } - - return _previous.FileExists(virtualPath); + var result = GetResolveResult(virtualPath); + if (result != null) + { + if (!result.IsBased) + { + return true; + } + else + { + if (result.Query.HasValue() && result.Query.IndexOf('.') >= 0) + { + // libSass tries to locate files by appending .[s]css extension to our querystring. Prevent this shit! + return false; + } + else + { + // Let system VPP check for this file + virtualPath = result.ResultVirtualPath ?? result.OriginalVirtualPath; + } + } + } + + return _previous.FileExists(virtualPath); } - + public override VirtualFile GetFile(string virtualPath) { - VirtualFile file = null; - string debugPath = null; - - var result = GetResolveResult(virtualPath); - if (result != null) - { - // File is an inherited theme file. Set the result virtual path. - virtualPath = result.ResultVirtualPath ?? result.OriginalVirtualPath; - if (!result.IsBased) - { - file = new InheritedVirtualThemeFile(result); - } - } - - if (result == null || file is InheritedVirtualThemeFile) - { - // Handle plugin and symlinked theme folders in debug mode. - debugPath = ResolveDebugFilePath(virtualPath); - if (debugPath != null) - { - file = new DebugVirtualFile(file?.VirtualPath ?? virtualPath, debugPath); - } - } - - return file ?? _previous.GetFile(virtualPath); + VirtualFile file = null; + string debugPath = null; + + var result = GetResolveResult(virtualPath); + if (result != null) + { + // File is an inherited theme file. Set the result virtual path. + virtualPath = result.ResultVirtualPath ?? result.OriginalVirtualPath; + if (!result.IsBased) + { + file = new InheritedVirtualThemeFile(result); + } + } + + if (result == null || file is InheritedVirtualThemeFile) + { + // Handle plugin and symlinked theme folders in debug mode. + debugPath = ResolveDebugFilePath(virtualPath); + if (debugPath != null) + { + file = new DebugVirtualFile(file?.VirtualPath ?? virtualPath, debugPath); + } + } + + return file ?? _previous.GetFile(virtualPath); + } + + public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) + { + if (virtualPathDependencies == null) + { + return _previous.GetFileHash(virtualPath, virtualPathDependencies); + } + + var fileNames = MapDependencyPaths(virtualPathDependencies.Cast(), out _); + var combiner = HashCodeCombiner.Start(); + + foreach (var fileName in fileNames) + { + combiner.Add(new FileInfo(fileName)); + } + + return combiner.CombinedHashString; + } + + public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) + { + if (virtualPathDependencies == null) + { + return null; + } + + var mappedPaths = MapDependencyPaths(virtualPathDependencies.Cast(), out var cacheKeys); + + return new CacheDependency(mappedPaths, cacheKeys, utcStart); + } + + /// + /// Maps virtual to physical paths. Used to compute cache dependecies and file hashes. + /// + internal string[] MapDependencyPaths(IEnumerable virtualPathDependencies, out string[] cacheKeys) + { + cacheKeys = null; + + var mappedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var cacheKeySet = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var dep in virtualPathDependencies) + { + var file = GetFile(dep); + + if (file is IFileDependencyProvider provider) + { + provider.AddFileDependencies(mappedPaths, cacheKeySet); + } + else if (file != null) + { + mappedPaths.Add(HostingEnvironment.MapPath(file.VirtualPath)); + } + } + + cacheKeys = cacheKeySet.ToArray(); + + var paths = mappedPaths.ToArray(); + Array.Sort(paths); + + return paths; } - public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) - { - if (virtualPathDependencies == null) - { - return _previous.GetFileHash(virtualPath, virtualPathDependencies); - } - - var fileNames = MapDependencyPaths(virtualPathDependencies.Cast(), out _); - var combiner = HashCodeCombiner.Start(); - - foreach (var fileName in fileNames) - { - combiner.Add(new FileInfo(fileName)); - } - - return combiner.CombinedHashString; - } - - public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) - { - if (virtualPathDependencies == null) - { - return null; - } - - var mappedPaths = MapDependencyPaths(virtualPathDependencies.Cast(), out var cacheKeys); - - return new CacheDependency(mappedPaths, cacheKeys, utcStart); - } - - /// - /// Maps virtual to physical paths. Used to compute cache dependecies and file hashes. - /// - internal string[] MapDependencyPaths(IEnumerable virtualPathDependencies, out string[] cacheKeys) - { - cacheKeys = null; - - var mappedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); - var cacheKeySet = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var dep in virtualPathDependencies) - { - var file = GetFile(dep); - - if (file is IFileDependencyProvider provider) - { - provider.AddFileDependencies(mappedPaths, cacheKeySet); - } - else if (file != null) - { - mappedPaths.Add(HostingEnvironment.MapPath(file.VirtualPath)); - } - } - - cacheKeys = cacheKeySet.ToArray(); - - var paths = mappedPaths.ToArray(); - Array.Sort(paths); - - return paths; - } - - private static InheritedThemeFileResult GetResolveResult(string virtualPath) - { - var d = _requestState.GetState(); - - if (!d.TryGetValue(virtualPath, out var result)) - { - result = d[virtualPath] = EngineContext.Current.Resolve().Resolve(virtualPath); - } - - return result; - } + private static InheritedThemeFileResult GetResolveResult(string virtualPath) + { + var d = _requestState.GetState(); + + if (!d.TryGetValue(virtualPath, out var result)) + { + result = d[virtualPath] = EngineContext.Current.Resolve().Resolve(virtualPath); + } + + return result; + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/TwoLevelViewLocationCache.cs b/src/Presentation/SmartStore.Web.Framework/Theming/TwoLevelViewLocationCache.cs index 165e3b194d..831cc4a036 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/TwoLevelViewLocationCache.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/TwoLevelViewLocationCache.cs @@ -6,53 +6,53 @@ namespace SmartStore.Web.Framework.Theming { - public class TwoLevelViewLocationCache : IViewLocationCache - { - private static readonly object s_key = new object(); - private readonly IViewLocationCache _innerCache; + public class TwoLevelViewLocationCache : IViewLocationCache + { + private static readonly object s_key = new object(); + private readonly IViewLocationCache _innerCache; - public TwoLevelViewLocationCache() - { - var cacheDuringDebug = CommonHelper.GetAppSetting("sm:EnableViewLocationCacheDuringDebug"); + public TwoLevelViewLocationCache() + { + var cacheDuringDebug = CommonHelper.GetAppSetting("sm:EnableViewLocationCacheDuringDebug"); - if (!cacheDuringDebug && (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled)) - { - _innerCache = DefaultViewLocationCache.Null; - } - else - { - _innerCache = new DefaultViewLocationCache(TimeSpan.FromHours(2)); - } + if (!cacheDuringDebug && (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled)) + { + _innerCache = DefaultViewLocationCache.Null; + } + else + { + _innerCache = new DefaultViewLocationCache(TimeSpan.FromHours(2)); + } - } + } - private static IDictionary GetRequestCache(HttpContextBase httpContext) - { - var d = httpContext.Items[s_key] as IDictionary; - if (d == null) - { - d = new Dictionary(); - httpContext.Items[s_key] = d; - } - return d; - } + private static IDictionary GetRequestCache(HttpContextBase httpContext) + { + var d = httpContext.Items[s_key] as IDictionary; + if (d == null) + { + d = new Dictionary(); + httpContext.Items[s_key] = d; + } + return d; + } - public string GetViewLocation(HttpContextBase httpContext, string key) - { - var d = GetRequestCache(httpContext); - string location; - if (!d.TryGetValue(key, out location)) - { - location = _innerCache.GetViewLocation(httpContext, key); - d[key] = location; - } - return location; - } + public string GetViewLocation(HttpContextBase httpContext, string key) + { + var d = GetRequestCache(httpContext); + string location; + if (!d.TryGetValue(key, out location)) + { + location = _innerCache.GetViewLocation(httpContext, key); + d[key] = location; + } + return location; + } - public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath) - { - _innerCache.InsertViewLocation(httpContext, key, virtualPath); - GetRequestCache(httpContext)[key] = virtualPath; - } - } + public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath) + { + _innerCache.InsertViewLocation(httpContext, key, virtualPath); + GetRequestCache(httpContext)[key] = virtualPath; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs index 8be02c7bac..7fe8956162 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs @@ -16,143 +16,59 @@ namespace SmartStore.Web.Framework.Theming { public abstract class WebViewPage : System.Web.Mvc.WebViewPage { - private WebViewPageHelper _helper; + private WebViewPageHelper _helper; - protected string CurrentPageType - { - get => _helper.CurrentPageType; - } + protected string CurrentPageType => _helper.CurrentPageType; - protected object CurrentPageId - { - get => _helper.CurrentPageId; - } + protected object CurrentPageId => _helper.CurrentPageId; - protected int CurrentCategoryId - { - get - { - return _helper.CurrentPageType == "category" ? _helper.CurrentPageId.Convert() : 0; - } - } + protected int CurrentCategoryId => _helper.CurrentPageType == "category" ? _helper.CurrentPageId.Convert() : 0; - protected int CurrentManufacturerId - { - get - { - return _helper.CurrentPageType == "manufacturer" ? _helper.CurrentPageId.Convert() : 0; - } - } + protected int CurrentManufacturerId => _helper.CurrentPageType == "manufacturer" ? _helper.CurrentPageId.Convert() : 0; - protected int CurrentProductId - { - get - { - return _helper.CurrentPageType == "product" ? _helper.CurrentPageId.Convert() : 0; - } - } + protected int CurrentProductId => _helper.CurrentPageType == "product" ? _helper.CurrentPageId.Convert() : 0; - protected int CurrentTopicId - { - get - { - return _helper.CurrentPageType == "topic" ? _helper.CurrentPageId.Convert() : 0; - } - } + protected int CurrentTopicId => _helper.CurrentPageType == "topic" ? _helper.CurrentPageId.Convert() : 0; - protected bool IsHomePage - { - get - { - return _helper.IsHomePage; - } - } + protected bool IsHomePage => _helper.IsHomePage; + + protected bool IsMobileDevice => _helper.IsMobileDevice; + + protected bool IsStoreClosed => _helper.IsStoreClosed; + + public bool EnableHoneypotProtection => _helper.EnableHoneypotProtection; - protected bool IsMobileDevice - { - get - { - return _helper.IsMobileDevice; - } - } + public string FileManagerUrl => _helper.FileManagerUrl; - protected bool IsStoreClosed + protected bool HasMessages => _helper.ResolveNotifications(null).Any(); + + protected ICollection GetMessages(NotifyType type) { - get - { - return _helper.IsStoreClosed; - } + return _helper.ResolveNotifications(type).AsReadOnly(); } - public bool EnableHoneypotProtection - { - get { return _helper.EnableHoneypotProtection; } - } - - public string FileManagerUrl - { - get { return _helper.FileManagerUrl; } - } - - protected bool HasMessages - { - get - { - return _helper.ResolveNotifications(null).Any(); - } - } - - protected ICollection GetMessages(NotifyType type) - { - return _helper.ResolveNotifications(type).AsReadOnly(); - } - /// /// Get a localized resource /// - public Localizer T - { - get - { - return _helper.T; - } - } + public Localizer T => _helper.T; - public ICommonServices CommonServices - { - get - { - return _helper.Services; - } - } + public ICommonServices CommonServices => _helper.Services; - public IWorkContext WorkContext - { - get - { - return _helper.Services.WorkContext; - } - } + public IWorkContext WorkContext => _helper.Services.WorkContext; - public ILinkResolver LinkResolver - { - get - { - return _helper.LinkResolver; - } - } + public ILinkResolver LinkResolver => _helper.LinkResolver; - public override void InitHelpers() + public override void InitHelpers() { base.InitHelpers(); - _helper = EngineContext.Current.Resolve(); - _helper.Initialize(this.ViewContext); + _helper = EngineContext.Current.Resolve(); + _helper.Initialize(this.ViewContext); } public HelperResult RenderWrappedSection(string name, object wrapperHtmlAttributes) { - Action action = delegate(TextWriter tw) + Action action = delegate (TextWriter tw) { var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(wrapperHtmlAttributes); var tagBuilder = new TagBuilder("div"); @@ -193,34 +109,19 @@ public override string Layout return layout; } - set - { - base.Layout = value; - } + set => base.Layout = value; } - /// - /// Gets the manifest of the current active theme - /// - protected ThemeManifest ThemeManifest - { - get - { - return _helper.ThemeContext?.CurrentTheme; - } - } + /// + /// Gets the manifest of the current active theme + /// + protected ThemeManifest ThemeManifest => _helper.ThemeContext?.CurrentTheme; /// /// Gets the current theme name. Override this in configuration views. /// [Obsolete("The theme name gets resolved automatically now. No need to override anymore.")] - protected virtual string ThemeName - { - get - { - return _helper.ThemeContext?.WorkingThemeName; - } - } + protected virtual string ThemeName => _helper.ThemeContext?.WorkingThemeName; public string GetThemeVariable(string varname, string defaultValue = "") { @@ -251,150 +152,144 @@ public string GetThemeVariable(string varname, string defaultValue = "") return defaultValue; } - /// - /// Gets the runtime theme variables as specified in the theme's config file - /// alongside the merged user-defined variables - /// - public dynamic ThemeVariables - { - get - { - return _helper.ThemeVariables; - } - } - - /// - /// Modifies a URL (appends/updates a query string part and optionally removes another query string). - /// - /// The URL to modifiy. If null, the current page's URL is resolved. - /// The new query string part. - /// A query string name to remove. - /// The modified URL. - public string ModifyUrl(string url, string query, string removeQueryName = null) - { - var webHelper = _helper.Services?.WebHelper; - if (webHelper == null) - { - return url; - } - - url = url.NullEmpty() ?? webHelper.GetThisPageUrl(true); - var url2 = webHelper.ModifyQueryString(url, query, null); - - if (removeQueryName.HasValue()) - { - url2 = webHelper.RemoveQueryString(url2, removeQueryName); - } - - return url2; - } - - public string GenerateHelpUrl(HelpTopic topic) - { - var seoCode = WorkContext?.WorkingLanguage?.UniqueSeoCode; - if (seoCode.IsEmpty()) - { - return topic?.EnPath; - } - - return SmartStoreVersion.GenerateHelpUrl(seoCode, topic); - } - - public string GenerateHelpUrl(string path) - { - var seoCode = WorkContext?.WorkingLanguage?.UniqueSeoCode; - if (seoCode.IsEmpty()) - { - return path; - } - - return SmartStoreVersion.GenerateHelpUrl(seoCode, path); - } - - /// - /// 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. - /// Optional. - /// Result - public LocalizationFileResolveResult ResolveLocalizationFile( - string culture, - string virtualPath, - string pattern, - string fallbackCulture = "en") - { - return _helper.LocalizationFileResolver.Resolve(culture, virtualPath, pattern, true, fallbackCulture); - } - - public bool HasMetadata(string name) - { - return TryGetMetadata(name, out _); - } - - /// - /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary - /// - /// Actual type of value - /// Name of entry - /// Result - public T GetMetadata(string name) - { - TryGetMetadata(name, out var value); - return value; - } - - /// - /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary - /// - /// Actual type of value - /// Name of entry - /// The default value to return if item does not exist. - /// Result - public T GetMetadata(string name, T defaultValue) - { - if (TryGetMetadata(name, out var value)) - { - return value; - } - - return defaultValue; - } - - /// - /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary - /// - /// Actual type of value - /// Name of entry - /// true if the entry exists in any of the dictionaries, false otherwise - public bool TryGetMetadata(string name, out T value) - { - value = default(T); - - var exists = ViewData.TryGetValue(name, out var raw); - if (!exists) - { - exists = ViewData.ModelMetadata?.AdditionalValues?.TryGetValue(name, out raw) == true; - } - - if (raw != null) - { - value = raw.Convert(); - } - - return exists; - } - } + /// + /// Gets the runtime theme variables as specified in the theme's config file + /// alongside the merged user-defined variables + /// + public dynamic ThemeVariables => _helper.ThemeVariables; + + /// + /// Modifies a URL (appends/updates a query string part and optionally removes another query string). + /// + /// The URL to modifiy. If null, the current page's URL is resolved. + /// The new query string part. + /// A query string name to remove. + /// The modified URL. + public string ModifyUrl(string url, string query, string removeQueryName = null) + { + var webHelper = _helper.Services?.WebHelper; + if (webHelper == null) + { + return url; + } + + url = url.NullEmpty() ?? webHelper.GetThisPageUrl(true); + var url2 = webHelper.ModifyQueryString(url, query, null); + + if (removeQueryName.HasValue()) + { + url2 = webHelper.RemoveQueryString(url2, removeQueryName); + } + + return url2; + } + + public string GenerateHelpUrl(HelpTopic topic) + { + var seoCode = WorkContext?.WorkingLanguage?.UniqueSeoCode; + if (seoCode.IsEmpty()) + { + return topic?.EnPath; + } + + return SmartStoreVersion.GenerateHelpUrl(seoCode, topic); + } + + public string GenerateHelpUrl(string path) + { + var seoCode = WorkContext?.WorkingLanguage?.UniqueSeoCode; + if (seoCode.IsEmpty()) + { + return path; + } + + return SmartStoreVersion.GenerateHelpUrl(seoCode, path); + } + + /// + /// 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. + /// Optional. + /// Result + public LocalizationFileResolveResult ResolveLocalizationFile( + string culture, + string virtualPath, + string pattern, + string fallbackCulture = "en") + { + return _helper.LocalizationFileResolver.Resolve(culture, virtualPath, pattern, true, fallbackCulture); + } + + public bool HasMetadata(string name) + { + return TryGetMetadata(name, out _); + } + + /// + /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary + /// + /// Actual type of value + /// Name of entry + /// Result + public T GetMetadata(string name) + { + TryGetMetadata(name, out var value); + return value; + } + + /// + /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary + /// + /// Actual type of value + /// Name of entry + /// The default value to return if item does not exist. + /// Result + public T GetMetadata(string name, T defaultValue) + { + if (TryGetMetadata(name, out var value)) + { + return value; + } + + return defaultValue; + } + + /// + /// Looks up an entry in ViewData dictionary first, then in ViewData.ModelMetadata.AdditionalValues dictionary + /// + /// Actual type of value + /// Name of entry + /// true if the entry exists in any of the dictionaries, false otherwise + public bool TryGetMetadata(string name, out T value) + { + value = default(T); + + var exists = ViewData.TryGetValue(name, out var raw); + if (!exists) + { + exists = ViewData.ModelMetadata?.AdditionalValues?.TryGetValue(name, out raw) == true; + } + + if (raw != null) + { + value = raw.Convert(); + } + + return exists; + } + } public abstract class WebViewPage : WebViewPage { diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs index 006a054f48..b8fa11ecfd 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs @@ -1,79 +1,70 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Web.Mvc; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.Security; using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Themes; using SmartStore.Services; +using SmartStore.Services.Cms; using SmartStore.Services.Common; -using SmartStore.Web.Framework.Filters; -using SmartStore.Core.Domain; using SmartStore.Services.Customers; -using SmartStore.Core.Domain.Security; -using SmartStore.Services.Cms; -using System.Web.Routing; -using System.Globalization; +using SmartStore.Web.Framework.Filters; namespace SmartStore.Web.Framework.Theming { - public class FileManagerUrlRequested - { - public UrlHelper UrlHelper { get; set; } - public string Url { get; set; } - } - - public class WebViewPageHelper - { - private bool _initialized; - private ControllerContext _controllerContext; - private ExpandoObject _themeVars; - private ICollection _internalNotifications; + public class FileManagerUrlRequested + { + public UrlHelper UrlHelper { get; set; } + public string Url { get; set; } + } + + public class WebViewPageHelper + { + private bool _initialized; + private ControllerContext _controllerContext; + private ExpandoObject _themeVars; + private ICollection _internalNotifications; private string _currentPageType; private object _currentPageId; private bool? _isHomePage; - private bool? _isMobileDevice; + private bool? _isMobileDevice; private bool? _isStoreClosed; - private bool? _enableHoneypot; - private string _fileManagerUrl; - - public WebViewPageHelper() - { - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - public ILocalizationFileResolver LocalizationFileResolver { get; set; } - public ICommonServices Services { get; set; } - public IThemeRegistry ThemeRegistry { get; set; } - public IThemeContext ThemeContext { get; set; } - public IMobileDeviceHelper MobileDeviceHelper { get; set; } - public ILinkResolver LinkResolver { get; set; } - public UrlHelper UrlHelper { get; set; } - - public void Initialize(ControllerContext controllerContext) - { - if (!_initialized) - { - _controllerContext = controllerContext.GetRootControllerContext(); - IdentifyPage(); - _initialized = true; - } - } + private bool? _enableHoneypot; + private string _fileManagerUrl; - public string CurrentPageType + public WebViewPageHelper() { - get => _currentPageType; + T = NullLocalizer.Instance; } - public object CurrentPageId + public Localizer T { get; set; } + public ILocalizationFileResolver LocalizationFileResolver { get; set; } + public ICommonServices Services { get; set; } + public IThemeRegistry ThemeRegistry { get; set; } + public IThemeContext ThemeContext { get; set; } + public IMobileDeviceHelper MobileDeviceHelper { get; set; } + public ILinkResolver LinkResolver { get; set; } + public UrlHelper UrlHelper { get; set; } + + public void Initialize(ControllerContext controllerContext) { - get => _currentPageId; + if (!_initialized) + { + _controllerContext = controllerContext.GetRootControllerContext(); + IdentifyPage(); + _initialized = true; + } } + public string CurrentPageType => _currentPageType; + + public object CurrentPageId => _currentPageId; + private void IdentifyPage() { var routeData = _controllerContext.RequestContext.RouteData; @@ -82,7 +73,7 @@ private void IdentifyPage() _currentPageType = "system"; _currentPageId = controllerName + "." + actionName; - + if (IsHomePage) { _currentPageType = "home"; @@ -120,34 +111,34 @@ private void IdentifyPage() } public bool IsHomePage - { - get - { - if (!_isHomePage.HasValue) - { - var routeData = _controllerContext.RequestContext.RouteData; - var response = _controllerContext.RequestContext.HttpContext.Response; - _isHomePage = response.StatusCode != 404 && - routeData.GetRequiredString("controller").IsCaseInsensitiveEqual("Home") && - routeData.GetRequiredString("action").IsCaseInsensitiveEqual("Index"); - } - - return _isHomePage.Value; - } - } - - public bool IsMobileDevice - { - get - { - if (!_isMobileDevice.HasValue) - { - _isMobileDevice = MobileDeviceHelper.IsMobileDevice(); - } - - return _isMobileDevice.Value; - } - } + { + get + { + if (!_isHomePage.HasValue) + { + var routeData = _controllerContext.RequestContext.RouteData; + var response = _controllerContext.RequestContext.HttpContext.Response; + _isHomePage = response.StatusCode != 404 && + routeData.GetRequiredString("controller").IsCaseInsensitiveEqual("Home") && + routeData.GetRequiredString("action").IsCaseInsensitiveEqual("Index"); + } + + return _isHomePage.Value; + } + } + + public bool IsMobileDevice + { + get + { + if (!_isMobileDevice.HasValue) + { + _isMobileDevice = MobileDeviceHelper.IsMobileDevice(); + } + + return _isMobileDevice.Value; + } + } public bool IsStoreClosed { @@ -155,112 +146,112 @@ public bool IsStoreClosed { if (!_isStoreClosed.HasValue) { - var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); - _isStoreClosed = Services.WorkContext.CurrentCustomer.IsAdmin() && settings.StoreClosedAllowForAdmins ? false : settings.StoreClosed; + var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); + _isStoreClosed = Services.WorkContext.CurrentCustomer.IsAdmin() && settings.StoreClosedAllowForAdmins ? false : settings.StoreClosed; } return _isStoreClosed.Value; } } - public bool EnableHoneypotProtection - { - get - { - if (!_enableHoneypot.HasValue) - { - var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); - _enableHoneypot = settings.EnableHoneypotProtection; - } - - return _enableHoneypot.Value; - } - } - - public string FileManagerUrl - { - get - { - if (_fileManagerUrl == null) - { - var defaultUrl = UrlHelper.Action("Index", "RoxyFileManager", new { area = "admin" }); - var message = new FileManagerUrlRequested - { - UrlHelper = UrlHelper, - Url = defaultUrl - }; - - Services.EventPublisher.Publish(message); - - _fileManagerUrl = message.Url ?? defaultUrl; - } - - return _fileManagerUrl; - } - } - - public IEnumerable ResolveNotifications(NotifyType? type) - { - IEnumerable result = Enumerable.Empty(); - - if (_internalNotifications == null) - { - string key = NotifyAttribute.NotificationsKey; - ICollection entries; - - var tempData = _controllerContext.Controller.TempData; - if (tempData.ContainsKey(key)) - { - entries = tempData[key] as ICollection; - if (entries != null) - { - result = result.Concat(entries); - } - } - - var viewData = _controllerContext.Controller.ViewData; - if (viewData.ContainsKey(key)) - { - entries = viewData[key] as ICollection; - if (entries != null) - { - result = result.Concat(entries); - } - } - - _internalNotifications = new HashSet(result); - } - - if (type == null) - { - return _internalNotifications.Select(x => x.Message); - } - - return _internalNotifications.Where(x => x.Type == type.Value).Select(x => x.Message); - } - - public dynamic ThemeVariables - { - get - { - if (_themeVars == null) - { - var storeContext = Services?.StoreContext; - var themeManifest = ThemeContext?.CurrentTheme; + public bool EnableHoneypotProtection + { + get + { + if (!_enableHoneypot.HasValue) + { + var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); + _enableHoneypot = settings.EnableHoneypotProtection; + } + + return _enableHoneypot.Value; + } + } + + public string FileManagerUrl + { + get + { + if (_fileManagerUrl == null) + { + var defaultUrl = UrlHelper.Action("Index", "RoxyFileManager", new { area = "admin" }); + var message = new FileManagerUrlRequested + { + UrlHelper = UrlHelper, + Url = defaultUrl + }; + + Services.EventPublisher.Publish(message); + + _fileManagerUrl = message.Url ?? defaultUrl; + } + + return _fileManagerUrl; + } + } + + public IEnumerable ResolveNotifications(NotifyType? type) + { + IEnumerable result = Enumerable.Empty(); + + if (_internalNotifications == null) + { + string key = NotifyAttribute.NotificationsKey; + ICollection entries; + + var tempData = _controllerContext.Controller.TempData; + if (tempData.ContainsKey(key)) + { + entries = tempData[key] as ICollection; + if (entries != null) + { + result = result.Concat(entries); + } + } + + var viewData = _controllerContext.Controller.ViewData; + if (viewData.ContainsKey(key)) + { + entries = viewData[key] as ICollection; + if (entries != null) + { + result = result.Concat(entries); + } + } + + _internalNotifications = new HashSet(result); + } + + if (type == null) + { + return _internalNotifications.Select(x => x.Message); + } + + return _internalNotifications.Where(x => x.Type == type.Value).Select(x => x.Message); + } + + public dynamic ThemeVariables + { + get + { + if (_themeVars == null) + { + var storeContext = Services?.StoreContext; + var themeManifest = ThemeContext?.CurrentTheme; if (storeContext == null || themeManifest == null) - { - _themeVars = new ExpandoObject(); - } - else - { - var repo = new ThemeVarsRepository(); - _themeVars = repo.GetRawVariables(themeManifest.ThemeName, storeContext.CurrentStore.Id); - } - } - - return _themeVars; - } - } - } + { + _themeVars = new ExpandoObject(); + } + else + { + var repo = new ThemeVarsRepository(); + _themeVars = repo.GetRawVariables(themeManifest.ThemeName, storeContext.CurrentStore.Id); + } + } + + return _themeVars; + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs b/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs index 43b71c4ea0..c6e9556940 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs @@ -1,43 +1,36 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Web.Framework.UI { - public interface IBreadcrumb - { - void Track(MenuItem item, bool prepend = false); - IReadOnlyList Trail { get; } - } + public interface IBreadcrumb + { + void Track(MenuItem item, bool prepend = false); + IReadOnlyList Trail { get; } + } - public class DefaultBreadcrumb : IBreadcrumb - { - private List _trail; + public class DefaultBreadcrumb : IBreadcrumb + { + private List _trail; - public void Track(MenuItem item, bool prepend = false) - { - Guard.NotNull(item, nameof(item)); + public void Track(MenuItem item, bool prepend = false) + { + Guard.NotNull(item, nameof(item)); - if (_trail == null) - { - _trail = new List(); - } + if (_trail == null) + { + _trail = new List(); + } - if (prepend) - { - _trail.Insert(0, item); - } - else - { - _trail.Add(item); - } - } + if (prepend) + { + _trail.Insert(0, item); + } + else + { + _trail.Add(item); + } + } - public IReadOnlyList Trail - { - get - { - return _trail; - } - } - } + public IReadOnlyList Trail => _trail; + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs index ad9ecb2725..e6fba32534 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs @@ -5,13 +5,13 @@ using System.Text; using System.Web; using System.Web.Optimization; -using BundleTransformer.Core.Orderers; using BundleTransformer.Core.Bundles; +using BundleTransformer.Core.Orderers; using SmartStore.Core; -using SmartStore.Services.Seo; -using SmartStore.Web.Framework.Theming.Assets; using SmartStore.Core.Themes; +using SmartStore.Services.Seo; using SmartStore.Utilities.Threading; +using SmartStore.Web.Framework.Theming.Assets; namespace SmartStore.Web.Framework.UI { @@ -30,13 +30,13 @@ public class BundleBuilder : IBundleBuilder { private readonly IStoreContext _storeContext; private readonly IWorkContext _workContext; - private readonly IThemeContext _themeContext; + private readonly IThemeContext _themeContext; public BundleBuilder(IStoreContext storeContext, IWorkContext workContext, IThemeContext themeContext) { _storeContext = storeContext; _workContext = workContext; - _themeContext = themeContext; + _themeContext = themeContext; } public string Build(BundleType type, IEnumerable files) @@ -53,14 +53,14 @@ public string Build(BundleType type, IEnumerable files) bundleFor = BundleTable.Bundles.GetBundleFor(bundleVirtualPath); if (bundleFor == null) { - var nullOrderer = new NullOrderer(); + var nullOrderer = new NullOrderer(); - Bundle bundle = (type == BundleType.Script) ? + Bundle bundle = (type == BundleType.Script) ? new CustomScriptBundle(bundleVirtualPath) as Bundle : new SmartStyleBundle(bundleVirtualPath) as Bundle; - bundle.Orderer = nullOrderer; + bundle.Orderer = nullOrderer; - bundle.Include(files.ToArray()); + bundle.Include(files.ToArray()); BundleTable.Bundles.Add(bundle); } @@ -68,13 +68,13 @@ public string Build(BundleType type, IEnumerable files) } if (type == BundleType.Script) - { - return Scripts.Render(bundleVirtualPath).ToString(); + { + return Scripts.Render(bundleVirtualPath).ToString(); - //// Uncomment this if you want to bypass asset caching on mobile browsers - //return Scripts.RenderFormat("", - // files.Select(x => VirtualPathUtility.ToAppRelative(x)).ToArray()).ToString(); - } + //// Uncomment this if you want to bypass asset caching on mobile browsers + //return Scripts.RenderFormat("", + // files.Select(x => VirtualPathUtility.ToAppRelative(x)).ToArray()).ToString(); + } return Styles.Render(bundleVirtualPath).ToString(); } @@ -92,8 +92,8 @@ protected virtual string GetBundleVirtualPath(BundleType type, IEnumerable x.EndsWith(".scss", StringComparison.OrdinalIgnoreCase))) { hash += "-s" + _storeContext.CurrentStore.Id; - hash += "-t" + _themeContext.CurrentTheme.ThemeName; + hash += "-t" + _themeContext.CurrentTheme.ThemeName; } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceItemModel.cs b/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceItemModel.cs index 6811ceac29..58856b71c6 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceItemModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceItemModel.cs @@ -1,21 +1,22 @@ -using System; -using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Framework.UI.Choices { - public abstract class ChoiceItemModel : EntityModelBase - { - public string Name { get; set; } - public string SeName { get; set; } - public string Alias { get; set; } - public string Color { get; set; } - public string PriceAdjustment { get; set; } - public decimal PriceAdjustmentValue { get; set; } - public int QuantityInfo { get; set; } - public bool IsPreSelected { get; set; } - public bool IsDisabled { get; set; } - public string ImageUrl { get; set; } + public abstract class ChoiceItemModel : EntityModelBase + { + public string Name { get; set; } + public string SeName { get; set; } + public string Title { get; set; } + public string Alias { get; set; } + public string Color { get; set; } + public string PriceAdjustment { get; set; } + public decimal PriceAdjustmentValue { get; set; } + public int QuantityInfo { get; set; } + public bool IsPreSelected { get; set; } + public bool IsDisabled { get; set; } + public bool IsUnavailable { get; set; } + public string ImageUrl { get; set; } - public abstract string GetItemLabel(); - } + public abstract string GetItemLabel(); + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceModel.cs b/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceModel.cs index 0b4b689452..a2b7832a02 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Choices/ChoiceModel.cs @@ -1,91 +1,92 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Framework.UI.Choices { - public abstract class ChoiceModel : EntityModelBase - { - protected ChoiceModel() - { - this.AllowedFileExtensions = new List(); - this.Values = new List(); - } - - public AttributeControlType AttributeControlType { get; set; } - - public string Alias { get; set; } - - public string Name { get; set; } - - public string Description { get; set; } - - public string TextPrompt { get; set; } - - public bool IsRequired { get; set; } - - public bool IsDisabled { get; set; } - - /// - /// Allowed file extensions for customer uploaded files - /// - public IList AllowedFileExtensions { get; set; } - - /// - /// Selected value for textboxes - /// - public string TextValue { get; set; } - /// - /// Selected day value for datepicker - /// - public int? SelectedDay { get; set; } - /// - /// Selected month value for datepicker - /// - public int? SelectedMonth { get; set; } - /// - /// Selected year value for datepicker - /// - public int? SelectedYear { get; set; } - /// - /// Begin year for datepicker - /// - public int? BeginYear { get; set; } - /// - /// End year for datepicker - /// - public int? EndYear { get; set; } - - public string UploadedFileGuid { get; set; } - public string UploadedFileName { get; set; } - - public virtual IList Values { get; set; } - - public abstract string BuildControlId(); - - public virtual string GetLabel() - { - return TextPrompt.NullEmpty() ?? Name; - } - - public virtual string GetDescription() - { - var containsImg = Description.IsEmpty() ? false : Description.Contains("(); + Values = new List(); + } + + public AttributeControlType AttributeControlType { get; set; } + + public string Alias { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string TextPrompt { get; set; } + + public string CustomData { get; set; } + + public bool IsRequired { get; set; } + + public bool IsDisabled { get; set; } + + /// + /// Allowed file extensions for customer uploaded files + /// + public IList AllowedFileExtensions { get; set; } + + /// + /// Selected value for textboxes + /// + public string TextValue { get; set; } + /// + /// Selected day value for datepicker + /// + public int? SelectedDay { get; set; } + /// + /// Selected month value for datepicker + /// + public int? SelectedMonth { get; set; } + /// + /// Selected year value for datepicker + /// + public int? SelectedYear { get; set; } + /// + /// Begin year for datepicker + /// + public int? BeginYear { get; set; } + /// + /// End year for datepicker + /// + public int? EndYear { get; set; } + + public string UploadedFileGuid { get; set; } + public string UploadedFileName { get; set; } + + public virtual IList Values { get; set; } + + public abstract string BuildControlId(); + + public virtual string GetLabel() + { + return TextPrompt.NullEmpty() ?? Name; + } + + public virtual string GetDescription() + { + var containsImg = Description.IsEmpty() ? false : Description.Contains(" _controlIdFn; - - public GenericChoiceModel(Func controlIdFn) - { - Guard.NotNull(controlIdFn, nameof(controlIdFn)); - - _controlIdFn = controlIdFn; - } - - public override string BuildControlId() - { - return _controlIdFn(this); - } - } - - public class GenericChoiceItemModel : ChoiceItemModel - { - private readonly Func _itemLabelFn; - - public GenericChoiceItemModel(Func itemLabelFn) - { - Guard.NotNull(itemLabelFn, nameof(itemLabelFn)); - - _itemLabelFn = itemLabelFn; - } - - public override string GetItemLabel() - { - return _itemLabelFn(this); - } - } + public class GenericChoiceModel : ChoiceModel + { + private readonly Func _controlIdFn; + + public GenericChoiceModel(Func controlIdFn) + { + Guard.NotNull(controlIdFn, nameof(controlIdFn)); + + _controlIdFn = controlIdFn; + } + + public override string BuildControlId() + { + return _controlIdFn(this); + } + } + + public class GenericChoiceItemModel : ChoiceItemModel + { + private readonly Func _itemLabelFn; + + public GenericChoiceItemModel(Func itemLabelFn) + { + Guard.NotNull(itemLabelFn, nameof(itemLabelFn)); + + _itemLabelFn = itemLabelFn; + } + + public override string GetItemLabel() + { + return _itemLabelFn(this); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/BoxSpacing.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/BoxSpacing.cs index 8b72005580..c447470207 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/BoxSpacing.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/BoxSpacing.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Newtonsoft.Json; namespace SmartStore.Web.Framework.UI { - public class BoxSpacing - { - [Range(0, 6)] - [JsonProperty("t")] - public byte? Top { get; set; } + public class BoxSpacing + { + [Range(0, 6)] + [JsonProperty("t")] + public byte? Top { get; set; } - [Range(0, 6)] - [JsonProperty("r")] - public byte? Right { get; set; } + [Range(0, 6)] + [JsonProperty("r")] + public byte? Right { get; set; } - [Range(0, 6)] - [JsonProperty("b")] - public byte? Bottom { get; set; } + [Range(0, 6)] + [JsonProperty("b")] + public byte? Bottom { get; set; } - [Range(0, 6)] - [JsonProperty("l")] - public byte? Left { get; set; } - } + [Range(0, 6)] + [JsonProperty("l")] + public byte? Left { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs index 10806732cb..c983dee479 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Web.Routing; -namespace SmartStore.Web.Framework.UI +namespace SmartStore.Web.Framework.UI { public abstract class Component : IUiComponent { @@ -13,16 +12,10 @@ protected Component() public string Id { - get - { - return !this.HtmlAttributes.ContainsKey("id") - ? this.Name + get => !this.HtmlAttributes.ContainsKey("id") + ? this.Name : (this.HtmlAttributes["id"] as string); - } - set - { - this.HtmlAttributes["id"] = value; - } + set => this.HtmlAttributes["id"] = value; } public string Name @@ -31,7 +24,7 @@ public string Name set; } - public IDictionary HtmlAttributes + public IDictionary HtmlAttributes { get; private set; @@ -39,12 +32,6 @@ public IDictionary HtmlAttributes - public virtual bool NameIsRequired - { - get - { - return false; - } - } - } + public virtual bool NameIsRequired => false; + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs index bb034c7061..e6029660ad 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Web; -using SmartStore.Utilities; -using SmartStore.Core.Infrastructure; using System.Web.Mvc; +using SmartStore.Core.Infrastructure; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI { - public abstract class ComponentBuilder : IHtmlString, IHideObjectMembers + public abstract class ComponentBuilder : IHtmlString, IHideObjectMembers where TComponent : Component where TBuilder : ComponentBuilder { @@ -17,7 +17,7 @@ protected ComponentBuilder(TComponent component, HtmlHelper htmlHelper) { Guard.NotNull(component, nameof(component)); Guard.NotNull(htmlHelper, nameof(htmlHelper)); - + this.Component = component; this.HtmlHelper = htmlHelper; } @@ -68,39 +68,39 @@ private void EnrichRenderer(ComponentRenderer renderer) renderer.ViewData = this.HtmlHelper.ViewData; } - public TBuilder WithRenderer(ComponentRenderer instance) - { - Guard.NotNull(instance, nameof(instance)); + public TBuilder WithRenderer(ComponentRenderer instance) + { + Guard.NotNull(instance, nameof(instance)); - return this.WithRenderer>(instance); - } + return this.WithRenderer>(instance); + } - public TBuilder WithRenderer(ComponentRenderer instance) + public TBuilder WithRenderer(ComponentRenderer instance) where T : ComponentRenderer { Guard.NotNull(instance, nameof(instance)); - this.Renderer = instance; - return this as TBuilder; - } + this.Renderer = instance; + return this as TBuilder; + } - public TBuilder WithRenderer() - where T : ComponentRenderer - { - return this.WithRenderer(typeof(T)); - } + public TBuilder WithRenderer() + where T : ComponentRenderer + { + return this.WithRenderer(typeof(T)); + } - public TBuilder WithRenderer(Type rendererType) + public TBuilder WithRenderer(Type rendererType) { Guard.NotNull(rendererType, nameof(rendererType)); Guard.Implements>(rendererType); - if (Activator.CreateInstance(rendererType) is ComponentRenderer renderer) - { - this.Renderer = renderer; - } + if (Activator.CreateInstance(rendererType) is ComponentRenderer renderer) + { + this.Renderer = renderer; + } - return this as TBuilder; + return this as TBuilder; } public virtual TBuilder Name(string name) @@ -109,7 +109,7 @@ public virtual TBuilder Name(string name) return this as TBuilder; } - public virtual TBuilder HtmlAttributes(object attributes) + public virtual TBuilder HtmlAttributes(object attributes) { return this.HtmlAttributes(CommonHelper.ObjectToDictionary(attributes)); } @@ -120,24 +120,24 @@ public virtual TBuilder HtmlAttributes(IDictionary attributes) return this as TBuilder; } - public virtual TBuilder HtmlAttribute(string name, object value) - { - Guard.NotEmpty(name, nameof(name)); - Guard.NotNull(value, nameof(value)); + public virtual TBuilder HtmlAttribute(string name, object value) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(value, nameof(value)); - this.Component.HtmlAttributes[name] = value; - return this as TBuilder; - } + this.Component.HtmlAttributes[name] = value; + return this as TBuilder; + } - public virtual TBuilder AddCssClass(string cssClass) - { - Guard.NotEmpty(cssClass, nameof(cssClass)); + public virtual TBuilder AddCssClass(string cssClass) + { + Guard.NotEmpty(cssClass, nameof(cssClass)); - this.Component.HtmlAttributes.AppendCssClass(cssClass); - return this as TBuilder; - } + this.Component.HtmlAttributes.AppendCssClass(cssClass); + return this as TBuilder; + } - public string ToHtmlString() + public string ToHtmlString() { return this.Renderer.ToHtmlString(); } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs index 455b58f2f3..a429f6128f 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs @@ -60,21 +60,21 @@ public virtual PagerBuilder Pager(IPageable model) return new PagerBuilder(new Pager(), this.HtmlHelper).Model(model); } - public virtual EntityPickerBuilder EntityPicker() - { - return new EntityPickerBuilder(new EntityPicker(), this.HtmlHelper); - } + public virtual EntityPickerBuilder EntityPicker() + { + return new EntityPickerBuilder(new EntityPicker(), this.HtmlHelper); + } - public virtual FileUploaderBuilder FileUploader() - { - return new FileUploaderBuilder(new FileUploader(), this.HtmlHelper); - } + public virtual FileUploaderBuilder FileUploader() + { + return new FileUploaderBuilder(new FileUploader(), this.HtmlHelper); + } public virtual MenuBuilder Menu() { return new MenuBuilder(new Menu(), this.HtmlHelper).WithRenderer(new MenuRenderer()); } - #endregion - } + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs index 36d21bec1a..115ab3ca81 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs @@ -1,11 +1,11 @@ using System; -using System.Web.Mvc; +using System.IO; using System.Web; +using System.Web.Mvc; using System.Web.UI; -using System.IO; namespace SmartStore.Web.Framework.UI -{ +{ public abstract class ComponentRenderer : IHtmlString where TComponent : Component { protected ComponentRenderer() @@ -51,40 +51,40 @@ public virtual void VerifyState() } } - protected string SanitizeId(string id) - { - return id.SanitizeHtmlId(); - } + protected string SanitizeId(string id) + { + return id.SanitizeHtmlId(); + } - public virtual void Render() + public virtual void Render() { - WriteHtml(ViewContext.Writer); - } + WriteHtml(ViewContext.Writer); + } public virtual string ToHtmlString() { using (var stringWriter = new StringWriter()) { - WriteHtml(stringWriter); - var str = stringWriter.ToString(); - return str; - } - } + WriteHtml(stringWriter); + var str = stringWriter.ToString(); + return str; + } + } - protected void WriteHtml(TextWriter writer) - { - this.VerifyState(); - this.Component.Id = SanitizeId(this.Component.Id); + protected void WriteHtml(TextWriter writer) + { + this.VerifyState(); + this.Component.Id = SanitizeId(this.Component.Id); - using (var htmlWriter = new HtmlTextWriter(writer)) - { - WriteHtmlCore(htmlWriter); - } - } + using (var htmlWriter = new HtmlTextWriter(writer)) + { + WriteHtmlCore(htmlWriter); + } + } - protected virtual void WriteHtmlCore(HtmlTextWriter writer) - { - throw new NotImplementedException(); - } + protected virtual void WriteHtmlCore(HtmlTextWriter writer) + { + throw new NotImplementedException(); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs index 4916e25252..9e10d5c572 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace SmartStore.Web.Framework.UI -{ +{ public static class ComponentRendererUtils { public static IDictionary AppendCssClass(this IDictionary attributes, Func cssClass) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs index fcca6a595b..12ed0951cb 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs @@ -2,48 +2,48 @@ { public class EntityPicker : Component { - public EntityPicker() + public EntityPicker() { - EntityType = "product"; - IconCssClass = "fa fa-search"; - HtmlAttributes["type"] = "button"; - HtmlAttributes.AppendCssClass("btn btn-secondary"); - HighlightSearchTerm = true; - AppendMode = true; - Delimiter = ","; - FieldName = "id"; - } - - public string EntityType { get; set; } + EntityType = "product"; + IconCssClass = "fa fa-search"; + HtmlAttributes["type"] = "button"; + HtmlAttributes.AppendCssClass("btn btn-secondary"); + HighlightSearchTerm = true; + AppendMode = true; + Delimiter = ","; + FieldName = "id"; + } + + public string EntityType { get; set; } public int LanguageId { get; set; } public string TargetInputSelector - { - get { return HtmlAttributes["data-target"] as string; } - set { HtmlAttributes["data-target"] = value; } - } - - public string Caption { get; set; } - public string IconCssClass { get; set; } - - public string DialogTitle { get; set; } - public string DialogUrl { get; set; } - - public bool DisableGroupedProducts { get; set; } - public bool DisableBundleProducts { get; set; } - public int[] DisabledEntityIds { get; set; } - public string[] Selected { get; set; } - - public bool EnableThumbZoomer { get; set; } - public bool HighlightSearchTerm { get; set; } - - public int MaxItems { get; set; } - public bool AppendMode { get; set; } - public string Delimiter { get; set; } - public string FieldName { get; set; } - - public string OnDialogLoadingHandlerName { get; set; } - public string OnDialogLoadedHandlerName { get; set; } - public string OnSelectionCompletedHandlerName { get; set; } - } + { + get => HtmlAttributes["data-target"] as string; + set => HtmlAttributes["data-target"] = value; + } + + public string Caption { get; set; } + public string IconCssClass { get; set; } + + public string DialogTitle { get; set; } + public string DialogUrl { get; set; } + + public bool DisableGroupedProducts { get; set; } + public bool DisableBundleProducts { get; set; } + public int[] DisabledEntityIds { get; set; } + public string[] Selected { get; set; } + + public bool EnableThumbZoomer { get; set; } + public bool HighlightSearchTerm { get; set; } + + public int MaxItems { get; set; } + public bool AppendMode { get; set; } + public string Delimiter { get; set; } + public string FieldName { get; set; } + + public string OnDialogLoadingHandlerName { get; set; } + public string OnDialogLoadedHandlerName { get; set; } + public string OnSelectionCompletedHandlerName { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs index 78b3d4867b..842771f5d5 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs @@ -10,22 +10,22 @@ public class EntityPickerBuilder : ComponentBuilder htmlHelper) : base(component, htmlHelper) { - WithRenderer(new ViewBasedComponentRenderer("EntityPicker")); - DialogUrl(UrlHelper.GenerateUrl( - null, - "Picker", - "Entity", - new RouteValueDictionary { { "area", "" } }, - RouteTable.Routes, - htmlHelper.ViewContext.RequestContext, - false)); - } - - public EntityPickerBuilder EntityType(string value) - { - base.Component.EntityType = value; - return this; - } + WithRenderer(new ViewBasedComponentRenderer("EntityPicker")); + DialogUrl(UrlHelper.GenerateUrl( + null, + "Picker", + "Entity", + new RouteValueDictionary { { "area", "" } }, + RouteTable.Routes, + htmlHelper.ViewContext.RequestContext, + false)); + } + + public EntityPickerBuilder EntityType(string value) + { + base.Component.EntityType = value; + return this; + } public EntityPickerBuilder LanguageId(int value) { @@ -34,128 +34,128 @@ public EntityPickerBuilder LanguageId(int value) } public EntityPickerBuilder Caption(string value) - { - base.Component.Caption = value; - return this; - } - - public EntityPickerBuilder IconCssClass(string value) - { - base.Component.IconCssClass = value; - return this; - } - - public EntityPickerBuilder Tooltip(string value) - { - base.Component.HtmlAttributes["title"] = value; - return this; - } - - public EntityPickerBuilder DialogTitle(string value) - { - base.Component.DialogTitle = value; - return this; - } - - public EntityPickerBuilder DialogUrl(string value) - { - base.Component.DialogUrl = value; - return this; - } - - - public EntityPickerBuilder For(Expression> expression) - { - Guard.NotNull(expression, nameof(expression)); - - return For(ExpressionHelper.GetExpressionText(expression)); - } - - public EntityPickerBuilder For(string expression) - { - base.Component.TargetInputSelector = "#" + this.HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(expression); - return this; - } - - - public EntityPickerBuilder DisableGroupedProducts(bool value) - { - base.Component.DisableGroupedProducts = value; - return this; - } - - public EntityPickerBuilder DisableBundleProducts(bool value) - { - base.Component.DisableBundleProducts = value; - return this; - } - - public EntityPickerBuilder DisabledEntityIds(params int[] values) - { - base.Component.DisabledEntityIds = values; - return this; - } - - public EntityPickerBuilder Selected(params string[] values) - { - base.Component.Selected = values; - return this; - } - - public EntityPickerBuilder EnableThumbZoomer(bool value) - { - base.Component.EnableThumbZoomer = value; - return this; - } - - public EntityPickerBuilder HighlightSearchTerm(bool value) - { - base.Component.HighlightSearchTerm = value; - return this; - } - - - public EntityPickerBuilder MaxItems(int value) - { - base.Component.MaxItems = value; - return this; - } - - public EntityPickerBuilder AppendMode(bool value) - { - base.Component.AppendMode = value; - return this; - } - - public EntityPickerBuilder Delimiter(string value) - { - base.Component.Delimiter = value; - return this; - } - - public EntityPickerBuilder FieldName(string value) - { - base.Component.FieldName = value; - return this; - } - - - public EntityPickerBuilder OnDialogLoading(string handlerName) - { - base.Component.OnDialogLoadingHandlerName = handlerName; - return this; - } - - public EntityPickerBuilder OnDialogLoaded(string handlerName) - { - base.Component.OnDialogLoadedHandlerName = handlerName; - return this; - } - - public EntityPickerBuilder OnSelectionCompleted(string handlerName) - { - base.Component.OnSelectionCompletedHandlerName = handlerName; - return this; - } - } + { + base.Component.Caption = value; + return this; + } + + public EntityPickerBuilder IconCssClass(string value) + { + base.Component.IconCssClass = value; + return this; + } + + public EntityPickerBuilder Tooltip(string value) + { + base.Component.HtmlAttributes["title"] = value; + return this; + } + + public EntityPickerBuilder DialogTitle(string value) + { + base.Component.DialogTitle = value; + return this; + } + + public EntityPickerBuilder DialogUrl(string value) + { + base.Component.DialogUrl = value; + return this; + } + + + public EntityPickerBuilder For(Expression> expression) + { + Guard.NotNull(expression, nameof(expression)); + + return For(ExpressionHelper.GetExpressionText(expression)); + } + + public EntityPickerBuilder For(string expression) + { + base.Component.TargetInputSelector = "#" + this.HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(expression); + return this; + } + + + public EntityPickerBuilder DisableGroupedProducts(bool value) + { + base.Component.DisableGroupedProducts = value; + return this; + } + + public EntityPickerBuilder DisableBundleProducts(bool value) + { + base.Component.DisableBundleProducts = value; + return this; + } + + public EntityPickerBuilder DisabledEntityIds(params int[] values) + { + base.Component.DisabledEntityIds = values; + return this; + } + + public EntityPickerBuilder Selected(params string[] values) + { + base.Component.Selected = values; + return this; + } + + public EntityPickerBuilder EnableThumbZoomer(bool value) + { + base.Component.EnableThumbZoomer = value; + return this; + } + + public EntityPickerBuilder HighlightSearchTerm(bool value) + { + base.Component.HighlightSearchTerm = value; + return this; + } + + + public EntityPickerBuilder MaxItems(int value) + { + base.Component.MaxItems = value; + return this; + } + + public EntityPickerBuilder AppendMode(bool value) + { + base.Component.AppendMode = value; + return this; + } + + public EntityPickerBuilder Delimiter(string value) + { + base.Component.Delimiter = value; + return this; + } + + public EntityPickerBuilder FieldName(string value) + { + base.Component.FieldName = value; + return this; + } + + + public EntityPickerBuilder OnDialogLoading(string handlerName) + { + base.Component.OnDialogLoadingHandlerName = handlerName; + return this; + } + + public EntityPickerBuilder OnDialogLoaded(string handlerName) + { + base.Component.OnDialogLoadedHandlerName = handlerName; + return this; + } + + public EntityPickerBuilder OnSelectionCompleted(string handlerName) + { + base.Component.OnSelectionCompletedHandlerName = handlerName; + return this; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs index 75bb64220f..96094d1808 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs @@ -1,29 +1,27 @@ -using System; - -namespace SmartStore.Web.Framework.UI -{ +namespace SmartStore.Web.Framework.UI +{ public enum BadgeStyle { - Secondary, - Primary, - Success, - Info, - Warning, + Secondary, + Primary, + Success, + Info, + Warning, Danger, - Light, - Dark - } + Light, + Dark + } - public enum ButtonStyle - { - Secondary, - Primary, - Success, - Info, - Warning, - Danger, - Light, - Dark, - Link - } + public enum ButtonStyle + { + Secondary, + Primary, + Success, + Info, + Warning, + Danger, + Light, + Dark, + Link + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs index fa9c08da20..12eb04dd6d 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; @@ -9,49 +8,56 @@ namespace SmartStore.Web.Framework.UI { public class FileUploader : Component { - public FileUploader() - : this(EngineContext.Current.Resolve()) - { - } + public FileUploader() + : this(EngineContext.Current.Resolve()) + { + } - public FileUploader(Localizer localizer) + public FileUploader(Localizer localizer) { - HtmlAttributes.AppendCssClass("fileupload"); - HtmlAttributes.Add("data-accept", "gif,jpeg,jpg,png"); - HtmlAttributes.Add("data-show-remove-after-upload", "false"); - - if (localizer != null) - { - UploadText = localizer("Common.Fileuploader.Upload"); - } - } + HtmlAttributes.AppendCssClass("fu-fileupload"); + HtmlAttributes.Add("data-accept", "gif,jpeg,jpg,png"); + HtmlAttributes.Add("data-show-remove-after-upload", "false"); + + if (localizer != null) + { + UploadText = localizer("Common.Fileuploader.Upload"); + } + } - public string Path { get; set; } = SystemAlbumProvider.Files; + public string Path { get; set; } = SystemAlbumProvider.Files; - public string UploadUrl - { - get { return HtmlAttributes["data-upload-url"] as string; } - set { HtmlAttributes["data-upload-url"] = value; } - } + public string UploadUrl + { + get => HtmlAttributes["data-upload-url"] as string; + set => HtmlAttributes["data-upload-url"] = value; + } - public string UploadText { get; set; } - public bool ShowRemoveButton { get; set; } - public bool Compact { get; set; } + public string UploadText { get; set; } + public bool ShowRemoveButton { get; set; } + public bool ShowRemoveButtonAfterUpload { get; set; } + public bool ShowBrowseMedia { get; set; } + public bool HasTemplatePreview { get; set; } + public string ClickableElement { get; set; } + public bool DownloadEnabled { get; set; } - public bool Multifile { get; set; } - public string PreviewContainerId { get; set; } - public int? MainFileId { get; set; } - public string EntityType { get; set; } - public int EntityId { get; set; } - public string DeleteUrl { get; set; } - public string SortUrl { get; set; } - public string EntityAssignmentUrl { get; set; } - public IEnumerable UploadedFiles { get; set; } - public string OnUploadingHandlerName { get; set; } - public string OnUploadCompletedHandlerName { get; set; } - public string OnErrorHandlerName { get; set; } - public string OnFileRemoveHandlerName { get; set; } - public string OnAbortedHandlerName { get; set; } - public string OnCompletedHandlerName { get; set; } - } + public bool Multifile { get; set; } + public string TypeFilter { get; set; } + public string PreviewContainerId { get; set; } + public int? MainFileId { get; set; } + public long? MaxFileSize { get; set; } + public string EntityType { get; set; } + public int EntityId { get; set; } + public string DeleteUrl { get; set; } + public string SortUrl { get; set; } + public string EntityAssignmentUrl { get; set; } + public IEnumerable UploadedFiles { get; set; } + public string OnUploadingHandlerName { get; set; } + public string OnUploadCompletedHandlerName { get; set; } + public string OnErrorHandlerName { get; set; } + public string OnFileRemoveHandlerName { get; set; } + public string OnAbortedHandlerName { get; set; } + public string OnCompletedHandlerName { get; set; } + public string OnMediaSelectedHandlerName { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs index 903cc0a149..e2d5dfb4a6 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs @@ -1,12 +1,9 @@ -using SmartStore.Core.Domain.Catalog; -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; using System.Web.Mvc; -using System.Web.Routing; -using System.Web.WebPages; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Infrastructure; +using SmartStore.Services.Media; namespace SmartStore.Web.Framework.UI { @@ -15,148 +12,181 @@ public class FileUploaderBuilder : ComponentBuilder htmlHelper) : base(component, htmlHelper) { - WithRenderer(new ViewBasedComponentRenderer("FileUploader")); - } - - public FileUploaderBuilder Path(string value) - { - base.Component.Path = value; - return this; - } - - public FileUploaderBuilder UploadUrl(string value) - { - base.Component.UploadUrl = value; - return this; - } - - public FileUploaderBuilder ShowRemoveButton(bool value) - { - base.Component.ShowRemoveButton = value; - return this; - } - - public FileUploaderBuilder ShowRemoveButtonAfterUpload(bool value) - { - base.Component.HtmlAttributes["data-show-remove-after-upload"] = value.ToString().ToLower(); - return this; - } - - public FileUploaderBuilder Compact(bool value) - { - base.Component.Compact = value; - return this; - } - - public FileUploaderBuilder Multifile(bool value) - { - base.Component.Multifile = value; - return this; - } - - public FileUploaderBuilder PreviewContainerId(string value) - { - base.Component.PreviewContainerId = value; - return this; - } - - public FileUploaderBuilder MainFileId(int? value) - { - base.Component.MainFileId = value; - return this; - } - - public FileUploaderBuilder UploadedFiles(IEnumerable value) - { - base.Component.UploadedFiles = value; - return this; - } - - public FileUploaderBuilder EntityType(string value) - { - base.Component.EntityType = value; - return this; - } - - public FileUploaderBuilder EntityId(int value) - { - base.Component.EntityId = value; - return this; - } - - public FileUploaderBuilder DeleteUrl(string value) - { - base.Component.DeleteUrl = value; - return this; - } - - public FileUploaderBuilder SortUrl(string value) - { - base.Component.SortUrl = value; - return this; - } - - public FileUploaderBuilder EntityAssignmentUrl(string value) - { - base.Component.EntityAssignmentUrl = value; - return this; - } - - public FileUploaderBuilder AcceptedFileTypes(string value) - { - if (value.IsEmpty()) - { - if (base.Component.HtmlAttributes.ContainsKey("data-accept")) - base.Component.HtmlAttributes.Remove("data-accept"); - } - else - { - base.Component.HtmlAttributes["data-accept"] = value; - } - - return this; - } - - public FileUploaderBuilder UploadText(string value) - { - base.Component.UploadText = value; - return this; - } - - public FileUploaderBuilder OnUploadingHandlerName(string handlerName) - { - base.Component.OnUploadingHandlerName = handlerName; - return this; - } - - public FileUploaderBuilder OnUploadCompletedHandlerName(string handlerName) - { - base.Component.OnUploadCompletedHandlerName = handlerName; - return this; - } - - public FileUploaderBuilder OnErrorHandlerName(string handlerName) - { - base.Component.OnErrorHandlerName = handlerName; - return this; - } - - public FileUploaderBuilder OnFileRemoveHandlerName(string handlerName) - { - base.Component.OnFileRemoveHandlerName = handlerName; - return this; - } - - public FileUploaderBuilder OnAbortedHandlerName(string handlerName) - { - base.Component.OnAbortedHandlerName = handlerName; - return this; - } - - public FileUploaderBuilder OnCompletedHandlerName(string handlerName) - { - base.Component.OnCompletedHandlerName = handlerName; - return this; - } - } + WithRenderer(new ViewBasedComponentRenderer("FileUploader")); + TypeFilter("*"); + } + + public FileUploaderBuilder Path(string value) + { + base.Component.Path = value; + return this; + } + + public FileUploaderBuilder UploadUrl(string value) + { + base.Component.UploadUrl = value; + return this; + } + + public FileUploaderBuilder ShowRemoveButton(bool value) + { + base.Component.ShowRemoveButton = value; + return this; + } + + public FileUploaderBuilder ShowRemoveButtonAfterUpload(bool value) + { + base.Component.HtmlAttributes["data-show-remove-after-upload"] = value.ToString().ToLower(); + base.Component.ShowRemoveButtonAfterUpload = value; + return this; + } + + public FileUploaderBuilder ShowBrowseMedia(bool value) + { + base.Component.ShowBrowseMedia = value; + return this; + } + + public FileUploaderBuilder HasTemplatePreview(bool value) + { + base.Component.HasTemplatePreview = value; + return this; + } + + public FileUploaderBuilder DownloadEnabled(bool value) + { + base.Component.DownloadEnabled = value; + return this; + } + + public FileUploaderBuilder ClickableElement(string value) + { + base.Component.ClickableElement = value; + return this; + } + + public FileUploaderBuilder Multifile(bool value) + { + base.Component.Multifile = value; + return this; + } + + public FileUploaderBuilder TypeFilter(string value) + { + var mediaTypeResolver = EngineContext.Current.Resolve(); + var extensions = mediaTypeResolver.ParseTypeFilter(value); + + base.Component.HtmlAttributes["data-accept"] = "." + String.Join(",.", extensions); + base.Component.TypeFilter = value; + + return this; + } + + public FileUploaderBuilder PreviewContainerId(string value) + { + base.Component.PreviewContainerId = value; + return this; + } + + public FileUploaderBuilder MainFileId(int? value) + { + base.Component.MainFileId = value; + return this; + } + + /// + /// Sets the maximum file size. + /// Use "0" for an unlimited file size and "null" for the default size (MediaSettings.MaxUploadFileSize). + /// + /// Maximum file size in KB. + public FileUploaderBuilder MaxFileSize(long? value) + { + base.Component.MaxFileSize = value; + return this; + } + + public FileUploaderBuilder UploadedFiles(IEnumerable value) + { + base.Component.UploadedFiles = value; + return this; + } + + public FileUploaderBuilder EntityType(string value) + { + base.Component.EntityType = value; + return this; + } + + public FileUploaderBuilder EntityId(int value) + { + base.Component.EntityId = value; + return this; + } + + public FileUploaderBuilder DeleteUrl(string value) + { + base.Component.DeleteUrl = value; + return this; + } + + public FileUploaderBuilder SortUrl(string value) + { + base.Component.SortUrl = value; + return this; + } + + public FileUploaderBuilder EntityAssignmentUrl(string value) + { + base.Component.EntityAssignmentUrl = value; + return this; + } + + public FileUploaderBuilder UploadText(string value) + { + base.Component.UploadText = value; + return this; + } + + public FileUploaderBuilder OnUploadingHandlerName(string handlerName) + { + base.Component.OnUploadingHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnUploadCompletedHandlerName(string handlerName) + { + base.Component.OnUploadCompletedHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnErrorHandlerName(string handlerName) + { + base.Component.OnErrorHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnFileRemoveHandlerName(string handlerName) + { + base.Component.OnFileRemoveHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnAbortedHandlerName(string handlerName) + { + base.Component.OnAbortedHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnCompletedHandlerName(string handlerName) + { + base.Component.OnCompletedHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnMediaSelectedHandlerName(string handlerName) + { + base.Component.OnMediaSelectedHandlerName = handlerName; + return this; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs index 9d37f832f1..21a87bc743 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Web; using System.Web.Mvc; using SmartStore.Utilities.ObjectPools; @@ -15,45 +14,45 @@ public static ComponentFactory SmartStore(this HtmlHelper(helper); } - public static IHtmlString Attrs(this HtmlHelper html, IDictionary attrs, params string[] exclude) - { - var sb = PooledStringBuilder.Rent(); + public static IHtmlString Attrs(this HtmlHelper html, IDictionary attrs, params string[] exclude) + { + var sb = PooledStringBuilder.Rent(); - using (var writer = new StringWriter(sb)) - { - RenderAttrs(attrs, writer, exclude); - } + using (var writer = new StringWriter(sb)) + { + RenderAttrs(attrs, writer, exclude); + } - return MvcHtmlString.Create(sb.ToStringAndReturn()); - } + return MvcHtmlString.Create(sb.ToStringAndReturn()); + } - public static void RenderAttrs(this HtmlHelper html, IDictionary attrs, params string[] exclude) - { - RenderAttrs(attrs, html.ViewContext.Writer, exclude); - } + public static void RenderAttrs(this HtmlHelper html, IDictionary attrs, params string[] exclude) + { + RenderAttrs(attrs, html.ViewContext.Writer, exclude); + } - private static void RenderAttrs(IDictionary attrs, TextWriter output, params string[] exclude) - { - Guard.NotNull(attrs, nameof(attrs)); + private static void RenderAttrs(IDictionary attrs, TextWriter output, params string[] exclude) + { + Guard.NotNull(attrs, nameof(attrs)); - if (attrs.Count == 0) - { - return; - } + if (attrs.Count == 0) + { + return; + } - var first = true; + var first = true; - foreach (var kvp in attrs) - { + foreach (var kvp in attrs) + { if (exclude.Contains(kvp.Key)) continue; - - if (!first) - { - output.Write(" "); - } - first = false; + if (!first) + { + output.Write(" "); + } + + first = false; output.Write(kvp.Key); @@ -63,7 +62,7 @@ private static void RenderAttrs(IDictionary attrs, TextWriter ou HttpUtility.HtmlAttributeEncode(kvp.Value.ToString(), output); output.Write("\""); } - } - } - } + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/IContentContainer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/IContentContainer.cs index 2f2753acfb..f252990485 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/IContentContainer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/IContentContainer.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Web.WebPages; namespace SmartStore.Web.Framework.UI diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs index 31974274b4..e2e2135aa2 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Web.Framework.UI { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/IUiComponent.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/IUiComponent.cs index 7352829014..ad697d1bb7 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/IUiComponent.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/IUiComponent.cs @@ -1,6 +1,4 @@ -using System; - -namespace SmartStore.Web.Framework.UI +namespace SmartStore.Web.Framework.UI { public interface IUiComponent : IHtmlAttributesContainer { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs index ccbc2fbbd6..a62bfc760c 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs @@ -1,27 +1,20 @@ -using System; -using SmartStore.Collections; +using SmartStore.Collections; namespace SmartStore.Web.Framework.UI { - public abstract class AdminMenuProvider : IMenuProvider - { - public void BuildMenu(TreeNode rootNode) - { - var pluginsNode = rootNode.SelectNodeById("plugins"); - BuildMenuCore(pluginsNode); - } + public abstract class AdminMenuProvider : IMenuProvider + { + public void BuildMenu(TreeNode rootNode) + { + var pluginsNode = rootNode.SelectNodeById("plugins"); + BuildMenuCore(pluginsNode); + } - protected abstract void BuildMenuCore(TreeNode pluginsNode); + protected abstract void BuildMenuCore(TreeNode pluginsNode); - public string MenuName - { - get { return "admin"; } - } + public string MenuName => "admin"; - public virtual int Ordinal - { - get { return 0; } - } + public virtual int Ordinal => 0; - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/CatalogMenuProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/CatalogMenuProvider.cs index 4c3e8f10f3..b3e9177e82 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/CatalogMenuProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/CatalogMenuProvider.cs @@ -1,21 +1,14 @@ -using System; -using SmartStore.Collections; +using SmartStore.Collections; namespace SmartStore.Web.Framework.UI { - public abstract class CatalogMenuProvider : IMenuProvider - { - public abstract void BuildMenu(TreeNode rootNode); + public abstract class CatalogMenuProvider : IMenuProvider + { + public abstract void BuildMenu(TreeNode rootNode); - public string MenuName - { - get { return "catalog"; } - } + public string MenuName => "catalog"; - public virtual int Ordinal - { - get { return 0; } - } + public virtual int Ordinal => 0; - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/IMenuProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/IMenuProvider.cs index d48441cc0d..6ce5cb41c4 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/IMenuProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/IMenuProvider.cs @@ -1,19 +1,18 @@ -using System; -using SmartStore.Collections; +using SmartStore.Collections; namespace SmartStore.Web.Framework.UI { - /// - /// Enables (plugins) developers to inject menu items to menus - /// - public interface IMenuProvider : IOrdered + /// + /// Enables (plugins) developers to inject menu items to menus + /// + public interface IMenuProvider : IOrdered { void BuildMenu(TreeNode rootNode); - /// - /// Gets the menu name to inject the menu items into (e.g. admin, catalog etc.) - /// - string MenuName { get; } + /// + /// Gets the menu name to inject the menu items into (e.g. admin, catalog etc.) + /// + string MenuName { get; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs index f17eae41e2..aa1088693e 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs @@ -6,6 +6,6 @@ public class Menu : Component public string Template { get; set; } - public RouteInfo Route { get; set; } + public RouteInfo Route { get; set; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuBuilder.cs index d3b2e75f5e..1703ee3988 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuBuilder.cs @@ -11,16 +11,16 @@ public MenuBuilder(Menu component, HtmlHelper htmlHelper) public MenuBuilder Template(string value) { - Component.Route = null; - Component.Template = value; + Component.Route = null; + Component.Template = value; return this; } - public MenuBuilder Template(string action, string controller, object routeValues = null) - { - Component.Template = null; - Component.Route = new RouteInfo(action, controller, routeValues); - return this; - } - } + public MenuBuilder Template(string action, string controller, object routeValues = null) + { + Component.Template = null; + Component.Route = new RouteInfo(action, controller, routeValues); + return this; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs index 432460ef32..7a49919a7c 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs @@ -12,15 +12,15 @@ namespace SmartStore.Web.Framework.UI { public static class MenuExtensions - { - public static IEnumerable> GetBreadcrumb(this TreeNode node) - { - Guard.NotNull(node, nameof(node)); + { + public static IEnumerable> GetBreadcrumb(this TreeNode node) + { + Guard.NotNull(node, nameof(node)); - return node.Trail.Where(x => !x.IsRoot); - } + return node.Trail.Where(x => !x.IsRoot); + } - public static string GetItemText(this TreeNode node, Localizer localizer) + public static string GetItemText(this TreeNode node, Localizer localizer) { string result = null; @@ -37,62 +37,62 @@ public static string GetItemText(this TreeNode node, Localizer localiz return result; } - /// - /// Gets the state of node within the passed currentPath, which is the navigation breadcrumb. - /// - /// The node to get the state for - /// The current path/breadcrumb - /// - /// enumeration indicating whether the node is in the current path (Selected or Expanded) - /// and whether it has children (Parent) - /// - public static NodePathState GetNodePathState(this TreeNode node, IEnumerable> currentPath) - { - return GetNodePathState(node, currentPath.Select(x => x.Value).ToList()); - } - - /// - /// Gets the state of node within the passed currentPath, which is the navigation breadcrumb. - /// - /// The node to get the state for - /// The current path/breadcrumb - /// - /// enumeration indicating whether the node is in the current path (Selected or Expanded) - /// and whether it has children (Parent) - /// - public static NodePathState GetNodePathState(this TreeNode node, IList currentPath) - { - Guard.NotNull(currentPath, nameof(currentPath)); - - var state = NodePathState.Unknown; - - if (node.HasChildren) - { - state = state | NodePathState.Parent; - } - - var lastInPath = currentPath.LastOrDefault(); - - if (currentPath.Count > 0) - { - if (node.Value.Equals(lastInPath)) - { - state = state | NodePathState.Selected; - } - else - { - if (node.Depth - 1 < currentPath.Count) - { - if (currentPath[node.Depth - 1].Equals(node.Value)) - { - state = state | NodePathState.Expanded; - } - } - } - } - - return state; - } + /// + /// Gets the state of node within the passed currentPath, which is the navigation breadcrumb. + /// + /// The node to get the state for + /// The current path/breadcrumb + /// + /// enumeration indicating whether the node is in the current path (Selected or Expanded) + /// and whether it has children (Parent) + /// + public static NodePathState GetNodePathState(this TreeNode node, IEnumerable> currentPath) + { + return GetNodePathState(node, currentPath.Select(x => x.Value).ToList()); + } + + /// + /// Gets the state of node within the passed currentPath, which is the navigation breadcrumb. + /// + /// The node to get the state for + /// The current path/breadcrumb + /// + /// enumeration indicating whether the node is in the current path (Selected or Expanded) + /// and whether it has children (Parent) + /// + public static NodePathState GetNodePathState(this TreeNode node, IList currentPath) + { + Guard.NotNull(currentPath, nameof(currentPath)); + + var state = NodePathState.Unknown; + + if (node.HasChildren) + { + state = state | NodePathState.Parent; + } + + var lastInPath = currentPath.LastOrDefault(); + + if (currentPath.Count > 0) + { + if (node.Value.Equals(lastInPath)) + { + state = state | NodePathState.Selected; + } + else + { + if (node.Depth - 1 < currentPath.Count) + { + if (currentPath[node.Depth - 1].Equals(node.Value)) + { + state = state | NodePathState.Expanded; + } + } + } + } + + return state; + } /// /// Applies serialized route informations to a tree node. @@ -136,7 +136,7 @@ public static MenuModel CreateModel(this IMenu menu, string template, Controller var model = new MenuModel { Name = menu.Name, - Template = template ?? menu.Name, + Template = template ?? menu.Name, Root = menu.Root, SelectedNode = menu.ResolveCurrentNode(context) }; @@ -194,7 +194,7 @@ void AddChildItems(TreeNode parentNode, int parentItemId) var entities = itemMap.ContainsKey(parentItemId) ? itemMap[parentItemId].OrderBy(x => x.DisplayOrder) : Enumerable.Empty(); - + foreach (var entity in entities) { if (!string.IsNullOrEmpty(entity.ProviderName) && itemProviders.TryGetValue(entity.ProviderName, out var provider)) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs index 0988079da8..eded7876f0 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs @@ -11,12 +11,12 @@ public class MenuItem : NavigationItem, ICloneable public MenuItem() { this.Attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + } - /// - /// If this menu item refers to an entity, the id of the backed entity (like category, products e.g.) - /// - public int EntityId { get; set; } + /// + /// If this menu item refers to an entity, the id of the backed entity (like category, products e.g.) + /// + public int EntityId { get; set; } public string EntityName { get; set; } public int MenuItemId { get; set; } @@ -40,10 +40,7 @@ public string Id return _id; } - set - { - _id = value; - } + set => _id = value; } public string ResKey { get; set; } @@ -54,7 +51,7 @@ public string Id public IDictionary Attributes { get; set; } - public MenuItemBuilder ToBuilder() + public MenuItemBuilder ToBuilder() { return new MenuItemBuilder(this); } @@ -64,14 +61,14 @@ public static implicit operator MenuItemBuilder(MenuItem menuItem) return menuItem.ToBuilder(); } - public MenuItem Clone() - { - return (MenuItem)this.MemberwiseClone(); - } + public MenuItem Clone() + { + return (MenuItem)this.MemberwiseClone(); + } - object ICloneable.Clone() - { - return this.Clone(); - } + object ICloneable.Clone() + { + return this.Clone(); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItemBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItemBuilder.cs index 785aa05415..cda64e9c12 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItemBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItemBuilder.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SmartStore.Web.Framework.UI { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs index 428f65ea31..3fddbd9200 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs @@ -5,28 +5,28 @@ namespace SmartStore.Web.Framework.UI { public class MenuModel - { - private TreeNode _selectedNode; - private bool _seekedSelectedNode; + { + private TreeNode _selectedNode; + private bool _seekedSelectedNode; public string Name { get; set; } - public string Template { get; set; } + public string Template { get; set; } - public TreeNode Root { get; set; } - public IList> Path { get; set; } + public TreeNode Root { get; set; } + public IList> Path { get; set; } - public TreeNode SelectedNode - { - get - { - if (!_seekedSelectedNode) - { - _selectedNode = Path?.LastOrDefault() ?? Root; - _seekedSelectedNode = true; - } + public TreeNode SelectedNode + { + get + { + if (!_seekedSelectedNode) + { + _selectedNode = Path?.LastOrDefault() ?? Root; + _seekedSelectedNode = true; + } - return _selectedNode ?? Root; - } + return _selectedNode ?? Root; + } set { _selectedNode = value; @@ -36,6 +36,6 @@ public TreeNode SelectedNode _seekedSelectedNode = true; } - } - } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuPublisher.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuPublisher.cs index 4078ca66fd..82609d838d 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuPublisher.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuPublisher.cs @@ -1,60 +1,57 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using SmartStore.Collections; using SmartStore.Core.Caching; using SmartStore.Core.Infrastructure; namespace SmartStore.Web.Framework.UI { - public interface IMenuPublisher - { - void RegisterMenus(TreeNode rootNode, string menuName); - } - - public class MenuPublisher : IMenuPublisher - { - private readonly ITypeFinder _typeFinder; - private readonly IRequestCache _requestCache; - - public MenuPublisher(ITypeFinder typeFinder, IRequestCache requestCache) - { - _typeFinder = typeFinder; - _requestCache = requestCache; - } - - public void RegisterMenus(TreeNode rootNode, string menuName) - { - Guard.NotNull(rootNode, nameof(rootNode)); - Guard.NotEmpty(menuName, nameof(menuName)); - - var providers = _requestCache.Get("sm.menu.providers.{0}".FormatInvariant(menuName), () => - { - var allInstances = _requestCache.Get("sm.menu.allproviders", () => - { - var instances = new List(); - var providerTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); - - foreach (var type in providerTypes) - { - try - { - var provider = EngineContext.Current.ContainerManager.ResolveUnregistered(type) as IMenuProvider; - instances.Add(provider); - } - catch { } - } - - return instances; - }); - - return allInstances.Where(x => x.MenuName.IsCaseInsensitiveEqual(menuName)).OrderBy(x => x.Ordinal).ToList(); - }); - - providers.Each(x => x.BuildMenu(rootNode)); - } - - } + public interface IMenuPublisher + { + void RegisterMenus(TreeNode rootNode, string menuName); + } + + public class MenuPublisher : IMenuPublisher + { + private readonly ITypeFinder _typeFinder; + private readonly IRequestCache _requestCache; + + public MenuPublisher(ITypeFinder typeFinder, IRequestCache requestCache) + { + _typeFinder = typeFinder; + _requestCache = requestCache; + } + + public void RegisterMenus(TreeNode rootNode, string menuName) + { + Guard.NotNull(rootNode, nameof(rootNode)); + Guard.NotEmpty(menuName, nameof(menuName)); + + var providers = _requestCache.Get("sm.menu.providers.{0}".FormatInvariant(menuName), () => + { + var allInstances = _requestCache.Get("sm.menu.allproviders", () => + { + var instances = new List(); + var providerTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); + + foreach (var type in providerTypes) + { + try + { + var provider = EngineContext.Current.ContainerManager.ResolveUnregistered(type) as IMenuProvider; + instances.Add(provider); + } + catch { } + } + + return instances; + }); + + return allInstances.Where(x => x.MenuName.IsCaseInsensitiveEqual(menuName)).OrderBy(x => x.Ordinal).ToList(); + }); + + providers.Each(x => x.BuildMenu(rootNode)); + } + + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs index bccc0d5ef1..668233de1d 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs @@ -1,61 +1,57 @@ -using System.Collections.Generic; -using System.Linq; -using System.Web.Mvc.Html; +using System.Web.Mvc.Html; using System.Web.UI; -using SmartStore.Collections; -using SmartStore.Core.Infrastructure; namespace SmartStore.Web.Framework.UI { public class MenuRenderer : ComponentRenderer { - protected override void WriteHtmlCore(HtmlTextWriter writer) - { - var route = Component.Route; + protected override void WriteHtmlCore(HtmlTextWriter writer) + { + var route = Component.Route; - if (route != null) - { - HtmlHelper.RenderAction(route.Action, route.Controller, route.RouteValues); - } - else - { - HtmlHelper.RenderAction("Menu", "Menu", new - { - area = "", - name = Component.Name, - template = Component.Template - }); - } - } + if (route != null) + { + HtmlHelper.RenderAction(route.Action, route.Controller, route.RouteValues); + } + else + { + HtmlHelper.RenderAction("Menu", "Menu", new + { + area = "", + name = Component.Name, + template = Component.Template + }); + } + } - public override void Render() - { - var c = Component; - var route = c.Route; + public override void Render() + { + var c = Component; + var route = c.Route; - if (route != null) - { - HtmlHelper.RenderAction(route.Action, route.Controller, route.RouteValues); - } - else - { - HtmlHelper.RenderAction("Menu", "Menu", new { area = "", name = c.Name, template = c.Template }); - } - } + if (route != null) + { + HtmlHelper.RenderAction(route.Action, route.Controller, route.RouteValues); + } + else + { + HtmlHelper.RenderAction("Menu", "Menu", new { area = "", name = c.Name, template = c.Template }); + } + } - public override string ToHtmlString() - { - var c = Component; - var route = c.Route; + public override string ToHtmlString() + { + var c = Component; + var route = c.Route; - if (route != null) - { - return HtmlHelper.Action(route.Action, route.Controller, route.RouteValues).ToHtmlString(); - } - else - { - return HtmlHelper.Action("Menu", "Menu", new { area = "", name = c.Name, template = c.Template }).ToHtmlString(); - } - } - } + if (route != null) + { + return HtmlHelper.Action(route.Action, route.Controller, route.RouteValues).ToHtmlString(); + } + else + { + return HtmlHelper.Action("Menu", "Menu", new { area = "", name = c.Name, template = c.Template }).ToHtmlString(); + } + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NodePathState.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NodePathState.cs index a567712ac5..076d3b6839 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NodePathState.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NodePathState.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; namespace SmartStore.Web.Framework.UI { - [Flags] - public enum NodePathState - { - Unknown = 0, - Parent = 1, - Expanded = 2, - Selected = 4 - } + [Flags] + public enum NodePathState + { + Unknown = 0, + Parent = 1, + Expanded = 2, + Selected = 4 + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs index e3982af7fe..18cb948efb 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs @@ -1,84 +1,71 @@ -using System; -using System.Web.Routing; +using System.Web.Routing; namespace SmartStore.Web.Framework.UI { - public abstract class NavigatableComponent : Component, INavigatable - { - private string _actionName; - private string _controllerName; - private string _routeName; - private string _url; + public abstract class NavigatableComponent : Component, INavigatable + { + private string _actionName; + private string _controllerName; + private string _routeName; + private string _url; - protected NavigatableComponent() - { - this.RouteValues = new RouteValueDictionary(); - } + protected NavigatableComponent() + { + this.RouteValues = new RouteValueDictionary(); + } - public string ActionName - { - get - { - return _actionName; - } - set - { - _actionName = value; - _routeName = (string)(_url = null); - } - } + public string ActionName + { + get => _actionName; + set + { + _actionName = value; + _routeName = (string)(_url = null); + } + } - public string ControllerName - { - get - { - return _controllerName; - } - set - { - _controllerName = value; - _routeName = (string)(_url = null); - } - } + public string ControllerName + { + get => _controllerName; + set + { + _controllerName = value; + _routeName = (string)(_url = null); + } + } - public string RouteName - { - get - { - return _routeName; - } - set - { - _routeName = value; - _controllerName = _actionName = (string)(_url = null); - } - } + public string RouteName + { + get => _routeName; + set + { + _routeName = value; + _controllerName = _actionName = (string)(_url = null); + } + } - public RouteValueDictionary RouteValues - { - get; - set; - } + public RouteValueDictionary RouteValues + { + get; + set; + } - public string Url - { - get - { - return _url; - } - set - { - _url = value; - _routeName = _controllerName = (string)(_actionName = null); - this.RouteValues.Clear(); + public string Url + { + get => _url; + set + { + _url = value; + _routeName = _controllerName = (string)(_actionName = null); + this.RouteValues.Clear(); - } - } + } + } - public ModifiedParameter ModifiedParam - { - get; - protected set; - } - } + public ModifiedParameter ModifiedParam + { + get; + protected set; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs index 5f6a13e8aa..4822affcce 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs @@ -3,7 +3,6 @@ using System.Web.Mvc; using System.Web.Routing; using System.Web.WebPages; -using SmartStore.Core; using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs index be5583d549..dfc5cb4ba3 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs @@ -6,8 +6,8 @@ namespace SmartStore.Web.Framework.UI { - [Serializable] - public abstract class NavigationItem : IHtmlAttributesContainer, INavigatable, IHideObjectMembers + [Serializable] + public abstract class NavigationItem : IHtmlAttributesContainer, INavigatable, IHideObjectMembers { private bool _selected; private bool _enabled; @@ -48,23 +48,23 @@ public IDictionary GetCombinedAttributes() { combined.Merge(LinkHtmlAttributes); } - + return combined; } public string ImageUrl { get; set; } - public int? ImageId { get; set; } + public int? ImageId { get; set; } - public string Icon { get; set; } + public string Icon { get; set; } public string Text { get; set; } - public bool Rtl { get; set; } + public bool Rtl { get; set; } - public string Summary { get; set; } + public string Summary { get; set; } - public string BadgeText { get; set; } + public string BadgeText { get; set; } public BadgeStyle BadgeStyle { get; set; } @@ -74,10 +74,7 @@ public IDictionary GetCombinedAttributes() public bool Selected { - get - { - return _selected; - } + get => _selected; set { _selected = value; @@ -90,10 +87,7 @@ public bool Selected public bool Enabled { - get - { - return _enabled; - } + get => _enabled; set { _enabled = value; @@ -105,78 +99,66 @@ public bool Enabled } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string ActionName + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string ActionName { - get - { - return _actionName; - } + get => _actionName; set { - if (_actionName != value) - { - _actionName = value; - _routeName = (string)(_url = null); - } + if (_actionName != value) + { + _actionName = value; + _routeName = (string)(_url = null); + } } } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string ControllerName + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string ControllerName { - get - { - return _controllerName; - } + get => _controllerName; set { - if (_controllerName != value) - { - _controllerName = value; - _routeName = (string)(_url = null); - } + if (_controllerName != value) + { + _controllerName = value; + _routeName = (string)(_url = null); + } } } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string RouteName + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string RouteName { - get - { - return _routeName; - } + get => _routeName; set { - if (_routeName != value) - { - _routeName = value; - _controllerName = _actionName = (string)(_url = null); - } + if (_routeName != value) + { + _routeName = value; + _controllerName = _actionName = (string)(_url = null); + } } } public RouteValueDictionary RouteValues { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string Url { - get - { - return _url; - } + get => _url; set { - if (_url != value) - { - _url = value; - _routeName = _controllerName = (string)(_actionName = null); - this.RouteValues.Clear(); - } + if (_url != value) + { + _url = value; + _routeName = _controllerName = (string)(_actionName = null); + this.RouteValues.Clear(); + } } } - [JsonIgnore] + [JsonIgnore] public ModifiedParameter ModifiedParam { get; @@ -187,9 +169,9 @@ public ModifiedParameter ModifiedParam /// Checks whether action/controller or routeName or url has been specified. /// public bool HasRoute() - { - return _actionName != null || _routeName != null || _url != null; - } + { + return _actionName != null || _routeName != null || _url != null; + } /// /// Checks whether url has been specified with '#' or 'javascript:void()' or empty string. @@ -218,7 +200,7 @@ public NavigationItemWithContent() this.ContentHtmlAttributes = new RouteValueDictionary(); } - public bool Ajax { get; set; } + public bool Ajax { get; set; } public IDictionary ContentHtmlAttributes { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs index 4479edac15..7242fec0a5 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Mvc; using System.Web.Mvc.Html; using System.Web.Routing; using System.Web.WebPages; -using SmartStore.Core; using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI @@ -36,20 +33,20 @@ public TBuilder Action(RouteValueDictionary routeValues) return (this as TBuilder); } - public TBuilder Action(string actionName) - { - return this.Action(actionName, null, null); - } + public TBuilder Action(string actionName) + { + return this.Action(actionName, null, null); + } - public TBuilder Action(string actionName, object routeValues) - { - return this.Action(actionName, null, routeValues); - } + public TBuilder Action(string actionName, object routeValues) + { + return this.Action(actionName, null, routeValues); + } - public TBuilder Action(string actionName, RouteValueDictionary routeValues) - { - return this.Action(actionName, null, routeValues); - } + public TBuilder Action(string actionName, RouteValueDictionary routeValues) + { + return this.Action(actionName, null, routeValues); + } public TBuilder Action(string actionName, string controllerName) { @@ -127,13 +124,13 @@ public TBuilder ImageUrl(string value) return (this as TBuilder); } - public TBuilder ImageId(int? value) - { - this.Item.ImageId = value; - return (this as TBuilder); - } + public TBuilder ImageId(int? value) + { + this.Item.ImageId = value; + return (this as TBuilder); + } - public TBuilder Icon(string value) + public TBuilder Icon(string value) { this.Item.Icon = value; return (this as TBuilder); @@ -145,13 +142,13 @@ public TBuilder Text(string value) return (this as TBuilder); } - public TBuilder Summary(string value) - { - this.Item.Summary = value; - return (this as TBuilder); - } + public TBuilder Summary(string value) + { + this.Item.Summary = value; + return (this as TBuilder); + } - public TBuilder Badge(string value, BadgeStyle style = BadgeStyle.Secondary, bool condition = true) + public TBuilder Badge(string value, BadgeStyle style = BadgeStyle.Secondary, bool condition = true) { if (condition) { @@ -210,29 +207,29 @@ protected HtmlHelper HtmlHelper private set; } - /// - /// Specifies whether the content should be loaded per AJAX into the content pane. - /// - /// value - /// builder - /// - /// This setting has no effect when no route is specified OR - /// static content was set. - /// - public TBuilder Ajax(bool value = true) - { - this.Item.Ajax = value; - return (this as TBuilder); - } + /// + /// Specifies whether the content should be loaded per AJAX into the content pane. + /// + /// value + /// builder + /// + /// This setting has no effect when no route is specified OR + /// static content was set. + /// + public TBuilder Ajax(bool value = true) + { + this.Item.Ajax = value; + return (this as TBuilder); + } public TBuilder Content(string value) { - if (value.IsEmpty()) - { - // do nothing - return (this as TBuilder); - } - return this.Content(x => new HelperResult(writer => writer.Write(value))); + if (value.IsEmpty()) + { + // do nothing + return (this as TBuilder); + } + return this.Content(x => new HelperResult(writer => writer.Write(value))); } public TBuilder Content(Func value) @@ -255,7 +252,7 @@ public TBuilder Content(HelperResult value) /// builder instance public TBuilder Content(string action, string controller, object routeValues) { - return Content(action, controller, new RouteValueDictionary(routeValues)); + return Content(action, controller, new RouteValueDictionary(routeValues)); } /// @@ -267,7 +264,7 @@ public TBuilder Content(string action, string controller, object routeValues) /// builder instance public TBuilder Content(string action, string controller, RouteValueDictionary routeValues) { - return this.Content(x => new HelperResult(writer => + return this.Content(x => new HelperResult(writer => { var value = this.HtmlHelper.Action(action, controller, routeValues); writer.Write(value); diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/ForumTopicPagerRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/ForumTopicPagerRenderer.cs index aabeef178d..2d36674971 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/ForumTopicPagerRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/ForumTopicPagerRenderer.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Generic; namespace SmartStore.Web.Framework.UI { - + public class ForumTopicPagerRenderer : PagerRenderer { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs index d023c08cb5..dcc932017a 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Routing; -using SmartStore.Core; +using SmartStore.Core; using SmartStore.Core.Infrastructure; using SmartStore.Services.Localization; @@ -11,8 +6,8 @@ namespace SmartStore.Web.Framework.UI { public enum PagerSize { - Mini, - Small, + Mini, + Small, Medium, Large } @@ -46,7 +41,7 @@ public Pager() } public Pager(ILocalizationService localizationService) - : base() + : base() { _localizationService = localizationService; @@ -82,16 +77,10 @@ public Pager(ILocalizationService localizationService) /// public string FirstButtonText { - get - { - return (!string.IsNullOrEmpty(_firstButtonText)) ? + get => (!string.IsNullOrEmpty(_firstButtonText)) ? _firstButtonText : _localizationService.GetResource("Pager.First"); - } - set - { - _firstButtonText = value; - } + set => _firstButtonText = value; } /// @@ -99,16 +88,10 @@ public string FirstButtonText /// public string LastButtonText { - get - { - return (!string.IsNullOrEmpty(_lastButtonText)) ? + get => (!string.IsNullOrEmpty(_lastButtonText)) ? _lastButtonText : _localizationService.GetResource("Pager.Last"); - } - set - { - _lastButtonText = value; - } + set => _lastButtonText = value; } /// @@ -116,16 +99,10 @@ public string LastButtonText /// public string NextButtonText { - get - { - return (!string.IsNullOrEmpty(_nextButtonText)) ? + get => (!string.IsNullOrEmpty(_nextButtonText)) ? _nextButtonText : _localizationService.GetResource("Pager.Next"); - } - set - { - _nextButtonText = value; - } + set => _nextButtonText = value; } /// @@ -133,16 +110,10 @@ public string NextButtonText /// public string PreviousButtonText { - get - { - return (!string.IsNullOrEmpty(_previousButtonText)) ? + get => (!string.IsNullOrEmpty(_previousButtonText)) ? _previousButtonText : _localizationService.GetResource("Pager.Previous"); - } - set - { - _previousButtonText = value; - } + set => _previousButtonText = value; } /// @@ -150,16 +121,10 @@ public string PreviousButtonText /// public string CurrentPageText { - get - { - return (!string.IsNullOrEmpty(_currentPageText)) ? + get => (!string.IsNullOrEmpty(_currentPageText)) ? _currentPageText : _localizationService.GetResource("Pager.CurrentPage"); - } - set - { - _currentPageText = value; - } + set => _currentPageText = value; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs index 1164c9fa3c..06601ae7f7 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs @@ -1,5 +1,4 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; using SmartStore.Core; namespace SmartStore.Web.Framework.UI diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerItem.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerItem.cs index ec9a8f6af4..608ba01265 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerItem.cs @@ -54,13 +54,7 @@ public PagerItem(string text, string url, PagerItemType itemType, PagerItemState public PagerItemType Type { get; set; } public string ExtraData { get; set; } - public bool IsNavButton - { - get - { - return (Type == PagerItemType.FirstPage || Type == PagerItemType.PreviousPage || Type == PagerItemType.NextPage || Type == PagerItemType.LastPage); - } - } + public bool IsNavButton => (Type == PagerItemType.FirstPage || Type == PagerItemType.PreviousPage || Type == PagerItemType.NextPage || Type == PagerItemType.LastPage); } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerRenderer.cs index 7e024d559d..6d770bf586 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerRenderer.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Web.UI; +using System.Collections.Generic; using System.Web.Routing; +using System.Web.UI; namespace SmartStore.Web.Framework.UI -{ +{ public class PagerRenderer : ComponentRenderer { public PagerRenderer() @@ -175,20 +174,20 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) var items = CreateItemList(); - // Alignment - if (pager.Alignment == PagerAlignment.Right) - { - pager.HtmlAttributes.AppendCssClass("text-right"); - } - else if (pager.Alignment == PagerAlignment.Centered) - { - pager.HtmlAttributes.AppendCssClass("text-center"); - } - - writer.AddAttributes(pager.HtmlAttributes); + // Alignment + if (pager.Alignment == PagerAlignment.Right) + { + pager.HtmlAttributes.AppendCssClass("text-right"); + } + else if (pager.Alignment == PagerAlignment.Centered) + { + pager.HtmlAttributes.AppendCssClass("text-center"); + } + + writer.AddAttributes(pager.HtmlAttributes); writer.AddAttribute("id", pager.Id); - writer.AddAttribute("aria-label", "Page navigation"); - writer.RenderBeginTag("nav"); + writer.AddAttribute("aria-label", "Page navigation"); + writer.RenderBeginTag("nav"); if (pager.ShowSummary && pager.Model.TotalPages > 1) { @@ -198,36 +197,36 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) writer.RenderEndTag(); // div } - var ulAttrs = new RouteValueDictionary(); - - ulAttrs.AppendCssClass(pager.Style == PagerStyle.Pagination ? "pagination" : "pagination" /* formerly "pager" */); // TODO: (mc) Seems there's no "pager" in BS4 anymore - - // Size - if (pager.Size == PagerSize.Large) - { - ulAttrs.AppendCssClass("pagination-lg"); - } - else if (pager.Size == PagerSize.Small) - { - ulAttrs.AppendCssClass("pagination-sm"); - } - else if (pager.Size == PagerSize.Mini) - { - ulAttrs.AppendCssClass("pagination-xs"); - } - - // BS 4 alignment - if (pager.Alignment == PagerAlignment.Centered) - { - ulAttrs.AppendCssClass("justify-content-center"); - } - else if (pager.Alignment == PagerAlignment.Right) - { - ulAttrs.AppendCssClass("justify-content-end"); - } - - writer.AddAttributes(ulAttrs); - writer.RenderBeginTag("ul"); + var ulAttrs = new RouteValueDictionary(); + + ulAttrs.AppendCssClass(pager.Style == PagerStyle.Pagination ? "pagination" : "pagination" /* formerly "pager" */); // TODO: (mc) Seems there's no "pager" in BS4 anymore + + // Size + if (pager.Size == PagerSize.Large) + { + ulAttrs.AppendCssClass("pagination-lg"); + } + else if (pager.Size == PagerSize.Small) + { + ulAttrs.AppendCssClass("pagination-sm"); + } + else if (pager.Size == PagerSize.Mini) + { + ulAttrs.AppendCssClass("pagination-xs"); + } + + // BS 4 alignment + if (pager.Alignment == PagerAlignment.Centered) + { + ulAttrs.AppendCssClass("justify-content-center"); + } + else if (pager.Alignment == PagerAlignment.Right) + { + ulAttrs.AppendCssClass("justify-content-end"); + } + + writer.AddAttributes(ulAttrs); + writer.RenderBeginTag("ul"); foreach (var item in items) { @@ -244,26 +243,26 @@ protected virtual void RenderItem(HtmlTextWriter writer, PagerItem item) if (item.State == PagerItemState.Disabled) { - attrs.AppendCssClass("disabled"); + attrs.AppendCssClass("disabled"); } else if (item.State == PagerItemState.Selected) { - attrs.AppendCssClass("active"); + attrs.AppendCssClass("active"); } if (item.Type == PagerItemType.Text) { - attrs.AppendCssClass("shrinked"); + attrs.AppendCssClass("shrinked"); } if (Component.Style == PagerStyle.Blog && item.IsNavButton) { - // TODO: (mg) BS4 doesn't seem to support this anymore - attrs.AppendCssClass((item.Type == PagerItemType.PreviousPage || item.Type == PagerItemType.FirstPage) ? "previous" : "next"); + // TODO: (mg) BS4 doesn't seem to support this anymore + attrs.AppendCssClass((item.Type == PagerItemType.PreviousPage || item.Type == PagerItemType.FirstPage) ? "previous" : "next"); } - attrs.AppendCssClass("page-item"); - + attrs.AppendCssClass("page-item"); + writer.AddAttributes(attrs); writer.RenderBeginTag("li"); @@ -274,9 +273,9 @@ protected virtual void RenderItem(HtmlTextWriter writer, PagerItem item) if (item.IsNavButton) { writer.AddAttribute("title", item.Text.AttributeEncode()); - writer.AddAttribute("aria-label", item.Text.AttributeEncode()); - writer.AddAttribute("tab-index", "-1"); - if (Component.Style != PagerStyle.Blog) + writer.AddAttribute("aria-label", item.Text.AttributeEncode()); + writer.AddAttribute("tab-index", "-1"); + if (Component.Style != PagerStyle.Blog) { writer.AddAttribute("rel", "tooltip"); writer.AddAttribute("class", "page-link page-nav"); @@ -298,9 +297,9 @@ protected virtual void RenderItem(HtmlTextWriter writer, PagerItem item) } else { - // write span - writer.AddAttribute("class", "page-link"); - writer.RenderBeginTag("span"); + // write span + writer.AddAttribute("class", "page-link"); + writer.RenderBeginTag("span"); } this.RenderItemInnerContent(writer, item); @@ -320,13 +319,13 @@ protected virtual void RenderItemInnerContent(HtmlTextWriter writer, PagerItem i writer.AddAttribute("class", "fa fa-angle-double-left"); break; case PagerItemType.PreviousPage: - writer.AddAttribute("class", "fa fa-angle-left"); + writer.AddAttribute("class", "fa fa-angle-left"); break; case PagerItemType.NextPage: - writer.AddAttribute("class", "fa fa-angle-right"); + writer.AddAttribute("class", "fa fa-angle-right"); break; case PagerItemType.LastPage: - writer.AddAttribute("class", "fa fa-angle-double-right"); + writer.AddAttribute("class", "fa fa-angle-double-right"); break; default: writer.WriteEncodedText(item.Text); diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/Tab.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/Tab.cs index 6b0cc233a0..8ce19a5f8b 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/Tab.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/Tab.cs @@ -1,16 +1,13 @@ using System; -using System.Collections.Generic; -using System.Web.Routing; -using System.Web.WebPages; namespace SmartStore.Web.Framework.UI { - [Serializable] - public class SelectedTabInfo - { - public string TabId { get; set; } - public string Path { get; set; } - } + [Serializable] + public class SelectedTabInfo + { + public string TabId { get; set; } + public string Path { get; set; } + } public enum TabPull { @@ -22,8 +19,8 @@ public class Tab : NavigationItemWithContent { public Tab() { - // [...] - } + // [...] + } public string Name { @@ -36,5 +33,5 @@ public TabPull Pull get; set; } - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs index 48443f9690..ebb7545bc8 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs @@ -20,5 +20,5 @@ public TabBuilder Pull(TabPull value) this.Item.Pull = value; return this; } - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs index 7fee6c00af..ba149e2de2 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Generic; using System.Web.Mvc; namespace SmartStore.Web.Framework.UI -{ +{ public class TabFactory : IHideObjectMembers { private readonly IList _items; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs index 7ab56a8bdc..670bcf4856 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Generic; using System.Web.WebPages; namespace SmartStore.Web.Framework.UI @@ -18,7 +15,7 @@ public enum TabsStyle { Tabs, Pills, - Material + Material } public class TabStrip : Component @@ -28,7 +25,7 @@ public TabStrip() this.Items = new List(); this.Fade = true; this.SmartTabSelection = true; - this.Breakpoint = " Items @@ -37,25 +34,31 @@ public List Items private set; } - public bool IsResponsive + public bool IsResponsive { get; set; } - public string Breakpoint - { - get; - set; - } + public bool HideSingleItem + { + get; + set; + } + + public string Breakpoint + { + get; + set; + } - public HelperResult TabContentHeaderContent - { - get; - set; - } + public HelperResult TabContentHeaderContent + { + get; + set; + } - public TabsPosition Position + public TabsPosition Position { get; set; @@ -79,37 +82,31 @@ public bool SmartTabSelection set; } - public string OnAjaxBegin - { - get; - set; - } - - public string OnAjaxSuccess - { - get; - set; - } - - public string OnAjaxFailure - { - get; - set; - } - - public string OnAjaxComplete - { - get; - set; - } - - public override bool NameIsRequired + public string OnAjaxBegin { - get - { - return true; - } + get; + set; } + + public string OnAjaxSuccess + { + get; + set; + } + + public string OnAjaxFailure + { + get; + set; + } + + public string OnAjaxComplete + { + get; + set; + } + + public override bool NameIsRequired => true; } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs index 390a7732d4..b0a1cfdf26 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs @@ -3,7 +3,7 @@ using System.Web.WebPages; namespace SmartStore.Web.Framework.UI -{ +{ public class TabStripBuilder : ComponentBuilder, TModel> { public TabStripBuilder(TabStrip Component, HtmlHelper htmlHelper) @@ -18,36 +18,42 @@ public TabStripBuilder Items(Action addAction) return this; } - public TabStripBuilder Responsive(bool value, string breakpoint = " TabContentHeaderContent(string value) - { - if (value.IsEmpty()) - { - // do nothing - return this; - } - - return this.TabContentHeaderContent(x => new HelperResult(writer => writer.Write(value))); - } - - public TabStripBuilder TabContentHeaderContent(Func value) - { - return this.TabContentHeaderContent(value(null)); - } - - public TabStripBuilder TabContentHeaderContent(HelperResult value) - { - base.Component.TabContentHeaderContent = value; - return this; - } - - public TabStripBuilder Position(TabsPosition value) + public TabStripBuilder Responsive(bool value, string breakpoint = " HideSingleItem(bool value) + { + base.Component.HideSingleItem = value; + return this; + } + + public TabStripBuilder TabContentHeaderContent(string value) + { + if (value.IsEmpty()) + { + // do nothing + return this; + } + + return this.TabContentHeaderContent(x => new HelperResult(writer => writer.Write(value))); + } + + public TabStripBuilder TabContentHeaderContent(Func value) + { + return this.TabContentHeaderContent(value(null)); + } + + public TabStripBuilder TabContentHeaderContent(HelperResult value) + { + base.Component.TabContentHeaderContent = value; + return this; + } + + public TabStripBuilder Position(TabsPosition value) { base.Component.Position = value; return this; @@ -71,28 +77,28 @@ public TabStripBuilder SmartTabSelection(bool value) return this; } - public TabStripBuilder OnAjaxBegin(string value) - { - base.Component.OnAjaxBegin = value; - return this; - } - - public TabStripBuilder OnAjaxSuccess(string value) - { - base.Component.OnAjaxSuccess = value; - return this; - } - - public TabStripBuilder OnAjaxFailure(string value) - { - base.Component.OnAjaxFailure = value; - return this; - } - - public TabStripBuilder OnAjaxComplete(string value) - { - base.Component.OnAjaxComplete = value; - return this; - } + public TabStripBuilder OnAjaxBegin(string value) + { + base.Component.OnAjaxBegin = value; + return this; + } + + public TabStripBuilder OnAjaxSuccess(string value) + { + base.Component.OnAjaxSuccess = value; + return this; + } + + public TabStripBuilder OnAjaxFailure(string value) + { + base.Component.OnAjaxFailure = value; + return this; + } + + public TabStripBuilder OnAjaxComplete(string value) + { + base.Component.OnAjaxComplete = value; + return this; + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs index 8179b70678..7a77fc0320 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs @@ -2,163 +2,167 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; -using System.Web.UI; using System.Web.Mvc; +using System.Web.UI; using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Framework.UI -{ +{ public class TabStripRenderer : ComponentRenderer { public TabStripRenderer() - { + { } - [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] - protected override void WriteHtmlCore(HtmlTextWriter writer) - { - var tab = base.Component; + [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] + protected override void WriteHtmlCore(HtmlTextWriter writer) + { + var tab = base.Component; MoveSpecialTabToEnd(tab.Items); var hasContent = tab.Items.Any(x => x.Content != null || x.Ajax); - var isTabbable = tab.Position != TabsPosition.Top; - var isStacked = tab.Position == TabsPosition.Left || tab.Position == TabsPosition.Right; - var urlHelper = new UrlHelper(this.ViewContext.RequestContext); - - if (tab.Items.Count == 0) - return; - - tab.HtmlAttributes.AppendCssClass("tabbable"); - - if (isTabbable) - { - tab.HtmlAttributes.AppendCssClass("tabs-{0}".FormatInvariant(tab.Position.ToString().ToLower())); - } - - if (tab.SmartTabSelection) - { - tab.HtmlAttributes.AppendCssClass("tabs-autoselect"); - tab.HtmlAttributes.Add("data-tabselector-href", urlHelper.Action("SetSelectedTab", "Common", new { area = "admin" })); - } - - if (isStacked) - { - tab.HtmlAttributes.AppendCssClass("row"); - } - - if (tab.OnAjaxBegin.HasValue()) - { - tab.HtmlAttributes.Add("data-ajax-onbegin", tab.OnAjaxBegin); - } - - if (tab.OnAjaxSuccess.HasValue()) - { - tab.HtmlAttributes.Add("data-ajax-onsuccess", tab.OnAjaxSuccess); - } - - if (tab.OnAjaxFailure.HasValue()) - { - tab.HtmlAttributes.Add("data-ajax-onfailure", tab.OnAjaxFailure); - } - - if (tab.OnAjaxComplete.HasValue()) - { - tab.HtmlAttributes.Add("data-ajax-oncomplete", tab.OnAjaxComplete); - } - - if (tab.IsResponsive) - { - tab.HtmlAttributes.AppendCssClass("nav-responsive"); - tab.HtmlAttributes.Add("data-breakpoint", tab.Breakpoint); - } - - writer.AddAttributes(tab.HtmlAttributes); - - writer.RenderBeginTag("div"); // root div - { - if (tab.Position == TabsPosition.Below && hasContent) - RenderTabContent(writer); - - if (isStacked) - { - writer.AddAttribute("class", "col-lg-auto nav-aside"); - writer.RenderBeginTag("aside"); // opening left/right tabs col - } - - // Tabs - var ulAttrs = new Dictionary(); - ulAttrs.AppendCssClass("nav"); - - if (tab.Style == TabsStyle.Tabs) - { - ulAttrs.AppendCssClass("nav-tabs"); - } - else if (tab.Style == TabsStyle.Pills) - { - ulAttrs.AppendCssClass("nav-pills"); - } - else if (tab.Style == TabsStyle.Material) - { - ulAttrs.AppendCssClass("nav-tabs nav-tabs-line"); - } - - if (isStacked) - { - ulAttrs.AppendCssClass("flex-row flex-lg-column"); - } - writer.AddAttributes(ulAttrs); - - string selector = null; - var loadedTabs = new List(); - writer.RenderBeginTag("ul"); - { - // enable smart tab selection - if (tab.SmartTabSelection) - { - selector = TrySelectRememberedTab(); - } - - int i = 1; - foreach (var item in tab.Items) - { - var loadedTabName = this.RenderItemLink(writer, item, i); - if (loadedTabName.HasValue()) - { - loadedTabs.Add(loadedTabName); - } - i++; - } - - writer.RenderEndTag(); // ul - } - - if (isStacked) - { - writer.RenderEndTag(); // closing left/right tabs col - } - - // TODO: (mc) render right positioned tabs also - if (tab.Position != TabsPosition.Below && hasContent) - { - if (isStacked) - { - writer.AddAttribute("class", "col-lg nav-content"); - writer.RenderBeginTag("div"); // opening left/right content col - } - - RenderTabContent(writer); - - if (isStacked) - { - writer.RenderEndTag(); // closing left/right content col - } - } - - if (selector != null) - { - writer.WriteLine( + var isTabbable = tab.Position != TabsPosition.Top; + var isStacked = tab.Position == TabsPosition.Left || tab.Position == TabsPosition.Right; + var urlHelper = new UrlHelper(this.ViewContext.RequestContext); + + if (tab.Items.Count == 0) + return; + + tab.HtmlAttributes.AppendCssClass("tabbable"); + + if (isTabbable) + { + tab.HtmlAttributes.AppendCssClass("tabs-{0}".FormatInvariant(tab.Position.ToString().ToLower())); + } + + if (tab.SmartTabSelection) + { + tab.HtmlAttributes.AppendCssClass("tabs-autoselect"); + tab.HtmlAttributes.Add("data-tabselector-href", urlHelper.Action("SetSelectedTab", "Common", new { area = "admin" })); + } + + if (isStacked) + { + tab.HtmlAttributes.AppendCssClass("row"); + } + + if (tab.OnAjaxBegin.HasValue()) + { + tab.HtmlAttributes.Add("data-ajax-onbegin", tab.OnAjaxBegin); + } + + if (tab.OnAjaxSuccess.HasValue()) + { + tab.HtmlAttributes.Add("data-ajax-onsuccess", tab.OnAjaxSuccess); + } + + if (tab.OnAjaxFailure.HasValue()) + { + tab.HtmlAttributes.Add("data-ajax-onfailure", tab.OnAjaxFailure); + } + + if (tab.OnAjaxComplete.HasValue()) + { + tab.HtmlAttributes.Add("data-ajax-oncomplete", tab.OnAjaxComplete); + } + + if (tab.IsResponsive) + { + tab.HtmlAttributes.AppendCssClass("nav-responsive"); + tab.HtmlAttributes.Add("data-breakpoint", tab.Breakpoint); + } + + writer.AddAttributes(tab.HtmlAttributes); + + writer.RenderBeginTag("div"); // root div + { + if (tab.Position == TabsPosition.Below && hasContent) + RenderTabContent(writer); + + if (isStacked) + { + writer.AddAttribute("class", "col-lg-auto nav-aside"); + writer.RenderBeginTag("aside"); // opening left/right tabs col + } + + // Tabs + var ulAttrs = new Dictionary(); + ulAttrs.AppendCssClass("nav"); + + if (tab.Style == TabsStyle.Tabs) + { + ulAttrs.AppendCssClass("nav-tabs"); + } + else if (tab.Style == TabsStyle.Pills) + { + ulAttrs.AppendCssClass("nav-pills"); + } + else if (tab.Style == TabsStyle.Material) + { + ulAttrs.AppendCssClass("nav-tabs nav-tabs-line"); + } + + if (tab.HideSingleItem && tab.Items.Count == 1) + { + ulAttrs.AppendCssClass("d-none"); + } + + if (isStacked) + { + ulAttrs.AppendCssClass("flex-row flex-lg-column"); + } + writer.AddAttributes(ulAttrs); + + string selector = null; + var loadedTabs = new List(); + writer.RenderBeginTag("ul"); + { + // enable smart tab selection + if (tab.SmartTabSelection) + { + selector = TrySelectRememberedTab(); + } + + int i = 1; + foreach (var item in tab.Items) + { + var loadedTabName = this.RenderItemLink(writer, item, i); + if (loadedTabName.HasValue()) + { + loadedTabs.Add(loadedTabName); + } + i++; + } + + writer.RenderEndTag(); // ul + } + + if (isStacked) + { + writer.RenderEndTag(); // closing left/right tabs col + } + + // TODO: (mc) render right positioned tabs also + if (tab.Position != TabsPosition.Below && hasContent) + { + if (isStacked) + { + writer.AddAttribute("class", "col-lg nav-content"); + writer.RenderBeginTag("div"); // opening left/right content col + } + + RenderTabContent(writer); + + if (isStacked) + { + writer.RenderEndTag(); // closing left/right content col + } + } + + if (selector != null) + { + writer.WriteLine( @"".FormatInvariant(selector)); - } - - if (loadedTabs.Count > 0) - { - foreach (var tabName in loadedTabs) - { - writer.WriteLine("", tabName); - } - } - - writer.RenderEndTag(); // div.tabbable - - if (tab.IsResponsive /* && tab.TabContentHeaderContent != null*/) - { - writer.WriteLine(@"".FormatInvariant(tab.Id)); - } - } - } + } + + if (loadedTabs.Count > 0) + { + foreach (var tabName in loadedTabs) + { + writer.WriteLine("", tabName); + } + } + + writer.RenderEndTag(); // div.tabbable + + if (tab.IsResponsive /* && tab.TabContentHeaderContent != null*/) + { + writer.WriteLine(@"".FormatInvariant(tab.Id)); + } + } + } private void MoveSpecialTabToEnd(List tabs) { @@ -198,84 +202,84 @@ private void MoveSpecialTabToEnd(List tabs) // returns a query selector private string TrySelectRememberedTab() - { - var tab = this.Component; - - if (tab.Id.IsEmpty()) - return null; - - if (ViewContext.ViewData.Model is EntityModelBase model && model.Id == 0) - { - // it's a "create" operation: don't select - return null; - } - - var rememberedTab = (SelectedTabInfo)ViewContext.TempData["SelectedTab." + tab.Id]; - if (rememberedTab != null && rememberedTab.Path.Equals(ViewContext.HttpContext.Request.RawUrl, StringComparison.OrdinalIgnoreCase)) - { - // get tab to select - var tabToSelect = GetTabById(rememberedTab.TabId); - - if (tabToSelect != null) - { - // unselect former selected tab(s) - tab.Items.Each(x => x.Selected = false); - - // select the new tab - tabToSelect.Selected = true; - - // persist again for the next request - ViewContext.TempData["SelectedTab." + tab.Id] = rememberedTab; - - if (tabToSelect.Ajax && tabToSelect.Content == null) - { - return ".nav a[data-ajax-url][href='#{0}']".FormatInvariant(rememberedTab.TabId); - } - } - } - - return null; - } - - private Tab GetTabById(string tabId) - { - int i = 1; - foreach (var item in Component.Items) - { - var id = BuildItemId(item, i); - if (id == tabId) - { - if (!item.Visible || !item.Enabled) - break; - - return item; - } - i++; - } - - return null; - } + { + var tab = this.Component; + + if (tab.Id.IsEmpty()) + return null; + + if (ViewContext.ViewData.Model is EntityModelBase model && model.Id == 0) + { + // it's a "create" operation: don't select + return null; + } + + var rememberedTab = (SelectedTabInfo)ViewContext.TempData["SelectedTab." + tab.Id]; + if (rememberedTab != null && rememberedTab.Path.Equals(ViewContext.HttpContext.Request.RawUrl, StringComparison.OrdinalIgnoreCase)) + { + // get tab to select + var tabToSelect = GetTabById(rememberedTab.TabId); + + if (tabToSelect != null) + { + // unselect former selected tab(s) + tab.Items.Each(x => x.Selected = false); + + // select the new tab + tabToSelect.Selected = true; + + // persist again for the next request + ViewContext.TempData["SelectedTab." + tab.Id] = rememberedTab; + + if (tabToSelect.Ajax && tabToSelect.Content == null) + { + return ".nav a[data-ajax-url][href='#{0}']".FormatInvariant(rememberedTab.TabId); + } + } + } + + return null; + } + + private Tab GetTabById(string tabId) + { + int i = 1; + foreach (var item in Component.Items) + { + var id = BuildItemId(item, i); + if (id == tabId) + { + if (!item.Visible || !item.Enabled) + break; + + return item; + } + i++; + } + + return null; + } protected virtual void RenderTabContent(HtmlTextWriter writer) { var tab = base.Component; - + writer.AddAttribute("class", "tab-content"); writer.RenderBeginTag("div"); - // Tab content header - if (tab.IsResponsive && tab.TabContentHeaderContent != null) - { - //writer.WriteLine(item.Content.ToHtmlString()); - writer.AddAttribute("class", "tab-content-header"); - writer.RenderBeginTag("div"); - { - writer.WriteLine(tab.TabContentHeaderContent.ToHtmlString()); - } - writer.RenderEndTag(); // div.tab-content-header - } - - // All tab items + // Tab content header + if (tab.IsResponsive && tab.TabContentHeaderContent != null) + { + //writer.WriteLine(item.Content.ToHtmlString()); + writer.AddAttribute("class", "tab-content-header"); + writer.RenderBeginTag("div"); + { + writer.WriteLine(tab.TabContentHeaderContent.ToHtmlString()); + } + writer.RenderEndTag(); // div.tab-content-header + } + + // All tab items int i = 1; foreach (var item in tab.Items) { @@ -287,162 +291,162 @@ protected virtual void RenderTabContent(HtmlTextWriter writer) } protected virtual string RenderItemLink(HtmlTextWriter writer, Tab item, int index) - { - string temp = ""; - string loadedTabName = null; + { + string temp = ""; + string loadedTabName = null; //
  • {text}
  • item.HtmlAttributes.AppendCssClass("nav-item"); // .active for BS2 - if (!item.Selected && !item.Visible) - { - item.HtmlAttributes.AppendCssClass("d-none"); - } - - if (item.Pull == TabPull.Right) - { - item.HtmlAttributes.AppendCssClass("float-right"); - } - - writer.AddAttributes(item.HtmlAttributes); - - writer.RenderBeginTag("li"); - { - var itemId = "#" + BuildItemId(item, index); - - writer.AddAttribute("class", "nav-link" + (item.Selected ? " active" : "")); - - if (item.Content != null) - { - writer.AddAttribute("href", itemId); - writer.AddAttribute("data-toggle", "tab"); - writer.AddAttribute("data-loaded", "true"); - loadedTabName = GetTabName(item) ?? itemId; - } - else - { - // no content, create real link instead - var url = item.GenerateUrl(base.ViewContext.RequestContext).NullEmpty(); - - if (url == null) - { - writer.AddAttribute("href", "#"); - } - else - { - if (item.Ajax) - { - writer.AddAttribute("href", itemId); - writer.AddAttribute("data-ajax-url", url); - writer.AddAttribute("data-toggle", "tab"); + if (!item.Selected && !item.Visible) + { + item.HtmlAttributes.AppendCssClass("d-none"); + } + + if (item.Pull == TabPull.Right) + { + item.HtmlAttributes.AppendCssClass("float-right"); + } + + writer.AddAttributes(item.HtmlAttributes); + + writer.RenderBeginTag("li"); + { + var itemId = "#" + BuildItemId(item, index); + + writer.AddAttribute("class", "nav-link" + (item.Selected ? " active" : "")); + + if (item.Content != null) + { + writer.AddAttribute("href", itemId); + writer.AddAttribute("data-toggle", "tab"); + writer.AddAttribute("data-loaded", "true"); + loadedTabName = GetTabName(item) ?? itemId; + } + else + { + // no content, create real link instead + var url = item.GenerateUrl(base.ViewContext.RequestContext).NullEmpty(); + + if (url == null) + { + writer.AddAttribute("href", "#"); + } + else + { + if (item.Ajax) + { + writer.AddAttribute("href", itemId); + writer.AddAttribute("data-ajax-url", url); + writer.AddAttribute("data-toggle", "tab"); + } + else + { + writer.AddAttribute("href", url); + } + } + } + + if (item.BadgeText.HasValue()) + { + item.LinkHtmlAttributes.AppendCssClass("clearfix"); + } + + writer.AddAttributes(item.LinkHtmlAttributes); + writer.RenderBeginTag("a"); + { + // Tab Icon + if (item.Icon.HasValue()) + { + writer.AddAttribute("class", item.Icon); + writer.RenderBeginTag("i"); + writer.RenderEndTag(); // i + } + else if (item.ImageUrl.HasValue()) + { + var url = new UrlHelper(this.ViewContext.RequestContext); + writer.AddAttribute("src", url.Content(item.ImageUrl)); + writer.AddAttribute("alt", "Icon"); + writer.RenderBeginTag("img"); + writer.RenderEndTag(); // img + } + + // Caption + writer.AddAttribute("class", "tab-caption"); + writer.RenderBeginTag("span"); + writer.WriteEncodedText(item.Text); + writer.RenderEndTag(); + + // Badge + if (item.BadgeText.HasValue()) + { + // Label/badge + temp = "ml-2 badge"; + temp += " badge-" + item.BadgeStyle.ToString().ToLower(); + if (base.Component.Position == TabsPosition.Left) + { + temp += " float-right"; // looks nicer } - else - { - writer.AddAttribute("href", url); - } - } - } - - if (item.BadgeText.HasValue()) - { - item.LinkHtmlAttributes.AppendCssClass("clearfix"); - } - - writer.AddAttributes(item.LinkHtmlAttributes); - writer.RenderBeginTag("a"); - { - // Tab Icon - if (item.Icon.HasValue()) - { - writer.AddAttribute("class", item.Icon); - writer.RenderBeginTag("i"); - writer.RenderEndTag(); // i - } - else if (item.ImageUrl.HasValue()) - { - var url = new UrlHelper(this.ViewContext.RequestContext); - writer.AddAttribute("src", url.Content(item.ImageUrl)); - writer.AddAttribute("alt", "Icon"); - writer.RenderBeginTag("img"); - writer.RenderEndTag(); // img - } - - // Caption - writer.AddAttribute("class", "tab-caption"); - writer.RenderBeginTag("span"); - writer.WriteEncodedText(item.Text); - writer.RenderEndTag(); - - // Badge - if (item.BadgeText.HasValue()) - { - // Label/badge - temp = "ml-2 badge"; - temp += " badge-" + item.BadgeStyle.ToString().ToLower(); - if (base.Component.Position == TabsPosition.Left) - { - temp += " float-right"; // looks nicer - } - writer.AddAttribute("class", temp); - writer.RenderBeginTag("span"); - writer.WriteEncodedText(item.BadgeText); - writer.RenderEndTag(); // span > badge - } - - // Nav link short summary for collapsed state - if (this.Component.IsResponsive && item.Summary.HasValue()) - { - writer.AddAttribute("class", "nav-link-summary"); - writer.RenderBeginTag("span"); - writer.WriteEncodedText(item.Summary); - writer.RenderEndTag(); - } - - writer.RenderEndTag(); // a - } - writer.RenderEndTag(); // li - } - - return loadedTabName; - } - - private string GetTabName(Tab tab) - { - if (tab.LinkHtmlAttributes.TryGetValue("data-tab-name", out object value)) - { - return value.ToString(); - } - return null; - } + writer.AddAttribute("class", temp); + writer.RenderBeginTag("span"); + writer.WriteEncodedText(item.BadgeText); + writer.RenderEndTag(); // span > badge + } + + // Nav link short summary for collapsed state + if (this.Component.IsResponsive && item.Summary.HasValue()) + { + writer.AddAttribute("class", "nav-link-summary"); + writer.RenderBeginTag("span"); + writer.WriteEncodedText(item.Summary); + writer.RenderEndTag(); + } + + writer.RenderEndTag(); // a + } + writer.RenderEndTag(); // li + } + + return loadedTabName; + } + + private string GetTabName(Tab tab) + { + if (tab.LinkHtmlAttributes.TryGetValue("data-tab-name", out object value)) + { + return value.ToString(); + } + return null; + } protected virtual void RenderItemContent(HtmlTextWriter writer, Tab item, int index) - { - //
    {content}
    - item.ContentHtmlAttributes.AppendCssClass("tab-pane"); - item.ContentHtmlAttributes.Add("role", "tabpanel"); - if (base.Component.Fade) - { - item.ContentHtmlAttributes.AppendCssClass("fade"); - } - if (item.Selected) - { - if (base.Component.Fade) - { - item.ContentHtmlAttributes.AppendCssClass("show"); - } - item.ContentHtmlAttributes.AppendCssClass("active"); - } - writer.AddAttributes(item.ContentHtmlAttributes); - writer.AddAttribute("id", BuildItemId(item, index)); - writer.RenderBeginTag("div"); - { + { + //
    {content}
    + item.ContentHtmlAttributes.AppendCssClass("tab-pane"); + item.ContentHtmlAttributes.Add("role", "tabpanel"); + if (base.Component.Fade) + { + item.ContentHtmlAttributes.AppendCssClass("fade"); + } + if (item.Selected) + { + if (base.Component.Fade) + { + item.ContentHtmlAttributes.AppendCssClass("show"); + } + item.ContentHtmlAttributes.AppendCssClass("active"); + } + writer.AddAttributes(item.ContentHtmlAttributes); + writer.AddAttribute("id", BuildItemId(item, index)); + writer.RenderBeginTag("div"); + { if (item.Content != null) - { - writer.WriteLine(item.Content.ToHtmlString()); - } - } - writer.RenderEndTag(); // div - } + { + writer.WriteLine(item.Content.ToHtmlString()); + } + } + writer.RenderEndTag(); // div + } private string BuildItemId(Tab item, int index) { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs index 0ef84c00a0..1a5cd76277 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs @@ -3,20 +3,20 @@ namespace SmartStore.Web.Framework.UI { public class ViewBasedComponentRenderer : ComponentRenderer where TComponent : Component - { - private readonly string _viewName; + { + private readonly string _viewName; - public ViewBasedComponentRenderer() - : this(typeof(TComponent).Name) - { - } + public ViewBasedComponentRenderer() + : this(typeof(TComponent).Name) + { + } public ViewBasedComponentRenderer(string viewName) - { - Guard.NotEmpty(viewName, nameof(viewName)); + { + Guard.NotEmpty(viewName, nameof(viewName)); - _viewName = viewName; - } + _viewName = viewName; + } protected virtual string GetViewName() { @@ -24,13 +24,13 @@ protected virtual string GetViewName() } public override void Render() - { - HtmlHelper.RenderPartial(GetViewName(), Component); - } + { + HtmlHelper.RenderPartial(GetViewName(), Component); + } - public override string ToHtmlString() - { - return HtmlHelper.Partial(GetViewName(), Component).ToString(); - } - } + public override string ToHtmlString() + { + return HtmlHelper.Partial(GetViewName(), Component).ToString(); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs index 154a7e57d3..29463fd2d7 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs @@ -1,43 +1,39 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Web.Routing; using System.Web.WebPages; namespace SmartStore.Web.Framework.UI { - public enum WindowSize - { - Small, - Medium, - Large, - Flex, - FlexSmall - } - - public class Window : Component + public enum WindowSize + { + Small, + Medium, + Large, + Flex, + FlexSmall + } + + public class Window : Component { public Window() { - this.Size = WindowSize.Medium; - this.Fade = true; - this.Focus = true; + this.Size = WindowSize.Medium; + this.Fade = true; + this.Focus = true; this.BackDrop = true; this.ShowClose = true; - this.Show = true; + this.Show = true; this.CloseOnEscapePress = true; - this.CloseOnBackdropClick = true; - this.RenderAtPageEnd = true; - this.BodyHtmlAttributes = new RouteValueDictionary(); - } - - public override bool NameIsRequired - { - get { return true; } + this.CloseOnBackdropClick = true; + this.RenderAtPageEnd = true; + this.BodyHtmlAttributes = new RouteValueDictionary(); } - public WindowSize Size { get; set; } + public override bool NameIsRequired => true; + + public WindowSize Size { get; set; } - public string Title { get; set; } + public string Title { get; set; } public HelperResult Content { get; set; } @@ -47,22 +43,22 @@ public override bool NameIsRequired public bool Fade { get; set; } - public bool Focus { get; set; } + public bool Focus { get; set; } - public bool Show { get; set; } + public bool Show { get; set; } - public bool BackDrop { get; set; } + public bool BackDrop { get; set; } public bool ShowClose { get; set; } - public bool CenterVertically { get; set; } + public bool CenterVertically { get; set; } - public bool CloseOnEscapePress { get; set; } + public bool CloseOnEscapePress { get; set; } - public bool CloseOnBackdropClick { get; set; } + public bool CloseOnBackdropClick { get; set; } - public bool RenderAtPageEnd { get; set; } + public bool RenderAtPageEnd { get; set; } - public IDictionary BodyHtmlAttributes { get; set; } - } + public IDictionary BodyHtmlAttributes { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs index 0b8e9d938b..ea685065a5 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs @@ -13,13 +13,13 @@ public WindowBuilder(Window Component, HtmlHelper htmlHelper) { } - public WindowBuilder Size(WindowSize value) - { - base.Component.Size = value; - return this; - } + public WindowBuilder Size(WindowSize value) + { + base.Component.Size = value; + return this; + } - public WindowBuilder Title(string value) + public WindowBuilder Title(string value) { base.Component.Title = value; return this; @@ -81,52 +81,52 @@ public WindowBuilder ShowClose(bool value) return this; } - public WindowBuilder Focus(bool value) - { - base.Component.Focus = value; - return this; - } + public WindowBuilder Focus(bool value) + { + base.Component.Focus = value; + return this; + } - public WindowBuilder CenterVertically(bool value) - { - base.Component.CenterVertically = value; - return this; - } + public WindowBuilder CenterVertically(bool value) + { + base.Component.CenterVertically = value; + return this; + } - public WindowBuilder Show(bool value) - { - base.Component.Show = value; - return this; - } + public WindowBuilder Show(bool value) + { + base.Component.Show = value; + return this; + } - public WindowBuilder CloseOnEscapePress(bool value) + public WindowBuilder CloseOnEscapePress(bool value) { base.Component.CloseOnEscapePress = value; return this; } - public WindowBuilder CloseOnBackdropClick(bool value) - { - base.Component.CloseOnBackdropClick = value; - return this; - } - - public WindowBuilder RenderAtPageEnd(bool value) - { - base.Component.RenderAtPageEnd = value; - return this; - } - - public WindowBuilder BodyHtmlAttributes(object attributes) - { - return this.BodyHtmlAttributes(CommonHelper.ObjectToDictionary(attributes)); - } - - public WindowBuilder BodyHtmlAttributes(IDictionary attributes) - { - base.Component.BodyHtmlAttributes.Clear(); - base.Component.BodyHtmlAttributes.Merge(attributes); - return this; - } - } + public WindowBuilder CloseOnBackdropClick(bool value) + { + base.Component.CloseOnBackdropClick = value; + return this; + } + + public WindowBuilder RenderAtPageEnd(bool value) + { + base.Component.RenderAtPageEnd = value; + return this; + } + + public WindowBuilder BodyHtmlAttributes(object attributes) + { + return this.BodyHtmlAttributes(CommonHelper.ObjectToDictionary(attributes)); + } + + public WindowBuilder BodyHtmlAttributes(IDictionary attributes) + { + base.Component.BodyHtmlAttributes.Clear(); + base.Component.BodyHtmlAttributes.Merge(attributes); + return this; + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs index 86d49ab9eb..6ba1c06b7c 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs @@ -1,117 +1,116 @@ -using System; -using System.Web.UI; +using System.Web.UI; namespace SmartStore.Web.Framework.UI { - // TODO: (mc) make modal window renderer BS4 ready (after backend has been updated to BS4) - public class WindowRenderer : ComponentRenderer + // TODO: (mc) make modal window renderer BS4 ready (after backend has been updated to BS4) + public class WindowRenderer : ComponentRenderer { - public override void Render() - { - if (Component.RenderAtPageEnd) - { - using (this.HtmlHelper.BeginZoneContent("end")) - { - base.Render(); - } - } - else - { - base.Render(); - } - } - - protected override void WriteHtmlCore(HtmlTextWriter writer) + public override void Render() + { + if (Component.RenderAtPageEnd) + { + using (this.HtmlHelper.BeginZoneContent("end")) + { + base.Render(); + } + } + else + { + base.Render(); + } + } + + protected override void WriteHtmlCore(HtmlTextWriter writer) { var win = base.Component; win.HtmlAttributes.AppendCssClass("modal"); - win.HtmlAttributes["role"] = "dialog"; - win.HtmlAttributes["tabindex"] = "-1"; - win.HtmlAttributes["aria-hidden"] = "true"; - win.HtmlAttributes["aria-labelledby"] = win.Id + "Label"; + win.HtmlAttributes["role"] = "dialog"; + win.HtmlAttributes["tabindex"] = "-1"; + win.HtmlAttributes["aria-hidden"] = "true"; + win.HtmlAttributes["aria-labelledby"] = win.Id + "Label"; - if (win.Fade) + if (win.Fade) { win.HtmlAttributes.AppendCssClass("fade"); } - // Other options - win.HtmlAttributes["data-keyboard"] = win.CloseOnEscapePress.ToString().ToLower(); - win.HtmlAttributes["data-show"] = win.Show.ToString().ToLower(); - win.HtmlAttributes["data-focus"] = win.Focus.ToString().ToLower(); - win.HtmlAttributes["data-backdrop"] = win.BackDrop - ? (win.CloseOnBackdropClick ? "true" : "static") - : "false"; + // Other options + win.HtmlAttributes["data-keyboard"] = win.CloseOnEscapePress.ToString().ToLower(); + win.HtmlAttributes["data-show"] = win.Show.ToString().ToLower(); + win.HtmlAttributes["data-focus"] = win.Focus.ToString().ToLower(); + win.HtmlAttributes["data-backdrop"] = win.BackDrop + ? (win.CloseOnBackdropClick ? "true" : "static") + : "false"; writer.AddAttributes(win.HtmlAttributes); - writer.RenderBeginTag("div"); // div.modal - { - var className = "modal-dialog"; - switch (win.Size) - { - case WindowSize.Small: - className += " modal-sm"; - break; - case WindowSize.Large: - className += " modal-lg"; - break; - case WindowSize.Flex: - className += " modal-flex"; - break; - case WindowSize.FlexSmall: - className += " modal-flex modal-flex-sm"; - break; - } - - if (win.CenterVertically) - { - className += " modal-dialog-centered"; - } - - writer.AddAttribute("class", className); - win.HtmlAttributes["role"] = "document"; - writer.RenderBeginTag("div"); // div.modal-dialog - { - writer.AddAttribute("class", "modal-content"); - writer.RenderBeginTag("div"); // div.modal-content - { - // HEADER - if (win.ShowClose && win.Title.HasValue()) - { - this.RenderHeader(writer); - } - - // BODY - this.RenderBody(writer); - - // FOOTER - if (win.FooterContent != null) - { - this.RenderFooter(writer); - } - } - writer.RenderEndTag(); // div.modal-content - } - writer.RenderEndTag(); // div.modal-dialog - } + writer.RenderBeginTag("div"); // div.modal + { + var className = "modal-dialog"; + switch (win.Size) + { + case WindowSize.Small: + className += " modal-sm"; + break; + case WindowSize.Large: + className += " modal-lg"; + break; + case WindowSize.Flex: + className += " modal-flex"; + break; + case WindowSize.FlexSmall: + className += " modal-flex modal-flex-sm"; + break; + } + + if (win.CenterVertically) + { + className += " modal-dialog-centered"; + } + + writer.AddAttribute("class", className); + win.HtmlAttributes["role"] = "document"; + writer.RenderBeginTag("div"); // div.modal-dialog + { + writer.AddAttribute("class", "modal-content"); + writer.RenderBeginTag("div"); // div.modal-content + { + // HEADER + if (win.ShowClose && win.Title.HasValue()) + { + this.RenderHeader(writer); + } + + // BODY + this.RenderBody(writer); + + // FOOTER + if (win.FooterContent != null) + { + this.RenderFooter(writer); + } + } + writer.RenderEndTag(); // div.modal-content + } + writer.RenderEndTag(); // div.modal-dialog + } writer.RenderEndTag(); // div.modal } protected virtual void RenderHeader(HtmlTextWriter writer) { var win = base.Component; - + writer.AddAttribute("class", "modal-header"); writer.RenderBeginTag("div"); - if (win.Title.HasValue()) - { - writer.Write("".FormatCurrent(win.Id + "Label", win.Title)); - } + if (win.Title.HasValue()) + { + writer.Write("".FormatCurrent(win.Id + "Label", win.Title)); + } - if (win.ShowClose) + if (win.ShowClose) { writer.Write(""); } @@ -123,18 +122,18 @@ protected virtual void RenderBody(HtmlTextWriter writer) { var win = base.Component; - win.BodyHtmlAttributes.PrependCssClass("modal-body"); - writer.AddAttributes(win.BodyHtmlAttributes); + win.BodyHtmlAttributes.PrependCssClass("modal-body"); + writer.AddAttributes(win.BodyHtmlAttributes); writer.RenderBeginTag("div"); - if (win.Content != null) - { - win.Content.WriteTo(writer); - } - else if (win.ContentUrl.HasValue()) - { - writer.Write("